Skip to content

perf(ban-dependencies): look up import paths instead of scanning all mappings - #156

Merged
43081j merged 3 commits into
e18e:mainfrom
tony-scio:perf/ban-dependencies-lookup
Sep 27, 2026
Merged

43081j merged 3 commits into
e18e:mainfrom
tony-scio:perf/ban-dependencies-lookup

Conversation

@tony-scio

@tony-scio tony-scio commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is a performance-only change to ban-dependencies. It does not add features or change which imports are flagged.

On main, each import is compared against every mapping in every manifest with Object.entries(...) plus startsWith. Most imports (react, ./foo, and so on) match nothing, so each one pays for about 876 preset comparisons before the rule gives up. This runs for every import in every linted file.

This PR replaces that scan with direct key lookups. For an import such as foo/bar/baz, findMapping checks foo/bar/baz, then foo/bar, then foo in the existing mappings object. That is 1–4 lookups per import, no matter how large the manifests are.

Reviewer guide

  • findMapping is the only new logic. Its for loop ends because end only decreases.
  • Object.hasOwn prevents inherited object properties such as constructor from matching.
  • Subpath matching is unchanged: main already matches subpaths through startsWith(`${moduleName}/`).
  • One edge case: if custom modules overlap (for example, foo and foo/bar), foo/bar/baz now reports the more specific name, foo/bar. It is still flagged either way. The built-in presets have no overlapping keys, so they are not affected.

New tests

The new test cases add coverage. They do not test new features.

  • 4 of the 5 new cases pass on both main and this PR: unknown subpaths, allowed package subpaths, built-in package subpaths, and overlapping custom modules listed as ['oogabooga/subpath', 'oogabooga'].
  • 1 case covers the edge case above: overlapping custom modules listed as ['oogabooga', 'oogabooga/subpath']. On main, it reports oogabooga. On this PR, it reports oogabooga/subpath.

Benchmark

Oxlint 1.83.0 with 12 threads on a 17,654-file TypeScript workspace, with only e18e/ban-dependencies enabled. Median of 5 interleaved runs:

Time
main 16.2s
This PR 3.7s (4.3× faster)

All runs reported the same diagnostics.

Validation

  • npm run build, npm run lint, and npm test (1,011 tests) pass.
  • Coverage for ban-dependencies.ts is not lower: branches stay at 85.71% (36/42), and statements go from 96.49% to 96.82%.
  • In the benchmark workspace, main and this PR report the same 4 diagnostics at the same locations, with the same messages.

— sent via Glean Tau

@tony-scio
tony-scio marked this pull request as ready for review September 14, 2026 05:21
@tony-scio

Copy link
Copy Markdown
Contributor Author

@43081j

@tony-scio

Copy link
Copy Markdown
Contributor Author

@webpro wdyt? This had a big win on our downstream repo. Happy to iterate if you have feedback.

@webpro

webpro commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Thanks, Tony! I can confirm it runs faster and without issues (against two real-world consumers). No findings in the implementation either.

@43081j

43081j commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

just working through my pile of notifications. ill get to this soon 👍

Comment thread src/rules/ban-dependencies.ts Outdated
Replace the Set pre-check plus ordered scan with a Map from module name to
its mapping and manifest position. Walk the import's parent paths once and
keep the earliest-listed match, preserving existing precedence.
@tony-scio
tony-scio requested a review from 43081j September 25, 2026 01:47
Comment thread src/rules/ban-dependencies.ts Outdated
let candidate = source;
while (true) {
const entry = manifest.mappings.get(candidate);
if (entry && (!match || entry.index < match.index)) {

@43081j 43081j Sep 26, 2026 •

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.

this still doesn't seem quite right 🙈

  1. im not sure we care what the "index" is, this doesn't seem to gain us anything
  2. if foo/bar comes before foo/bar/baz in the manifest, this loop continues until it reaches the foo/bar one rather than stopping

at this point im wondering if this is actually just Object.entries(mappings). i.e. this loop just iterates the entries (for...of) and breaks when it finds a match, slices on / if it doesn't.

remembering this PR is two things in one:

  1. ability to have nested mappings
  2. some performance gain

for 2, do we still have such a gain now that we're just turning an object into a map or array?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Apologies for the back and forth on this and thanks for the patient reviews.

My intent w/ this PR is only the speedup since it's quite significant in our repo (as you can see). I think I got a little carried away w/ the changes before.

Here's a smaller diff that's as close to the original as I can keep it and still get the full benefit. Please take a look at the rewritten PR description for full explaination.

…ings

Replace the per-import scan of every manifest entry with direct lookups of the import path and its parent paths in the existing mappings object.
@tony-scio tony-scio changed the title perf(ban-dependencies): index replacement mappings perf(ban-dependencies): look up import paths instead of scanning all mappings Sep 27, 2026
@tony-scio
tony-scio requested a review from 43081j September 27, 2026 00:59
@43081j
43081j merged commit a506204 into e18e:main Sep 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants