Skip to content

perf(linter/eslint/max-depth): derive node types#23896

Merged
camc314 merged 3 commits into
mainfrom
perf/max-depth-node-types
Jun 28, 2026
Merged

perf(linter/eslint/max-depth): derive node types#23896
camc314 merged 3 commits into
mainfrom
perf/max-depth-node-types

Conversation

@connorshea

@connorshea connorshea commented Jun 28, 2026

Copy link
Copy Markdown
Member

Run on VS Code before and after to compare call #s and also to ensure that the number of violations were identical (which they are). Generated with Claude Code, tested and reviewed by me.

This rule is the last rule (after the other open PR is also merged) in the Rust set that runs on every node in a given codebase. All other rules had less than 15.2M calls when run on the vscode codebase. --debug=timings doesn't include type-aware rules, so I haven't looked at those at all, and I'm not sure we can limit AST Nodes like this for those rules anyway.

What

eslint/max-depth matched on AST kinds inside its should_count helper rather than inline in run, so the linter codegen couldn't derive a NODE_TYPES bitset and emitted None. That placed the rule in the any_type bucket, dispatching run on every AST node.

This restructures run to lead with a narrowing match node.kind() that the codegen's EarlyDivergeDetector recognizes, so codegen now emits a bitset of the 9 statement kinds the rule actually acts on. should_count/should_stop are unchanged, so the IfStatement parent-guard and ancestor-counting logic — and thus the rule's behavior — are identical.

Results

Measured on 11,674 files with only this rule enabled:

Calls Rule time
Before 15,251,059 223.1 ms
After 157,335 5.5 ms

~97× fewer run calls, ~40× faster.

Test plan

  • cargo test -p oxc_linter max_depth passes (behavior unchanged)
  • cargo lintgen regenerates the bitset; generated impl now carries the 9 node types

🤖 Generated with Claude Code

`eslint/max-depth` matched on AST kinds inside its `should_count` helper
rather than inline in `run`, so the linter codegen couldn't derive a
`NODE_TYPES` bitset and emitted `None`. That placed the rule in the
`any_type` bucket, dispatching `run` on every AST node.

Restructure `run` to lead with a narrowing `match node.kind()` that the
`EarlyDivergeDetector` recognizes, so codegen emits a bitset of the 9
statement kinds the rule acts on. `should_count`/`should_stop` are
unchanged, so behavior is identical.

Measured on 11674 files with only this rule enabled:
- calls: 15,251,059 -> 157,335 (~97x fewer)
- rule time: 223.1ms -> 5.5ms (~40x faster)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the A-linter Area - Linter label Jun 28, 2026
@connorshea connorshea changed the title perf(linter): derive node types for eslint/max-depth perf(linter/max-depth): derive node types for eslint/max-depth Jun 28, 2026
@connorshea connorshea marked this pull request as ready for review June 28, 2026 14:09
@connorshea connorshea requested a review from camc314 as a code owner June 28, 2026 14:09
Comment thread crates/oxc_linter/src/rules/eslint/max_depth.rs
@codspeed-hq

codspeed-hq Bot commented Jun 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 66 skipped benchmarks1


Comparing perf/max-depth-node-types (4646499) with main (0b3021f)

Open in CodSpeed

Footnotes

  1. 66 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

The `match node.kind()` added for `NODE_TYPES` derivation made the
following `if should_count(...)` gate redundant. Fold the only behaviour
the gate added over the match — skipping an `else if`'s `IfStatement`
(its parent is an `IfStatement`) — into the match as a diverging arm,
flattening `run` by a level. `should_count` is retained for the ancestor
filter, which must still test arbitrary node kinds.

Codegen still derives the 9-kind bitset; behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@camc314 camc314 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.

🚀

