`bun check` - a TypeScript type checker builtin to Bun by Jarred-Sumner · Pull Request #44361 · oven-sh/bun

· GitHub

59 min read Original article ↗
### What does this PR do?

Adds two commands:

- **`bun lint`**: a linter that reads ESLint and oxlint configs. 1,021
rules built in, type-aware rules included.
- **`bun format`**: a formatter that reads Prettier and oxfmt configs
and prints what they print.

Both are native, use every core, and need no packages and no config
file. Both can be configured in `bunfig.toml`.

## Compatibility

### The tools' own test suites

| Tool | Suite | Pass | Fail | Not run |
| --- | --- | ---: | ---: | ---: |
| ESLint 10.12, typescript-eslint 8.71, eslint-plugin-react 7.37.5,
-import 2.32 | their own rule tests, 505 rules | 69,640 | **0** | 303 |
| the same rules | more cases, judged by the real ESLint, 632 rules |
76,552 | 4 | 258 |
| oxlint 1.87 | its own rule tests, 504 rules | 27,702 | **0** | 63 |
| Prettier 3.9.9 | `tests/format` | 9,728 | 5 | 88 |
| oxfmt 0.72 | fixtures | 1,337 | 161 | 0 |

A lint case passes when message, location, fix and suggestions are what
the real tool reports. A format case passes when the bytes are the same.
"Total" includes the cases that are not run (15, 6, 96, 316, 101, 88,
0); failing: 0, 0, 0, 0, 0, 5, 161. Of eslint-plugin-import's 316, the
plugin itself throws under ESLint 10 in 135, 90 need babel-eslint and 71
the webpack resolver. The suites are checked in under
`test/cli/lint/conformance` and `test/cli/format/conformance` and run in
CI.

<details>
<summary>Prettier by language</summary>

| Language | Pass | Total |
| --- | ---: | ---: |
| JavaScript | 1,357 | 1,357 |
| JSX | 186 | 186 |
| TypeScript | 756 | 756 |
| Flow | 1,500 | 1,505 |
| JSON | 785 | 785 |
| CSS | 162 | 162 |
| Less | 238 | 238 |
| SCSS | 136 | 136 |
| GraphQL | 104 | 104 |
| YAML | 1,623 | 1,623 |
| Markdown | 1,572 | 1,572 |
| MDX | 32 | 32 |
| HTML | 272 | 272 |
| Vue | 189 | 189 |
| Angular | 109 | 109 |
| Handlebars | 128 | 128 |
| MJML, LWC, `misc` | 579 | 579 |
| Not run (stage-2 proposals that only Babel parses, Prettier plugins) |
0 | 88 |

</details>

### Real repositories

330 open-source repositories at a pinned commit, 135 of them with their
dependencies installed, in a sandbox. Each project's own lint and format
commands are run by the project's own tool and by `bun`. Counted by run.
Every result is of the release build of 6208ecd9 or newer; none is
carried over from an older build. Of its 975 results 12 moved against
the pass on 8c1bd4bc, about 400 performance steps earlier: 8 because
ESLint's 74 deprecated formatting rules have left `bun lint` (5 ESLint
runs that were the same now lack the reports of `quotes`, `max-len`,
`key-spacing` .., and nothing else), 2 because the `jsdoc` plugin has, 1
towards oxlint (napi-rs: which files are ignored), and 1 is a project's
own oxfmt that crashes now and then. The commits since then change what
becomes of oxlint's `jsdoc` and `vue` plugins, which are not built in,
and how `import/*` resolves beside an oxlint config. So 644 results ran
again on 7df1455c (one oxlint run, ComfyUI_frontend's, on ade5c82a),
among them all 35 oxlint runs that name one of the two plugins: 27 only
list it: a note, and oxlint's exit code; 8 turn rules of theirs on by
name: exit code 2, as for any rule that is not built in. oxlint 1.87
reports nothing of these rules in any of the 35.

| Compared with | What has to match | Same | Of | Rate |
| --- | --- | ---: | ---: | ---: |
| ESLint | exit code | 96 | 102 | 94 % |
| ESLint | `--fix`, bytes | 93 | 102 | 91 % |
| ESLint | every message, against the project's own ESLint and plugins |
73 | 102 | 72 % |
| ESLint | every message, against that or the releases that the built-in
rules follow | 80 | 102 | 78 % |
| oxlint 1.87 + tsgolint | exit code | 203 | 215 | 94 % |
| oxlint 1.87 + tsgolint | `--fix`, bytes | 208 | 215 | 97 % |
| oxlint 1.87 + tsgolint | every message | 191 | 215 | 89 % |
| oxfmt 0.72 | bytes | 227 | 240 | 95 % |
| Prettier | bytes, against theirs or 3.9.9 | 57 | 74 | 77 % |
| Prettier | exit code of `--check` | 66 | 74 | 89 % |

"Every message" compares lists, not sets: rule, severity, both ends,
text, fix objects, suggestions, suppressed messages, the files that were
linted, and the exit code.

| Beyond sameness | | Of |
| --- | ---: | ---: |
| Runs of `bun` that ended by themselves with exit code 0, 1 or 2: none
killed, hung or crashed | 951 | 951 |
| A second run in the same copy says the same | 631 | 631 |
| The project's lint command at a terminal (a pty, no `-f`): the exit
code of `-f json`, no crash | 150 | 150 |
| Projects whose config `bun lint` cannot load | 0 | |

**The 6 ESLint exit codes that differ.** Open: ComfyUI_frontend (12
`import-x/no-unresolved`: the plugin's resolver works there only with
the `NODE_PATH` that pnpm's shim exports), discord.js
`packages/discord.js` (4 `unicorn/consistent-function-scoping`). A
version that the project has and `bun` does not follow: TanStack
router-core (ESLint 9.22's `no-var`), eslint/eslint (eslint-plugin-n's
table), shadcn-ui/ui (react-hooks 7.0.1), sinon (ESLint 10.3's
`new-cap`).

Of the 88 runs that are not the same, 27 are open, 30 are a version's
drift and 31 are known differences. Some that are the same in every
message, fix and exit code: nodejs/node's whole lint command (1,383
messages in 7,924 files), eslint-plugin-unicorn (628), react-hook-form
(603), rollup (290), react-router (246).

<details>
<summary>Every run that is not the same, with its class</summary>

What "the same" means:

- **lint**: as said above; for oxlint also the length of the span.
- **`--fix`** and **format**: the bytes of every file afterwards, and
which files were read.
- **judges**: the project's own installed version first. `bun lint` and
`bun format` follow one release of each tool (ESLint 10.12.0,
typescript-eslint 8.71.1, eslint-plugin-react-hooks 7.1.1, oxlint 1.87.0
with oxlint-tsgolint 7.0.2003, oxfmt 0.72.0, Prettier 3.9.9), so each
run is also judged by that release with the project's configuration; for
oxlint and oxfmt that release is the judge.
- A tool of the project that cannot run here (it needs a build, or a
package that is not published) takes its run out of the count.

Each column "judged on" names the commit of this branch whose release
build gave the verdict. Nothing is older than 6208ecd93. The runs at a
terminal are of 6208ecd93.

| By run | the same | of | | judged on 6208ecd93 | judged on 7df1455c4 |
judged on ade5c82ae |
|---|---|---|---|---|---|---|
| ESLint: lint, identical to the project's own ESLint and plugins | 73 |
102 | 72 % | 97 | 5 | 0 |
| ESLint: lint, identical to that, or to the releases that bun follows
(ESLint 10.12; typescript-eslint 8.71.1, react-hooks 7.1.1) | 80 | 102 |
78 % | 97 | 5 | 0 |
| ESLint: --fix, the same bytes | 93 | 102 | 91 % | 97 | 5 | 0 |
| ESLint: the same exit code | 96 | 102 | 94 % | 97 | 5 | 0 |
| oxlint: lint, identical to 1.87 (rule, severity, place, length, text,
files) | 191 | 215 | 89 % | 3 | 211 | 1 |
| oxlint: --fix, the same bytes as 1.87 | 208 | 215 | 97 % | 3 | 211 | 1
|
| oxlint: the same exit code as 1.87 | 203 | 215 | 94 % | 3 | 211 | 1 |
| oxfmt: the same bytes as 0.72 | 227 | 240 | 95 % | 41 | 199 | 0 |
| oxfmt: the same exit code of --check | 229 | 240 | 95 % | 41 | 199 | 0
|
| Prettier: identical to theirs or to 3.9.9 | 57 | 74 | 77 % | 74 | 0 |
0 |
| Prettier: the same exit code of --check | 66 | 74 | 89 % | 74 | 0 | 0
|

Not the same: 88 runs: open 27, drift of a version 30, known differences
31. A run is listed once, under the first row of the table above that it
fails.

#### Open (27): a difference that is bun's to close

| Run | Judge | Exit theirs / bun | What |
|---|---|---|---|
| Comfy-Org/ComfyUI_frontend | ESLint 10.10.0 | 0 / 1 |
import-x/no-unresolved 12 only bun: the plugin's resolver works only
with the NODE_PATH that pnpm's shim exports; their exit code flaps too
(better-tailwindcss) |
| NarHakobyan/awesome-nest-boilerplate | ESLint 9.38.0 | 1 / 1 | spec
files: `vi.fn` / `jest.fn` is "a type that cannot be resolved" for bun:
no-unsafe-* 16 only bun; sonarjs' typed rules 11 only theirs (a plugin
in JavaScript gets no TypeScript program); deprecated formatting rules
(`quote-props` 61 ..) 65 only theirs |
| discordjs/discord.js:packages/discord.js | ESLint 9.39.5 | 0 / 1 |
unicorn/consistent-function-scoping 4 only bun (errors); no-eval's text
is ESLint 9's |
| graphql/graphql-js | ESLint 9.39.3 | 0 / 0 | 1 suppressed message:
import/unambiguous is placed on the whole file by bun (1:1), from the
first token by them (4:1) |
| mermaid-js/mermaid | ESLint 9.35.0 | 1 / 1 | typed rules in a .js file
(svgDraw.js): no-floating-promises 9, restrict-plus-operands 1 only bun,
with the plugin version bun follows |
| pixijs/pixijs | ESLint 9.39.2 | 0 / 0 | 18 fix objects of
jsdoc/require-param are missing in -f json; --fix writes the same bytes.
Deprecated formatting rules (`max-len` 147 ..) 149 only theirs; all else
the same (380) |
| withastro/astro | ESLint 10.12.0 | 1 / 1 | eslint-plugin-regexp: 3
suppressed reports only theirs, so 3 "unused directive" warnings only
bun; 1 file "not found by the project service" only theirs |
| Pawn-Appetit/pawn-appetit | oxlint 1.87 | 0 / 0 | React Compiler rule:
preserve-manual-memoization 1 only bun |
| arcjet/arcjet-js | oxlint 1.87 | 1 / 1 | "Unused oxlint-disable
directive" 58 only theirs |
| digirati-co-uk/iiif-manifest-editor | oxlint 1.87 | 1 / 1 |
react-hooks(exhaustive-deps) 2: another text |
| glide-browser/glide | oxlint 1.87 | 1 / 1 | "A 'declare' modifier
cannot be used in an already ambient context": 2 diagnostics against
their 16, and without the code TS(1038) |
| karakeep-app/karakeep:apps/web | oxlint 1.87 | 0 / 0 | React Compiler
rule: static-components 1 only theirs |
| monkeytypegame/monkeytype:frontend | oxlint 1.87 | 1 / 1 |
typescript(TS2307) 1 only theirs |
| openclaw/openclaw | oxlint 1.87 | 1 / 1 |
eslint(no-useless-assignment) 5 only bun |
| petyosi/react-virtuoso:packages/masonry | oxlint 1.87 | 1 / 1 | the
checker's own diagnostics (TS7031, TS7006, TS2307) 69 only theirs |
| petyosi/react-virtuoso:apps/virtuoso.dev | oxlint 1.87 | 1 / 1 | the
checker's own diagnostics (TS2307, TS7031, TS7006) 21 only theirs |
| reserve-protocol/register | oxlint 1.87 | 0 / 0 | React Compiler rule:
preserve-manual-memoization 1 only theirs |
| ietf-tools/purple | oxfmt 0.72 | 0 / 1 | 4 files only bun would
change: 1 .json, 2 .html, 1 .yaml |
| olton/metroui | oxfmt 0.72 | 2 / 2 | 1 .css, 1 .md differ; 4 .less
that 0.72 refuses; both exit 2 |
| zendeskgarden/react-components | oxfmt 0.72 | 1 / 1 | 1 .ts file only
bun would change |
| chakra-ui/chakra-ui | Prettier 3.8.3 | 1 / 1 | 8 .md only bun would
change (code blocks: their import-sorting plugin; Markdown) |
| renovatebot/renovate | Prettier 3.9.9 | 0 / 1 | 65 .md files that only
bun would change, against the same release 3.9.9 |
| tailwindlabs/tailwindcss | Prettier 3.9.6 | 0 / 1 | embedded languages
in templates (html``, css``): 32 .ts files only bun would change |
| timlrx/tailwind-nextjs-starter-blog | Prettier 3.8.1 | 1 / 1 | 1 .mdx
only theirs, 1 .cjs only bun; prettier-plugin-tailwindcss: 1 .css, 1
.tsx |
| vercel/next.js | Prettier 3.6.2 | 2 / 2 | the same bytes in 26,334
files; one file with a Babel proposal
(examples/with-custom-babel-config/pages/index.js) that their 3.6.2
formats and bun refuses; 3.9.9 exits 2 too, on another file |
| vuejs/eslint-plugin-vue | Prettier 3.9.10 | 1 / 1 | 1 .md differs |
| withastro/astro | Prettier 3.9.9 | 0 / 1 | 1 .md only bun would change
|

