Add `bun lint` and `bun format`

Merged
JA
25d ago
+636338-42255

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

ToolSuitePassFailNot run
ESLint 10.12, typescript-eslint 8.71, eslint-plugin-react 7.37.5, -import 2.32their own rule tests, 505 rules69,6400303
the same rulesmore cases, judged by the real ESLint, 632 rules76,5524258
oxlint 1.87its own rule tests, 504 rules27,702063
Prettier 3.9.9tests/format9,728588
oxfmt 0.72fixtures1,3371610

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
LanguagePassTotal
JavaScript1,3571,357
JSX186186
TypeScript756756
Flow1,5001,505
JSON785785
CSS162162
Less238238
SCSS136136
GraphQL104104
YAML1,6231,623
Markdown1,5721,572
MDX3232
HTML272272
Vue189189
Angular109109
Handlebars128128
MJML, LWC, misc579579
Not run (stage-2 proposals that only Babel parses, Prettier plugins)088

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 withWhat has to matchSameOfRate
ESLintexit code9610294 %
ESLint--fix, bytes9310291 %
ESLintevery message, against the project's own ESLint and plugins7310272 %
ESLintevery message, against that or the releases that the built-in rules follow8010278 %
oxlint 1.87 + tsgolintexit code20321594 %
oxlint 1.87 + tsgolint--fix, bytes20821597 %
oxlint 1.87 + tsgolintevery message19121589 %
oxfmt 0.72bytes22724095 %
Prettierbytes, against theirs or 3.9.9577477 %
Prettierexit code of --check667489 %

"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 samenessOf
Runs of bun that ended by themselves with exit code 0, 1 or 2: none killed, hung or crashed951951
A second run in the same copy says the same631631
The project's lint command at a terminal (a pty, no -f): the exit code of -f json, no crash150150
Projects whose config bun lint cannot load0

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.
  • --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 runthe sameofjudged on 6208ecd93judged on 7df1455c4judged on ade5c82ae
ESLint: lint, identical to the project's own ESLint and plugins7310272 %9750
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)8010278 %9750
ESLint: --fix, the same bytes9310291 %9750
ESLint: the same exit code9610294 %9750
oxlint: lint, identical to 1.87 (rule, severity, place, length, text, files)19121589 %32111
oxlint: --fix, the same bytes as 1.8720821597 %32111
oxlint: the same exit code as 1.8720321594 %32111
oxfmt: the same bytes as 0.7222724095 %411990
oxfmt: the same exit code of --check22924095 %411990
Prettier: identical to theirs or to 3.9.9577477 %7400
Prettier: the same exit code of --check667489 %7400

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

