Skip to content

feat(linter): implement constructor-super rule#14506

Merged
camc314 merged 28 commits intooxc-project:mainfrom
taearls:linter/eslint-constructor-super
Oct 17, 2025
Merged

feat(linter): implement constructor-super rule#14506
camc314 merged 28 commits intooxc-project:mainfrom
taearls:linter/eslint-constructor-super

Conversation

@taearls
Copy link
Copy Markdown
Contributor

@taearls taearls commented Oct 11, 2025

Closes #2280

I implemented the constructor-super eslint rule. I think the implementation is pretty straightforward. I tried to keep it as simple as possible within the constraints of the rule.

I'm open to any feedback you have on it!

I used Claude to help generate a high level overview description of the code changes and reviewed it myself for accuracy.

Implementation Overview: constructor-super Rule

Hybrid AST + CFG Approach

This implementation uses a sophisticated hybrid approach combining AST analysis with Control Flow Graph (CFG) traversal to achieve accurate detection of super() violations:

  1. AST-based Detection for LogicalExpressions: CFG doesn't properly handle logical expressions like a && super(), so these are detected using AST traversal
  2. CFG-based Path Analysis: Uses DFS traversal of the CFG to analyze all execution paths through the constructor
  3. Per-Path Validation: Tracks super() calls across different execution paths to detect:
    • Missing super() calls (never called)
    • Conditional super() calls (not in all paths)
    • Duplicate super() calls (called multiple times)
    • Loop-based super() calls (invalid due to 0 or N executions)

Key Features

  • Accurate duplicate detection: Reports only the duplicate calls (2nd, 3rd, etc.), not the first occurrence, with spans sorted by source position
  • Complex control flow handling: Properly handles try/catch, switches, loops, conditionals, and nested scopes
  • Invalid superclass detection: Recognizes invalid superclass expressions like literals (extends 5), arithmetic assignments (extends (B += C)), etc.
  • Edge case support: Handles ternary operators, logical expressions, early returns, and acceptable exits (throw/return with value)
  • ESLint compatibility: Diagnostic messages match ESLint format exactly (including trailing periods)

Superclass Classification

The rule classifies superclasses into four types:

  • None: No extends clause
  • Null: extends null (special case requiring explicit return)
  • Invalid: Literals or invalid expressions (extends 5, extends (B += C))
  • Valid: Potentially valid class expressions

Path Analysis Results

CFG path analysis produces four possible results per path:

  • NoSuper: Path doesn't call super()
  • CalledOnce: Path calls super() exactly once (valid)
  • CalledMultiple: Path calls super() multiple times (duplicate)
  • ExitedWithoutSuper: Path exits early via throw/return with value (acceptable)

Test Coverage

  • Comprehensive snapshot tests covering 40+ test cases
  • Tests for all control flow patterns: if/else, switch, try/catch, loops, ternaries
  • Edge cases: nested classes, arrow functions, logical expressions, invalid superclasses
  • All diagnostic messages verified against ESLint's output format

Changes

  • Added constructor_super.rs with 873 lines of implementation
  • Added snapshot tests with 310 lines covering all edge cases
  • Registered rule in rules.rs and generated rule runner implementations
  • Rule categorized as nursery (pending promotion after validation)

Related

@taearls taearls requested a review from camc314 as a code owner October 11, 2025 15:55
@github-actions github-actions bot added A-linter Area - Linter C-enhancement Category - New feature or request labels Oct 11, 2025
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Oct 11, 2025

CodSpeed Performance Report

Merging #14506 will not alter performance

Comparing taearls:linter/eslint-constructor-super (a9572af) with main (cdf2d07)1

Summary

✅ 4 untouched
⏩ 33 skipped2

Footnotes

  1. No successful run was found on main (8b322d4) during the generation of this report, so cdf2d07 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

  2. 33 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.

@github-actions github-actions bot added the A-cli Area - CLI label Oct 11, 2025
@camc314 camc314 self-assigned this Oct 12, 2025
@taearls taearls force-pushed the linter/eslint-constructor-super branch 2 times, most recently from 810163d to 819a57d Compare October 14, 2025 22:18
@taearls taearls force-pushed the linter/eslint-constructor-super branch from 819a57d to cee68d2 Compare October 16, 2025 14:35
@camc314 camc314 changed the base branch from main to c/10-16-fix_semantic_add_condition_basic_blocks_to_cfg_for_logical_expressions October 16, 2025 16:26
@camc314 camc314 requested a review from Dunqing as a code owner October 16, 2025 16:32
@github-actions github-actions bot added the A-semantic Area - Semantic label Oct 16, 2025
@camc314 camc314 force-pushed the linter/eslint-constructor-super branch 2 times, most recently from 5b2ba58 to ed66efe Compare October 16, 2025 16:34
@graphite-app graphite-app bot force-pushed the c/10-16-fix_semantic_add_condition_basic_blocks_to_cfg_for_logical_expressions branch from 2ce7edd to cdf2d07 Compare October 17, 2025 10:46
@graphite-app graphite-app bot deleted the branch oxc-project:main October 17, 2025 10:52
@graphite-app graphite-app bot closed this Oct 17, 2025
@camc314
Copy link
Copy Markdown
Contributor

