perf(linter/getter-return): remove unnecessary Vec collect in CFG edge traversal#22166
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes the getter_return ESLint rule’s CFG depth-first traversal by removing an unnecessary Vec allocation during TreeEdge handling, improving per-edge traversal performance while keeping the same edge-filtering semantics.
Changes:
- Replaces
edges_connecting(a, b).collect::<Vec<_>>()+iter().any(...)with a directedges_connecting(a, b).any(...)call to avoid allocation and enable early-exit.
Merging this PR will not alter performance
Comparing Footnotes
|
camchenry
approved these changes
May 6, 2026
Member
|
Nice! |
Member
Merge activity
|
…e traversal (#22166) This is a simple change with no real downside that I can see, just a minor performance/memory usage win for the rule. Below is Claude's summary ---- ## Summary - Remove unnecessary `.collect::<Vec<_>>()` in the `getter_return` rule's CFG DFS traversal, calling `.any()` directly on the iterator instead. - This allocation fires on every `TreeEdge` during depth-first search, so avoiding it is measurable. ## Benchmark results Paired interleaved benchmark on a 51,499-line synthetic fixture with ~3,000 getter definitions: ``` === GETTER_RETURN RULE (paired analysis) === N = 60 Baseline mean: 17.91 ms (sd=0.69) Optimized mean: 16.62 ms (sd=0.62) Paired diff: 1.293 ms (sd=0.891) t-stat: 11.25 Win/Loss/Tie: 54/6/0 === PARSE-ONLY CONTROL (paired analysis) === Paired diff: 0.058 ms (sd=1.079) t-stat: 0.41 === RULE-ATTRIBUTABLE DELTA === Rule delta: 1.235 ms ``` - **~36% reduction in rule cost** (from ~3.4ms to ~2.2ms after subtracting parse-only baseline) - Parse-only control confirms no binary-layout effect (t=0.41, well below significance threshold) - Behavioral parity confirmed via identical `--format=json` output ## Test plan - [x] Existing `getter_return` tests pass (`cargo test -p oxc_linter -- getter_return`) - [x] Behavioral parity confirmed (identical `--format=json` output between baseline and optimized binaries) - [x] Benchmarked with paired interleaved methodology (parse-only control shows no binary-layout effect) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
0a8c4f1 to
c9ce045
Compare
3 tasks
connorshea
added a commit
that referenced
this pull request
May 6, 2026
Remove `.collect::<Vec<_>>()` in three rules' CFG DFS traversals, calling iterator methods directly instead of allocating a Vec on every TreeEdge. Same fix as #22166 (getter_return) applied to always_return, return_in_computed_property, and no_multiple_resolved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
graphite-app Bot
pushed a commit
that referenced
this pull request
May 6, 2026
…22167) Same idea as #22166, the pattern was reused elsewhere so this fixes those as well. Benchmarked and the change is at worst neutral, at best a decent improvement for these code paths, and pretty consistently saves allocations. Claude's description is below. --- ## Summary Remove unnecessary `.collect::<Vec<_>>()` in CFG DFS traversals for three linter rules: - `promise/always-return` - `promise/no-multiple-resolved` - `vue/return-in-computed-property` Same optimization as #22166 (`getter-return`). In all cases, `graph.edges_connecting(a, b)` was collected into a `Vec` just to call `.iter().any()` or iterate with `for`. Since `edges_connecting` returns a cheap iterator over the adjacency list, the Vec allocation is pure overhead — and it fires on every `TreeEdge` during DFS, making it the hottest allocation in these rules. The pattern was introduced in the first CFG-based rule and copy-pasted into the others without deliberate reason (confirmed via git blame — all four sites were present from initial implementation). ## Benchmark results Paired interleaved benchmarks (N=60, alternating execution order, parse-only control). ### Wall-clock (paired analysis) | Rule | Baseline mean | Optimized mean | Rule-attributable delta | t-stat | Win/Loss | |---|---|---|---|---|---| | `always-return` | 12.12 ms | 11.99 ms | 0.050 ms | 0.88 | 38/22 | | `no-multiple-resolved` | 283.36 ms | 281.00 ms | 2.262 ms | 2.03 | 36/24 | - `no-multiple-resolved` shows a borderline-significant improvement (t=2.03, threshold ~2.0), with a clean parse-only control (t=0.63). - `always-return` is too cheap (~2ms rule cost) for the improvement to register at binary level, but the allocation reduction is real. - `return-in-computed-property` was not benchmarked (Vue-only rule requiring `.vue` file fixtures), but uses the identical pattern. ### Memory (counting global allocator, rule-attributable = total minus parse-only) | Rule | Metric | Baseline | Optimized | Saved | % reduction | |---|---|---|---|---|---| | `always-return` | alloc count | 66,045 | 60,045 | **6,000** | 9.1% | | | alloc bytes | 6,115,232 | 5,539,251 | **575,981 (563 KB)** | 9.4% | | `no-multiple-resolved` | alloc count | 183,041 | 171,041 | **12,000** | 6.6% | | | alloc bytes | 13,159,807 | 12,007,826 | **1,151,981 (1.1 MB)** | 8.8% | For reference, the same fix on `getter-return` (#22166) eliminated **77,300 allocations and 7.1 MB** of heap churn (72.7% reduction in rule-attributable allocations). All fixtures use ~3,000 instances of the pattern each rule checks (promise `.then()` callbacks, `new Promise` constructors, getter definitions). ## Test plan - [x] All three rules' tests pass (`cargo test -p oxc_linter`) - [x] Behavioral parity confirmed (identical `--format=json` output between baseline and optimized binaries) - [x] Parse-only controls confirm no binary-layout effects 🤖 Generated with [Claude Code](https://claude.com/claude-code)
4 tasks
graphite-app Bot
pushed a commit
that referenced
this pull request
May 6, 2026
…h rfind (#22172) Another minor optimization + code simplification. Claude's summary is below. As with all of these, the benchmark is based on running the rule on a file with a lot of specific code that isn't likely to occur in real codebases. So the % drops shouldn't be seen as representative of much beyond "this specific code path is faster". --- ## Summary Two allocations per `@property` JSDoc tag with a dot — the `Vec<&str>` from `split + collect` and the `String` from `join(".")` — collapse to a single slice index using `rfind('.')`. ```rust // before if type_name.contains('.') { let mut parts = type_name.split('.').collect::<Vec<_>>(); parts.pop(); // `foo[].bar` -> `foo[]` let parent_name = parts.join("."); let parent_name = parent_name.trim_end_matches("[]"); ... } // after if let Some(dot_idx) = type_name.rfind('.') { let parent_name = type_name[..dot_idx].trim_end_matches("[]"); // `foo[].bar` -> `foo[]` -> `foo` ... } ``` `rfind('.')` matches the last dot; the original `split + pop + join` reassembled all-but-the-last segment with `.` as the separator — equivalent. Same family as #22164/#22166/#22167/#22168. ## Benchmark Synthetic fixture: 30 JS files × 60 typedef blocks × 16 `@property` tags ≈ **28,800 tags**, ~80% with dots. Counting global allocator (system alloc, no mimalloc) on the rule-on and parse-only configs; rule-attributable = total − parse-only. ### Wall-clock (paired interleaved, N=60) | | Baseline | Optimized | Δ | |---|---|---|---| | Rule ON | 21.564 ms | 20.550 ms | **−1.014 ms** | | Parse-only | 7.319 ms | 7.326 ms | +0.007 ms (noise) | - Paired t-stat: **5.02** (rule-on); **−0.05** (parse-only control) - Win/Loss/Tie: **42 / 18 / 0** - Parse-only is flat, so the delta isn't a binary-layout artifact. ### Allocations (rule-attributable) | Metric | Baseline | Optimized | Saved | Reduction | |---|---|---|---|---| | Count | 318,002 | 282,603 | **35,399** | **11.1%** | | Bytes | 30.7 MB | 29.4 MB | **1.32 MB** | **4.3%** | Saves ~1.5 allocations per dotted tag on this fixture (Vec is sometimes elided when the path is `a.b`, but `String` from `join` always allocates). ## Test plan - [x] `cargo test -p oxc_linter --lib jsdoc::check_property_names` — passes - [x] `--format=json` parity confirmed across baseline and optimized binaries on the fixture (only `start_time` differs) - [x] Parse-only control confirms no binary-layout effect (t = −0.05) - [x] `just fmt` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code)
camc314
added a commit
that referenced
this pull request
May 11, 2026
# Oxlint ### 💥 BREAKING CHANGES - 00ce512 oxlint/lsp: [**BREAKING**] Don't fix suggestions on fixAll code actions & command (#22195) (Sysix) ### 🚀 Features - 0eeceaf linter/no-unused-vars: Rename parameter with initializer (#22308) (camc314) - fa0232b linter/no-unused-vars: Add param rename suggestion (#22285) (Ryota Misumi) - ae59305 linter/promise/no-promise-in-callback: Add `exemptDeclarations` option (#22275) (Mikhail Baev) - 60bed4a linter: Extends `no-redundant-roles` and `prefer-tag-over-role` support roles (#22069) (mehm8128) - 545c80f linter/eslint: Implement `prefer-regex-literals` rule (#22192) (Mikhail Baev) - cf86d7a linter: Bulk suppression (#19328) (Said Atrahouch) - 23abd22 linter/jsx-a11y: Implement no-noninteractive-element-to-interactive-role (#21264) (Pedro Tainha) - fbb8f22 linter: Support `ignores` in overrides (#22148) (camc314) - 5a4414d oxlint/lsp: Support `rulesCustomization` lsp option (#21858) (Sysix) ### 🐛 Bug Fixes - 610f4c7 linter/no-unused-vars: Avoid renaming captured vars (#22310) (camc314) - 6b50f23 oxlint/cli: Load root config by searching up parent directories (#22272) (Sysix) - 31a5de7 linter: Rename override `ignores` to `excludeFiles` (#22283) (camc314) - 26d5d7b linter: Add missing vitest/valid-describe-callback functionality (#22279) (camchenry) - 784530f linter: `valid-title`: detect `String.raw` strings (#22271) (Sysix) - 080d90e linter: Move `no-debugger` fix to suggestion (#22256) (Sysix) - 25b7017 linter: Undocument override `ignores` option (#22213) (camc314) - 7bb00dd linter: Fix role-has-required-aria-props (#22097) (mehm8128) - d25279e linter/disable-directives: Improve parsing of names, descriptions (#22184) (camc314) - a59e447 linter/disable-directives: Ignore invalid enable suffixes (#22179) (camc314) - aafef0f ci: Disable bulk supression test on big endian (#22175) (camc314) - 281daec linter/vue/define-props-destructuring: Add `only-when-assigned` config opt (#22142) (camc314) - 46ab679 linter/plugins: Trim leading newline for partial sources (#20928) (bab) - 29ff6d9 linter: Update docs for no_alias_methods rule to be Vitest-specific and add toThrowError alias (#22129) (camchenry) ### ⚡ Performance - 9414bee linter/role-has-required-aria-props: Avoid intermediate vec (#22212) (camc314) - 3883ea3 linter/no-useless-escape: Drop unnecessary Vec collect (#22171) (connorshea) - 42c3029 linter/check-property-names: Replace split-collect-pop-join with rfind (#22172) (connorshea) - 9551d53 linter: Remove unnecessary Vec collect in CFG edge traversal (#22167) (connorshea) - 26fa2fc linter/aria-role: Remove unnecessary string allocations in run method (#22168) (connorshea) - c9ce045 linter/getter-return: Remove unnecessary Vec collect in CFG edge traversal (#22166) (connorshea) - 72bd846 linter/no-this-in-sfc: Reorder cheap name check, avoid String allocation (#22164) (connorshea) ### 📚 Documentation - 4da212a linter/no-unused-vars: Add docs to `rename_unused_function_parameter` (#22311) (camc314) - 27c4628 linter/forbid-dom-props: Escape jsx examples in lint rule docs (#22254) (4MBL) - 3f81147 linter: Improve the `react/jsx-key` rule docs. (#22162) (connorshea) - 07f03cc linter/consistent-return: Add note about `noImplicitReturns` coverage (#22156) (camc314) - 7c1e049 oxlint/lsp: Improve autogenerated lsp docs (#22154) (Sysix) - 87b3e38 linter: Update docs to be vitest-specific for consistent-test-it (#22128) (camchenry) # Oxfmt ### 💥 BREAKING CHANGES - 5c6c390 oxfmt: [**BREAKING**] Respect more git ignore options, align with Oxlint (#22210) (leaysgur) ### 🚀 Features - 6e8e818 oxfmt: Experimental .svelte support (#21700) (leaysgur) ### 🐛 Bug Fixes - e2a20b6 formatter: Add space after commas in import attributes (#22274) (Leonabcd123) ### ⚡ Performance - b756682 oxfmt: Optimize nested config prescan (#22232) (Jovi De Croock) - f14e81e formatter/sort_imports: Skip sort for single import runs (#22204) (leaysgur) - 32255b1 formatter: Process `ImportDeclaration`s in a run (#22079) (overlookmotel) ### 📚 Documentation - 4da6f4c formatter: Correct comment (#22217) (overlookmotel) - ef3507d formatter/sort_imports: Refresh docs (#22203) (leaysgur) Co-authored-by: Cameron <cameron.clark@hey.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a simple change with no real downside that I can see, just a minor performance/memory usage win for the rule.
Below is Claude's summary
Summary
.collect::<Vec<_>>()in thegetter_returnrule's CFG DFS traversal, calling.any()directly on the iterator instead.TreeEdgeduring depth-first search, so avoiding it is measurable.Benchmark results
Paired interleaved benchmark on a 51,499-line synthetic fixture with ~3,000 getter definitions:
--format=jsonoutputTest plan
getter_returntests pass (cargo test -p oxc_linter -- getter_return)--format=jsonoutput between baseline and optimized binaries)🤖 Generated with Claude Code