# ts-r-lint: linter

> A linter with 103 ESLint and typescript-eslint rules that reads eslint.config, .oxlintrc.json or biome.json. No type-aware rules yet.

`ts-r-lint` runs ESLint-compatible rules over JavaScript and TypeScript. It reads the config
you already have, reports in ESLint's formats and exits with ESLint's codes, so it can stand in
for `eslint` in scripts and CI when your config only uses the rules it has.

## What works today

- 103 rules: all 64 rules of ESLint 10's `recommended` set, the typescript-eslint 8.71
  `recommended` and `strict` rules that don't need type information, and a few more.
  `ts-r-lint --list-rules` prints them.
- Every case of the published ESLint and typescript-eslint tests for those rules matches:
  8,597 of 8,597.
- Configs: ESLint flat config, `.oxlintrc.json` and `biome.json` (see below).
- `--fix` for rules with fixes.
- `eslint-disable` comments and `/* eslint */` inline config, with reports of unused disable
  comments.
- `.gitignore` is honoured unless you pass `--no-gitignore`.
- Output formats `stylish`, `json`, `sarif` and `github` (GitHub Actions annotations).

## Not yet

- No type-aware rules. Rules that need type information are skipped and listed on stderr as
  "needs type information".
- No ESLint plugins other than typescript-eslint. Rules from other plugins are skipped and
  listed as "plugin not implemented".

Design record: [0006 lint](https://github.com/Webb-Ventures/typescript-rust/blob/main/docs/design/0006-lint.md).

## Command line

```text
ts-r-lint [options] [paths...]

Lints JavaScript and TypeScript files with ESLint-compatible rules and configs
(eslint.config.*, .oxlintrc.json, biome.json).

Options:
  --fix                         Apply fixes and write files
  -f, --format <name>           stylish (default), json, sarif, github
  -c, --config <path>           Use this config for every file
  --max-warnings <n>            Exit 1 when there are more than n warnings
  --quiet                       Report errors only (warn rules are not run)
  -o, --output-file <path>      Write the report to a file
  --threads <n>                 Worker threads (default: min(4, cores))
  --rule <name>                 Only run this rule (repeatable)
  --no-gitignore                Do not honour .gitignore files
  --no-warn-ignored             Do not warn about ignored files named explicitly
  --no-inline-config            Ignore eslint-disable and /* eslint */ comments
  --report-unused-disable-directives
                                Report unused eslint-disable comments as errors
  --report-unused-disable-directives-severity <sev>
                                off, warn or error
  --list-rules                  Print the built-in rules and exit
  -h, --help                    Show this help
  -v, --version                 Show the version
```

- `--quiet` doesn't run `warn` rules at all, rather than running them and hiding the result.
- `--rule` narrows a run to one or more rules, which helps when you compare against ESLint.
- Only the report goes to stdout (or to `--output-file`), so `-f json` can be piped. Notes go
  to stderr.
- ESLint doesn't read `.gitignore`. Use `--no-gitignore` when you compare the two.

Exit codes match ESLint's: 0 when there are no errors (and warnings are within
`--max-warnings`), 1 when there are errors or too many warnings, 2 for a config or usage error.

You can also run it through the launcher as `ts-r lint`.

## Config

For each file, ts-r-lint looks at or above the file's directory, in this order:

| Order | Config | How it is read |
|---|---|---|
| 1 | Nearest `eslint.config.{js,mjs,cjs,ts,mts,cts}` | Evaluated by Node (all configs in one process), then cached in the `lint-config` namespace of `~/.cache/ts-r` |
| 2 | Nearest `.oxlintrc.json` | Read directly |
| 3 | Nearest `biome.json` or `biome.jsonc` | Read directly; Biome rules with an ESLint equivalent |
| 4 | None found | ESLint `recommended`, plus typescript-eslint `recommended` for TypeScript files |

Flat configs merge per file with ESLint 10's rules. The config cache is invalidated when the
config, anything it imports locally or the nearest lockfile changes. `--config` uses one
config for every file instead, with patterns relative to the current directory.

Rules a config turns on that ts-r-lint doesn't have are listed once per config on stderr,
with the reason: needs type information, plugin not implemented, not implemented yet, or
unknown.

Environment variables:

| Variable | Effect |
|---|---|
| `TS_R_NODE` | Node binary used to evaluate `eslint.config.*` (default: `node` on `PATH`) |
| `TS_R_LINT_NODE_WRAPPER` | Command to wrap that Node process with |
| `TS_R_LINT_NO_CONFIG_CACHE=1` | Evaluate the config every run (for cold timings) |

## Next to ESLint

ts-r-lint works on its own. A common split is to run ts-r-lint on every save or commit for
the fast rules, and keep ESLint in CI for type-aware rules and plugins ts-r-lint doesn't have.
In the editor, [ts-r-lsp](https://ts-r.dev/docs/tools/ts-r-lsp/) can show the same diagnostics.

## Measured

Like-for-like, on tRPC's server and client sources (147 files): tRPC's own ESLint config with
every rule ts-r-lint doesn't implement removed, the same file for both tools
(`cargo xtask bench --repos trpc lint-trpc-same-rules`, median of 5 runs):

| Run | ts-r-lint | ESLint 10.10.0 |
|---|---|---|
| Same 48 rules, no type information, config evaluated by Node every run | 733 ms | 1,015 ms |
| Same, with ts-r-lint's config cache (its default) | 15.5 ms | 1,015 ms |

Both report 0 problems from those rules. Most of ts-r-lint's uncached time is Node evaluating
`eslint.config.js`. With the 32 type-aware rules as well the runs aren't comparable yet: on tRPC
the parity gate turns them off, and with the gate off ts-r-lint reports 11 problems ESLint
doesn't.

Apple M-series Mac (a shared laptop), 4 threads, each project's own lint config, on public
projects pinned in `bench/repos.toml` (`cargo xtask bench --repos`, median of 3 to 5 runs):

| Project | ts-r-lint | ts-r-lint, no type check | Project's linter |
|---|---|---|---|
| tRPC server and client sources | 186 ms | 20 ms | ESLint (type-aware): 2,566 ms |
| TypeORM `packages/typeorm/src` | 810 ms | 194 ms | ESLint (type-aware): 6,718 ms |
| Playwright core sources | 725 ms | 59 ms | ESLint: 5,441 ms |
| date-fns `pkgs/core/src` | 76 ms | | oxlint: 348 ms |

These aren't like-for-like yet. ESLint runs plugin rules ts-r-lint doesn't have (on TypeORM,
most of ESLint's problems come from `jsdoc/*` rules), and when ts-r-check's diagnostics differ
from tsc's on a project, ts-r-lint runs its rules without type information (the parity gate),
so the first column measures a type check plus the untyped rules. The type-aware rules
themselves match ESLint's test suites (see Correctness).

Method and commits: [same rules](https://ts-r.dev/benchmarks/#lint-same-rules),
[lint](https://ts-r.dev/benchmarks/#lint) and
[correctness](https://ts-r.dev/benchmarks/#correctness).

Source: https://ts-r.dev/docs/tools/ts-r-lint/