camc314 commented Oct 17, 2025

ugh i don't know why graphite closed this.

@camc314 camc314 reopened this Oct 17, 2025
@camc314 camc314 changed the base branch from c/10-16-fix_semantic_add_condition_basic_blocks_to_cfg_for_logical_expressions to main October 17, 2025 11:07
taearls and others added 3 commits October 17, 2025 12:07
Implements the constructor-super ESLint rule that validates super() calls
in class constructors. This rule ensures:

- Derived classes (extends) must call super() in constructors
- Non-derived classes must not call super()
- super() is called in all code paths (no conditional/missing calls)
- No duplicate super() calls except in mutually exclusive branches

Implementation uses AST-only structural analysis without CFG:

- Classifies superclass expressions (valid/invalid/null/none)
- Detects guaranteed super() execution via control flow analysis
- Handles complex patterns: if/else, switch with fallthrough, ternaries,
  try-finally, loops with fallback, acceptable exits (throw/return)
- Distinguishes duplicate calls from mutually exclusive branches

Key edge cases handled:
- extends null requires return statement or throws error
- Invalid superclass expressions (literals, arithmetic assignments)
- Switch fallthrough detection prevents duplicate super() calls
- Loop-with-fallback pattern (super() inside loop + after loop)
- Conditional exits (throw/return-with-value) allow super() after

Passes all 165 valid cases and 80 invalid cases from ESLint test suite.

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

Co-Authored-By: Claude <noreply@anthropic.com>
taearls and others added 23 commits October 17, 2025 12:07
Refactored the `constructor-super` ESLint rule to leverage the Control
Flow Graph (CFG) instead of iterating over all AST nodes, significantly
improving performance and code clarity.

## Key Changes

### CFG-Based Super Call Detection
- Replaced full AST iteration with targeted CFG traversal using
  `neighbors_filtered_by_edge_weight`
- Only examines CFG block instructions, avoiding unnecessary node visits
- Properly stops at function boundaries (NewFunction edges) to avoid
  counting nested function super() calls

### Per-Block Super Call Counting
- Changed from tracking boolean "has super" to counting super() calls
  per basic block
- Enables detection of multiple super() calls in single block
- Properly handles ternary expressions where both branches are in the
  same CFG block (e.g., `a ? super() : super()` counts as 1, not 2)

### DFS-Based Path Analysis
- Implemented custom DFS traversal to analyze all execution paths
- Tracks super() call counts per path from constructor entry to exit
- Only reports results at actual path terminations (Return, Throw,
  ImplicitReturn instructions)
- Uses per-path visited tracking to explore all paths while avoiding
  infinite loops

### Proper Edge Type Handling
- **Jump/Normal/Join edges**: Follow with accumulated super count
- **Error edges**: Reset to pre-block count (exception paths discard
  try block execution)
- **Finalize edges**: Preserve super count (finally blocks execute
  after try/catch)
- **NewFunction/Unreachable edges**: Stop traversal
- **Backedge handling**: Detects loops containing super() (in progress)

### Early Exit Detection
- Distinguishes between acceptable and unacceptable early exits
- `return <value>` and `throw`: acceptable without super()
- `return;` (implicit undefined): requires super()
- Uses `ReturnInstructionKind::NotImplicitUndefined` to differentiate

### Conditional Expression Support
- Handles ternary operators within ExpressionStatements
- Checks both consequent and alternate branches for super() calls
- CFG instructions point to ExpressionStatement, not inner expressions

## Implementation Details

- Added `Direction` import from petgraph for edge traversal
- Changed return type from `FxHashSet<BlockNodeId>` to
  `FxHashMap<BlockNodeId, usize>` to track call counts
- Added `ExitedWithoutSuper` path result variant for early exits
- Maintains span tracking for diagnostic reporting

## Known Limitations

Loop detection (Backedge handling) needs refinement to accurately
detect super() calls within loop bodies vs. before loops. Current
implementation may have false positives/negatives for loops.

## Performance Impact

Eliminates O(n) iteration over all AST nodes, replacing with O(paths)
CFG traversal where paths << total nodes in most cases.

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

Co-Authored-By: Claude <noreply@anthropic.com>
…h analysis

Fix critical bug in DFS path traversal where implicit error propagation edges
were being followed as real execution paths, causing false positives.

The issue occurred when CFG blocks had Error(Implicit) edges leading to error
sink blocks. These edges represent exception propagation/escape routes, not
actual execution paths within the constructor. Following them caused the DFS
to visit error sink blocks with super_count=0, incorrectly recording
PathResult::NoSuper.

