Add `bun lint` and `bun format`
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.
Prettier by language
| 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 |
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).
Every run that is not the same, with its class
What "the same" means:
- lint: as said above; for oxlint also the length of the span.
--fixand format: the bytes of every file afterwards, and which files were read.- judges: the project's own installed version first.
bun lintandbun formatfollow 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 ofreact/*(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.
Usage
bun lint
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 ofeslintand ofoxlint, 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 forbun 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-exportas errors,no-disabled-testsas 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'sjsdocandvueplugins. 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 andbun/. 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-liteunderimport, or@eslint-reactunderreact, answer for themselves. -
Opting out:
--no-native-plugin-ruleshands every plugin back to its package;--native-plugin-rules=import,reactnames those that stay built in. -
Globals from the project's types, behind a flag:
--infer-globalstakes the globals of a file from the tsconfig or jsconfig that includes it (lib,types, everydeclare global).--infer-globals=fastreads only the compiler options and loads the types of a program only whenno-undefis 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
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
sveltein an oxfmt config,.sveltefiles are left as they are, exit 2. - Config:
.prettierrc*,prettier.config.*,"prettier"in package.json,overrides,.editorconfig,.prettierignore;.oxfmtrc.json,oxfmt.config.ts. Flags ofprettierand ofoxfmt. - Built in: import sorting (
@trivago/prettier-plugin-sort-imports,@ianvs/prettier-plugin-sort-imports,prettier-plugin-organize-imports, oxfmt'ssortImports), oxfmt'ssortTailwindcss,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
[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
- An
eslint.config.*and an.oxlintrc.jsonin one directory:bun lintstops (exit 2) and asks for--flavor=eslintor--flavor=oxlint. - A rule in
.oxlintrc.jsonthatbun lintdoes not have: oxlint refuses the file.bun lintlints with everything else, names the rule at the end and exits with 2;--allow-unsupportedmakes that a warning. - 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).
- Under an ESLint config, files in other languages or for other parsers (JSON, Markdown, CSS, Vue, Svelte, Astro, MDX) are linted by the
eslintpackage of the project, in JavaScript. Without it they are named at the end and the exit code is 2. - 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. - 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
--fixcan 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,--fixwrites 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. - 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 formatandbun checkhave 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 aRangeError. 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:stylishstops padding its rows where ESLint ends withRangeError: Invalid string length.--threadsabove 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. - A plugin or rule in JavaScript that leaves a rejection unhandled: ESLint stops with exit 2,
bun lintprints the error and goes on. -f junit,checkstyle,gitlabandsarifprint oxlint's layout under an ESLint config too, under the namebun lint. ESLint 10 ships none of them.bun format:printWidthandtabWidthhave 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 -1as two arguments is an unknown flag here, a warning in Prettier;0x10,1_000and05in a.prettierrc.jsonare read, whereJSON.parserefuses the file. Beside an oxfmt config nothing that oxfmt 0.72 takes is refused or printed otherwise (222 cases);tabWidth: 0is formatted, where oxfmt panics. The flags--check,--list-differentand--writeare 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 hasError:).- What ESLint 10.12 gets wrong,
bun lintgets wrong with it. One case is known:--fixwithno-varandone-vartogether turnsif (x) { var a = 1; var b = 2; } return b;intolet a = 1, b = 2;, wherebis 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. - The same for oxlint 1.87. Of 4,624 self-checking programs (JavaScriptCore's tests), run before and after
--fixwith one rule at a time, 479 end otherwise after a fix of oxlint's own, andbun lintwrites the same bytes:no-var259,unicorn/catch-error-name105,sort-keys19,no-new-wrappers15,unicorn/prefer-spread12,prefer-arrow-callback11, 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. - Beside an oxlint config,
no-varleaves avarthat is declared twice, each time in atryblock of its own; oxlint writesconstfor both. - 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.
- 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.parsewords it. <!--and-->comments: several comment rules at once--fixother bytes than ESLint 10.12, whose-- >is no comment.- 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. - 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 formatis what does their work. - oxlint's
jsdocandvue: 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 anote:on stderr, in every format and with--quiet. - A search that leaves the project (
bun lint ..,'/**/x.js') runs noeslint.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 forbun format. - Beside an oxlint config,
no-useless-assignment,constructor-superand 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++ },trywithfinally). - oxlint stops at an error of its semantic checks, such as a name declared twice, and runs no rule on that file.
bun lintgoes on. - 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 formatrefuses the file and leaves it.
eslint-plugin-react and eslint-plugin-import
settings.react.version: "detect"asks the nearest installed react for each file; the plugin lets the first file decide for the process.- Where the plugin throws and ESLint's run ends (settings of the wrong kind,
jsx-handler-nameswithignoreComponentNameson<A.B>), nothing is reported for that rule and the run goes on. jsx-sort-propscompares names in English order. The plugin uses the machine's default locale, or the rule'slocale, so withignoreCaseunder a locale with a collation of its own (cs, sk, hu, tr, sv, da, nb, lt), or with such alocale, reports and the order that--fixwrites can differ.- A file with Flow types goes to the project's own parser, and there the package's rules answer.
import/resolver: typescriptis answered by Bun's resolver following eslint-import-resolver-typescript 4.4.5. Its optionsextensions,conditionNames,mainFieldsandalwaysTryTypes: falseare not read;./a.jswhere onlya.jsxexists is found.- With any other resolver, another parser in
import/parsers, or a regular expression object inimport/ignore, the installed package's rules run instead. import/no-unused-modulesandimport/no-cycleanswer 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(anddefine,loader..) ofbunfig.tomlstill applies to the process that evaluateseslint.config.*,prettier.config.*and the like. Elsewhere that process gets--config=/dev/null. It needs a way to tellbunto 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=fastas 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: typescripton 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 checkonmainhas none: a file of gigabytes, or a link to a device, astsconfig.json(with--infer-globals) or as thepackage.jsonof an imported package (beside an oxlint config withimportrules that look at other modules) is read until there is no memory. The reader ofbun lintandbun formathas the bound; it belongs in that one too, as a change of its own. - Memory and time of
bun formaton 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;
tsc7.0 14 s wherebun checktakes 25 s). -
jsx-sort-propswith a collator for the machine's locale, or the rule'slocale. - 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 trailingundefinedare taken or refused as by ESLint 10.12, in its words (103 of 128 cases). Left: other words for aDate, a boxed number and a circular object; a key of a config object that isundefinedis read as absent, where ESLint refuses the config. Beside anoxlint.config.ts,Infinityis 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
--fixthat would leave a syntax error drops all of the fixes of that pass, not only those that break the file. -
bun check, onmain(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).tsc6.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 forexport *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 -bon a chain of projects, each of which refers to the next, that are all in error (compositewithnoEmit): every project repeats the errors of all below it, and 35 KB are held for each: 400 projects deep take 2.8 GB, onmain0.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 thanmain(212 to 311 MB; 4.2 to 16.1 s) andtsc6.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") wheretscwith 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 ifand 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.tswhere it has a.prettierrc(Prettier would not). Closed for a flat config and for no config. -
--stdin-filenameand--print-configfor a path outside the project that has noeslint.config.*: built-in rules, where ESLint 10 says "Could not find config file". - 37
.resolves/.rejectsthat nothing awaits in Bun's own tests, found bybun: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-varabove, 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).
For reviewers: what changes outside src/lint, src/format and src/sema/parser
-
src/js_parser: 27,798 lines deleted.bun check,bun lintandbun formatparse withsrc/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 onmain.Bun.Transpileron 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, ofscan, ofscanImports): 0 rows differ frommain. -
Bun.markdownchanges its output for 0.45 % of real documents (232 of 51,234.mdfiles with the GFM extensions on, 86 with them off). The Markdown formatter is built onsrc/md; each change is a fix with a test intest/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 ofsrc/runtime/node/path.rs),src/text_diff(bun test's diff),src/glob(one matcher forBun.Glob, minimatch, picomatch and ignore files), and text helpers inbun_core.test/internal/source-lints/shared-helpers.test.tskeeps 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 -tgoes 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/purityis 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 onmaintoo (4c54c9bc6). Each has anitBundledtest that executes; those of the first six fail onmain, and for 7 and 8 the same program, built plain and compiled bymain, prints two different things:- Dead code elimination removed an unused read in a
try, whose throw was caught. - A variable reassigned in a scope inside another scope was
undefinedfrom the second render on. - 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;withstart0, 0, 7, 0 gives 7, 7, 7, 7 onmainand 7, 7, 14, 7 here. a++in a scope did not count as a reassignment, so the update was lost in a render that reuses the scope.memo(function Tree() { .. <Tree/> .. })was compiled with an unbound name, aReferenceError. Upstream's invariant is back: the function is left alone, as in babel-plugin-react-compiler 1.0.0.- A regular expression that ends in an escaped slash (
/^https?:\/\//) lost its closing slash in a compiled function: a syntax error in the bundle. - A function of the module's own that is called
useCallbackoruseMemowas taken for React's and the call was dropped:function useMemo() { return "mine"; }was never called. - The same for a member of any namespace import:
import * as X from "./hooks.js"lost itsX.useMemo(..)andX.useCallback(..), and with--target=bunX.useEffect(..)wasundefinedandconst [s] = X.useState("init")was"init". Named imports from other packages were right.React.useMemoof"react"is dropped as before.
Fix Real files whose output changes, of 104,658 What changes 1 5 something in a tryis kept; 1 had the bug2 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
mainand in babel-plugin-react-compiler 1.0.0 alike: what depends on whether atrythrows 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 acatcha dependency outside the handler, aReferenceErrorin 3 of 5,541 real files with atry. The test for that stays. New:react-compiler-fixtures-run.test.tsbuilds 849 of upstream's fixtures with and without the compiler, executes both and compares every render (1 s on a release build). - Dead code elimination removed an unused read in a
-
src/lint: a rule is a type, not a table of callbacks.impl Rulewithconst META,const ON(the node kinds it listens to) and one method per kind; nodyn, 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 checkshares declaration files between projects withmain'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_nameandimplied_by_pattern_inner, ask now: they are onmainas well, where a parser and a binder with larger frames gave up before them. Intsconfig.json,extendsis followed by a loop, andincludeandexcludeare matched without recursion or backtracking:**/a/16 times against 34 directories took over 60 s (tsc6.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 ofBun.YAML.parse, besideparse_as_writtenfor the formatter (+125 lines, no branch of it taken byBun.YAML.parse).a: [⏎], the end of a flow collection at its parent's indentation, was an error and is{a: []}:yaml2.9 and js-yaml 4.1 read it, the specification does not, and Bun read the same bracket behind a scalar.a: [x]⏎: ywas 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/yaml2,814 pass. -
src/bunfig:load_config_for_toolhands 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
lintorformatnow seesBUN_INTERNAL_SCRIPTS_OF_COMMANDSin its environment: that is how"lint": "bun lint"means the linter inside the script. -
LICENSE.md: attributions for the ported projects.
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-compileron 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:threadswithout 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 --fixwith 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-coercionandno-varbeside 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 maintoo, fixed: anincludeof 133,271**/intsconfig.jsonSource under bun lint, every rule756 86,522 0 Source for the React Compiler, bun lintandbun build50 23,309 0 Source under bun format142 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 ofno-unused-varsfor patterns in each other, where oxlint 1.87 ends with a segmentation fault too; two aborts in JSDoc; the guard of amatchcase of Flow inbun format(6 of 143 sizes; 0 now, on a main thread with 4 MB too); dotted namespaces and patterns inbun check(13 of 213 sizes; 0 now); patterns in a parameter (every size tried from 5,000 to 17,500 on 4 MB;mainfalls at 5,000); a chain of 2,287extendsintsconfig.json, which is read on the main thread (mainsays "Cannot read file" from there on,tscends with aRangeErrorfrom 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 formathad (depths, lengths, counts), and the 16 and 8 bits ofprintWidthandtabWidth, 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 13node:pathfunctions, 90,000 throughBun.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:
bun run build:release
BUN=$PWD/build/release/bun
cd some/project
$BUN lint
$BUN lint --type-aware
$BUN format --check