@camc314 camc314 changed the title perf(linter/max-depth): derive node types for eslint/max-depth perf(linter/eslint/max-depth): derive node types Jun 28, 2026
@camc314 camc314 self-assigned this Jun 28, 2026
@camc314 camc314 merged commit 4ac7a8e into main Jun 28, 2026
30 checks passed
@camc314 camc314 deleted the perf/max-depth-node-types branch June 28, 2026 14:23
camc314 added a commit that referenced this pull request Jun 29, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 88f4455 str: [**BREAKING**] `Str` and `Ident` methods take
`&GetAllocator` (#23781) (overlookmotel)

### 🚀 Features

- f2091b3 ast: Unify old and new `AstBuilder`s (#23875) (overlookmotel)
- 1c8f50c linter: Add schema for `eslint/no-restricted-import` (#23642)
(Sysix)

### 🐛 Bug Fixes

- 7cb85c4 linter/eslint/no-negated-condition: Add autofix for negated
conditions (#23825) (Yagiz Nizipli)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d891990 linter/jsx-a11y/role-supports-aria-props: Ignore nullish prop
values (#23865) (Mikhail Baev)
- 94b6599 linter: Deduplicate missing plugin errors (#23853) (camc314)
- eff3eff linter/oxc/branches-sharing-code: Avoid else-if false
positives (#23843) (camc314)
- 2a2d3b9 linter/eslint/prefer-destructuring: Skip
`AssignmentExpression` autofixes (#23818) (camc314)
- ddc24ae linter/eslint/id-length: Respect checkGeneric for mapped type
keys (#23802) (bab)
- cd89202 linter/react/exhaustive-deps: Skip wrapper expression when
analyzing hook initializers (#23793) (camc314)
- 20e8285 linter/unicorn/prefer-native-coercion-function: Allow ts type
predicates (#23774) (camc314)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)
- 52032cf linter: Newline-terminate tsgolint errors (#23762) (Mikhail
Baev)
- 368fda7 linter/eslint/no-warning-comments: Avoid dropping generated
regex patterns (#23741) (camc314)
- ce44fbd linter/valid-title: Escape disallowed words regex (#23742)
(camc314)
- 3100d11 linter/prefer-called-exactly-once-with: Avoid out-of-bounds
slice panic at end of file (#23625) (Jerry Zhao)
- 742be36 refactor/node/handle-callback-err: Reject invalid regex config
(#23740) (camc314)
- d7be179 linter/eslint/no-restricted-globals: Handle shadowed locals
(#23736) (camc314)
- b3b1ff8 linter/vitest/expect-expect: Handle global vitest detection
correctly (#23734) (camc314)

### ⚡ Performance

- 68f9472 linter/jsx-a11y: Skip lowercasing non-aria attribute names
(#23906) (Lawrence Lin)
- b9312b4 linter/unicorn/prefer-export-from: Use keyed binding lookup
(#23893) (Marius Schulz)
- cd5204e linter/typescript/no-unsafe-declaration-merging: Use keyed
binding lookup (#23894) (Marius Schulz)
- e948498 linter/eslint/prefer-named-capture-group: Only dispatch for
relevant node types (#23868) (Connor Shea)
- 4ac7a8e linter/eslint/max-depth: Derive node types (#23896) (Connor
Shea)
- daeed09 linter/eslint/no-restricted-globals: Only scan unresolved
references (#23890) (camc314)
- e808514 linter/jest-vitest: Speed up no-standalone-expect (#23883)
(camc314)
- 8b165e5 linter/react/exhaustive-deps: Skip non-reactive calls early
(#23882) (camc314)
- 54005e7 linter/eslint/no-unused-vars: Precompute exported bindings
(#23881) (camc314)
- 9bc2f8c linter/unicorn/prefer-number-properties: Speed up global
checks (#23880) (camc314)
- 4ff104f linter: Optimize `require-hook` and `prefer-mock-*` rules to
run on specific node types (#23871) (Connor Shea)
- cc2213b linter: Run `no-underscore-dangle` only when relevant node
types are present (#23867) (Connor Shea)
- 3e55c21 linter/promise/always-return: Narrow to function node types
(#23878) (Connor Shea)
- 7136182 linter/jest-vitest: Speed up no-commented-out-tests (#23864)
(camc314)
- f138264 linter/eslint/no-script-url: Match javascript: prefix without
allocating (#23861) (Lawrence Lin)
- 7ef6895 linter/react/no-array-index-key: Delay index symbol lookup
(#23857) (camc314)
- 26bc171 linter/react/no-array-index-key: Match callback methods
directly (#23856) (camc314)
- 44fbbda linter/jsx-a11y/interactive-supports-focus: Check cheap
conditions first (#23854) (camc314)
- 84a5aa3 linter/eslint/no-extend-native: Skip lowercase references
early (#23851) (camc314)
- 88a74b2 linter/eslint/no-nonoctal-decimal-escape: Scan decimal escapes
as bytes (#23850) (camc314)
- fca69a8 linter: Skip traversal without this expressions (#23845)
(camc314)
- 838fd63 linter: Reduce preallocation for per-file diagnostics `Vec`
(#23705) (Marius Schulz)
- 417b506 linter/typescript/array-type: Remove full source text clone
(#23751) (Marius Schulz)

### 📚 Documentation

- 57e4469 linter/unicorn: Update prefer-dom-node-text-content rationale
(#23933) (Mikhail Baev)
- 3d61dea all: Correct capitalization in comments (#23887)
(overlookmotel)

### 🛡️ Security

- 3cdd18f deps: Update npm packages (#23690) (renovate[bot])
# Oxfmt
### 💥 BREAKING CHANGES

- 259e0cd oxfmt,formatter_graphql: [**BREAKING**] Support draft syntax
with removing prettier fallback (#23326) (leaysgur)
- accbc49 oxfmt: [**BREAKING**] Format `parser:css,less,scss` files +
css-in-js by `oxc_formatter_css` (#23321) (leaysgur)

### 🚀 Features

- dffa4b3 formatter_css: Implement `oxc_formatter_css` (#23320)
(leaysgur)
- 01de9ec oxfmt: Format `parser:graphql` files by
`oxc_formatter_graphql` (#23318) (leaysgur)
- 4e66212 formatter_graphql: Implement oxc_formatter_graphql (#23317)
(leaysgur)

### 🐛 Bug Fixes

- 67325ae formatter_css: Handle frontmatter language (#23819) (leaysgur)
- 3f355e5 formatter_graphql: Improve major prettier diffs (#23419)
(leaysgur)
- 48e2d78 formatter_css: Improve major prettier diffs (#23327)
(leaysgur)
- 8c07cad all: Enable `disable_old_builder` Cargo feature for `oxc_ast`
crate in tests (#23888) (overlookmotel)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)

### ⚡ Performance

- 4ddcba0 formatter_core: Add printable-ASCII fast path to TextWidth
(#23913) (Lawrence Lin)

### 📚 Documentation

- b4d0dc9 oxfmt,formatter,formatter_css,formatter_core: Update AGENTS.md
(#23814) (leaysgur)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
camc314 pushed a commit that referenced this pull request Jul 3, 2026
Run on VS Code before and after to compare call #s and also to ensure
that the number of violations were identical (which they are). Generated
with Claude Code, tested and reviewed by me.

This rule is the last rule (after the other open PR is also merged) in
the Rust set that runs on every node in a given codebase. All other
rules had less than 15.2M calls when run on the vscode codebase.
`--debug=timings` doesn't include type-aware rules, so I haven't looked
at those at all, and I'm not sure we can limit AST Nodes like this for
those rules anyway.

## What

`eslint/max-depth` matched on AST kinds inside its `should_count` helper
rather than inline in `run`, so the linter codegen couldn't derive a
`NODE_TYPES` bitset and emitted `None`. That placed the rule in the
`any_type` bucket, dispatching `run` on **every** AST node.

This restructures `run` to lead with a narrowing `match node.kind()`
that the codegen's `EarlyDivergeDetector` recognizes, so codegen now
emits a bitset of the 9 statement kinds the rule actually acts on.
`should_count`/`should_stop` are unchanged, so the `IfStatement`
parent-guard and ancestor-counting logic — and thus the rule's behavior
— are identical.

## Results

Measured on 11,674 files with only this rule enabled:

| | Calls | Rule time |
|---|---|---|
| Before | 15,251,059 | 223.1 ms |
| After | 157,335 | 5.5 ms |

~97× fewer `run` calls, ~40× faster.

## Test plan

- `cargo test -p oxc_linter max_depth` passes (behavior unchanged)
- `cargo lintgen` regenerates the bitset; generated impl now carries the
9 node types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
camc314 added a commit that referenced this pull request Jul 3, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 88f4455 str: [**BREAKING**] `Str` and `Ident` methods take
`&GetAllocator` (#23781) (overlookmotel)

### 🚀 Features

- f2091b3 ast: Unify old and new `AstBuilder`s (#23875) (overlookmotel)
- 1c8f50c linter: Add schema for `eslint/no-restricted-import` (#23642)
(Sysix)

### 🐛 Bug Fixes

- 7cb85c4 linter/eslint/no-negated-condition: Add autofix for negated
conditions (#23825) (Yagiz Nizipli)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d891990 linter/jsx-a11y/role-supports-aria-props: Ignore nullish prop
values (#23865) (Mikhail Baev)
- 94b6599 linter: Deduplicate missing plugin errors (#23853) (camc314)
- eff3eff linter/oxc/branches-sharing-code: Avoid else-if false
positives (#23843) (camc314)
- 2a2d3b9 linter/eslint/prefer-destructuring: Skip
`AssignmentExpression` autofixes (#23818) (camc314)
- ddc24ae linter/eslint/id-length: Respect checkGeneric for mapped type
keys (#23802) (bab)
- cd89202 linter/react/exhaustive-deps: Skip wrapper expression when
analyzing hook initializers (#23793) (camc314)
- 20e8285 linter/unicorn/prefer-native-coercion-function: Allow ts type
predicates (#23774) (camc314)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)
- 52032cf linter: Newline-terminate tsgolint errors (#23762) (Mikhail
Baev)
- 368fda7 linter/eslint/no-warning-comments: Avoid dropping generated
regex patterns (#23741) (camc314)
- ce44fbd linter/valid-title: Escape disallowed words regex (#23742)
(camc314)
- 3100d11 linter/prefer-called-exactly-once-with: Avoid out-of-bounds
slice panic at end of file (#23625) (Jerry Zhao)
- 742be36 refactor/node/handle-callback-err: Reject invalid regex config
(#23740) (camc314)
- d7be179 linter/eslint/no-restricted-globals: Handle shadowed locals
(#23736) (camc314)
- b3b1ff8 linter/vitest/expect-expect: Handle global vitest detection
correctly (#23734) (camc314)

### ⚡ Performance

- 68f9472 linter/jsx-a11y: Skip lowercasing non-aria attribute names
(#23906) (Lawrence Lin)
- b9312b4 linter/unicorn/prefer-export-from: Use keyed binding lookup
(#23893) (Marius Schulz)
- cd5204e linter/typescript/no-unsafe-declaration-merging: Use keyed
binding lookup (#23894) (Marius Schulz)
- e948498 linter/eslint/prefer-named-capture-group: Only dispatch for
relevant node types (#23868) (Connor Shea)
- 4ac7a8e linter/eslint/max-depth: Derive node types (#23896) (Connor
Shea)
- daeed09 linter/eslint/no-restricted-globals: Only scan unresolved
references (#23890) (camc314)
- e808514 linter/jest-vitest: Speed up no-standalone-expect (#23883)
(camc314)
- 8b165e5 linter/react/exhaustive-deps: Skip non-reactive calls early
(#23882) (camc314)
- 54005e7 linter/eslint/no-unused-vars: Precompute exported bindings
(#23881) (camc314)
- 9bc2f8c linter/unicorn/prefer-number-properties: Speed up global
checks (#23880) (camc314)
- 4ff104f linter: Optimize `require-hook` and `prefer-mock-*` rules to
run on specific node types (#23871) (Connor Shea)
- cc2213b linter: Run `no-underscore-dangle` only when relevant node
types are present (#23867) (Connor Shea)
- 3e55c21 linter/promise/always-return: Narrow to function node types
(#23878) (Connor Shea)
- 7136182 linter/jest-vitest: Speed up no-commented-out-tests (#23864)
(camc314)
- f138264 linter/eslint/no-script-url: Match javascript: prefix without
allocating (#23861) (Lawrence Lin)
- 7ef6895 linter/react/no-array-index-key: Delay index symbol lookup
(#23857) (camc314)
- 26bc171 linter/react/no-array-index-key: Match callback methods
directly (#23856) (camc314)
- 44fbbda linter/jsx-a11y/interactive-supports-focus: Check cheap
conditions first (#23854) (camc314)
- 84a5aa3 linter/eslint/no-extend-native: Skip lowercase references
early (#23851) (camc314)
- 88a74b2 linter/eslint/no-nonoctal-decimal-escape: Scan decimal escapes
as bytes (#23850) (camc314)
- fca69a8 linter: Skip traversal without this expressions (#23845)
(camc314)
- 838fd63 linter: Reduce preallocation for per-file diagnostics `Vec`
(#23705) (Marius Schulz)
- 417b506 linter/typescript/array-type: Remove full source text clone
(#23751) (Marius Schulz)

### 📚 Documentation

- 57e4469 linter/unicorn: Update prefer-dom-node-text-content rationale
(#23933) (Mikhail Baev)
- 3d61dea all: Correct capitalization in comments (#23887)
(overlookmotel)

### 🛡️ Security

- 3cdd18f deps: Update npm packages (#23690) (renovate[bot])
# Oxfmt
### 💥 BREAKING CHANGES

- 259e0cd oxfmt,formatter_graphql: [**BREAKING**] Support draft syntax
with removing prettier fallback (#23326) (leaysgur)
- accbc49 oxfmt: [**BREAKING**] Format `parser:css,less,scss` files +
css-in-js by `oxc_formatter_css` (#23321) (leaysgur)

### 🚀 Features

- dffa4b3 formatter_css: Implement `oxc_formatter_css` (#23320)
(leaysgur)
- 01de9ec oxfmt: Format `parser:graphql` files by
`oxc_formatter_graphql` (#23318) (leaysgur)
- 4e66212 formatter_graphql: Implement oxc_formatter_graphql (#23317)
(leaysgur)

### 🐛 Bug Fixes

- 67325ae formatter_css: Handle frontmatter language (#23819) (leaysgur)
- 3f355e5 formatter_graphql: Improve major prettier diffs (#23419)
(leaysgur)
- 48e2d78 formatter_css: Improve major prettier diffs (#23327)
(leaysgur)
- 8c07cad all: Enable `disable_old_builder` Cargo feature for `oxc_ast`
crate in tests (#23888) (overlookmotel)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)

### ⚡ Performance

- 4ddcba0 formatter_core: Add printable-ASCII fast path to TextWidth
(#23913) (Lawrence Lin)

### 📚 Documentation

- b4d0dc9 oxfmt,formatter,formatter_css,formatter_core: Update AGENTS.md
(#23814) (leaysgur)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants