fix(linter): rewrite constructor-super to use iterative dataflow analysis#16706
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
CodSpeed Performance ReportMerging #16706 will not alter performanceComparing Summary
Footnotes
|
d511468 to
5986993
Compare
Merge activity
|
…ysis (#16706) The constructor-super rule's DFS path analysis had exponential O(2^n) complexity, causing oxlint to hang indefinitely on files with complex control flow graphs (e.g., next.js compiled bundles with 59+ classes). ## Root Cause The previous algorithm explored all possible paths through the CFG, removing blocks from the visited set after each path to allow re-exploration. With k branch points, this creates 2^k paths. ## Solution Replaced the DFS path enumeration with iterative dataflow analysis: - New `SuperCallState` enum tracks abstract states: Unreached, Never, Once, Multiple, Mixed - Merge operation at CFG join points combines states from different paths - Transfer function computes state after executing super() calls - Worklist algorithm propagates states until fixpoint ## Complexity - Before: O(2^n) where n = number of branch points - After: O(n × m) where n = blocks, m = edges The state lattice has finite height (5 states), guaranteeing termination. ## Performance | File | Before | After | |------|--------|-------| | fetch.js (796KB, 59 classes) | hung indefinitely | 16ms | | load.js (800KB, 59 classes) | hung indefinitely | 50ms | | tar/index.js (98KB, 30 classes) | hung indefinitely | 6ms | | Full next.js (19,535 files) | hung indefinitely | 1.2s | 🤖 generated with help from Claude Opus 4.5
5986993 to
e08b1c3
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical performance issue in the constructor-super rule by replacing exponential-complexity DFS path enumeration with linear-complexity iterative dataflow analysis. The previous algorithm had O(2^n) time complexity that caused oxlint to hang indefinitely on files with complex control flow (59+ classes), while the new algorithm achieves O(n × m) complexity, processing previously problematic files in milliseconds.
Key Changes:
- Introduced
SuperCallStateenum to represent abstract states in dataflow analysis (Unreached, Never, Once, Multiple, Mixed) - Replaced recursive DFS traversal with worklist-based iterative algorithm using state propagation and merging
- Refactored several match expressions to use more idiomatic Rust patterns
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
The shift to a worklist dataflow analysis is a strong improvement, but a few semantic edge cases look off. In particular, exceptional control-flow (EdgeType::Error(Explicit)) currently propagates a pre-transfer state, which can undercount super() in try blocks, and exit blocks may still propagate to successors, diverging from prior behavior. The loop handling via loop_with_super => NoSuper is too coarse and likely produces incorrect results on common CFG shapes; consider a more conservative Multiple/SCC-based approach.
Summary of changes
What changed
Performance fix: replace path-enumeration DFS with dataflow
- Replaced the previous DFS path exploration (exponential
O(2^k)over branch points) with an iterative worklist dataflow analysis over CFG blocks. - Introduced a 5-state lattice
SuperCallState(Unreached,Never,Once,Multiple,Mixed) with:merge()for join pointsadd_super_calls()as a transfer function based on per-blocksuper()counts
- Added a
VecDeque-based worklist to propagate states to fixpoint.
CFG scanning improvements
- Simplified
classify_super_classpattern matching. - Refactored
ExpressionStatementhandling and added special-case detection for:- ternary
cond ? super() : ... super() || super()(counts as duplicate because RHS always runs)
- ternary
Path result construction
- Collected exit states (
(SuperCallState, is_acceptable_exit)) and converted them intoPathResultvalues after fixpoint. - Added a heuristic flag
loop_with_superto account for loops involvingsuper()via backedges.
e08b1c3 to
acc93da
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
The new worklist dataflow approach addresses the performance problem, but there are still correctness risks in the abstract interpretation. In particular, SuperCallState::add_super_calls collapses Mixed too aggressively, and propagating pre-transfer state across EdgeType::Error(Explicit) can undercount super() within a basic block. Loop handling remains heuristic-driven and may be both over- and under-conservative compared to SCC-based reasoning. Finally, diagnostics behavior changed to emit “missing super on some paths” even when duplicates exist, which may increase noise.
Additional notes (1)
- Maintainability |
crates/oxc_linter/src/rules/eslint/constructor_super.rs:270-276
some_missingdiagnostics behavior changed: previously missing-super-on-some-paths was suppressed when there was also a duplicate (!has_duplicate). Now it always emits missing-super diagnostics even when duplicates exist.
This can create noisy or contradictory reports on the same constructor (e.g., one path calls super() twice while another calls it zero times). If the rule’s intent is to prioritize duplicate-super as the primary issue (as the prior code suggests), this is a behavioral change that should be justified or restored.
Summary of changes
Summary of changes
constructor-super rule: switch from path-enumeration DFS to dataflow
- Replaced the previous DFS path exploration (with cycle re-visitation) with a worklist-based iterative dataflow analysis over CFG blocks.
- Added a 5-element abstract domain
SuperCallState(Unreached,Never,Once,Multiple,Mixed) with:merge()join operatoradd_super_calls()transfer based on per-blocksuper()counts
- Introduced
VecDequeworklist andblock_states: FxHashMap<BlockNodeId, SuperCallState>to compute a fixpoint.
CFG/AST scanning tweaks
- Simplified
classify_super_classto a more idiomatic match. - Improved
super()discovery inExpressionStatementby special-casing:- ternary
cond ? super() : ...(counts once) super() || super()(counts both; RHS always executes)
- ternary
Path result construction
- Collected exit states into
exit_resultsand converted them intoVec<PathResult>after the fixpoint. - Added conservative loop handling via a
loop_with_superflag that injects bothNoSuperandCalledMultiple.
Tests
- Added passing cases covering
super()before a potential throw andsuper()insidefinally.
There was a problem hiding this comment.
The new dataflow approach fixes the exponential blowup, but several semantics look incorrect or overly heuristic. In particular, SuperCallState::add_super_calls collapses Mixed too aggressively, explicit error-edge propagation likely undercounts super() within try blocks, and the loop heuristic can both miss loop-contained super() and incorrectly force NoSuper. Additionally, the super() || super() special-case relies on an incorrect JS assumption (that super() is falsy), which can directly cause false duplicate reports.
Summary of changes
Summary
This change rewrites the constructor-super rule’s control-flow analysis to avoid exponential path enumeration.
Key updates
- Replaced DFS path exploration with a worklist-based iterative dataflow analysis using a new
SuperCallStatelattice (Unreached | Never | Once | Multiple | Mixed). - Added state operations:
merge()for join pointsadd_super_calls()as the transfer function per basic block
- Updated CFG traversal:
- Uses
VecDequeworklist andblock_statesmap. - Treats blocks with exit instructions (
return/throw/implicit return) as terminal. - Detects loops via
EdgeType::Backedgeand emits conservative results.
- Uses
- Improved AST scanning in
find_super_calls_in_cfgto countsuper()in:- ternaries (
cond ? super() : ...) super() || super()(counts as duplicate because RHS always runs)
- ternaries (
- Simplified superclass classification logic and expanded documentation/comments.
- Added new test cases around
try/catch/finallyand throwing behavior.
There was a problem hiding this comment.
The dataflow rewrite fixes the exponential path explosion, but there are correctness regressions: SuperCallState::add_super_calls collapses Mixed into Multiple in cases where it should stay Mixed, which can change which diagnostics fire. Explicit error-edge propagation uses a pre-transfer state based on a questionable CFG invariant and can undercount super() executed before a throw in the same block. Loop handling via a loop_with_super flag remains too heuristic, and the super() || super() duplicate rule is based on incorrect JavaScript semantics and can produce false positives.
Summary of changes
Overview
This change rewrites the constructor-super rule’s path analysis to avoid exponential CFG path enumeration.
Key changes
- Replaced recursive DFS path exploration with an iterative worklist dataflow analysis using
VecDeque. - Introduced a 5-state lattice
SuperCallState(Unreached,Never,Once,Multiple,Mixed) withmerge()andadd_super_calls()transfer. - Updated CFG propagation logic:
- Terminal blocks (
return/throw/implicit return) are collected as exits and no longer propagate to successors. EdgeType::Error(Explicit)is handled specially (propagates pre-transfer state).- Backedges are not followed; loops set a flag that contributes both
NoSuperandCalledMultipleresults.
- Terminal blocks (
- Simplified
classify_super_classmatching. - Extended super-call detection in expression statements for conditional expressions and the special case
super() || super(). - Added tests covering
super()aroundthrow/try/finallypatterns.
There was a problem hiding this comment.
The new worklist-based dataflow is a big performance improvement, but there are likely semantic regressions in the core abstract interpretation. SuperCallState::add_super_calls() is too lossy and can incorrectly jump to Multiple, the super() || super() special-case is based on incorrect JS semantics, and the remaining loop heuristic can both over- and under-report. Additionally, the diagnostic gating change may introduce noisy double-reporting when duplicates and missing paths co-occur.
Additional notes (1)
- Maintainability |
crates/oxc_linter/src/rules/eslint/constructor_super.rs:270-276
The rule now reportsmissing_super_somewheneversome_missingis true, even if the same analysis also detects duplicates (has_duplicate). Previously it suppressed the missing-path diagnostic when duplicates were present (some_missing && !has_duplicate).
That behavior change is likely noisy: in a constructor with both a missing path and a duplicate path, you’ll now emit both “missing on some paths” and “duplicate super” diagnostics. In ESLint’s constructor-super, the duplicate is usually the more actionable primary error; piling on can be confusing and may be considered a regression in output quality.
Summary of changes
Summary
This diff refactors the constructor-super lint rule to avoid exponential CFG path exploration by switching from recursive DFS path enumeration to an iterative worklist dataflow analysis.
Key updates
- Replaced
FxHashSet-based DFS cycle handling with aVecDequeworklist and per-block abstract states stored inFxHashMap<BlockNodeId, SuperCallState>. - Added a 5-element lattice
SuperCallState(Unreached,Never,Once,Multiple,Mixed) withmerge()(join) andadd_super_calls()(transfer). - Adjusted path-result construction to derive
PathResultfrom exit states and loop heuristics rather than enumerating all paths. - Improved superclass classification with more idiomatic match guards.
- Expanded super-call detection in
ExpressionStatementfor ternaries andsuper() || super(). - Added tests covering
super()aroundtry/catch/finallyand potential throws.
acc93da to
38a129b
Compare
…ysis (#16706) The constructor-super rule's DFS path analysis had exponential O(2^n) complexity, causing oxlint to hang indefinitely on files with complex control flow graphs (e.g., next.js compiled bundles with 59+ classes). ## Root Cause The previous algorithm explored all possible paths through the CFG, removing blocks from the visited set after each path to allow re-exploration. With k branch points, this creates 2^k paths. ## Solution Replaced the DFS path enumeration with iterative dataflow analysis: - New `SuperCallState` enum tracks abstract states: Unreached, Never, Once, Multiple, Mixed - Merge operation at CFG join points combines states from different paths - Transfer function computes state after executing super() calls - Worklist algorithm propagates states until fixpoint ## Complexity - Before: O(2^n) where n = number of branch points - After: O(n × m) where n = blocks, m = edges The state lattice has finite height (5 states), guaranteeing termination. ## Performance | File | Before | After | |------|--------|-------| | fetch.js (796KB, 59 classes) | hung indefinitely | 16ms | | load.js (800KB, 59 classes) | hung indefinitely | 50ms | | tar/index.js (98KB, 30 classes) | hung indefinitely | 6ms | | Full next.js (19,535 files) | hung indefinitely | 1.2s | 🤖 generated with help from Claude Opus 4.5
38a129b to
50e0a23
Compare

The constructor-super rule's DFS path analysis had exponential O(2^n)
complexity, causing oxlint to hang indefinitely on files with complex
control flow graphs (e.g., next.js compiled bundles with 59+ classes).
Root Cause
The previous algorithm explored all possible paths through the CFG,
removing blocks from the visited set after each path to allow
re-exploration. With k branch points, this creates 2^k paths.
Solution
Replaced the DFS path enumeration with iterative dataflow analysis:
SuperCallStateenum tracks abstract states: Unreached, Never,Once, Multiple, Mixed
Complexity
The state lattice has finite height (5 states), guaranteeing termination.
Performance
🤖 generated with help from Claude Opus 4.5