RunJudgeExit theirs / bunWhat
Comfy-Org/ComfyUI_frontendESLint 10.10.00 / 1import-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-boilerplateESLint 9.38.01 / 1spec 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.jsESLint 9.39.50 / 1unicorn/consistent-function-scoping 4 only bun (errors); no-eval's text is ESLint 9's
graphql/graphql-jsESLint 9.39.30 / 01 suppressed message: import/unambiguous is placed on the whole file by bun (1:1), from the first token by them (4:1)
mermaid-js/mermaidESLint 9.35.01 / 1typed rules in a .js file (svgDraw.js): no-floating-promises 9, restrict-plus-operands 1 only bun, with the plugin version bun follows
pixijs/pixijsESLint 9.39.20 / 018 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/astroESLint 10.12.01 / 1eslint-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-appetitoxlint 1.870 / 0React Compiler rule: preserve-manual-memoization 1 only bun
arcjet/arcjet-jsoxlint 1.871 / 1"Unused oxlint-disable directive" 58 only theirs
digirati-co-uk/iiif-manifest-editoroxlint 1.871 / 1react-hooks(exhaustive-deps) 2: another text
glide-browser/glideoxlint 1.871 / 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/weboxlint 1.870 / 0React Compiler rule: static-components 1 only theirs
monkeytypegame/monkeytype:frontendoxlint 1.871 / 1typescript(TS2307) 1 only theirs
openclaw/openclawoxlint 1.871 / 1eslint(no-useless-assignment) 5 only bun
petyosi/react-virtuoso:packages/masonryoxlint 1.871 / 1the checker's own diagnostics (TS7031, TS7006, TS2307) 69 only theirs
petyosi/react-virtuoso:apps/virtuoso.devoxlint 1.871 / 1the checker's own diagnostics (TS2307, TS7031, TS7006) 21 only theirs
reserve-protocol/registeroxlint 1.870 / 0React Compiler rule: preserve-manual-memoization 1 only theirs
ietf-tools/purpleoxfmt 0.720 / 14 files only bun would change: 1 .json, 2 .html, 1 .yaml
olton/metrouioxfmt 0.722 / 21 .css, 1 .md differ; 4 .less that 0.72 refuses; both exit 2
zendeskgarden/react-componentsoxfmt 0.721 / 11 .ts file only bun would change
chakra-ui/chakra-uiPrettier 3.8.31 / 18 .md only bun would change (code blocks: their import-sorting plugin; Markdown)
renovatebot/renovatePrettier 3.9.90 / 165 .md files that only bun would change, against the same release 3.9.9
tailwindlabs/tailwindcssPrettier 3.9.60 / 1embedded languages in templates (html, css): 32 .ts files only bun would change
timlrx/tailwind-nextjs-starter-blogPrettier 3.8.11 / 11 .mdx only theirs, 1 .cjs only bun; prettier-plugin-tailwindcss: 1 .css, 1 .tsx
vercel/next.jsPrettier 3.6.22 / 2the 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-vuePrettier 3.9.101 / 11 .md differs
withastro/astroPrettier 3.9.90 / 11 .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

RunJudgeExit theirs / bunWhat
TanStack/router:packages/router-coreESLint 9.22.00 / 1two 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 #1ESLint 8.37.00 / 0ESLint 8.37: places of complexity, no-unused-vars
chartjs/Chart.js #2ESLint 8.37.00 / 0ESLint 8.37 / typescript-eslint 5
eslint/eslintESLint 10.12.00 / 1eslint-plugin-n's table of Node.js versions: bun follows the newest release of the plugin
mastodon/mastodonESLint 9.39.50 / 0ESLint 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/pinoESLint 9.39.50 / 0ESLint 9: the text of no-eval
sadmann7/skateshopESLint 8.57.01 / 1.eslintrc with typescript-eslint 6
webpack/webpackESLint 9.39.51 / 1ESLint 9: the text of no-eval (2 messages); everything else of 5,521 files the same
TanStack/router:packages/react-router --fixESLint 9.22.0lint is identical with the plugin versions bun follows; the fix is that version's
apollographql/apollo-client #1 --fixESLint 9.31.0lint is identical with the plugin versions bun follows; the fix is that version's
sveltejs/svelte --fixESLint 10.0.0ESLint 10.0: no-useless-assignment; lint is identical to 10.12
shadcn-ui/ui:apps/v4 exit codeESLint 9.26.00 / 1eslint-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 codeESLint 10.3.00 / 1ESLint 10.3: new-cap 1; identical to 10.12
diced/zipline, sanity-io/next-sanityoxlint 1.78.00 / 21.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-Boilerplateoxlint 1.78.01 / 2as zipline, and 20 jsdoc rules
makeplane/plane:apps/web, :apps/admin, :apps/space, :apps/live, :packages/editor, :packages/utilsoxlint 1.51.00 / 01.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/metrouioxlint 1.42.00 / 01.87 refuses their configuration ("Rule 'no-unused-vars' not found in plugin 'import'"); the same exit code as their 1.42
preactjs/preactoxlint 1.8.00 / 01.87 refuses their configuration (four rules it does not have: camelcase, jsx-no-bind ..); the same exit code as their 1.8
vercel/aioxlint 1.56.00 / 11.87 refuses their configuration ("Rule 'no-unresolved' not found in plugin 'import'"); against their 1.56 the categories hold other rules
apollographql/apollo-clientPrettier 3.1.12 / 1Prettier 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/dubPrettier 3.2.51 / 1Prettier 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-authPrettier 3.3.32 / 1Prettier 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/precedentPrettier 2.8.81 / 1Prettier 2.8.8 with plugins of that age

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

