Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Linting

Rules for the mistakes the type checker cannot catch. They run as a plugin for ESLint and Oxlint, and as a standalone command.

pnpm add -D oxc-parser   # valof does not install it for you

Rules

rulereportsdefault
unused-membera member registered with .impl or .implTrait that nothing readswarning
duplicate-branda brand string claimed by more than one top-level aliaserror
brand-mismatcha brand whose last segment is not the name of the type it brandserror
unnecessary-aliasa type alias that is a second name for a Val, a Trait or an Enumerror
unimplemented-traita Trait a Val or an Enum declares that its companion does not implementerror
companion-mismatcha companion, or a variant, bound to a name other than its ownerror
detached-implan impl step written outside the chain that declares its companionerror
split-companiona companion for a type that another file declareserror
bypassed-companiona Val.of for a type whose companion is how it is builtwarning
unnamed-ofa Val.of that names no type, taking one from its targetwarning
incomplete-disablea disable comment leaving out the rules it silences, or its scopeerror
unused-disablea disable comment naming a rule that reports nothing therewarning

A rule warns where the code around the finding still works, and errors where a Val is broken: two types the checker stops distinguishing, or a name that has to agree with another and does not. incomplete-disable errors for a reason of its own: a comment naming no rule silences every one, a rule added next year included.

The severity is what the plugin sets, and a project can give any rule its own. The command prints every finding the same way and exits 1 on any of them.

A member registered with .impl({…}) is not tree-shaken, and knip does not report it when it becomes unused.

Disable comments

Silence one line with a comment above it,

// valof-lint-disable-next-line unused-member -- public API
shout: (u) => u.toUpperCase(),

or a whole file with one anywhere in it:

// valof-lint-disable-whole-file unused-member -- every export here is public API
// valof-lint-disable-all-whole-file -- generated, do not lint

Name the rules it silences, separated by a space or a comma. The comments follow two rules of their own, incomplete-disable and unused-disable, which are left out of a run and given a severity like any other.

Plugin for ESLint and Oxlint

The same rules, one per finding kind: name one to give it its own severity, or turn it off. The project to read is one setting for all of them, a path or a list of them, and defaults to src/**/*.ts. A path starting with ! is excluded from it.

ESLint needs a parser that reads your TypeScript.

// eslint.config.js
import valof from "valof/eslint-plugin";

export default [
  {
    plugins: { valof },
    rules: { ...valof.configs.recommended.rules, "valof/unused-member": "off" },
    settings: { valof: { project: ["src/**/*.ts", "!src/generated/**"] } },
  },
];

Oxlint takes the same plugin, through its JS plugins.

// oxlint.config.ts
import { defineConfig } from "oxlint";
import valof from "valof/eslint-plugin";

export default defineConfig({
  jsPlugins: ["valof/eslint-plugin"],
  extends: [valof.configs.recommended],
  rules: { "valof/unused-member": "off" },
  settings: { valof: { project: "src" } },
});

CLI

pnpm exec valof-lint src                                # the whole project
pnpm exec valof-lint src src/billing/id.ts              # report on the changed file
pnpm exec valof-lint 'src/**/*.ts' '!src/generated/**'  # leave a generated tree out
argumentwhat it is
the first paththe project to read, a directory or a glob
the paths after itthe files to report on, the whole project when there are none
--project, --report-onthe same two by name, in either order. Either can be repeated, and takes !path
--no-<rule>a rule to leave out of the run, by the rule name in the finding

A single file given as the project is refused: a duplicate brand needs the other alias to be seen.

A !path is excluded wherever it is written, and is removed from the run rather than only from the report, so what a generated tree declares no longer applies to the rest.