#### Drift of a version (30): the project has an older release than the
one bun follows, and that release explains the difference

| Run | Judge | Exit theirs / bun | What |
|---|---|---|---|
| TanStack/router:packages/router-core | ESLint 9.22.0 | 0 / 1 | two
drifts at once: ESLint 9.22's no-var reports `declare global { var }`,
10.12's does not (their disable comment is unused for bun); against
10.12 their older typescript-eslint is left
(no-unnecessary-type-assertion 2) |
| chartjs/Chart.js #1 | ESLint 8.37.0 | 0 / 0 | ESLint 8.37: places of
complexity, no-unused-vars |
| chartjs/Chart.js #2 | ESLint 8.37.0 | 0 / 0 | ESLint 8.37 /
typescript-eslint 5 |
| eslint/eslint | ESLint 10.12.0 | 0 / 1 | eslint-plugin-n's table of
Node.js versions: bun follows the newest release of the plugin |
| mastodon/mastodon | ESLint 9.39.5 | 0 / 0 | ESLint 9 looks the
configuration up from the cwd, 10 from each file: streaming/ (9 files)
is linted by 10 and by bun: 1 suppressed message |
| pinojs/pino | ESLint 9.39.5 | 0 / 0 | ESLint 9: the text of no-eval |
| sadmann7/skateshop | ESLint 8.57.0 | 1 / 1 | .eslintrc with
typescript-eslint 6 |
| webpack/webpack | ESLint 9.39.5 | 1 / 1 | ESLint 9: the text of
no-eval (2 messages); everything else of 5,521 files the same |
| TanStack/router:packages/react-router --fix | ESLint 9.22.0 | | lint
is identical with the plugin versions bun follows; the fix is that
version's |
| apollographql/apollo-client #1 --fix | ESLint 9.31.0 | | lint is
identical with the plugin versions bun follows; the fix is that
version's |
| sveltejs/svelte --fix | ESLint 10.0.0 | | ESLint 10.0:
no-useless-assignment; lint is identical to 10.12 |
| shadcn-ui/ui:apps/v4 exit code | ESLint 9.26.0 | 0 / 1 |
eslint-plugin-react-hooks 7.0.1 against 7.1.1: set-state-in-effect 38,
refs 16; identical with 7.1.1 in the place of theirs |
| sinonjs/sinon exit code | ESLint 10.3.0 | 0 / 1 | ESLint 10.3: new-cap
1; identical to 10.12 |
| diced/zipline, sanity-io/next-sanity | oxlint 1.78.0 | 0 / 2 | 1.87
refuses their configuration ("Rule 'react-compiler' not found in plugin
'react'"); bun lints and says that this rule did not run: exit 2 unless
--allow-unsupported is given |
| ixartz/Next-js-Boilerplate | oxlint 1.78.0 | 1 / 2 | as zipline, and
20 `jsdoc` rules |
| makeplane/plane:apps/web, :apps/admin, :apps/space, :apps/live,
:packages/editor, :packages/utils | oxlint 1.51.0 | 0 / 0 | 1.87 refuses
their configuration ("Rule 'prevent-abbreviations' not found in plugin
'unicorn'", 'prop-types' in 'react'); against their 1.51: other names of
plugins, other rules in the categories; the same exit code |
| olton/metroui | oxlint 1.42.0 | 0 / 0 | 1.87 refuses their
configuration ("Rule 'no-unused-vars' not found in plugin 'import'");
the same exit code as their 1.42 |
| preactjs/preact | oxlint 1.8.0 | 0 / 0 | 1.87 refuses their
configuration (four rules it does not have: camelcase, jsx-no-bind ..);
the same exit code as their 1.8 |
| vercel/ai | oxlint 1.56.0 | 0 / 1 | 1.87 refuses their configuration
("Rule 'no-unresolved' not found in plugin 'import'"); against their
1.56 the categories hold other rules |
| apollographql/apollo-client | Prettier 3.1.1 | 2 / 1 | Prettier 3.1.1:
12 files only bun would change; 3.9.9 fails with their plugin (exit 2);
6 .mdx only theirs; bun reads 438 of their 724 files (.mdx; files behind
their overrides): that part is open |
| dubinc/dub | Prettier 3.2.5 | 1 / 1 | Prettier 3.2.5 with plugins of
that age: 3.9.9 prints broken code with them, so neither judge is clean;
against 3.9.9: 4 differ, 10 only theirs |
| nextauthjs/next-auth | Prettier 3.3.3 | 2 / 1 | Prettier 3.3.3: 20
files only bun (and 3.9) would change; 3.9.9 fails with their plugin on
one file (exit 2); no byte differs from 3.9.9 elsewhere |
| shadcn-ui/taxonomy, steven-tey/precedent | Prettier 2.8.8 | 1 / 1 |
Prettier 2.8.8 with plugins of that age |

#### Known differences (31): the other tool fails here, or bun does
something else on purpose

| Run | Judge | Exit theirs / bun | What |
|---|---|---|---|
| angular/angular-cli | ESLint 10.12.0 | 0 / 0 | as jest: `max-len` 82,
`padding-line-between-statements` 1; all else the same (413) |
| jestjs/jest | ESLint 10.10.0 | 0 / 0 | ESLint's deprecated formatting
rules do not run in bun: `quotes` 2 only theirs; all else the same (653)
|
| jquery/jquery | ESLint 9.39.4 | 0 / 0 | as jest: `key-spacing` 9; all
else the same (19) |
| microsoft/playwright | ESLint 9.39.4 | 0 / 0 | as jest: `quotes` 276,
`key-spacing` 158, `comma-spacing` 117, `object-curly-spacing` 112; all
else the same (436) |
| nextauthjs/next-auth | ESLint 9.9.1 | 1 / 1 | 3 files "not found by
the project service" are fatal for them, linted by bun; in 2 .svelte
files svelte-eslint-parser throws in both: the TypeError's text is the
engine's |
| nuxt/nuxt | ESLint 10.12.0 | 1 / 1 | 37 files "not found by the
project service" are fatal for them, linted by bun; no-deprecated 3 / 1
|
| vueuse/vueuse | ESLint 10.9.1 | 0 / 0 | as jest: `spaced-comment` 2;
all else the same (68) |
| Sandakan/Nora | oxlint 1.87 | 0 / 0 | import(no-named-as-default) 1
only bun. The `import` plugin is named only inside an override; there
1.87 does not look at other modules, so its rules that ask another
module are silent; bun builds the module graph whenever such a rule is
on. The report is true of the code |
| s00d/nuxt-i18n-micro | oxlint 1.87 | 0 / 1 | not installed here:
tsconfig.json extends a file that `nuxt prepare` writes; 1.87 then
resolves no import, bun goes on without it: import(no-duplicates) 1 only
bun |
| Comfy-Org/ComfyUI_frontend exit code | oxlint 1.87 | 1 / 2 | as vitest
|
| eikowagenknecht/lootscraper exit code, jumpserver/luna #1,
jumpserver/luna #2, lihbr/lihbr-apex, nuxt-modules/prismic,
vitest-dev/vitest | oxlint 1.87 | 0 / 2 | oxlint's `jsdoc` and `vue`
plugins are not built in: rules of theirs that are on by name did not
run, and bun says so: exit 2 unless --allow-unsupported is given; every
message the same |
| chhoumann/PodNotes | oxfmt 0.72 | 2 / 0 | not installed here: 0.72
needs svelte/compiler and exits 2; bun leaves the 26 .svelte files as
they are, and says so |
| digirati-co-uk/iiif-manifest-editor | oxfmt 0.72 | 2 / 1 | 0.72 FAILS
on one file ("Invalid document: Expected end tag of kind LineSuffix"),
exit 2; bun formats it; all other bytes the same |
| generalaction/emdash:apps/emdash-desktop, mengxi-ream/read-frog |
oxfmt 0.72 | 2 / 1 | not installed here: sortTailwindcss: 0.72 exits 2
("Can't resolve 'tw-animate-css'"), bun warns and goes on; the bytes are
the same |
| heyhuynhgiabuu/proxypal | oxfmt 0.72 | 2 / 1 | not installed here:
0.72 cannot load their Tailwind 3 configuration and exits 2; bun warns;
the bytes are the same |
| ontola/atomic-server:browser/data-browser | oxfmt 0.72 | 2 / 1 | as
iiif-manifest-editor (2 files) |
| radix-ui/primitives | oxfmt 0.72 | 2 / 1 | 0.72's CSS parser refuses
one .module.css (exit 2); bun formats it; all other bytes the same |
| reshaped-ui/reshaped | oxfmt 0.72 | 2 / 1 | 0.72's CSS parser refuses
`@each` in 7 .module.css files (exit 2); bun formats them |
| salsita/node-pg-migrate | oxfmt 0.72 | 2 / 1 | 0.72 refuses `export`
in a .cjs file (exit 2); bun formats it |
| storybookjs/storybook | oxfmt 0.72 | 2 / 0 | 0.72 refuses `import` in
a .cjs file (exit 2); bun formats it |
| PrismJS/prism | Prettier 3.5.3 | 1 / 1 | three Prettier plugins that
lay JavaScript out otherwise (brace-style, space-before-function-paren,
merge): bun runs no Prettier plugin: 244 .js; 12 .md |
| alii/website | Prettier 3.9.6 | 1 / 0 | prettier-plugin-tailwindcss:
bun runs no Prettier plugin: 2 files only theirs would change |
| mickasmt/next-saas-stripe-starter | Prettier 3.9.6 | 1 / 1 | as
alii/website: 1 .tsx differs |
| shadcn-ui/ui:apps/v4 | Prettier 3.6.2 | 2 / 1 | their Prettier exits 2
here (prettier-plugin-tailwindcss needs a build); 19 files only bun
would change |
| trpc/trpc | Prettier 3.9.6 | 2 / 0 | their Prettier exits 2 here
("Unable to load your Tailwind CSS v3 config": www is not built); no
byte differs |

#### By repository

| | target | 9 October, 19:38 UTC (d9fee8c85) | now |
|---|---|---|---|
| oxlint + oxfmt in one repository: the same exit codes in both | 85 % |
132 of 143 (92 %) | 128 of 143 (90 %) |
| oxfmt: repositories with the same bytes as 0.72 | 95 % | 203 of 217
(94 %) | 204 of 217 (94 %) |
| oxlint: repositories identical to 1.87: rule, severity, place, length,
text, files, exit code, --fix | 75 % | 139 of 161 (86 %) | 143 of 161
(89 %) |
| of those: every label (span, text) the same too | | 103 of 139 (74 %)
| 142 of 144 (99 %) |
| of those: the help the same too | | 89 of 139 (64 %) | 143 of 144 (99
%) |
| Prettier: repositories identical | 80 % | 56 of 71 (79 %) | 54 of 71
(76 %) |
| ESLint: runs with the same exit code | 85 % | 89 of 102 (87 %) | 96 of
102 (94 %) |
| ESLint: repositories identical (with --fix) | 60 % | 60 of 86 (70 %) |
61 of 86 (71 %) |
| ESLint: repositories identical to the project's own version or to
10.12.0 with its configuration and plugins | | 62 of 86 (72 %) | 63 of
86 (73 %) |

#### What these numbers cannot say

- These projects are clean: their own CI keeps them so. Most runs
compare silence with silence. All 33 ESLint runs with the TypeScript
import resolver together hold 85 messages of `import/*` (counted on
0c0224b00); all 24 runs with eslint-plugin-react hold 42 messages of
`react/*` (counted on 611d8cbad). The rules' own test suites and the
differential fuzzers speak for the rest.
- No wall time in this table: the runs stood side by side on a crowded
machine.
- The terminal's own format is compared by exit code, signal and "has it
printed a summary", not by text.

</details>

## Usage

### `bun lint`

```sh
bun lint                      # eslint:recommended + typescript-eslint/recommended if there is no config
bun lint --fix src test
bun lint --type-aware         # rules that need types, on bun check's checker: no tsc, no tsgolint
bun lint --type-aware --type-check   # type errors too, in the same pass
bun lint -f json -o report.json .
bun lint --rules              # what is built in
```

- **Config**: `eslint.config.{js,mjs,cjs,ts}`, `.eslintrc.*`,
`.oxlintrc.json`, `oxlint.config.ts`. It takes the flags and output
formats of `eslint` and of `oxlint`, and behaves like the tool whose
config it finds.
- **Rules built in** (62 need types):

  | Plugin | Rules | Built in under an ESLint config |
  | --- | ---: | ---: |
  | ESLint core | 218 | 218 |
  | unicorn | 138 | |
  | typescript-eslint | 137 | 136 |
  | react | 106 | 102 |
  | vitest | 73 | |
  | `bun:test/` (new) | 65 | 65 |
  | jest | 60 | |
  | import | 46 | 46 |
  | jsx-a11y | 36 | |
  | `bun/` (new) | 29 | 29 |
  | oxc | 27 | 1 |
  | React Compiler | 26 | 26 |
  | nextjs | 21 | |
  | promise | 16 | |
  | n | 15 | 4 |
  | react-perf | 4 | |
  | react-hooks | 3 | 3 |
  | prettier | 1 | 1 |
  | **All** | **1,021** | **631** |

- **`bun:test/*`**: the rules of eslint-plugin-jest and -vitest that
make sense for `bun test`, one implementation for all three, chosen by
what the file imports. They look at files that import from `"bun:test"`
and know Bun's own API (orders of arguments, modifiers, matchers).
Without a config six are on: `no-focused-tests`, `valid-expect`,
`valid-expect-in-promise`, `valid-describe-callback`, `no-export` as
errors, `no-disabled-tests` as a warning. A fix that can break a passing
test is a suggestion.
- **Left out on purpose**: the 75 rules about layout that ESLint and
eslint-plugin-react have deprecated in favour of a formatter (`indent`,
`quotes`, `semi` ..), and oxlint's `jsdoc` and `vue` plugins. See "Known
differences".

- **Which of them answer depends on the config.** With an oxlint config,
all of them. With an ESLint config, those of the last column, each exact
by the original's own test suite: ESLint core, typescript-eslint, all
102 rules of eslint-plugin-react that are not about layout, all 46 of
eslint-plugin-import, react-hooks with the React Compiler's rules,
`prettier/prettier`, 4 rules of eslint-plugin-n and `bun/`. The other
plugins are ports of oxlint's rules, which differ from the original
plugins, so there the project's own plugin runs. A prefix means the
package only if the plugin object comes from that package:
`eslint-plugin-import-lite` under `import`, or `@eslint-react` under
`react`, answer for themselves.
- **Opting out**: `--no-native-plugin-rules` hands every plugin back to
its package; `--native-plugin-rules=import,react` names those that stay
built in.
- **Globals from the project's types**, behind a flag: `--infer-globals`
takes the globals of a file from the tsconfig or jsconfig that includes
it (`lib`, `types`, every `declare global`). `--infer-globals=fast`
reads only the compiler options and loads the types of a program only
when `no-undef` is about to report a name. Both are off by default.
- **ESLint plugins run as they are**, in JavaScript, on several VMs at
once: `context`, `sourceCode`, tokens, scopes, code paths, selectors,
fixes, processors.

### `bun format`

```sh
bun format                    # writes, prints what changed
bun format --check
bun format --stdin-filepath a.ts < a.ts
```

- **Languages**: JS, JSX, TS, Flow, JSON, CSS, SCSS, Less, GraphQL,
YAML, Markdown, MDX, HTML, Vue, Angular, Handlebars, and what is
embedded in them. A language that only a plugin of Prettier reads
(Svelte, Astro, PHP ..) goes to the project's own Prettier, if it and
the plugin are installed. With `svelte` in an oxfmt config, `.svelte`
files are left as they are, exit 2.
- **Config**: `.prettierrc*`, `prettier.config.*`, `"prettier"` in
package.json, `overrides`, `.editorconfig`, `.prettierignore`;
`.oxfmtrc.json`, `oxfmt.config.ts`. Flags of `prettier` and of `oxfmt`.
- **Built in**: import sorting (`@trivago/prettier-plugin-sort-imports`,
`@ianvs/prettier-plugin-sort-imports`,
`prettier-plugin-organize-imports`, oxfmt's `sortImports`), oxfmt's
`sortTailwindcss`, `sortPackageJson`, JSDoc. Other plugins of Prettier
are not: files of a project that names one
(`prettier-plugin-tailwindcss` ..) are left as they are, exit 2, or
formatted without it under `--allow-unsupported`.
- **It checks its output before it writes**: the result is parsed again
and compared with the input's syntax tree, comments and literals. A file
whose meaning would change is left alone and reported.

### `bunfig.toml`

```toml
[lint]
typeAware = true
maxWarnings = 0
ignorePatterns = ["vendor/**"]
nativePluginRules = ["import"]   # true (default), false, or the plugins whose built-in rules may answer

[lint.rules]
eqeqeq = ["error", "smart"]
"no-console" = "warn"

[format]
semi = false
singleQuote = true
```

- A key is the flag of the same name in camelCase, and `[format]` takes
Prettier's option names. `[lint.rules]`, `[lint.globals]`,
`[lint.parserOptions]` and `[lint.categories]` are tables.
- **Order**: command line, then `bunfig.toml`, then the tools' config
files, then the defaults. `configPrecedence = "prefer-file"` makes
`[format]` a fallback.
- An unknown key, a wrong type or a bad value is an error with file,
line and column, "Did you mean", and exit code 2. Flags that say what
one run does (`--fix`, `--init`, `--stdin`) are refused as keys.

### If the project already uses the name

A `lint` or `format` script in package.json, `./lint.ts`,
`./lint/index.*` (what can be run, so not `lint.json`),
`node_modules/.bin/lint` or an executable `lint` beside package.json
keep what `bun lint` meant before this PR. The same goes for `format`.

## Benchmarks

`microsoft/vscode` `src/` (10,149 files, 143 MB), Linux x64, 8 pinned
cores.

### Against oxlint 1.87 and oxfmt 0.72

15 alternating rounds on a shared machine, a release build of the commit
at which the tree was closed; the rows of `format` are of 8c1bd4bc. Both
linters report the same to within 0.03 % (see "How did you verify").

**Wall time, best round**

| Workload | `bun` | oxlint / oxfmt | Speedup |
| --- | ---: | ---: | ---: |
| lint, `correctness` | 0.21 s | 0.43 s | 2.0x |
| lint, recommended | 0.25 s | 0.51 s | 2.0x |
| lint, all 709 rules that both have | 0.73 s | 1.82 s | 2.5x |
| format `--check` | 0.42 s | 0.79 s | 1.9x |

**CPU time (user), median**

| Workload | `bun` | oxlint / oxfmt | Speedup |
| --- | ---: | ---: | ---: |
| lint, `correctness` | 1.52 s | 2.65 s | 1.7x |
| lint, recommended | 1.70 s | 2.96 s | 1.7x |
| lint, all 709 rules that both have | 4.86 s | 13.59 s | 2.8x |
| format `--check` | 3.02 s | 5.79 s | 1.9x |

**Instructions (user), least of 15**

| Workload | `bun` | oxlint / oxfmt | Fewer |
| --- | ---: | ---: | ---: |
| lint, `correctness` | 7.7 G | 15.8 G | 2.1x |
| lint, recommended | 8.4 G | 17.7 G | 2.1x |
| lint, all 709 rules that both have | 19.4 G | 66.7 G | 3.4x |
| format `--check` | 19.7 G | 39.9 G | 2.0x |

**Peak memory, median**

| Workload | `bun` | oxlint / oxfmt | Less memory |
| --- | ---: | ---: | ---: |
| lint, `correctness` | 119 MB | 271 MB | 2.3x |
| lint, recommended | 124 MB | 371 MB | 3.0x |
| lint, all 709 rules that both have | 517 MB | 1264 MB | 2.4x |
| format `--check` | 181 MB | 557 MB | 3.1x |

By instructions on six other repositories (next.js, node, twenty,
PostHog, ant-design, workers-sdk), with the default rules and with two
React configs: 1.6x to 2.9x fewer than oxlint. All 742 shared rules with
`import/*`, on vscode: 56 G against 303 G, 5.4x.

### Against ESLint and Prettier

Measured on earlier builds; being measured again.

**vscode's own ESLint config (8 plugins, 13,069 files), wall time.**
9,823 messages identical, fixes included; 3 differ.

| `bun lint` | ESLint 10 | Speedup |
| ---: | ---: | ---: |
| 15.8 s | 404 s | 26x |

**`prettier/prettier` as a lint rule, 8,042 real files, wall time.**
8,038 files get the same reports.

| `bun lint` (native rule) | ESLint + eslint-plugin-prettier + Prettier
| Speedup |
| ---: | ---: | ---: |
| 2.9 s | 67 s | 23x |

**Formatters, `--check` on vscode, CPU time**

| `bun format` | oxfmt 0.72 | Biome 2.5 | Prettier 3.9.9 |
| ---: | ---: | ---: | ---: |
| 2.9 s | 5.5 s | 20 s | 295 s |

With heavy third-party JavaScript plugins the gain over ESLint is
smaller: median 1.7x in wall time over 74 real runs of 5 s or more.

### Binary size

| Target | `main` | This PR | Added |
| --- | ---: | ---: | ---: |
| linux-x64 | 85.18 MB | 100.09 MB | +14.91 MB |
| linux-aarch64 | 84.68 MB | 99.80 MB | +15.13 MB |
| darwin-aarch64 | 67.85 MB | 82.50 MB | +14.66 MB |
| darwin-x64 | 74.81 MB | 90.49 MB | +15.68 MB |
| windows-x64 | 92.27 MB | 107.52 MB | +15.25 MB |

## Native eslint-plugin-react on real repositories

27 runs in 24 repositories that have the plugin in their resolved config
(versions 7.32.2 to 7.37.5), one build, three ways: built in, the
package in Bun's plugin host (`--native-plugin-rules` without `react`),
and the repository's own ESLint.

| Check, over the 24 runs whose config loaded on that build | Result |
| --- | --- |
| Built in against the package: messages, order, fix objects,
suggestions | the same in 24 |
| `--fix`, bytes | the same in 24 |
| `react/` messages, suppressed ones included | 42, the same in all
three |
| Runs that need no JavaScript engine any more | 2 |
| Runs that load fewer modules | 15 of the first 20 (react-hook-form:
750 to 24) |

These repositories are clean, so this shows silence where ESLint is
silent more than it shows the same reports.

| Repository | Built in | The package | Their ESLint |
| --- | ---: | ---: | ---: |
| shadcn-ui/ui | 2.1 s | 5.8 s | 90.4 s |
| chakra-ui | 0.59 s | 1.43 s | 22.6 s |
| mastodon | 6.6 s | 7.4 s | 100.0 s |
| playwright | 4.9 s | 4.5 s | 53.4 s |

## Known differences

**Configuration and command line**

1. An `eslint.config.*` and an `.oxlintrc.json` in one directory: `bun
lint` stops (exit 2) and asks for `--flavor=eslint` or
`--flavor=oxlint`.
2. A rule in `.oxlintrc.json` that `bun lint` does not have: oxlint
refuses the file. `bun lint` lints with everything else, names the rule
at the end and exits with 2; `--allow-unsupported` makes that a warning.
3. Beside an oxlint config, an option name that a rule does not know is
refused in oxlint's words; a wrong type or value that oxlint refuses is
accepted. Nothing that oxlint 1.87 accepts is refused (29,443 option
sets).
4. Under an ESLint config, files in other languages or for other parsers
(JSON, Markdown, CSS, Vue, Svelte, Astro, MDX) are linted by the
`eslint` package of the project, in JavaScript. Without it they are
named at the end and the exit code is 2.
5. The built-in rules follow ESLint 10.12, typescript-eslint 8.71,
eslint-plugin-react-hooks 7.1, -import 2.32, -n 18.4 and -react 7.37,
whichever versions are installed. A `note:` says so where the installed
one is older by a minor version or more.
6. Two rules that report at one place can come in another order than in
ESLint, which has first what was listened on the outer node. Where both
fix the same range the first wins, so `--fix` can write other bytes. The
rules of eslint-plugin-react say on which node they listen: with its 30
fixable rules at once on the 11,367 texts of its own tests, `--fix`
writes the plugin's bytes in 11,360, and in those looked at of the other
7 the plugin's text does not parse; the order of messages still differs
in 67 texts with 102 rules on. The core rules do not say it yet: 255 of
745 ties in ESLint's own sources come in another order. The 26 rules of
the React Compiler report while ESLint creates them, before it visits a
node, so at one place they come before every other rule and among
themselves in the order of the config: the same here.
7. There is no cap on what a file, a rule or a function may have: no
count of reports, nodes, steps or levels. What stands before the stack
is a check of the real stack, so how deep code, a config or a comment
can nest depends on the thread's stack and not on a number. The parser
and the binder nest as deep as the stack of a thread of Bun's pool
allows (4 MB; 18 MB on Windows); the threads of `bun lint`, `bun format`
and `bun check` have eight times that, for the checker, the rules and
the printers that walk the same tree. Beyond the stack the answer is an
error ("The code is nested too deeply.", Prettier's "Invalid string
length", `react/todo` "Support functions of this size", ESLint's
"Maximum call stack size exceeded"), where oxlint 1.87 ends with a
segmentation fault and ESLint with a `RangeError`. Hostile input costs
what it costs in the tool that is stood in for: 33 shapes in 47 rules
are the square of their size, as in ESLint, and patterns beyond
minimatch's and brace-expansion's reach are as slow as there. What is
left are bounds of the machine: `stylish` stops padding its rows where
ESLint ends with `RangeError: Invalid string length`. `--threads` above
1,024 counts as 1,024. A file of more than 2^29 - 24 bytes, which
JavaScript could not hold as a string, is not read (`EFBIG`); of what is
no regular file (`/dev/null`, a pipe) at most 64 MB are.
8. A plugin or rule in JavaScript that leaves a rejection unhandled:
ESLint stops with exit 2, `bun lint` prints the error and goes on.
9. `-f junit`, `checkstyle`, `gitlab` and `sarif` print oxlint's layout
under an ESLint config too, under the name `bun lint`. ESLint 10 ships
none of them.
10. `bun format`: `printWidth` and `tabWidth` have no upper bound and
take what Prettier takes, from every source (flags, JSON, YAML, TOML,
JSON5, programs, overrides, `.editorconfig`): of 536 cases none prints
other bytes. What is no integer is refused in Prettier's words, with its
exit code. Left: `--print-width -1` as two arguments is an unknown flag
here, a warning in Prettier; `0x10`, `1_000` and `05` in a
`.prettierrc.json` are read, where `JSON.parse` refuses the file. Beside
an oxfmt config nothing that oxfmt 0.72 takes is refused or printed
otherwise (222 cases); `tabWidth: 0` is formatted, where oxfmt panics.
The flags `--check`, `--list-different` and `--write` are refused
together where the tool that is stood in for refuses them, with its
sentence and exit code and nothing written (893 cases; `[error]` stands
where oxfmt has `Error:`).
11. What ESLint 10.12 gets wrong, `bun lint` gets wrong with it. One
case is known: `--fix` with `no-var` and `one-var` together turns `if
(x) { var a = 1; var b = 2; } return b;` into `let a = 1, b = 2;`, where
`b` is then not defined. ESLint has fixed it (eslint/eslint#21389), in
no release yet. The fix objects of all 106 fixable core rules are those
of 10.12.0 (10,330 cases), and the recorded suites compare them, so this
follows with the release that has it.
12. The same for oxlint 1.87. Of 4,624 self-checking programs
(JavaScriptCore's tests), run before and after `--fix` with one rule at
a time, 479 end otherwise after a fix of oxlint's own, and `bun lint`
writes the same bytes: `no-var` 259, `unicorn/catch-error-name` 105,
`sort-keys` 19, `no-new-wrappers` 15, `unicorn/prefer-spread` 12,
`prefer-arrow-callback` 11, 19 more rules 1 to 7 each. These tests look
at what ordinary code does not (the text of a function, the order of
keys), so this is an upper bound.
13. Beside an oxlint config, `no-var` leaves a `var` that is declared
twice, each time in a `try` block of its own; oxlint writes `const` for
both.
14. A fix that would leave a file with a syntax error is not applied,
and a warning says so. ESLint, oxlint and the plugins apply it: of the
1,908 texts of eslint-plugin-import's tests, 50 end as text that does
not parse with the plugin and are left alone, or stopped a pass earlier,
here.
16. YAML is read by Bun's own parser. Prettier's suite (7,598 checks)
and 1,219 files in four sets of options print the same bytes. Of 96,458
damaged texts, 250 are refused that Prettier formats, 150 formatted that
it refuses, 9 print other bytes, and a syntax error is worded as
`Bun.YAML.parse` words it.
18. `<!--` and `-->` comments: several comment rules at once `--fix`
other bytes than ESLint 10.12, whose `-- >` is no comment.
19. The config file that is found says whose run it is, wherever it lies
and whatever flags there are. A flag of the other tool has its plain
meaning (`-D <rule>` is `--rule`) or is refused in one line with exit 2.
20. The deprecated rules about layout are not built in. A config that
turns them on gets one `note:` on stderr that names them; the exit code
is that of the rules that ran. `bun format` is what does their work.
21. oxlint's `jsdoc` and `vue`: a rule of theirs that a config turns on
by name is named and ends the run with 2, like any rule that is not
built in. A plugin that is only listed, so that a category would turn
its rules on, is a `note:` on stderr, in every format and with
`--quiet`.
22. A search that leaves the project (`bun lint ..`, `'/**/x.js'`) runs
no `eslint.config.*` that ESLint 10.12 would not run, and ends as ESLint
does. The working directory's own config is always run. Where the
working directory has no config, one counts only in and below it, or
where a plain path names it: what a pattern merely reaches gets the
defaults, and nothing of it is loaded. The same for `bun format`.
23. Beside an oxlint config, `no-useless-assignment`,
`constructor-super` and the other rules on code paths follow ESLint's
code paths. oxlint has a control flow graph of its own, which answers
otherwise in a few corners (`a = (a = f()) ? a.b : 1`, `class A { x =
id++ }`, `try` with `finally`).
24. oxlint stops at an error of its semantic checks, such as a name
declared twice, and runs no rule on that file. `bun lint` goes on.
25. A NUL in the text of an HTML element is the end of the file for
Prettier 3.9.9 and oxfmt 0.72, which drop what follows and exit with 0.
`bun format` refuses the file and leaves it.

**eslint-plugin-react and eslint-plugin-import**

1. `settings.react.version: "detect"` asks the nearest installed react
for each file; the plugin lets the first file decide for the process.
2. Where the plugin throws and ESLint's run ends (settings of the wrong
kind, `jsx-handler-names` with `ignoreComponentNames` on `<A.B>`),
nothing is reported for that rule and the run goes on.
3. `jsx-sort-props` compares names in English order. The plugin uses the
machine's default locale, or the rule's `locale`, so with `ignoreCase`
under a locale with a collation of its own (cs, sk, hu, tr, sv, da, nb,
lt), or with such a `locale`, reports and the order that `--fix` writes
can differ.
4. A file with Flow types goes to the project's own parser, and there
the package's rules answer.
5. `import/resolver: typescript` is answered by Bun's resolver following
eslint-import-resolver-typescript 4.4.5. Its options `extensions`,
`conditionNames`, `mainFields` and `alwaysTryTypes: false` are not read;
`./a.js` where only `a.jsx` exists is found.
6. With any other resolver, another parser in `import/parsers`, or a
regular expression object in `import/ignore`, the installed package's
rules run instead.
8. `import/no-unused-modules` and `import/no-cycle` answer each file as
if it were the first one linted; the package's answers can depend on the
order of the files.

## TODO

- [ ] On Windows a top-level `preload` (and `define`, `loader` ..) of
`bunfig.toml` still applies to the process that evaluates
`eslint.config.*`, `prettier.config.*` and the like. Elsewhere that
process gets `--config=/dev/null`. It needs a way to tell `bun` to read
no bunfig.toml.

**Compatibility**

- [ ] Defaults, locations and texts of ESLint 9 where ESLint 9 is
installed (45 of the 102 real ESLint runs).
- [ ] oxfmt's own departures from Prettier (161 fixtures).
- [ ] 5 Flow cases of Prettier's suite; a comment before the first `.`
of a member chain that breaks.
- [ ] JavaScript plugins get no type information.
- [ ] Types are TypeScript 7's, also where typescript-eslint runs on
TypeScript 5 or 6.
- [ ] `--infer-globals=fast` as the default: on projects that never load
the types it costs +0.1 % to +8.1 % instructions. Prepared as a change
of its own.
- [ ] `import/resolver: typescript` on one resolver instead of
TypeScript's resolution with corrections.

**Speed and robustness**

- [ ] A catastrophic regular expression from a config or an `/* eslint
rule: [..] */` comment costs about 1.8 s per match (Yarr's own
backtracking limit). Nothing limits a file or a run.
- [ ] `x = (x = (x = ( ..` 1,500 deep around 20,000 lines takes the
parser 36 s (lines x depth). ESLint refuses the file; oxlint 1.87 does
not end in 200 s.
- [ ] The type checker's file reader has no bound, as `bun check` on
`main` has none: a file of gigabytes, or a link to a device, as
`tsconfig.json` (with `--infer-globals`) or as the `package.json` of an
imported package (beside an oxlint config with `import` rules that look
at other modules) is read until there is no memory. The reader of `bun
lint` and `bun format` has the bound; it belongs in that one too, as a
change of its own.
- [ ] Memory and time of `bun format` on one very large YAML file have
not been measured again since YAML moved to Bun's parser (858 real
files: 3 % more instructions).
- [ ] Tens of thousands of different keys in one object of a config file
cost the square: 80,000 keys, about 1 MB, take 8 to 48 s (Prettier 67 s
on its own; `tsc` 7.0 14 s where `bun check` takes 25 s).
- [ ] `jsx-sort-props` with a collator for the machine's locale, or the
rule's `locale`.
- [ ] What JSON has no word for, as an option of a rule in
`eslint.config.*`: `Infinity` (as in `{ max: Infinity }`), `-Infinity`,
`NaN`, functions, symbols, bigints, `Map`, `Set`, `RegExp`, class
instances and a trailing `undefined` are taken or refused as by ESLint
10.12, in its words (103 of 128 cases). Left: other words for a `Date`,
a boxed number and a circular object; a key of a config object that is
`undefined` is read as absent, where ESLint refuses the config. Beside
an `oxlint.config.ts`, `Infinity` is no limit, where oxlint 1.87 refuses
the file.
- [ ] A binary file under an extension that is read leniently is
rewritten, as Prettier rewrites it. A look for NUL bytes would leave it
alone.
- [ ] A pass of `--fix` that would leave a syntax error drops all of the
fixes of that pass, not only those that break the file.
- [ ] `bun check`, on `main` (4c54c9bc6) as here: a call of a generic
function with 6,000 type parameters that are each constrained by the
next ends with a segmentation fault (`bun lint --type-aware`: from
52,000). `tsc` 6.0 says TS2589 from 110 on, a limit of its own that this
path never reaches, so from there the answers differ. The same for
function types some thousand deep in each other, and for `export *`
through 6,678 modules.
- [ ] `bun check`: generic arrow functions 5,200 deep in each other:
four functions of the checker go up through the enclosing arrows and
never ask for the stack. It holds on the 32 MB of the threads and fell
on 4 MB.
- [ ] `bun check -b` on a chain of projects, each of which refers to the
next, that are all in error (`composite` with `noEmit`): every project
repeats the errors of all below it, and 35 KB are held for each: 400
projects deep take 2.8 GB, on `main` 0.35 GB, for the same 79,800
errors. It comes with the depth: a tree of 364 projects and 10 layers of
10, in error or healthy, take 142 to 201 MB and 1.1 to 4.0 s on one
thread, less memory than `main` (212 to 311 MB; 4.2 to 16.1 s) and `tsc`
6.0 (257 to 413 MB; 1.4 to 4.2 s), and print the same bytes.
- [ ] `bun check`: an expression more than 1,000 queries deep is refused
at once ("ran out of stack") where `tsc` with a large stack completes.
Going on where the work stood is written; past the real stack it printed
TS2719 for a placeholder, so it is not in. `await`, `else if` and
ternaries at the parser's limit cost the square (15 to 70 G).
- [ ] A pattern that leaves the project still loads the configs it
reaches where the working directory has a config of oxlint or of ESLint
8 (8.57.1 does the same), and `oxfmt.config.ts` where it has a
`.prettierrc` (Prettier would not). Closed for a flat config and for no
config.
- [ ] `--stdin-filename` and `--print-config` for a path outside the
project that has no `eslint.config.*`: built-in rules, where ESLint 10
says "Could not find config file".
- [ ] 37 `.resolves` / `.rejects` that nothing awaits in Bun's own
tests, found by `bun:test/valid-expect`.
- [ ] A regular expression in the options of a rule with 26,000 groups
in each other takes 15 s in Yarr's compiler.
- [ ] Beside an oxlint config the check for "this fix would leave a
syntax error" does not see a name that is declared twice (a file that is
linted without types is not bound). It would hold back most of the 259
fixes of `no-var` above, and be other bytes than oxlint's on purpose.
- [ ] The node that a rule listens on, for the core rules too, and for
the rest of eslint-plugin-react (`jsx-tag-spacing`, `jsx-key`, an
element that closes itself).

<details>
<summary><b>For reviewers: what changes outside <code>src/lint</code>,
<code>src/format</code> and <code>src/sema/parser</code></b></summary>

- **`src/js_parser`: 27,798 lines deleted.** `bun check`, `bun lint` and
`bun format` parse with `src/sema/parser`, so the mode that #44361 added
to Bun's parser for the type checker is gone. What is left is the text
from before that PR plus 141 lines, all of which are on `main`.
`Bun.Transpiler` on 751,511 files with every loader that applies
(1,949,442 rows: a hash of the output or of each thrown message with its
place, of `scan`, of `scanImports`): 0 rows differ from `main`.
- **`Bun.markdown` changes its output for 0.45 % of real documents**
(232 of 51,234 `.md` files with the GFM extensions on, 86 with them
off). The Markdown formatter is built on `src/md`; each change is a fix
with a test in `test/js/bun/md`, judged by commonmark.js 0.31 and
micromark 4: table alignment per table, `a~~b~~c`, email autolinks,
lower-case `<!doctype`, nested links, fences outside a container, `1. `
not interrupting a paragraph, `- [ ]` over `- [x]`. 70 MB of real
Markdown to HTML takes 30 % fewer instructions.
- **Moved so that lint and format can share them**: `src/node_path`
(Node's path algorithms, out of `src/runtime/node/path.rs`),
`src/text_diff` (`bun test`'s diff), `src/glob` (one matcher for
`Bun.Glob`, minimatch, picomatch and ignore files), and text helpers in
`bun_core`. `test/internal/source-lints/shared-helpers.test.ts` keeps an
inventory of private copies that can only shrink.
- **`src/yarr`** (new), `RegularExpression.cpp`: regular expressions in
configs and rules run on JavaScriptCore's Yarr. `bun test -t` goes
through the same code.
- **`src/react_compiler`**: the React Compiler's lint rules run on Bun's
port of it, which got checks of the real stack (no count and no budget),
speed fixes, and a cure for type inference that did not end on small
inputs. Every assertion that input could reach returns an error now. On
4,801 real files its lint rules take 22 % fewer instructions than before
the speed fixes (131.3 G to 102.1 G); 8,000 JSX children 0.66 s instead
of 7.3 s. 608 of 20,000 generated programs took all the memory there is
(oxlint 1.87 does not end in 60 s on the three that were tried): the
join of two ref types went down shared structure as if it were a tree.
Two types that are held by pointers are joined once now: 25.8 s for all
608 in one process, 103 MB at the peak; where the old code ended (114 of
202 tried) the output is the same. Beside oxlint 1.87 on the 104,658
real files, all 26 rules: 23,256 findings there, 15 only here, 8 only
there; `react/purity` is 1,548 in both and in 1.86, and 698 in 1.85.
- **`src/react_compiler`, `src/ast/e.rs`,
`src/js_parser/react_compiler_host.rs`: eight fixes for wrong output**,
found by executing compiled against uncompiled code over several
renders, by parsing every output of 104,658 real files, and by looking
at where the lint rules differ from oxlint's. All eight are on `main`
too (4c54c9bc6). Each has an `itBundled` test that executes; those of
the first six fail on `main`, and for 7 and 8 the same program, built
plain and compiled by `main`, prints two different things:
1. Dead code elimination removed an unused read in a `try`, whose throw
was caught.
2. A variable reassigned in a scope inside another scope was `undefined`
from the second render on.
3. A scope that depends on a variable and reassigns it stored the new
value as the dependency. `let sum = props.start; for (..) sum = sum +
x;` with `start` 0, 0, 7, 0 gives 7, 7, 7, 7 on `main` and 7, 7, 14, 7
here.
4. `a++` in a scope did not count as a reassignment, so the update was
lost in a render that reuses the scope.
5. `memo(function Tree() { .. <Tree/> .. })` was compiled with an
unbound name, a `ReferenceError`. Upstream's invariant is back: the
function is left alone, as in babel-plugin-react-compiler 1.0.0.
6. A regular expression that ends in an escaped slash (`/^https?:\/\//`)
lost its closing slash in a compiled function: a syntax error in the
bundle.
7. A function of the module's own that is called `useCallback` or
`useMemo` was taken for React's and the call was dropped: `function
useMemo() { return "mine"; }` was never called.
8. The same for a member of any namespace import: `import * as X from
"./hooks.js"` lost its `X.useMemo(..)` and `X.useCallback(..)`, and with
`--target=bun` `X.useEffect(..)` was `undefined` and `const [s] =
X.useState("init")` was `"init"`. Named imports from other packages were
right. `React.useMemo` of `"react"` is dropped as before.

  | Fix | Real files whose output changes, of 104,658 | What changes |
  | --- | ---: | --- |
  | 1 | 5 | something in a `try` is kept; 1 had the bug |
  | 2 | 4 | 1 had the bug, 3 loops are memoized that were not |
| 3 | 28 | one kind only; 15 of 30 (scope, variable) pairs could go
stale |
  | 4 | 0 | no real file has the shape |
| 5 | 4 | the function is left alone; 3 crashed, 1 was right and loses
its memoization |
  | 6 | 83 | exactly the 83 files whose output did not parse |
| 7 | 1 | a component that was left alone is compiled, with the call
kept |
  | 8 | 0 | no real file has the shape in a compiled component |

Fixes 2, 3 and 4 are one change and must not be separated. Memoized
functions stay at 122,303. After the first six: outputs that do not
parse 0; upstream's 849 fixtures executed, release and ASAN, 849 pass.
After all eight and the speed fixes: that one file is the only one of
the 104,658 whose bytes move, and no recorded fixture output does.

**Found and not fixed**, on `main` and in babel-plugin-react-compiler
1.0.0 alike: what depends on whether a `try` throws is not reactive
(`try { JSON.parse(props.text) } catch { valid = false }` keeps the
first render's answer); `b = f((e = 0))` loses the assignment in a
render that reuses the scope. A fix for the first was in 6a982bb2 and
991d1b9b and is out again: it made the parameter of a `catch` a
dependency outside the handler, a `ReferenceError` in 3 of 5,541 real
files with a `try`. The test for that stays. New:
`react-compiler-fixtures-run.test.ts` builds 849 of upstream's fixtures
with and without the compiler, executes both and compares every render
(1 s on a release build).
- **`src/lint`: a rule is a type, not a table of callbacks.** `impl
Rule` with `const META`, `const ON` (the node kinds it listens to) and
one method per kind; no `dyn`, and the enabled rules of a file are one
bitset. Against the callback version, 237 rules on vscode: 4.2 % fewer
instructions, 1,420,096 report lines byte for byte the same.
- **`src/sema`**: binder modes without the flow graph, services for lint
rules on the checker, a reduced program that answers only which globals
exist. `bun check` shares declaration files between projects with
`main`'s code, unchanged. It runs on a pool of its own ("Bun Check n")
with eight times the stack, as the two new commands do. Three walks of
the checker that never asked for the stack, `modules_declaration`,
`check_binding_name` and `implied_by_pattern_inner`, ask now: they are
on `main` as well, where a parser and a binder with larger frames gave
up before them. In `tsconfig.json`, `extends` is followed by a loop, and
`include` and `exclude` are matched without recursion or backtracking:
`**/a/` 16 times against 34 directories took over 60 s (`tsc` 6.0: 29 s)
and takes 0.4 s. The same answers as before on 1,500 random projects
(everything that is printed, and the exit code) and on the 7,908 real
configs of 372 repositories (1,112,029 files listed).
- **`src/parsers/yaml.rs`: three changes of `Bun.YAML.parse`**, beside
`parse_as_written` for the formatter (+125 lines, no branch of it taken
by `Bun.YAML.parse`). `a: [⏎]`, the end of a flow collection at its
parent's indentation, was an error and is `{a: []}`: `yaml` 2.9 and
js-yaml 4.1 read it, the specification does not, and Bun read the same
bracket behind a scalar. `a: [x]⏎: y` was an error and is read, as the
specification has it (js-yaml refuses). `{:a b}` was `{null: "a b"}` and
is `{":a b": null}`, as both and the specification. A test for each;
`test/js/bun/yaml` 2,814 pass.
- **`src/bunfig`**: `load_config_for_tool` hands the parsed file to the
command: the same search, reader and TOML parser as for every other
section. **`src/sys`**: `extended_attributes(fd)`, so that a rewritten
file keeps its xattrs, ACL entries and SELinux label.
- **`src/jsc`, `src/runtime/cli`**: the two commands, the plugin VMs
(`lint_js.rs`), `LintNodes.cpp` (the syntax tree that a JavaScript rule
sees is built natively), `VirtualMachine::exit_and_free`. How many VMs a
run starts depends on the bytes that need one and on the memory limit,
not on time: 192 of 192 runs at four sizes, two loads, with and without
a 16 GB limit, start the same number. Eight real repositories against
the build of the day before: 1.9x to 4.0x faster in seven, within the
noise in one, with less CPU time in all.
- **`src/jsc/bindings/highway_dispatch.h`**: a call site of the SIMD
dispatch macro was a guarded function-local static; it is now one
relaxed atomic load and an indirect call, and resolving the target is a
cold function of its own, so LTO no longer inlines the guard into
thousands of callers. Every Bun process goes through it, and it is
independent of the rest of this PR.
- **A script named `lint` or `format`** now sees
`BUN_INTERNAL_SCRIPTS_OF_COMMANDS` in its environment: that is how
`"lint": "bun lint"` means the linter inside the script.
- `LICENSE.md`: attributions for the ported projects.

</details>

### How did you verify your code works?

**CI.** The whole run on the head, a52c2279, with `main` merged in,
passed: Linux 120 of 120, ASAN 20 of 20, Windows 16 of 16, macOS 4 of 4;
so did the one before the merge. An earlier one, on the commit at which
the tree was closed: Linux 120 of 120, ASAN 20 of 20 (with Rust's debug
assertions), macOS 4 of 4, Windows 15 of 16. The red test was ours and
was right: 15,000 assignments in each other got five TS2719 for a
placeholder under `bun check`, from a step that went on where the
checker used to refuse; on Linux it shows from 18,000. The step is out
(see TODO). Not required, and red in all: `binary-size`, by design, and
the lane of macOS 27, which `main` does not run
(`sql-close-pending-connection`, `node-net`); nothing here touches what
they test, but that they are unrelated is not shown.

**What left the PR.** On 11 October 104,000 lines: two development
harnesses with the oracles that only they could run, fixtures that
nothing loaded, the ports of two Prettier plugins, tables stored twice,
a second algorithm for hostile sizes in 47 rules, YAML's own parser,
twin rules. On 12 October 24,500 more: the 75 rules about layout,
oxlint's `jsdoc` and `vue`. Each step that could change behaviour was
proved old against new, byte for byte, on the suites and thousands of
files.

- The seven test suites above and 55 differential tests against the real
tools run in CI (`test/cli/lint`, `test/cli/format`, `test/cli/check`).
- The real repositories in the tables above, each with its own command
and config in a sandbox, against the real tool.
- Every rule that both have, against oxlint 1.87 itself, place for
place: 709 rules on seven real repositories (vscode, next.js, PostHog,
twenty, node, workers-sdk, ant-design). 11,169,328 reports are the same
and 3,130 are not (99.97 %); 514 of the 641 rules that report anything
agree everywhere. On the 157,000 inputs of the three tools' own suites
that oxlint can read, with all 709 rules on each: 1,488,153 the same,
2,148 not. `import/*` on vscode with its packages installed: 211,422 of
211,422.
- The work on speed changes no report. Each fast path has a debug
assertion against the slow one: 76 runs of a debug build over all those
inputs and repositories, none fired, and CI's ASAN lane has them on.
`bun build --react-compiler` on 16,981 real components, before against
after: 0 outputs differ. The compiler's 20 rules alone and in groups, 90
pairs of a config and a set of files: 0 differ. What was changed for
oxlint's flavour moved nothing under an ESLint config: the whole JSON
output on 162,946 inputs has the same checksum.
- Coverage-guided fuzzing of the parsers, printers, config readers and
the glob matcher. Black-box fuzzing of CI's release and ASAN builds with
mutated configs, options and hostile files, in a clean environment and
on a pseudo-terminal: 232,294 runs on 0c0224b0, no signal, no hang, no
exit code over 2. Files that are not valid UTF-8 with a plugin that
checks every node, token and comment: 5.2 million reports compared with
ESLint 10.12 on 09859818, none differs. Extreme numbers in every flag
and every key of `bunfig.toml` (6,160 runs), in the formatter's options
(1,092) and elsewhere in config files (1,260); config files that are
deep, long or many, to a million levels, keys or overrides (4,001); file
trees with links, devices, pipes, odd names and modes (312), and config
files that name a device (280): on the last commits no signal, and
nothing written outside the project. What these found is fixed:
`threads` without a bound, and files, links and config paths that were
read without one.
- Does the code still do what it did? 4,624 self-checking programs are
run, rewritten and run again. `bun format --write`: 5 end otherwise, as
with Prettier 3.9.9. `bun lint --fix` with the 106 fixable core rules at
once: 38, all with ESLint 10.12.0's bytes. Beside an oxlint config, all
rules at once and the 68 that fix one at a time (18,547 files written):
none that oxlint 1.87.0 does not break too. For JSX the question is what
React would be given: the 30 fixable rules of eslint-plugin-react one at
a time (139,639 files written) and all at once: none that the plugin
does not change with the same bytes. This found three faults, which are
fixed: `no-implicit-coercion` and `no-var` beside an oxlint config, and
the order in number 6 of the known differences. The fix objects of all
106 fixable core rules are those of ESLint 10.12.0 (10,330 cases), those
of 42 rules of react, react-hooks and import those of the plugins
(2,132).
- Large input. 176 and 342 real files joined into one file (25,117 and
50,234 lines), the same inside one function, 300 and 600 generated
components, one generated component of 25,665 and of 51,614 lines: every
field of every diagnostic is the same as oxlint 1.87.0's (every
category, 7 plugins; up to 62,801 diagnostics a file) and as ESLint
10.12.0's (366 rules; up to 79,836), in a fifth to a three-hundredth of
the time. The React Compiler's rules on generated components with up to
4,000 hooks, callbacks or JSX children and 16,000 statements: oxlint's
findings in all 28 sizes at which it ends; at 6,000 and 8,000, where it
ends with a segmentation fault, the finding is reported.
- Depth. Where a number used to stand before the stack there is a check
of the real stack, and a check in the parser does not protect what walks
the tree afterwards. Such a crash lives in a window of depths a few
percent wide, just below where the parser refuses, and in a release
build only: with ASAN or in a debug build the check keeps four times as
much free. So: every size in steps of 1 to 3 %, one process a file, on
CI's release builds (127c0994 to 8f6f7ed9; the older of them still have
4 MB for the threads of a command and are the more sensitive).

  | What is nested | Shapes | Runs | Signals |
  | --- | ---: | ---: | ---: |
| Config files, comments, selectors, regular expressions, globs, ignore
files | 354 | 100,249 | 1, on `main` too, fixed: an `include` of 133,271
`**/` in `tsconfig.json` |
  | Source under `bun lint`, every rule | 756 | 86,522 | 0 |
| Source for the React Compiler, `bun lint` and `bun build` | 50 |
23,309 | 0 |
  | Source under `bun format` | 142 | 22,878 | 0 |

Found this way and fixed: an `/* eslint */` comment (54 signals in 500
runs on the commit before: what was left of a tree was freed at the
bottom of a nearly full stack); a suggestion of `no-unused-vars` for
patterns in each other, where oxlint 1.87 ends with a segmentation fault
too; two aborts in JSDoc; the guard of a `match` case of Flow in `bun
format` (6 of 143 sizes; 0 now, on a main thread with 4 MB too); dotted
namespaces and patterns in `bun check` (13 of 213 sizes; 0 now);
patterns in a parameter (every size tried from 5,000 to 17,500 on 4 MB;
`main` falls at 5,000); a chain of 2,287 `extends` in `tsconfig.json`,
which is read on the main thread (`main` says "Cannot read file" from
there on, `tsc` ends with a `RangeError` from 1,500; 19,061 files are
read in 1.1 s now).

And a tool for what a sweep cannot see because another function happens
to have the larger frame: a debugger tells the stack checks of one
directory that the stack ends early and sets a hardware watchpoint below
that end. What asks stops above it; what does not ask writes there, and
the backtrace names it. Each run has a control that must hit.
`src/lint`, 6,108 files with every rule in both flavours: 20 hits in 4
functions, 0 now. `src/format`, 12,272 files in ten languages: 3 kinds,
0 now. `src/react_compiler`, 295 files: 0.
- The 21 numbers that `bun format` had (depths, lengths, counts), and
the 16 and 8 bits of `printWidth` and `tabWidth`, are gone. 25 inputs
beyond them: 25 print the bytes of Prettier 3.9.9 or oxfmt 0.72. About
20,000 shapes were then run at three depths for anything whose time or
memory doubles: none. By the clock: lists in Markdown and expansions of
Angular 40,000 to 120,000 deep, and 32,000 parameters in SCSS, took 6 to
22 s (the square of the size) and take 0.17 s. For a wrong answer
without an error: 7,000 files formatted once with 200 KB of stack and
once as usual: what is not refused and differs is always another
language inside the file, left as it is written, as Prettier does when
its own stack ends there.
- What lies outside the new directories, against `main`: 11,586 files
through the transpiler (transform, minify, scan) plus 96,000 generated
and mutated programs, 20,000 random inputs through 13 `node:path`
functions, 90,000 through `Bun.semver`, TOML, JSONC, JSON5 and JSONL,
1,713 React Compiler fixture builds: 0 differences. `Bun.markdown`:
135,000 inputs against commonmark.js and micromark, 0 regressions.

To try it:

```sh
bun run build:release
BUN=$PWD/build/release/bun
cd some/project
$BUN lint
$BUN lint --type-aware
$BUN format --check
```