RunJudgeExit theirs / bunWhat
angular/angular-cliESLint 10.12.00 / 0as jest: max-len 82, padding-line-between-statements 1; all else the same (413)
jestjs/jestESLint 10.10.00 / 0ESLint's deprecated formatting rules do not run in bun: quotes 2 only theirs; all else the same (653)
jquery/jqueryESLint 9.39.40 / 0as jest: key-spacing 9; all else the same (19)
microsoft/playwrightESLint 9.39.40 / 0as jest: quotes 276, key-spacing 158, comma-spacing 117, object-curly-spacing 112; all else the same (436)
nextauthjs/next-authESLint 9.9.11 / 13 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/nuxtESLint 10.12.01 / 137 files "not found by the project service" are fatal for them, linted by bun; no-deprecated 3 / 1
vueuse/vueuseESLint 10.9.10 / 0as jest: spaced-comment 2; all else the same (68)
Sandakan/Noraoxlint 1.870 / 0import(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-microoxlint 1.870 / 1not 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 codeoxlint 1.871 / 2as vitest
eikowagenknecht/lootscraper exit code, jumpserver/luna #1, jumpserver/luna #2, lihbr/lihbr-apex, nuxt-modules/prismic, vitest-dev/vitestoxlint 1.870 / 2oxlint'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/PodNotesoxfmt 0.722 / 0not 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-editoroxfmt 0.722 / 10.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-frogoxfmt 0.722 / 1not installed here: sortTailwindcss: 0.72 exits 2 ("Can't resolve 'tw-animate-css'"), bun warns and goes on; the bytes are the same
heyhuynhgiabuu/proxypaloxfmt 0.722 / 1not 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-browseroxfmt 0.722 / 1as iiif-manifest-editor (2 files)
radix-ui/primitivesoxfmt 0.722 / 10.72's CSS parser refuses one .module.css (exit 2); bun formats it; all other bytes the same
reshaped-ui/reshapedoxfmt 0.722 / 10.72's CSS parser refuses @each in 7 .module.css files (exit 2); bun formats them
salsita/node-pg-migrateoxfmt 0.722 / 10.72 refuses export in a .cjs file (exit 2); bun formats it
storybookjs/storybookoxfmt 0.722 / 00.72 refuses import in a .cjs file (exit 2); bun formats it
PrismJS/prismPrettier 3.5.31 / 1three Prettier plugins that lay JavaScript out otherwise (brace-style, space-before-function-paren, merge): bun runs no Prettier plugin: 244 .js; 12 .md
alii/websitePrettier 3.9.61 / 0prettier-plugin-tailwindcss: bun runs no Prettier plugin: 2 files only theirs would change
mickasmt/next-saas-stripe-starterPrettier 3.9.61 / 1as alii/website: 1 .tsx differs
shadcn-ui/ui:apps/v4Prettier 3.6.22 / 1their Prettier exits 2 here (prettier-plugin-tailwindcss needs a build); 19 files only bun would change
trpc/trpcPrettier 3.9.62 / 0their Prettier exits 2 here ("Unable to load your Tailwind CSS v3 config": www is not built); no byte differs

By repository

target9 October, 19:38 UTC (d9fee8c85)now
oxlint + oxfmt in one repository: the same exit codes in both85 %132 of 143 (92 %)128 of 143 (90 %)
oxfmt: repositories with the same bytes as 0.7295 %203 of 217 (94 %)204 of 217 (94 %)
oxlint: repositories identical to 1.87: rule, severity, place, length, text, files, exit code, --fix75 %139 of 161 (86 %)143 of 161 (89 %)
of those: every label (span, text) the same too103 of 139 (74 %)142 of 144 (99 %)
of those: the help the same too89 of 139 (64 %)143 of 144 (99 %)
Prettier: repositories identical80 %56 of 71 (79 %)54 of 71 (76 %)
ESLint: runs with the same exit code85 %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 plugins62 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.

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 of eslint and of oxlint, and behaves like the tool whose config it finds.

  • Rules built in (62 need types):

    PluginRulesBuilt in under an ESLint config
    ESLint core218218
    unicorn138
    typescript-eslint137136
    react106102
    vitest73
    bun:test/ (new)6565
    jest60
    import4646
    jsx-a11y36
    bun/ (new)2929
    oxc271
    React Compiler2626
    nextjs21
    promise16
    n154
    react-perf4
    react-hooks33
    prettier11
    All1,021631
  • 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

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

[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

Workloadbunoxlint / oxfmtSpeedup
lint, correctness0.21 s0.43 s2.0x
lint, recommended0.25 s0.51 s2.0x
lint, all 709 rules that both have0.73 s1.82 s2.5x
format --check0.42 s0.79 s1.9x

CPU time (user), median

Workloadbunoxlint / oxfmtSpeedup
lint, correctness1.52 s2.65 s1.7x
lint, recommended1.70 s2.96 s1.7x
lint, all 709 rules that both have4.86 s13.59 s2.8x
format --check3.02 s5.79 s1.9x

Instructions (user), least of 15

Workloadbunoxlint / oxfmtFewer
lint, correctness7.7 G15.8 G2.1x
lint, recommended8.4 G17.7 G2.1x
lint, all 709 rules that both have19.4 G66.7 G3.4x
format --check19.7 G39.9 G2.0x

Peak memory, median

Workloadbunoxlint / oxfmtLess memory
lint, correctness119 MB271 MB2.3x
lint, recommended124 MB371 MB3.0x
lint, all 709 rules that both have517 MB1264 MB2.4x
format --check181 MB557 MB3.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 lintESLint 10Speedup
15.8 s404 s26x

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 + PrettierSpeedup
2.9 s67 s23x

Formatters, --check on vscode, CPU time

bun formatoxfmt 0.72Biome 2.5Prettier 3.9.9
2.9 s5.5 s20 s295 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

TargetmainThis PRAdded
linux-x6485.18 MB100.09 MB+14.91 MB
linux-aarch6484.68 MB99.80 MB+15.13 MB
darwin-aarch6467.85 MB82.50 MB+14.66 MB
darwin-x6474.81 MB90.49 MB+15.68 MB
windows-x6492.27 MB107.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 buildResult
Built in against the package: messages, order, fix objects, suggestionsthe same in 24
--fix, bytesthe same in 24
react/ messages, suppressed ones included42, the same in all three
Runs that need no JavaScript engine any more2
Runs that load fewer modules15 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.

RepositoryBuilt inThe packageTheir ESLint
shadcn-ui/ui2.1 s5.8 s90.4 s
chakra-ui0.59 s1.43 s22.6 s
mastodon6.6 s7.4 s100.0 s
playwright4.9 s4.5 s53.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.
  15. 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.
  16. <!-- and --> comments: several comment rules at once --fix other bytes than ESLint 10.12, whose -- > is no comment.
  17. 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.
  18. 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.
  19. 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.
  20. 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.
  21. 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).
  22. 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.
  23. 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.
  7. 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).
For reviewers: what changes outside src/lint, src/format and src/sema/parser
  • 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.
    FixReal files whose output changes, of 104,658What changes
    15something in a try is kept; 1 had the bug
    241 had the bug, 3 loops are memoized that were not
    328one kind only; 15 of 30 (scope, variable) pairs could go stale
    40no real file has the shape
    54the function is left alone; 3 crashed, 1 was right and loses its memoization
    683exactly the 83 files whose output did not parse
    71a component that was left alone is compiled, with the call kept
    80no 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.

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 nestedShapesRunsSignals
    Config files, comments, selectors, regular expressions, globs, ignore files354100,2491, on main too, fixed: an include of 133,271 **/ in tsconfig.json
    Source under bun lint, every rule75686,5220
    Source for the React Compiler, bun lint and bun build5023,3090
    Source under bun format14222,8780

    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:

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