Skip to content

bun lint: compare a joined directory as a path, not as glob syntax - #46913

Open
robobun wants to merge 4 commits into
mainfrom
robobun/a76942fd/lint-glob-literal-dir
Open

robobun wants to merge 4 commits into
mainfrom
robobun/a76942fd/lint-glob-literal-dir

Conversation

@robobun

@robobun robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In a folder named [proj], the import/no-restricted-paths docs example prints No problems in 2 files at exit 0 with only a bunfig.toml.
  • Three places compile a directory with the pattern after it, so [proj] is a class: Zone::new (src/lint/plugins/rules/import_no_restricted_paths.rs:87), Scope::new (src/lint/graph/lib.rs:254) and movedPattern (src/lint/js_plugin/worker/eslint.js:101).

Fix

  • The directory is now a path in tsconfig scope and in the basePath of ignore patterns (ESLint 9.30+). The second commit does the same for the import rules without an ESLint or oxlint config.
  • [proj] now gives what proj gives. oxlint 1.89 agrees in 6 of 6 rows (main 1). ESLint 9.39.5 agrees in 8 of 8 folder names (main 3).
  • Verified: test/cli/lint/glob.test.ts (41 new cases fail on main), 12 oxlint 1.87.0 oracle rows, all of test/cli/lint.
  • Self-reviewed: 30 concerns raised, 26 addressed. Rejected: another matcher for the resolver route (changes plain folders), a cut in bun_glob (Do not read the directory of a sideEffects glob or a --filter path as glob syntax #44939's files), Windows share roots (no machine).

Background

Downsides

  • Plain folders change: a tsconfig.json entry that names src/[id].ts applies to that file, as in oxlint.
  • [proj] now shares the faults of proj with two import/resolver projects per file: 12 of 90 rows no longer equal the resolver, and the exit code can differ between runs.
  • +3,024 bytes of text. ESLint 9.30+ with ignore patterns from another directory: +36 M instructions per run, +42,000 per file.
Notes

Where this comes from. There is no user report. The fault showed up during the work on #44939, which covers sideEffects globs and --filter paths. #46439 is open to ship bun lint in release builds.

Commits.

  • lint: read the directory of a tsconfig.json and of --ignore-pattern as a path: conformance with oxlint and ESLint.
  • lint: without an ESLint or oxlint config, the import rules read the working directory as a path: Bun's own behaviour, and the commit that fixes the docs example. Without this part, the first part is still complete.
  • Two later commits change one test key for Windows and make each new comment one line.
  • src in total: +269 -79 in 11 files.

The places, against the tool that each one follows.

Place Reference main This PR
Scope::new, with an .oxlintrc.json oxlint 1.89.0 equal in 1 of 6 rows 6 of 6. Two of the fixed rows are in a plain folder
reviveOverride (worker) ESLint 9.39.5 and 10.13.0 equal in 3 of 8 folder names 8 of 8. A plain folder is fixed for '**' then '!keep.txt'
Scope::new, with settings["import/resolver"].typescript and 2 or more projects ESLint 9.39.5, eslint-import-resolver-typescript 4.4.5 21 of 70 rows differ 7 of 70, all in s*. [proj] gives what proj gives
Zone::new, GlobsFromCwd with an ESLint config ESLint 9.39.5 and 10.13.0, eslint-plugin-import 2.32.0 equal in 5 of 5 folder names unchanged
Zone::new, GlobsFromCwd without a config none no report at exit 0, 2 false reports the reports of proj

What splits a joined pattern. paths::split_joined takes one name off the directory for each .. that leads out of the option text. What is left of the directory is a path. The rest is what the option says, so it stays a pattern. ../lib/src in [ws]/app keeps [ws] as a path. A bunfig.toml entry app/[locale]/test/** is a pattern from every working directory, also from inside app/[locale], where the driver writes ../../app/[locale]/test/**. An absolute option is a pattern as a whole in a zone. A tsconfig.json entry that is absolute is cut only if it goes through the directory of the file.

And #44939. Its bun_glob::literal_base_len takes the longest prefix that the two strings share. The two rules differ where text leaves the directory and comes back by name: ../[x] inside [x] is a path there and a pattern here. With the shared-prefix rule, the test with four working directories fails from inside app/[locale]. The tree needs one rule. This branch does not touch src/glob/lib.rs, so #44939 is the place to settle it. split_joined can move to bun_glob then, and Under stays in lint.

Considered and dropped. The import-rule change under every config, and a change of the ESLint 8 ignore lists. In both, ESLint equals main: with a flat config and the real plugin, bun lint equals ESLint 9.39.5 and 10.13.0 in proj, [proj], {a,b}, c++(old) and app@(v2), on main and with this PR. Also basePath only for folder names with glob syntax: then '**' with '!keep.txt' stays wrong in a plain folder.

tsconfig scope, with an .oxlintrc.json.

  • oxc-resolver compares the directory of a tsconfig.json as a path where no include is written, and it compares an entry with the file path before it matches the entry as a pattern. Both are ported.
  • 12 new oracle rows in test/cli/lint/oracle/plugins/oxlint/projects.ts, recorded with oxlint 1.87.0 and equal with 1.89.0: h21 to h28, r11 to r14.
  • Plain folders change. include: ["src/a.ts", "src/[id].ts"] and exclude: ["src/[id].ts"] now apply to the file src/[id].ts.
  • In [x], oxlint applies no include pattern and no exclude pattern. bun lint keeps both (rows h12, h13, h27, h28, r13). So a tsconfig.json without include and with exclude: ["b"] gives more reports in [x] than in x, as in oxlint.
  • In a folder named s* or C#, oxlint 1.89.0 drops the paths of a tsconfig.json. bun lint does not, on main and here. That is not ported: the [x] case has recorded rows, this one is a loss of every alias.

tsconfig scope, with the TypeScript resolver.

  • The directory of a tsconfig.json is a path now. What follows it is still a pattern as oxc-resolver reads it. The resolver uses get-tsconfig, where a [ or a { is never syntax.
  • 77 rows (11 folder names, 7 include forms) against the real tools. 7 rows are in a?b, where Bun cannot load a config file (see bun test fails while searching for test files if path contains '?' #7928, Decide in _resolve whether a ? is part of a path or starts a query #36627, bun -e: keep the whole path of the entry module when the cwd has a ? #45048). Of the other 70, main differs in 21 and this PR in 7, all in s*.
  • A second set of 61 rows: main differs in 33, this PR in 28. The 28 are plain-folder differences, for example include: ["app/[locale]"].
  • bun lint uses the closest project that includes the file, alone. The resolver does that once the project has resolved an import. Before, it also tries the next projects. 90 rows with two projects for one file: main differs from the resolver in 42, this PR in 30. 12 rows were equal on main and differ now, all in [ws] and {a,b}, where the first import of the file is an alias of the farther project. 24 rows go the other way. Rows that depend on the folder name: 36 on main, 0 here.
  • For a file that no project includes, the resolver itself depends on the folder name. bun lint gives the result of a plain folder everywhere.
  • Where include names one of two files of a directory, the exit code changes from run to run in proj, on main too. [proj] now does the same. The cause is a cache with one entry for each directory. A probe with include: ["a", "b/t.ts"] and the same import in b/t.ts and b/u.ts, 24 runs each: main exits with 1 in 7 runs in proj and in 0 runs in [proj]. This PR exits with 1 in 5 and in 6 runs.
  • The resolver has no single result there. eslint b/t.ts b/u.ts reports nothing and eslint b/u.ts b/t.ts reports both files, because eslint-module-utils 2.14.0 caches a resolution by the directory of the file. A cache entry for each file would give b/u.ts alone, which is what ESLint reports when it lints each file alone. That rule changes plain folders, so it is not in this PR.
  • The fix for that route is a port of get-tsconfig's createFilesMatcher, which the resolver calls. It changes results in plain folders, so it is a change of its own. bun_sema's Roots::has is a port of TypeScript's getFileNamesFromConfigSpecs, which is another rule.

ESLint worker.

  • ESLint 9.39.5 pushes { basePath: cwd, ignores: ignorePatterns } (lib/config/config-loader.js). ESLint 9.29.0 joins the path into each pattern.
  • bun lint now passes basePath when the installed ESLint is 9.30 or later and the directory is not the base path of the config array. That is for --ignore-pattern and for ignorePatterns of bunfig.toml, from below and from above the config file.
  • --ignore-pattern '**' --ignore-pattern '!keep.txt' from sub: ESLint lints keep.txt, main prints No matching configuration found. Written as sub/**, the first pattern ignores the directory itself.
  • ESLint before 9.30: unchanged. In [pkg] a file that a pattern lets in and that goes to that ESLint gets No matching configuration found. ESLint 9.29.0 does not lint it.

Import rules.

  • The rule instances are shared by all files of a run, and one run can have files with and without an ESLint config. So the zones and globs are built once for each kind (reads_cwd_as_path).
  • Only the pattern that a rule joins to the working directory changes. A glob that a rule matches as written still sees the whole path. So with devDependencies = ["**/test/**"] a project below a folder named test loses the report, on main and here. The keys are devDependencies, optionalDependencies, peerDependencies, bundledDependencies, allow of import/no-unassigned-import, except of import/no-restricted-paths, exceptions of import/no-import-module-exports, and allow and forbid of import/no-internal-modules.
  • .oxlintrc.json: unchanged. oxlint 1.89.0 does not have import/no-restricted-paths or import/no-extraneous-dependencies, so no tool says what the result is there.
  • The docs examples with "root": true are the ones whose options are paths. They now run below a directory named [p]{a,b}+(c). rule-docs.test.ts fails on main for import/no-restricted-paths and passes here.

Measured. Release builds with --lto=off of main 179664e and of this PR. bun lint --threads=1 on 3,000 files, user instructions per file from perf_event_open, the minimum of 11 interleaved runs. The working directory has 24 or 224 bytes.

Corpus main, 24 bytes PR main, 224 bytes PR
bunfig.toml, import/no-restricted-paths off 45,062 45,062 46,073 46,073
bunfig.toml, 8 zones with a plain target 47,728 47,620 49,071 48,928
bunfig.toml, 8 zones with a glob target 57,752 52,378 97,386 53,715
bunfig.toml, 8 devDependencies globs 46,001 46,009 47,005 47,011
.eslintrc.json, 8 zones with a glob target 63,293 63,219 102,862 102,787
.eslintrc.json, 8 devDependencies globs 50,901 50,900 51,840 51,842
.oxlintrc.json, no tsconfig.json 140,070 140,003 149,143 149,143
.oxlintrc.json, tsconfig.json without include 142,457 138,472 174,664 146,698
.oxlintrc.json, include: ["src"] 142,685 142,795 174,860 174,970
  • The last row costs 110 instructions more per file. The globs of devDependencies cost 6 to 8 more. Rows that run the same code on both sides move by up to 67.
  • Allocation calls (gdb breakpoint counts on the mi_malloc entries, 200 and 400 files): the same count per file in all nine corpora. valgrind is not in the build container, so there are no dhat numbers.
  • size: text 102,140,288 to 102,143,312 bytes, data and bss unchanged.
  • Worker route, real ESLint 9.39.5, --ignore-pattern 'gen/**' from a directory below the config file, every file sent to ESLint. 300 files: 1.043 G to 1.091 G instructions. 600 files: 1.228 G to 1.289 G. Medians of 15 and 9 interleaved runs. Two control runs without basePath move by 2 M and 5 M. The cost is what ESLint's config array does for an object with a basePath.

Tests.

  • glob.test.ts: 60 new cases (55 on Windows). 41 fail on main. The other 19 are plain names and pins: what ESLint reports with an ESLint config in [proj], a basePath with a wildcard, an absolute target, a bunfig.toml entry that names app/[locale] from 4 working directories, my.app, include: ["src"] in [proj], a file that no project includes, ESLint 9.29 and 8.57.
  • The tests for the installed ESLint use a stand-in package, as js-parsers.test.ts does. It ignores like @eslint/config-array 0.21.2: the last pattern decides, an object with a basePath is for what is below it, and nothing below an ignored directory is linted. It says whether it got a basePath.
  • With a release build, all 29 files of test/cli/lint pass. One timing test of the React Compiler rules fails in the full parallel run, on main too, and passes alone.
  • Nothing was run on Windows by hand. split_joined has rows in the Windows style. From the code, not from a run: on a Windows share, a tsconfig.json without include now applies in a plain folder, and its include and exclude patterns stay what they are on main.

Still folder-dependent, on purpose or left for later.

  • The import rules with an ESLint or oxlint config, as in ESLint.
  • Globs that the import rules match as written, as above. A dot-folder above the project is the same case: the ** of minimatch does not cross it.
  • ESLint before 9.30 on the worker route.
  • include and exclude patterns next to an .oxlintrc.json in [x], as in oxlint.
  • Absolute patterns that a config program builds from its own directory, absolute globs on the command line, and the project list of the TypeScript resolver.
  • full_glob for .editorconfig in bun fmt, for a backslash in a folder name only.

Not done from the self-review.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/lint/glob.test.ts

…s a path

With an .oxlintrc.json, a tsconfig.json without `include` applies to the
files below its directory, and an `include` or `exclude` entry that names
a file applies to that file. oxlint compares paths in both cases. `bun
lint` compiled the directory and the entry as one pattern, so a directory
named `[x]` or a file named `[id].ts` was a character class there.

With settings["import/resolver"].typescript, the directory of a
tsconfig.json is a path in its `include` and `exclude` patterns, as in
eslint-import-resolver-typescript.

For the files that go to the installed ESLint, the patterns of
`--ignore-pattern` and of bunfig.toml get their directory as `basePath`
if that ESLint is 9.30 or later, which is what ESLint passes itself. An
older ESLint gets the directory in front of each pattern, as before.
…orking directory as a path

`import/no-restricted-paths`, `import/no-extraneous-dependencies` and
`import/no-unassigned-import` join the working directory in front of the
paths and globs of their options, and compile the result. In a directory
named `[proj]`, `{a,b}` or `c++(old)` a zone then matched no file, and
neither did a glob.

eslint-plugin-import does the same, so with an ESLint or oxlint config
nothing changes. Without one, the working directory is compared as a
path, and only what the options say is a pattern.

The docs examples that are their own project now run below a directory
named `[p]{a,b}+(c)`. The page of `import/no-restricted-paths` says how
a folder named `[id]` is written in a zone.
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 179664e and 10ac5fb.

Files selected for processing: 16
  • docs/lint/rules/import/no-restricted-paths.mdx
  • docs/lint/rules/reports.json
  • src/codegen/lint-rule-docs.ts
  • src/lint/driver/for_tests.rs
  • src/lint/glob.rs
  • src/lint/graph/lib.rs
  • src/lint/js_plugin/eslint.rs
  • src/lint/js_plugin/worker/eslint.js
  • src/lint/paths.rs
  • src/lint/plugins/import_minimatch.rs
  • src/lint/plugins/rules/import_no_extraneous_dependencies.rs
  • src/lint/plugins/rules/import_no_restricted_paths.rs
  • src/lint/plugins/rules/import_no_unassigned_import.rs
  • test/cli/lint/glob.test.ts
  • test/cli/lint/oracle/plugins/oxlint/expected.json
  • test/cli/lint/oracle/plugins/oxlint/projects.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The change updates lint path handling for glob-like directory names. It adds path splitting utilities, changes TypeScript project scope matching and import-rule glob matching, and adjusts ESLint override conversion by installed version.

Changes

Glob-aware lint path resolution

Layer / File(s) Summary
Path splitting primitives
src/lint/paths.rs, src/lint/glob.rs, src/lint/driver/for_tests.rs
Path helpers split joined paths using platform separators. The test runner reports split results for resolved, joined, and included paths.
TypeScript project scope matching
src/lint/graph/lib.rs, test/cli/lint/oracle/plugins/oxlint/*, test/cli/lint/glob.test.ts
Project scopes distinguish pattern-based and path-based interpretation. TypeScript resolver selection uses path-based scopes. Fixtures cover glob-like project and file paths.
CWD-aware import-rule glob matching
src/lint/plugins/import_minimatch.rs, src/lint/plugins/rules/import_*.rs, test/cli/lint/glob.test.ts, docs/lint/rules/import/no-restricted-paths.mdx, docs/lint/rules/reports.json, src/codegen/lint-rule-docs.ts
Import-rule matching uses the file’s path mode and supports patterns joined below the working directory. Tests and documentation cover glob-like directory names and restricted-path patterns.
ESLint override version handling
src/lint/js_plugin/eslint.rs, src/lint/js_plugin/worker/eslint.js, test/cli/lint/glob.test.ts
Override conversion passes basePath to supported ESLint versions and rewrites ignore patterns for older versions. Tests cover versions 8, 9, and 10.

Suggested reviewers: jarred-sumner

Priority: Normal

Merge Risk: Minimal · up to 10ac5

No actionable issue remains from this review; the change is ready for normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title names the core change: a joined directory is compared as a path rather than as glob syntax in bun lint. The raw summary, objectives, and description all describe this same change to direct…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is complete and directly addresses the required content. It explains what the PR changes, the problem, scope, tradeoffs, remaining limitations, and verification results. It does not us…
Full details: Title check

Explanation

The title names the core change: a joined directory is compared as a path rather than as glob syntax in bun lint. The raw summary, objectives, and description all describe this same change to directory and pattern handling (tsconfig scope, ESLint basePath, import rules). The title is specific and concise, and it is not vague.

Full details: Description check

Explanation

The description is complete and directly addresses the required content. It explains what the PR changes, the problem, scope, tradeoffs, remaining limitations, and verification results. It does not use the template headings exactly, but it includes equivalent information, including how the changes were verified.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:01 PM PT - Oct 25th, 2026

✅ @robobun, your commit 10ac5fb32680e3c22414c6635816d5507ae5ea3b passed in Build #132764! 🎉


🧪   To try this PR locally:

bunx bun-pr 46913

That installs a local version of the PR into your bun-46913 executable, so you can run:

bun-46913 --bun

@robobun

robobun commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Status

How I reproduced it: the example of the import/no-restricted-paths docs page with only a bunfig.toml, in folders named proj, [proj], {a,b}, c++(old) and app@(v2). On main, bun lint reports the import in proj and prints No problems in 2 files at exit 0 in the other four. With this PR, all five folders give the report at exit 1.

For the tsconfig scope and for --ignore-pattern, I compared bun lint with oxlint 1.87.0 and 1.89.0 and with ESLint 9.39.5 and 10.13.0 in the same folders.

PR: #46913

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/lint/graph/lib.rs Resolved
Comment thread src/lint/paths.rs Resolved
…in its path

On Windows `path.join` makes `pkg\eslint.config.mjs` of it, and the
harness only makes the directory of a file whose path has a `/`.
Comment thread src/codegen/lint-rule-docs.ts Resolved
Comment thread src/lint/glob.rs Resolved
Comment thread src/lint/graph/lib.rs Resolved
Comment thread src/lint/graph/lib.rs Resolved
Comment thread src/lint/graph/lib.rs Resolved
Comment thread src/lint/graph/lib.rs Resolved
Comment thread src/lint/graph/lib.rs Resolved
Comment thread src/lint/js_plugin/worker/eslint.js Resolved
Comment thread src/lint/js_plugin/worker/eslint.js Resolved
Comment thread src/lint/paths.rs Resolved
Comment thread src/lint/plugins/import_minimatch.rs Resolved
Comment thread src/lint/plugins/import_minimatch.rs Resolved
Comment thread src/lint/plugins/import_minimatch.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

Still open from earlier reviews (2):

  • Unresolved: 2 minor or pre-existing.

The worker reads the ESLint version in `reviveOverride`, where it writes `basePath`. The record of `eslintFrom` is as on main.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant