Linting
Lint JavaScript and TypeScript with bun lint, a fast linter that is compatible with ESLint
bun lint lints JavaScript and TypeScript. It reads your eslint.config.js, runs the rules of ESLint and typescript-eslint, prints what eslint prints, and uses every CPU core.
bun lint/app/src/index.ts
3:7 error 'count' is never reassigned. Use 'const' instead prefer-const
9:14 warning Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
✖ 2 problems (1 error, 1 warning)
1 error and 0 warnings potentially fixable with the `--fix` option.bun lint exits with code 1 if it finds an error, 0 if it finds none, and 2 if it could not lint, for example because the configuration is invalid. See Exit codes.
You don't need the eslint package, and you don't need a configuration file.
Without a configuration file#
bun lint uses eslint:recommended, plus typescript-eslint/recommended for TypeScript files, plus six rules for files that import from bun:test. It lints .js, .mjs, .cjs, .jsx, .ts, .mts, .cts, and .tsx files, and knows the globals of browsers, Node.js, and Bun, and in test files those of bun test. It skips node_modules, *.min.js, and what .gitignore ignores. It follows symbolic links to directories, and passes over a link that leads nowhere or back up. A file that cannot be opened is an error in the report, and the other files are linted.
With --infer-globals, or inferGlobals = true in bunfig.toml, the globals of a file that a tsconfig.json or jsconfig.json includes are what the types of the project declare: its lib, its types, and declare global. So window is undefined in a project without the dom library. In JavaScript that the project does not check (checkJs), and beside a configuration file, they are added to the globals there are. If the packages of the project are not installed, or an entry of types is missing, nothing is inferred. With the dom library, name, event, status, length, parent, top and about 90 more such names count as defined everywhere, as for tsc.
Loading the libraries and the types takes 40 to 450 ms, more than linting a small project. A file that is linted with --type-aware has them anyway, so there it is the default.
--infer-globals=fast is for JavaScript without a configuration file, and reads only the compiler options at first. lib, target and types choose among the environments that bun lint knows: dom is browser, webworker is worker, es2022 is es2022, and the types of node, bun, jest, mocha, jasmine, jquery and vitest/globals are the environments of those names. The types are loaded only when no-undef is about to report a name, and then they decide. Such a run takes 25 to 110 ms longer: SvelteKit lints in 140 ms instead of 34 ms, and 75 false reports of globals that its declaration files declare are gone. Every other rule knows the names of those environments only.
To change a rule, add a configuration file:
export default [
{
rules: {
eqeqeq: "error",
"no-console": "warn",
},
},
];A configuration file replaces the defaults. See Configure ESLint for everything it can contain.
Replace ESLint#
bun lint reads the same configuration, lints the same files, and takes the same flags, so most projects only change the command.
Change the lint script
{
"scripts": {
"lint": "bun lint --max-warnings 0"
}
}Keep the flags and the arguments. eslint --max-warnings 0 src becomes bun lint --max-warnings 0 src.
Change the script before you try the command. While the script says eslint, bun lint runs the script, so it runs ESLint.
Run it
bun run lintThe report and the exit code are those of eslint. If the run ends with The configuration asks for what cannot be done yet, the lines below that name what bun lint left out.
Change CI, lint-staged, and Git hooks
Replace eslint, npx eslint, and bunx eslint with bun lint. See Run in CI and Check before you commit.
Keep the packages that the configuration imports
Bun runs your eslint.config.js, so typescript-eslint, @eslint/js, globals, and the plugins stay in devDependencies.
Keep eslint too in any of these cases:
- The configuration imports
typescript-eslint, or a plugin that importseslint. - The ESLint extension of your editor uses it.
- The configuration lints other languages.
In any other project, run bun remove eslint.
| Instead of | Run |
|---|---|
eslint | bun lint |
eslint src test | bun lint src test |
eslint --fix | bun lint --fix |
eslint --max-warnings 0 | bun lint --max-warnings 0 |
eslint -f json -o report.json . | bun lint -f json -o report.json . |
bun lint takes every flag of ESLint 10 except --inspect-config and --mcp. See Differences from ESLint.
If the project runs oxlint too, write bun lint --flavor=eslint. See A project with both tools.
Rule names, options, messages, locations, fixes, and eslint-disable comments are those of ESLint 10 and typescript-eslint 8, whichever versions you have installed. A project on ESLint 9 sees what it would see after the upgrade.
Plugins#
The rules of ESLint and typescript-eslint are built into Bun, and so are the rules of react-hooks, including its React Compiler rules, and some of import, n, and oxc. They run as native code. With an .oxlintrc.json, so do the rules of oxlint's plugins.
A built-in plugin runs as native code under its usual name: @typescript-eslint, react-hooks, import, n. Under another name, such as ts or node in @antfu/eslint-config, it runs in JavaScript like any other plugin.
Every other plugin runs as it is. bun lint loads the plugins that your eslint.config.js has in plugins, and runs their rules in JavaScript, with the same context, sourceCode, scopes, and fixers that ESLint gives them.
import unicorn from "eslint-plugin-unicorn";
export default [
{
plugins: { unicorn },
rules: { "unicorn/prefer-node-protocol": "error" },
},
];Rules in JavaScript are slower than the built-in ones. A file costs nothing extra if no rule in JavaScript is on for it, and a plugin is only loaded where a file is linted that has one of its rules on. If a module of the plugin exports the rule, only that module is loaded, without the other rules. Use --timing to see how much JavaScript a run has loaded.
Import plugins from a module. For a plugin that is written in eslint.config.js itself, Bun has to run that file again, with everything it imports, to get at the rules.
Plugins run inside bun lint, on threads of their own, as they run inside eslint:
- What a plugin prints goes to the terminal, and
process.exit()ends the run. - Its
process.on("exit")handlers run after the report. - A rule that throws ends the run with exit code
2. With an.oxlintrc.jsonthe error is reported for the file, and the run goes on. - Each thread that runs rules in JavaScript loads the plugins for itself, so state at the top level of a module is per thread, and so are the handlers.
- The rules in JavaScript run on one thread, on two if the files that they are for have 2 MB, on 4 from 9 MB, on 8 from 35 MB, on 16 from 135 MB, and never on more than
--threads: each has to load the plugins again, and takes memory. Files of 256 KB and more are not counted: they all go to one of these threads, one at a time. Together they plan to stay within half of the memory that is free, in the machine and in the container: there are fewer of them if the plugins are large.
Types are not there for a rule in JavaScript. parserServices.program is null, also with --type-aware. A rule that asks for types with getParserServices() of typescript-eslint does not run. See what cannot be linted.
Run the rules of the package#
The built-in rules of @typescript-eslint, react-hooks, import, n, react, and prettier run in place of the rules of the package that your configuration loads. Use --no-native-plugin-rules to run the rules of the installed packages, in JavaScript. The rules of ESLint itself stay built in.
bun lint --no-native-plugin-rules
bun lint --native-plugin-rules=@typescript-eslint,import # only these two stay built inIn bunfig.toml:
[lint]
nativePluginRules = false
# Or name the plugins that stay built in:
# nativePluginRules = ["@typescript-eslint", "import"]- Write the name that the configuration has for the plugin, before the
/of its rules. - A plugin goes to its package only where the configuration loads that package. Everywhere else, as without a configuration file, the built-in rules stay.
- With an
.eslintrcthat names the plugin, the package has to be installed. A built-in plugin otherwise needs no package there. - The rules of
@typescript-eslintthat need types do not run in JavaScript. The run says which, and fails. Keep@typescript-eslintin the list for them. - With an
.oxlintrc.jsonthe option changes nothing.pluginsnames the plugins that are built into oxlint, and whatjsPluginsnames runs in JavaScript. bun lint --print-config <file>names the rules that run in JavaScript for that file, on stderr.
prettier/prettier#
The rule of eslint-plugin-prettier is built in. It compares each file with what bun format prints, so no Prettier is started. The options, the messages, the places and the fixes are those of the package.
import prettier from "eslint-plugin-prettier";
export default [{ plugins: { prettier }, rules: { "prettier/prettier": "error" } }];It answers only where its text is Prettier's, byte for byte. For every other file the rule of the package runs in JavaScript, and a note says why:
prettier3.9 andeslint-plugin-prettier5 are not what is installed- a plugin of Prettier that
bun formatdoes not have may print the file - the rule or the configuration has an option that
bun formatdoes not know - the file is not JavaScript or TypeScript
bun formatfinds a syntax error in the file
Other languages#
A file for which the configuration has a language of a plugin, such as json/json of @eslint/json, markdown/gfm or css/css, or a parser for another kind of file, such as yaml-eslint-parser or vue-eslint-parser, or eslint-plugin-html, is linted by the Linter of the eslint package that the project has installed. bun lint finds the files, works out the configuration, applies the fixes and prints the report. All rules for such a file run in JavaScript, also those that are built in. If its parser is asked for types, all such files are linted on one thread: each thread would make a TypeScript program of its own. Keep eslint, 8.57 or later, in devDependencies for these files. Without it they cannot be linted.
Processors#
A processor takes code out of files that are not JavaScript, such as the code blocks of a Markdown file. bun lint calls the processor, lints each block with the configuration for its name, such as README.md/0_example.js, and lets the processor put the problems where they belong. --fix fixes the blocks in the file if the processor supports it.
import markdown from "@eslint/markdown";
export default [
{ files: ["**/*.md"], plugins: { markdown }, processor: "markdown/markdown" },
{ files: ["**/*.md/*.js"], rules: { "no-var": "error" } },
];Only the processor runs in JavaScript. The built-in rules lint the blocks as native code. Rules that need types do not run on a file with a processor.
.eslintrc#
Where there is no eslint.config.js, bun lint reads the configuration files of ESLint 8, as ESLint 8 does:
.eslintrc.js,.eslintrc.cjs,.eslintrc.yaml,.eslintrc.yml,.eslintrc.json,.eslintrc, andeslintConfiginpackage.json.extendsof files, of packages such as"next/core-web-vitals", and ofplugin:configurations.plugins, their rules, environments and processors, and--resolve-plugins-relative-to.- The files of the parent directories count too, up to one with
root: true. .eslintignore, andeslintIgnoreinpackage.json.- Of a directory,
.jsfiles are linted, and whatoverridesor--extname. A pattern such as"src/**/*.ts"lints every file that it matches. - Symbolic links to directories are followed, and a link that leads nowhere is passed over. A link that leads to itself or back up ends the run, as it ends ESLint 8:
ELOOP: too many symbolic links encountered. /* eslint-env */comments,--env,--rulesdir,--no-eslintrc,--ignore-path, andESLINT_USE_FLAT_CONFIG.eslint:recommendedandeslint:allare those of ESLint 8.57.- Without
parser, the code is read as ESLint 8 reads it: as a script in ES5, unlessparserOptionsorenvsay otherwise.let ais thenParsing error: Unexpected token a. - A rule that ESLint 8 does not have is reported in every file:
Definition for rule 'x' was not found.
The rules are those of ESLint 10, with the default options, the option schemas, the fixes and the suggestions of ESLint 8.57.
The rules of typescript-eslint are those of version 8. Where version 5, 6 or 7 is installed, they have the default options of that version, and a line on stderr says so.
--print-config prints the configuration as a flat one.
Replace oxlint#
bun lint reads .oxlintrc.json and oxlint.config.ts, including plugins, categories, env, globals, overrides, ignorePatterns, extends, options, and nested configuration files. It takes the flags of oxlint, lints the same files, and prints the same report with the same exit code.
Change the lint script
{
"scripts": {
"lint": "bun lint --deny-warnings"
}
}Keep the flags and the arguments. oxlint --deny-warnings src becomes bun lint --deny-warnings src.
If the project runs ESLint too, add --flavor=oxlint: oxlint --fix becomes bun lint --flavor=oxlint --fix. Without it, bun lint stops and asks.
Change the script before you try the command. While the script says oxlint, bun lint runs the script, so it runs oxlint.
Run it
bun run lintIf the run ends with The configuration asks for what cannot be done yet, the lines below that name what bun lint left out.
Change CI, lint-staged, and Git hooks
Replace oxlint, npx oxlint, and bunx oxlint with bun lint.
Write a configuration file if there is none
bun run lint --initWithout a configuration file, bun lint knows from the oxlint dependency that it stands in for oxlint. --init writes oxlint's defaults to an .oxlintrc.json, so the choice stays when you remove the package.
Remove the packages
bun remove oxlint oxlint-tsgolintimport { defineConfig } from "oxlint" in an oxlint.config.ts keeps working without the package. The rules that need types use the type checker that is built into Bun.
Keep oxlint if the oxc extension of your editor uses it. bun lint has no language server.
| Instead of | Run |
|---|---|
oxlint | bun lint |
oxlint -D no-console src | bun lint -D no-console src |
oxlint --type-aware | bun lint --type-aware |
oxlint --type-check | bun lint --type-check |
oxlint --deny-warnings | bun lint --deny-warnings |
oxlint --import-plugin | bun lint --import-plugin |
oxlint --fix-suggestions | bun lint --fix-suggestions |
oxlint -f json | bun lint -f json |
oxlint --rules | bun lint --rules |
oxlint --init | bun lint --init |
bun lint takes every flag of oxlint 1.87 except --lsp. See Differences from oxlint.
--rules -f json lists the rules as oxlint does, with any configuration or none. Its default is true for the rules that oxlint turns on without a configuration file, which are not those of bun lint.
If you run oxlint && eslint, say which tool each command replaces: bun lint --flavor=oxlint && bun lint --flavor=eslint. See A project with both tools.
In a project with vite-plus in its package.json and no configuration file of a linter, bun lint reads what vp lint reads: lint in the vite.config.ts.
Rules#
The rules of oxlint's plugins are built in: unicorn, oxc, react, react-perf, jsx-a11y, nextjs, import, promise, node, jest, and vitest. These 500 rules do what oxlint 1.87 does, with its options, messages, and fixes. The 22 React Compiler rules of react, such as react/refs and react/set-state-in-effect, run the React Compiler that is built into Bun. With an eslint.config.js these names mean the plugins that the configuration imports.
The rules that oxlint shares with ESLint and typescript-eslint are ports of the originals. With an .oxlintrc.json they report what oxlint reports, at its places, with its messages. In the few cases where they still differ, bun lint does what ESLint does.
The plugins in jsPlugins are loaded and run. As in oxlint, a rule that is written n/.. is the one of the plugin that is called n, and node/.. is the one that is built in. If rules ran in JavaScript that are built in, a note after the report says so. Run bun lint --rules to list the rules that are built in. A rule that is not built in cannot be linted.
What an .oxlintrc.json changes#
Where an .oxlintrc.json is the configuration, bun lint behaves like oxlint, not like ESLint:
- It skips what the
.gitignorefiles of the repository ignore, even a directory that is named as an argument, and follows symbolic links.node_modulesis skipped because.gitignorehas it. - A file that cannot be opened is an error in the report, and the other files are linted. ESLint stops there.
--ignore-pathtakes the name of a file, not a path: it is looked for in every directory, as.eslintignoreis.- A configuration file with a key that oxlint does not know is an error.
- If the command line or a configuration file cannot be used, the exit code is
1. - Every format but
prettyandjson-with-metadataprints what oxlint prints: paths from the working directory, columns in bytes, rules aseslint(no-var). Scripts and problem matchers written for oxlint keep working. - The default format is
pretty. In GitHub Actions it isgithub, and no annotations are added to another format. - Everything is printed on stdout: the report, the summary line,
No files found to lint, and why a configuration file cannot be used. Only a command line that cannot be read, and warnings about whatbun lintleaves out, go to stderr. --quietand--silenthide problems. They still count, for--max-warnings,--deny-warnings, and the exit code.- Suppressions in
oxlint-suppressions.jsonthat no longer occur are an error in the report, so the exit code is1. - Rules that need types are skipped without
--type-aware, and nothing is said about it. - An argument that matches nothing is not an error, as long as there is a file to lint. If there is none, the exit code is
1. --report-unused-disable-directiveswarns.--fixapplies the fixes once. ESLint lints again, up to ten times. What is left is reported where it was before the fixes.- The
<script>blocks of.vueand.sveltefiles are linted, and the frontmatter and the<script>blocks of.astrofiles. Each block is linted by itself. The template is not read, sono-unused-varsis off in these files, andprefer-constis off in.vueand.sveltefiles.
Bun's own rules#
The rules that start with bun/ exist only in bun lint. All of them are off until a configuration file turns them on. No plugin has to be named.
ESLint and oxlint do not know these rules. They refuse a configuration file or a comment that names one.
{
"rules": {
"bun/no-eager-dynamic-import": "error",
"bun/no-env-at-module-scope": ["warn", { "allow": ["NODE_ENV", "CI"] }]
}
}Several rules are about code that runs while a module is evaluated: everything outside functions, plus functions that are called where they are written, static fields and static blocks. That code runs on every start, whether or not its result is used.
bun/no-eager-dynamic-import#
An import() outside any function starts to load at once. It looks lazy and is not.
const parser = import("./parser"); // ✗
const loadParser = () => import("./parser"); // ✓vi.mock(import("./module")) is left alone: that is how Vitest is told which module to mock.
bun/no-eager-native-addon#
A native addon (*.node) that is imported or required outside any function is loaded on every start.
const addon = require("./build/Release/addon.node"); // ✗
let addon;
const getAddon = () => (addon ??= require("./build/Release/addon.node")); // ✓bun/no-env-at-module-scope#
An environment variable that is read while a module is evaluated is fixed before anything can set it, such as a test or a .env loader. The rule knows process.env, Bun.env and import.meta.env.
const port = process.env.PORT; // ✗
const getPort = () => process.env.PORT; // ✓| Option | Default | Meaning |
|---|---|---|
allow | ["NODE_ENV"] | Variables that can be read anywhere |
NODE_ENV is allowed by default because bundlers replace it with a constant.
bun/no-literal-temp-dir#
/tmp does not exist on Windows, and TMPDIR can point elsewhere.
const file = "/tmp/report.json"; // ✗
const file = join(tmpdir(), "report.json"); // ✓| Option | Default | Meaning |
|---|---|---|
directories | ["/tmp", "/var/tmp"] | The directories that are reported |
bun/no-negative-repeat-count#
"x".repeat(n) throws a RangeError if n is negative. A count that comes out of a subtraction needs a lower bound. The rule suggests one.
const padding = " ".repeat(width - label.length); // ✗
const padding = " ".repeat(Math.max(0, width - label.length)); // ✓bun/no-redundant-catch-type#
With strict or useUnknownInCatchVariables, the variable of a catch clause is unknown already. --fix removes the annotation.
try {
run();
} catch (error: unknown) {} // ✗bun/no-useless-spread-fallback#
Spreading false, null or undefined into an object does nothing, so the empty object is allocated for nothing. The rule suggests &&.
const options = { ...(verbose ? { level: "debug" } : {}) }; // ✗
const options = { ...(verbose && { level: "debug" }) }; // ✓| Option | Default | Meaning |
|---|---|---|
ignoreConditionsThatCall | [] | Conditions that call one of these functions are left alone, such as feature from bun:bundle |
bun/no-array-just-to-count#
.split(",").length builds an array of all the pieces to count them. The rule reports separators of one character.
const columns = row.split(",").length; // ✗| Option | Default | Meaning |
|---|---|---|
countFunction | none | A function (text, separator) => number that counts occurrences. --fix calls it wherever it is in scope |
bun/prefer-builtin-sleep#
A promise that only waits for a timer is Bun.sleep(). The rule suggests it.
await new Promise(done => setTimeout(done, 250)); // ✗
await Bun.sleep(250); // ✓bun/react-prefer-updater-function#
State that a callback has captured can be out of date when the setter is called. A function gets the current state.
setRows([...rows, row]); // ✗
setRows(previous => [...previous, row]); // ✓bun/prefer-lazy-schema#
A validation schema that is built outside any function costs time on every start, whether or not something is validated. By default the rule looks at what starts with z, as Zod is usually imported.
const User = z.object({ name: z.string() }); // ✗
const getUser = once(() => z.object({ name: z.string() })); // ✓| Option | Default | Meaning |
|---|---|---|
roots | ["z"] | The names that a schema starts with |
methods | [] | Methods that make a schema of another one, such as extend |
factorySuffix | none | Functions whose name ends like this make a schema |
lazyWrapper | none | A function of yours that the message recommends |
bun/no-side-effects-on-import#
A statement outside any function that calls something, or assigns to a property, makes import do more than define things. Bundlers cannot drop such a module. module.exports and exports are left alone.
connect(); // ✗
export function start() {
connect(); // ✓
}| Option | Default | Meaning |
|---|---|---|
allow | [] | Calls that are fine, as they are written: "console.log" |
bun/no-cwd-dependent-path#
What a relative path means to node:fs, path.resolve(), Bun.file() and Bun.write() depends on the directory that the process was started in.
const text = readFileSync("./schema.sql", "utf8"); // ✗
const text = readFileSync(new URL("./schema.sql", import.meta.url), "utf8"); // ✓The patterns of fs.glob() are left alone.
| Option | Default | Meaning |
|---|---|---|
functions | [] | More functions whose first argument is a path |
bun/no-unportable-commands#
The rule looks at the commands that node:child_process and Bun.spawn() start, where they are written out:
npm,npx,pnpm,pnpxandyarnare.cmdscripts on Windows, which need a shell.- Windows has no
ls,grep,rmand so on. &&,|,>and the like are read by a shell, and which shell that is depends on the system.- The options of
psdiffer from system to system. This is reported in any string.
spawn("npm", ["install"]); // ✗
execSync("rm -rf dist"); // ✗
await rm("dist", { recursive: true, force: true }); // ✓| Option | Default | Meaning |
|---|---|---|
scripts | the five above | Commands that are scripts on Windows |
posixOnly | 33 utilities of POSIX | Commands that Windows does not have |
launchers | [] | More functions whose first argument is a command |
bun/no-console-window-flash#
A child process gets a console window of its own on Windows, which opens and closes at once. It does not if windowsHide is set, or if the child shares a stream with its parent ("inherit").
spawn("git", ["status"]); // ✗
spawn("git", ["status"], { windowsHide: true }); // ✓bun/no-unseeded-random-in-property-test#
In a property of fast-check, values that come from fc.sample() or Math.random() are not decided by the seed. A failure cannot be replayed or shrunk.
fc.property(fc.nat(), a => check(a, fc.sample(fc.nat(), 1)[0])); // ✗
fc.property(fc.nat(), fc.nat(), (a, b) => check(a, b)); // ✓bun/react-no-namespace-hooks#
--fix imports the hook by name.
const [count, setCount] = React.useState(0); // ✗
const [count, setCount] = useState(0); // ✓bun/react-no-latest-value-ref#
React can render a component more than once, or throw a render away, so a ref must not change while it renders. An effect that only copies a value into a ref is what useEffectEvent is for.
function Search({ query }) {
const lastQuery = useRef(query);
lastQuery.current = query; // ✗
}bun/jsx-no-forbidden-nesting#
By default the rule knows what HTML rules out: <a> and <button> inside <a> or <button>, <form> inside <form>, <label> inside <label>.
<a href="/">
<button>Home</button> {/* ✗ */}
</a>| Option | Default | Meaning |
|---|---|---|
pairs | see above | { "outer": "Text", "inner": "View" }: no <View> anywhere inside a <Text> |
bun/consistent-directive-prefix#
bun lint and oxlint read both eslint-disable and oxlint-disable comments. ESLint reads only its own. --fix rewrites the others.
| Option | Default | Meaning |
|---|---|---|
prefix | "eslint" | "eslint" or "oxlint" |
bun/no-restricted-text#
Reports text that matches a regular expression. It looks at text as it is written in the source, not at identifiers.
{
"rules": {
"bun/no-restricted-text": [
"error",
{ "patterns": [{ "pattern": "\\bTODO\\b", "message": "Open an issue instead.", "in": ["comments"] }] }
]
}
}| Of a pattern | Default | Meaning |
|---|---|---|
pattern | The regular expression | |
flags | "u" | Its flags |
message | What to say | |
in | all | "strings", "templates", "jsxText", "comments", "regularExpressions" |
bun/prefer-timeout-signal#
An AbortController that nothing but a timer aborts, and of which nothing but the signal is used, is what AbortSignal.timeout() makes.
const controller = new AbortController(); // ✗
setTimeout(() => controller.abort(), 2000);
await fetch(address, { signal: controller.signal });
await fetch(address, { signal: AbortSignal.timeout(2000) }); // ✓| Option | Default | Meaning |
|---|---|---|
factories | [] | Functions of yours that make a controller |
bun/prefer-url-for-sibling-files#
Bundlers follow new URL("./file", import.meta.url) to the file and copy it. They cannot follow a path that is joined onto import.meta.dirname or __dirname.
const schema = join(import.meta.dirname, "schema.sql"); // ✗
const schema = new URL("./schema.sql", import.meta.url); // ✓A path that ends in .., . or / names a directory and is left alone.
bun/react-no-local-render-helpers#
A component that makes a function such as renderCell in its body, and uses it in its JSX, makes it anew on every render. React cannot tell what it renders from the component around it. It should be a component of its own.
function Table({ rows }) {
const renderCell = row => <tr key={row.id} />; // ✗
return <table>{rows.map(renderCell)}</table>;
}| Option | Default | Meaning |
|---|---|---|
pattern | "^render[A-Z]" | A regular expression for the names of helpers |
bun/prefer-local-module#
For a project that wraps a package in a module of its own. --fix rewrites the import.
{
"rules": {
"bun/prefer-local-module": [
"error",
{
"modules": [{ "package": "axios", "use": "@/http", "default": "http" }],
"exemptFiles": ["src/http.ts"]
}
]
}
}import axios from "axios"; // ✗
import { http as axios } from "@/http"; // ✓| Of a module | Meaning |
|---|---|
package | The package |
use | The specifier to import in its place, as it is to be written |
default | The name under which your module exports the default export of the package |
names | { "theirName": "yourName" } for exports that have another name in your module |
exemptFiles are the ends of paths: "src/http.ts" is every file of that name in a directory src.
bun/no-unbounded-requests#
axios has no timeout unless it is given one: a server that never answers holds the request for ever. A request, and an instance made with axios.create(), needs a timeout above 0 or a signal.
await axios.get("/rows"); // ✗
await axios.get("/rows", { timeout: 0 }); // ✗
await axios.get("/rows", { timeout: 8000 }); // ✓
await axios.get("/rows", { signal }); // ✓A configuration that is not written out in the call is left alone, and so is a file that sets axios.defaults.timeout.
| Option | Default | Meaning |
|---|---|---|
clients | [] | { "module": "./http", "names": ["http"] }: what else takes the arguments of axios. Without names: the default export |
bun/no-unhandled-emitter-errors#
An EventEmitter that emits error and has no listener for it throws the error, which ends the process. So a child process that cannot be started, or a file that cannot be opened, takes the program down.
spawn("convert", [file]).on("exit", done); // ✗
spawn("convert", [file]).on("exit", done).on("error", fail); // ✓
await pipeline(createReadStream(file), target); // ✓It looks at the functions of Node.js that begin to work when they are called and return an object with a documented 'error' event: spawn() and fork(), cluster.fork(), fs.createReadStream(), createWriteStream() and watch(), request() and get() of http and https, http2.connect(), net.connect() and createConnection(), tls.connect(). What is returned, stored in an object or handed to a function is left alone.
| Option | Default | Meaning |
|---|---|---|
factories | [] | { "module": "./observer", "names": ["observe"] }: what else returns an emitter. Without names: the default export |
bun/no-cast-to-brand#
A branded type, such as string & { readonly brand: unique symbol }, says that a value has been checked. A cast says so without a check. With this rule only the module that declares the type casts to it, and only the listed files import the function that makes one of any value.
{
"rules": {
"bun/no-cast-to-brand": [
"error",
{ "branded": [{ "module": "src/ids", "type": "UserId", "mint": ["toUserId"], "mintedIn": ["src/session.ts"] }] }
]
}
}import type { UserId } from "../ids";
const id = text as UserId; // ✗
const all = texts as readonly UserId[]; // ✗The type is found in arrays, tuples and unions, under another name, through import * as ids and through an alias.
| Key | Meaning |
|---|---|
module | The end of the path of the file that declares the type, without the extension |
type | The name of the type |
mint | The functions of the module that make a value of the type without a check |
mintedIn | The ends of the paths of the files that may import them, besides the module |
bun/format#
The same rule for a project without Prettier: it reports where a file differs from what bun format prints, whatever is installed. The [format] section of bunfig.toml counts, as for bun format.
A file that it cannot check gets one report, which says why: the configuration names a plugin of Prettier that is not built in, or the file has classes that sortTailwindcss sorts. Only bun format --check asks Tailwind CSS for their order. Beside an oxlint configuration it does not look at .vue, .svelte and .astro files, of which only the scripts are linted.
{ "rules": { "bun/format": "error" } }| Option | Default | Meaning |
|---|---|---|
| the first | {} | Options of Prettier, which win over those of the configuration file |
usePrettierrc | true | Whether the configuration file and .editorconfig count |
fileInfoOptions | {} | ignorePath, withNodeModules |
Rules for tests#
The rules that start with bun:test/ are about the tests that bun test runs. They are the rules that oxlint has for Jest and Vitest, with the same names and options. They know what bun:test has, such as test.if, test.skipIf, test.todoIf, test.failing, and test.serial.
A rule looks at a file only if the file imports from "bun:test": with import, or with require or await import at the top level. A test file that uses test and expect without importing them gets none of these rules. In a file with the import, these count as Bun's:
- What the file imports from
"bun:test", under any name. test,it,describe,expect,expectTypeOf,beforeAll,beforeEach,afterEach,afterAll,jest,vi,xit,xtest, andxdescribe, if the file does not declare them. They are mistakes there:bun testdeclares these names only in a file that imports nothing from"bun:test".
A name that the file imports from "vitest", "@jest/globals", or another module is not Bun's. Most jest/ and vitest/ rules of oxlint pass over what a file imports from "bun:test".
Without a configuration file, these six rules are on. bun:test/no-disabled-tests is a warning, so a test that is skipped on purpose does not fail bun lint. The others are errors.
| Rule | Reports |
|---|---|
bun:test/no-focused-tests | .only |
bun:test/no-disabled-tests | .skip, xit, xtest, xdescribe, and a test without a function. Not .skipIf |
bun:test/no-export | An export in a file with tests that is named like a test, such as a.test.ts |
bun:test/valid-describe-callback | A describe whose callback is missing, is no function, or takes parameters |
bun:test/valid-expect | An expect without a matcher, and .resolves or .rejects that nothing awaits |
bun:test/valid-expect-in-promise | An expect in a .then() that the test neither awaits nor returns |
A configuration file replaces these defaults. It turns a rule on by its name. No plugin has to be named, and no category of oxlint has these rules.
export default [
{
rules: {
"bun:test/no-focused-tests": "error",
"bun:test/expect-expect": "warn",
},
},
];--rule and /* eslint */ comments take the name with or without quotes: /* eslint bun:test/no-focused-tests: "error" */.
The other 59 rules are off until a configuration turns them on:
consistent-test-filename, consistent-test-it, expect-expect, max-expects, max-nested-describe, no-alias-methods, no-commented-out-tests, no-conditional-expect, no-conditional-in-test, no-conditional-tests, no-done-callback, no-duplicate-hooks, no-hooks, no-identical-title, no-interpolation-in-snapshots, no-large-snapshots, no-restricted-jest-methods, no-restricted-matchers, no-standalone-expect, no-test-prefixes, no-test-return-statement, no-unneeded-async-expect-function, padding-around-after-all-blocks, padding-around-test-blocks, prefer-called-once, prefer-called-times, prefer-called-with, prefer-comparison-matcher, prefer-describe-function-title, prefer-each, prefer-ending-with-an-expect, prefer-equality-matcher, prefer-expect-assertions, prefer-expect-resolves, prefer-expect-type-of, prefer-hooks-in-order, prefer-hooks-on-top, prefer-lowercase-title, prefer-mock-promise-shorthand, prefer-mock-return-shorthand, prefer-snapshot-hint, prefer-spy-on, prefer-strict-boolean-matchers, prefer-strict-equal, prefer-to-be, prefer-to-be-falsy, prefer-to-be-truthy, prefer-to-contain, prefer-to-have-been-called, prefer-to-have-been-called-times, prefer-to-have-length, prefer-todo, require-hook, require-mock-type-parameters, require-test-timeout, require-to-throw-message, require-top-level-describe, valid-title, and warn-todo.
Where Bun differs from Jest and Vitest, so do the rules:
expect(value, message)takes a message, andexpect().pass()andexpect().fail()take no value. A fix keeps the message..not,.resolves, and.rejectscome in any order.expect.resolvesToandexpect.rejectsToare modifiers too.testanddescribeneed no title, and take options or a timeout before or after the callback.- The callback of
describecan beasyncand can return something. The title can be a function, a class, or a number. prefer-called-with,prefer-comparison-matcher,prefer-describe-function-title,prefer-expect-resolves,prefer-strict-boolean-matchers, andprefer-to-containsuggest their change.--fixdoes not apply it, because a passing test can fail afterwards.- No fix changes a title in a file with
toMatchSnapshot(), becausebun testfinds a snapshot by the titles. no-exportallows the export of a type.- The fixes of
consistent-test-itandno-test-prefixeswriteit,test, ordescribeonly if the file imports that name. test.if(a)("b", ..)andtest.if(!a)("b", ..)are not identical titles.expect-expectcountsexpectandexpectTypeOf.require-test-timeoutknowssetDefaultTimeout()andjest.setTimeout().require-hookallowssetDefaultTimeout(),mock.module(), andexpect.extend()outside hooks.no-restricted-jest-methodsandrequire-mock-type-parametersare about bothjestandvi.require-mock-type-parametershas nocheckImportFunctions, andno-large-snapshotshas noallowedSnapshots.
What cannot be linted#
A configuration can ask for more than bun lint can do:
- A rule of ESLint or oxlint that is not built in.
- Files in another language, without the
eslintpackage. - A rule in JavaScript that asks for types.
- A rule that only a
/* eslint plugin/rule: "error" */comment turns on, of a plugin that has no rule on in the configuration.
bun lint lints everything else, prints the report, and then fails with exit code 2 and an error that names what it left out. It never reports less than the tool it replaces without saying so.
error: The configuration asks for what cannot be done yet. All else was linted.
3 files were not linted, only JavaScript and TypeScript can be (2 *.css, 1 *.json): a.css, b.css, package.json. The package "eslint" lints other languages, if it is installed
--allow-unsupported makes this a warning.With --allow-unsupported these lines are warnings, and the exit code is that of the report.
Exit codes#
| Code | With a configuration of ESLint, or none | With a configuration of oxlint |
|---|---|---|
0 | No errors | No errors |
1 | Errors, or more warnings than --max-warnings | The same. Also warnings with --deny-warnings, no file to lint, and a command line or configuration that cannot be used |
2 | A command line or configuration that cannot be used, an argument that matches no file, suppressions that no longer occur | Never, as in oxlint |
2 | The configuration asks for what cannot be linted | The same |
2 | Both tools have a configuration, and nothing says which one the run is for | The same |
2 | A flag of the other tool that means nothing with this configuration | The same |
Which configuration file#
No tool reads the configuration files of another. So bun lint first decides which tool it stands in for. It looks in the working directory, and in each parent directory, for these files:
- oxlint:
.oxlintrc.json,.oxlintrc.jsonc,oxlint.config.ts,oxlint.config.mts - ESLint:
eslint.config.js,.mjs,.cjs,.ts,.mts,.cts - ESLint 8, if there is none of the above:
.eslintrcfiles
If one tool has a file there, that file decides. No flag except --flavor, -c, and --no-config-lookup makes bun lint leave it out: see Flags of the other tool.
Only the files of that tool count, also in the directories further down. For each file, bun lint uses the nearest of them: it looks in the directory of the file, then in each parent directory. Apart from the files of ESLint 8, configuration files are not merged.
Use -c to use one file for everything, or --no-config-lookup to use none.
bun lint -c eslint.ci.config.jsBun runs eslint.config.js in a separate process, where process.argv is the command line of bun lint. A TypeScript configuration file needs no extra package.
bunfig.toml#
The [lint] section of bunfig.toml sets the defaults of the flags. A key is the name of the flag in camelCase.
[lint]
maxWarnings = 0 # --max-warnings 0
inlineConfig = false # --no-inline-config
ignorePatterns = ["vendor/"] # --ignore-pattern vendor/
[lint.rules] # --rule
"no-console" = "warn"
eqeqeq = ["error", "smart"]The command line overrides bunfig.toml. bunfig.toml overrides the configuration file where the flag does.
A key does not say which tool the run stands in for, as a flag of only one tool does. Use flavor for that. A key that means nothing to that tool has no effect.
Bun reads the bunfig.toml of the working directory. Write bun --config=other.toml lint for another file.
See bunfig.toml for every key.
A project with both tools#
A project that runs oxlint && eslint has files of both. Say which tool each run stands in for:
{
"scripts": {
"lint": "bun lint --flavor=oxlint && bun lint --flavor=eslint"
}
}A flag that only one of the two tools has says so too, such as -D, --deny-warnings, and --type-aware of oxlint, or --cache and --ext of ESLint. Most commands of oxlint have none: oxlint, oxlint ., and oxlint --fix become bun lint --flavor=oxlint, with the same arguments.
If nothing says so, bun lint lints nothing and exits with code 2. A run for the wrong tool would leave out the rules of the other without a word.
error: Both /app/eslint.config.js and /app/.oxlintrc.json are here. Say which one this run is for: --flavor=oxlint or --flavor=eslint.oxlint needs no configuration file. So bun lint asks the same in a project with an eslint.config.js and oxlint in the dependencies of package.json. There a flag of oxlint does not answer: only --flavor=oxlint leaves the eslint.config.js out.
Flags of the other tool#
bun lint takes the flags of both tools. With a configuration file of one tool, a flag of the other keeps its meaning.
| With a configuration of ESLint | Does |
|---|---|
--silent | Prints no problems. The exit code stays |
--deny-warnings | Exits with 1 if there are warnings, like --max-warnings 0 |
-A, -W, -D <rule> | The same as --rule '<rule>: off', warn, and error |
--disable-nested-config, --fix-suggestions, --fix-dangerously, and --type-check work too. With a configuration of oxlint, so do --rule, --global, --parser-options, --no-inline-config, --fix-type, --fix-dry-run, --stdin, and -o.
A flag that can mean nothing there is an error, and bun lint exits with code 2:
- With a configuration of ESLint:
-A,-W, and-Dwith a category orall, and--import-plugin,--disable-oxc-plugin, and the like. - With a configuration of oxlint:
--ext,--parser,--plugin, and--report-unused-inline-configs. - With an
eslint.config.jsor a configuration of oxlint:--env,--rulesdir,--no-eslintrc, and--resolve-plugins-relative-toof ESLint 8.
error: --import-plugin is a flag of oxlint. It means nothing with /app/eslint.config.js, a configuration file of ESLint.No configuration file#
Without any configuration file, bun lint uses the defaults of oxlint if the command line has a flag that only oxlint has, or if package.json depends on oxlint. Run bun lint --init to write these defaults to an .oxlintrc.json. Such a flag does not leave out a configuration file in a directory further down.
A configuration file is a program, or names programs. So where the working directory has none, in it or above it, a configuration file counts only for what is in or below the working directory, and for a file or a directory that you name. What a pattern such as "../**/*.js" or "/**/*.js" finds elsewhere is linted with the defaults, and nothing of its project is loaded.
Choose what to lint#
Pass files, directories, or glob patterns. Without arguments, bun lint lints the current directory.
bun lint src/index.ts
bun lint src test
bun lint "src/**/*.test.ts"In a directory, bun lint lints the files that a files pattern of the configuration matches, plus .js, .mjs, and .cjs files. node_modules and .git are always skipped.
In a project with an eslint.config.js, an argument that leads out of the project, such as .. or "/**/*.js", is searched as ESLint searches it. Where a directory has no eslint.config.js in it or above it, only the working directory is entered from there, and a file there ends the run with exit code 2. So the configuration file of another project does not run, unless you name that project.
To ignore files, add ignores to the configuration, or use --ignore-pattern:
export default [{ ignores: ["dist/", "**/*.generated.ts"] }];bun lint --ignore-pattern "dist/"If you name a file that is ignored, bun lint warns about it. lint-staged does that often: add --no-warn-ignored.
Rules that need types#
Rules such as @typescript-eslint/no-floating-promises need type information. bun lint gets it from the type checker of bun check, which is built in and uses every CPU core. You don't need the typescript package.
These rules run if the configuration asks for types, as for typescript-eslint:
import tseslint from "typescript-eslint";
export default tseslint.config(...tseslint.configs.recommendedTypeChecked, {
languageOptions: { parserOptions: { projectService: true } },
});Or use --type-aware, whatever the configuration says. Without a configuration file, that flag selects typescript-eslint/recommended-type-checked.
bun lint --type-awareBun checks each file with the tsconfig.json your editor uses for it: the nearest one. Use -p to name another. bun lint does not report type errors. Run bun check for those, or add --type-check, as for oxlint:
bun lint --type-aware --type-checkFix problems#
--fix applies the fixes that rules offer and writes the files.
bun lint --fix
bun lint --fix --fix-type layout,suggestion--fix-dry-run fixes without writing. With -f json, the fixed code is in output.
Adopt a rule in an old codebase#
Turn the rule on as an error, then record the errors that exist today. bun lint tolerates those and reports new ones, like ESLint's bulk suppressions.
bun lint --suppress-rule @typescript-eslint/no-explicit-anyThat writes eslint-suppressions.json. Commit it. When you have fixed some of the errors, bun lint exits with code 2 until you remove them from the file:
bun lint --prune-suppressionsRun in CI#
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun lint --max-warnings 0In GitHub Actions, bun lint also prints a workflow command for each problem. GitHub shows them as annotations on the lines of the pull request diff. They go to stdout after a report for people (stylish, pretty), or alone if --output-file takes the report. They are never in that file, and never among the output of a format that programs read.
::error file=src/index.ts,line=3,col=7,endLine=3,endColumn=12,title=prefer-const::'count' is never reassigned. Use 'const' instead.- Install first.
eslint.config.jsimports packages such astypescript-eslint. - The report goes to stdout. Warnings about the run, and the summary line, go to stderr.
- Warnings don't fail the job. Use
--max-warnings 0if they should. - An empty run fails. If an argument matches no file,
bun lintexits with code2.
Check before you commit#
With lint-staged:
{
"lint-staged": {
"*.{js,ts,tsx}": "bun lint --fix --no-warn-ignored"
}
}If package.json has a lint script#
Your script wins. bun lint runs it, like bun run lint.
A lint script in package.json, a file such as lint.ts, or node_modules/.bin/lint keeps the name: bun lint runs that, as it did before. A package called lint somewhere in node_modules, or a directory called lint without an index file, does not.
{
"scripts": {
"lint": "bun lint --max-warnings 0"
}
}Inside the lint script, and in any script it starts, bun lint is the linter. That makes the script above work.
The first line of the output tells you that the script ran: $ bun lint --max-warnings 0.
In another script, bun lint runs the lint script too, with the flags added. With the script above, "lint:fix": "bun lint --fix" runs bun lint --max-warnings 0 --fix.
A version of Bun that has no linter runs "lint": "bun lint" again and again, without end. If not everyone on your
team has upgraded, keep eslint in the script until they have.
Output#
Choose a format with -f.
| Format | What it prints |
|---|---|
stylish | ESLint's default format. The default |
pretty | The source code around each problem, like bun check in a terminal |
json | ESLint's JSON format |
json-with-metadata | The same, with the metadata of the run |
unix | One line per problem: file:line:column: message [Error/rule] |
github | Workflow commands for GitHub Actions |
agent | A tagged block per problem, with the source lines |
checkstyle | Checkstyle XML |
junit | JUnit XML |
gitlab | GitLab's Code Quality report |
sarif | SARIF 2.1.0 |
stylish and json are byte for byte what ESLint prints, so problem matchers, editors, and scripts written for ESLint keep working. checkstyle, junit, gitlab, and sarif are what oxlint prints. With an .oxlintrc.json, all formats are oxlint's.
2 | export function total(items) {
3 | let count = items.length;
^
error: 'count' is never reassigned. Use 'const' instead. prefer-const
at src/index.ts:3:7Above 50 problems, pretty and agent show each distinct problem once, with how often and where it occurs. Use --all to show every problem.
AI agents#
When AGENT=1, CLAUDECODE=1, or REPL_ID=1 is set, the default format is agent. With an .oxlintrc.json that is oxlint's agent: one line per problem. There the variables that oxlint looks at count too, such as AI_AGENT, CURSOR_AGENT, GEMINI_CLI, and CODEX_SANDBOX, unless AGENT is set.
<error file="src/index.ts" line="3" column="7" rule="prefer-const" fixable="true">
'count' is never reassigned. Use 'const' instead.
<source>
2 | export function total(items) {
3 | let count = items.length;
^^^^^
4 | return count;
</source>
</error>Differences from ESLint#
bun lint aims to report exactly what ESLint 10 with typescript-eslint 8 reports. If it reports something else for a rule it supports, that is a bug in Bun. Open an issue on GitHub with code that shows the difference.
These differences are intentional:
- No configuration file is not an error. ESLint stops.
bun lintuses defaults. In a project with aneslint.config.js, a file outside of it that has none is an error, as in ESLint. - The configuration file of the working directory always runs, even if every argument is in another project. ESLint runs only those of the files that it lints.
- The rules are those of ESLint 10.12, typescript-eslint 8.71, eslint-plugin-react-hooks 7.1, eslint-plugin-import 2.32, eslint-plugin-n 18.4, and eslint-plugin-react 7.37, whichever versions you have installed. If a plugin is installed in an older version, a note after the report says so.
- The deprecated formatting rules do not run.
semi,quotes,indent, and the 71 other rules about layout that ESLint has deprecated are not built in, nor isreact/jsx-space-before-closing.bun formatdoes their work. A configuration or a comment that names one is not an error,--print-configprints it, and a note on stderr says which did not run. The rules of@stylistic/eslint-pluginrun like those of any other plugin. - Custom formatters do not load. The rules, the processors, the parsers and the languages of plugins run.
--inspect-configand--mcpare errors:bun lint does not support --inspect-config.--initis oxlint's. It writes an.oxlintrc.json. In a project with a configuration file of ESLint it is an error.- One parser. Bun's parser reads all files. The wording of a syntax error can differ from espree's.
- A rule that needs types never stops the run. If the configuration does not ask for types, typescript-eslint throws.
bun lintskips the rule and says so. - A file that no project includes is still linted. If
parserOptions.projectis a path, a pattern or a list of them, each file is checked with the first of thesetsconfig.jsonthat includes it, as typescript-eslint does with--fixand in an editor. A file that none includes is checked with its nearesttsconfig.json; ESLint stops there. Without--fix, ESLint also accepts a listed project that only imports the file. - Types are those of TypeScript 7, as in
bun check, whichever version oftypescriptyou have installed. --cacheand--statshave no effect. Every run lints every file.--print-configleaves out the default options of rules.- A rule reports at most 65,536 problems in one file, and at most 256 MB of messages and fixes. One more problem then says that the rest are not shown.
--fixleaves a file as it is if the fixes would make it larger than 64 MB and than 64 times its size. A warning says so.--fixdoes not write a file that no longer parses. If the fixes of a pass leave a syntax error,bun linttakes them back, reports the problems as not fixed, and names the rules in a warning. ESLint writes the file.--fixdoes not write through a link that leads out of the repository, or out of the working directory if that is in no repository. ESLint and oxlint do. The run fails and names the file.- A file that TypeScript finds too large for control flow analysis (TS2563) gets a warning. What rules that need types say about it may be wrong or incomplete, as in typescript-eslint, which says nothing.
- A configuration file that leaves a timer or a server running does not keep the run alive. ESLint prints its report and never exits.
- What a configuration file prints is shown only if it fails. ESLint runs the file in its own process, so a
console.login it ends up in the report. - A summary line on stderr says how many files were linted and how long it took.
Differences from oxlint#
With an .oxlintrc.json, bun lint aims to report exactly what oxlint 1.87 reports. If it reports something else, that is a bug in Bun. Open an issue on GitHub with code that shows the difference.
These differences are intentional:
- The rules are those of oxlint 1.87, whichever version you had installed. A project on an older version sees what it would see after the upgrade.
- No language server.
--lspis an error:bun lint does not support --lsp. - The rules of
jsdocandvueare not built in. A configuration can name the two plugins and theirsettings, and turn their rules off. If it turns one of their rules on by name, the run fails with exit code2like for any other rule that is not built in. If it only lists the plugin, so that a category such ascorrectnesswould turn its rules on, a note on stderr says that they did not run, in every format, and the exit code stays as it is. The<script>blocks of.vuefiles are linted with all other rules. - A rule that is not built in does not stop the run. oxlint refuses the configuration.
bun lintruns the other rules, and then fails with exit code2. A configuration that is written for a newer oxlint still runs. - The default format is
pretty, which looks likebun check. Every other format prints what oxlint prints. - Files and problems are sorted. oxlint prints a file when a thread is done with it.
- One parser. Bun's parser reads all files. The wording and the place of a syntax error can differ. oxlint refuses some programs that
bun lintlints, such aslet a; let a;. - Types come from the type checker of
bun check, not fromoxlint-tsgolint. Both follow TypeScript 7.bun lintsays nothing about atsconfig.jsonthat cannot be loaded. - In a file with a byte order mark, what needs types is reported where it is. oxlint places it 3 bytes too far to the left.
checkstyle,junit, andgithubescape the names of files and rules. oxlint prints them as they are, so a name with"or<breaks its report.- A problem that is at no place in a file keeps its message and its severity in
unix,github,gitlab,junit, andcheckstyle. oxlint prints an empty message there, and calls an error a warning. --initleaves an.oxlintrc.jsonthat exists as it is. oxlint writes over it.- More is accepted: an argument with
..in it,--max-warnings -1, and a configuration file that starts with a byte order mark. oxlint refuses them. - A pattern in the options of a rule is a JavaScript regular expression. oxlint compiles it with Rust's
regex, where(?x),\z, and[[:alpha:]]mean something else. no-useless-assignmentfollows ESLint's code paths. oxlint has an analysis of its own, which answers differently in a few corners. It reports the firstaofa = (a = f()) ? a.b : 1, theid++ofclass A { x = id++ }, and assignments in unreachable code. It says nothing abouta = a ? f(a) : aas the last use ofa.- The rules about the ways through a function follow ESLint's code paths.
constructor-super,no-this-before-super,array-callback-return,getter-return,no-fallthrough, andno-unreachableof oxlint walk a control flow graph of its own, which answers differently in a few corners. Therefor (;;) {}andwhile (true) {}can end, and acatchis reached whatever is in thetry: oxlint reportsa.map(() => { for (;;) {} })andget a() { try { return 1; } catch (e) {} }, and not thef()oftry { return; } catch (e) {} f();. It reports a getter whosetryblock runs on into afinally, asget a() { try { f(); } finally { g(); } return 1; }. It says nothing about aswitch (a ? b : c)that falls through, or about the secondsuper()oftry { super(); } catch (e) { super(); }. --fixdoes not write a file that no longer parses.bun lintleaves the file as it is, reports the problems as not fixed, and names the rules in a warning. oxlint writes the file:foo(...[,])becomesfoo(,).--fixdoes not write through a link that leads out of the repository, as under Differences from ESLint.- The limits on problems and fixes in Differences from ESLint apply too.
CLI usage#
bun lint [flags] [...files, directories or patterns]| Flag | Description |
|---|---|
-c, --config <path> | Use this configuration file instead of looking for one |
--no-config-lookup | Do not look for a configuration file |
--flavor <tool> | The tool that the run stands in for: eslint or oxlint |
--rule <rule> | Configure a rule: --rule 'eqeqeq: [error, smart]' |
--global <names> | Define global variables: --global a,b:true |
--parser-options <options> | Set parser options: --parser-options projectService:true |
--ext <extensions> | Lint files with these extensions too |
--fix | Fix what can be fixed, and write the files |
--fix-dry-run | Fix without writing |
--fix-type <types> | Only directive, problem, suggestion, or layout fixes |
--fix-suggestions | Apply suggestions, as oxlint does. Without --fix, nothing else |
--fix-dangerously | Apply every fix and suggestion, as oxlint does |
--ignore-pattern <pattern> | Ignore the files that match |
--no-ignore | Lint ignored files too |
--no-warn-ignored | Do not warn about an ignored file that is named as an argument |
--stdin, --stdin-filename <path> | Lint the code on standard input, as that file |
--quiet | Report errors only |
--max-warnings <n> | Exit with 1 if there are more warnings than this |
-f, --format <name> | The output format |
--all | Show every problem. Without it, pretty groups them above 50 |
-o, --output-file <path> | Write the report to a file |
--color, --no-color | Always or never use colors |
--no-inline-config | Ignore eslint-disable and other configuration comments |
--report-unused-disable-directives | Report eslint-disable comments that disable nothing |
--suppress-all, --suppress-rule <rule> | Record the errors that exist in eslint-suppressions.json |
--prune-suppressions | Remove from that file what no longer occurs |
--no-error-on-unmatched-pattern | Do not fail if an argument matches no file |
--pass-on-no-patterns | Exit with 0 if there are no arguments |
--exit-on-fatal-error | Exit with 2 if a file cannot be parsed |
--allow-unsupported | Only warn about what cannot be linted |
--native-plugin-rules <plugins> | Plugins that stay built in |
--no-native-plugin-rules | Run the rules of the installed plugins, not the built-in ones |
--print-config <path> | Print the configuration of a file, and lint nothing |
--type-aware, --no-type-aware | Run or skip the rules that need types |
--infer-globals, --no-infer-globals | Take the globals of a file from the types of its project, or not |
--type-check | With --type-aware: report type errors too |
-p, --project <path> | The tsconfig.json for the rules that need types |
-A, -W, -D <rule or category> | Allow, warn about, or deny a rule, or a category of oxlint |
--<name>-plugin | Turn a plugin of oxlint on: --import-plugin, --react-plugin |
--disable-<name>-plugin | Turn unicorn, oxc, or typescript off |
--deny-warnings | Exit with 1 if there are warnings |
--silent | Print no problems |
--ignore-path <path> | A file with patterns to ignore, in place of .eslintignore |
--disable-nested-config | Use the configuration file of the working directory for every file |
--init | Write an .oxlintrc.json with the defaults of oxlint |
--rules | List the rules that are built in |
--threads <n> | Number of threads. Default: one per CPU core |
--timing | Print how long each phase took, and how much JavaScript was loaded |
--cwd <path> | Set the working directory |
-h, --help | Print the help menu |