Changes:
- Distinguish between Error(Explicit) and Error(Implicit) edge types
- Only follow Error(Explicit) edges (try/catch exception handlers)
- Skip Error(Implicit) edges (error propagation to outer scopes)
- For explicit error edges, use super_count from before the block
  (exception thrown = rest of try block doesn't execute)

This fixes false positives like:
- `if (true) { super(); } else { super(); }` incorrectly flagging missing super
- Other cases where all paths called super() but error sinks created phantom paths

Improved diagnostics:
- `a && super()` now correctly reports "Expected to call 'super()'" instead of
  "Lacked a call in some code paths" (no super() call at all in false path)
- `super() || super()` similarly improved (short-circuit behavior)
- Duplicate diagnostics now consistently point to first super() call

All 804 linter tests passing.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Apply clippy suggestions:
- Merge empty match arms (NewFunction, Unreachable, Error::Implicit)
- Collapse nested if statement using let-chain pattern

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

Co-Authored-By: Claude <noreply@anthropic.com>
Improved performance and readability of the constructor-super rule's
CFG-based super() detection:

- Simplified edge filtering by checking only for NewFunction boundaries
- Hoisted nested scope check to avoid redundant ancestor traversals
  (reduced from ~4x to 1x per instruction)
- Extracted record_super closure to eliminate code duplication
- Simplified ternary expression handling using helper closure with
  .or_else() chaining (~50 lines reduced to ~16)
- Removed unnecessary count capping (simple addition suffices)
- Simplified dead-end path condition check

These changes reduce code size by ~70 lines while maintaining all
correct CFG-based behavior and improving performance.

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

Co-Authored-By: Claude <noreply@anthropic.com>
…ression with super()

The CFG-based approach had a bug where LogicalExpressions containing
super() calls (e.g., `a && super()` or `super() || super()`) were not
being detected correctly, producing "Expected to call 'super()'" instead
of "Lacked a call of 'super()' in some code paths".

This commit adds a hybrid approach:
- Added `has_logical_expression_super()` to recursively search the AST
  for LogicalExpressions containing super() calls
- Modified `find_super_calls_in_cfg()` to return a `has_conditional_super`
  flag when it encounters LogicalExpression with super() in the CFG
- Updated validation logic to check the conditional flag first and emit
  the appropriate diagnostic

The fix ensures that logical expressions with super() are correctly
identified as conditional paths, matching the behavior of the original
AST-based implementation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
…ot the first one

Previously, the duplicate super() diagnostic would highlight the first
super() call encountered during CFG traversal, which could be either
the first or a subsequent call depending on block traversal order.

This commit improves the diagnostic by:
- Changing super_call_spans from HashMap to Vec to track all super() calls
- Sorting spans by source position before reporting duplicates
- Reporting only the duplicate calls (all except the first in source order)

Now diagnostics correctly highlight only the duplicate super() calls:
- `super(); super();` → highlights the second super()
- `if (a) super(); super();` → highlights the second super()
- `switch (a) { case 0: super(); default: super(); }` → highlights the second super()

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

Co-Authored-By: Claude <noreply@anthropic.com>
…match ESLint

Updated all diagnostic messages to include periods at the end, matching
the exact format of the original ESLint constructor-super rule:

- "Expected to call 'super()'." (was: "Expected to call 'super()'")
- "Lacked a call of 'super()' in some code paths." (was: "Lacked a call of 'super()' in some code paths")
- "Unexpected duplicate 'super()'." (was: "Unexpected duplicate 'super()'")
- "Unexpected 'super()' because 'super' is not a constructor." (was: "Unexpected 'super()' because 'super' is not a constructor")

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

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove redundant clones when sorting super() call spans
- Remove unused parameters from nested helper functions
Simplified comments throughout the constructor-super rule implementation
while preserving important technical context. Removed redundant comments
and condensed multi-line explanations into more concise descriptions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Update snapshot to reflect changes in output format after rebasing onto main.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Update snapshot to reflect changes in output format after rebasing onto main.

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

Co-Authored-By: Claude <noreply@anthropic.com>
…nstructor-super

The CFG now properly handles logical expressions with condition basic blocks,
so the AST-based workaround is no longer needed. The rule now relies entirely
on CFG path analysis, with a special case for 'super() || super()' which
always executes both calls since super() returns undefined (falsy).
@camc314 camc314 force-pushed the linter/eslint-constructor-super branch from ed66efe to a778820 Compare October 17, 2025 11:07
Copy link
Copy Markdown
Contributor

@camc314 camc314 left a comment

Choose a reason for hiding this comment

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

Great work! thank you!

@camc314 camc314 merged commit 5858641 into oxc-project:main Oct 17, 2025
19 checks passed
@overlookmotel
Copy link
Copy Markdown
Member

overlookmotel commented Oct 17, 2025

From #479:

0/61 recommended rules are remaining as TODO

This was the last one. Full house! 🎉

@taearls taearls deleted the linter/eslint-constructor-super branch November 3, 2025 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-linter Area - Linter A-semantic Area - Semantic C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

eslint/constructor-super

4 participants