Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 235 additions & 23 deletions internal/rules/constructor_super/constructor_super.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,118 @@ func findDuplicateSuperCalls(body *ast.Node, superCallLocations []*ast.Node) []*
}
}
}

case ast.KindTryStatement:
tryStmt := stmt.AsTryStatement()
if tryStmt != nil {
tryHasSuper := tryStmt.TryBlock != nil && analyzeStatements(tryStmt.TryBlock.Statements())
finallyHasSuper := tryStmt.FinallyBlock != nil && analyzeStatements(tryStmt.FinallyBlock.Statements())
if tryHasSuper && finallyHasSuper {
// finally always runs after the try block, so a super()
// call there is a genuine duplicate alongside the try
// block's own call.
findSuperCallsInNode(tryStmt.FinallyBlock, superCallLocations, &duplicates)
}
if tryStatementHasSuper(tryStmt) {
foundFirstSuper = true
}
}
}
}

return duplicates
}

// findAllSuperDuplicates finds super() calls that are duplicates either
// because they sit inside a loop that can revisit them (see
// isSuperCallInLoopWithoutExit) or because they can execute sequentially on
// the same guaranteed path (see findDuplicateSuperCalls).
func findAllSuperDuplicates(body *ast.Node, superCallLocations []*ast.Node) []*ast.Node {
var duplicates []*ast.Node
for _, loc := range superCallLocations {
if isSuperCallInLoopWithoutExit(body, loc) {
duplicates = append(duplicates, loc)
}
}
for _, loc := range findDuplicateSuperCalls(body, superCallLocations) {
if !contains(duplicates, loc) {
duplicates = append(duplicates, loc)
}
}
return duplicates
}

// isSuperCallInLoopWithoutExit reports whether a super() call sits inside its
// nearest enclosing loop without a guaranteed break/return/throw right after
// it. ESLint's code-path analysis treats any super() call that could be
// revisited via a loop back-edge as a duplicate - even when the loop's
// condition provably allows only one iteration (e.g. `do { super(); } while
// (false)`) - so the loop's condition truthiness is deliberately not
// consulted here, only whether the call is unconditionally followed by a
// clean exit out of the loop.
func isSuperCallInLoopWithoutExit(body *ast.Node, superCall *ast.Node) bool {
for p := superCall.Parent; p != nil && p != body; p = p.Parent {
switch p.Kind {
case ast.KindWhileStatement:
ws := p.AsWhileStatement()
return ws == nil || !loopExitsAfterSuper(loopBodyStatements(ws.Statement))
case ast.KindDoStatement:
ds := p.AsDoStatement()
return ds == nil || !loopExitsAfterSuper(loopBodyStatements(ds.Statement))
case ast.KindForStatement:
fs := p.AsForStatement()
return fs == nil || !loopExitsAfterSuper(loopBodyStatements(fs.Statement))
case ast.KindForInStatement, ast.KindForOfStatement:
// These can run zero times and are already reported as missing;
// don't also claim a guaranteed duplicate here.
return false
}
}
return false
}

// loopExitsAfterSuper reports whether, once a loop body's top-level
// statements reach a super() call, they are guaranteed to hit a break (or
// return/throw) before falling through to the end of the body or hitting a
// continue - either of which would loop back and revisit the call. Bails out
// conservatively (assumes no guaranteed exit, i.e. a possible duplicate) for
// anything not on the small safe allowlist of statements that can't hide a
// break/continue.
func loopExitsAfterSuper(stmts []*ast.Node) bool {
seenSuper := false
for _, stmt := range stmts {
if stmt == nil {
continue
}
if !seenSuper {
if hasSuperCall(stmt) {
seenSuper = true
continue
}
switch stmt.Kind {
case ast.KindExpressionStatement, ast.KindVariableStatement, ast.KindEmptyStatement:
continue
default:
return false
}
}

switch stmt.Kind {
case ast.KindBreakStatement, ast.KindReturnStatement, ast.KindThrowStatement:
return true
case ast.KindContinueStatement:
return false
case ast.KindExpressionStatement, ast.KindVariableStatement, ast.KindEmptyStatement:
continue
default:
return false
}
}
// Reached the end of the body without a guaranteed exit: falls through
// and loops back around.
return false
}

// findSuperCallsInNode finds super() calls in a node (excluding direct super calls at the statement level)
func findSuperCallsInNode(node *ast.Node, superCallLocations []*ast.Node, duplicates *[]*ast.Node) {
if node == nil {
Expand Down Expand Up @@ -484,7 +590,7 @@ func checkAllPathsHaveSuper(body *ast.Node) bool {

// analyzeStatements checks if super() is called in all code paths
func analyzeStatements(statements []*ast.Node) bool {
for i, stmt := range statements {
for _, stmt := range statements {
if stmt == nil {
continue
}
Expand All @@ -507,11 +613,9 @@ func analyzeStatements(statements []*ast.Node) bool {
// Has else clause - both branches must have super or terminate
elseHasSuper := statementHasSuper(elseStmt)
if thenHasSuper && elseHasSuper {
// Both branches have super or terminate, so we can continue
// Check remaining statements
if i+1 < len(statements) {
return analyzeStatements(statements[i+1:])
}
// Both branches call super() (or terminate) on every path,
// so the requirement is already satisfied - trailing
// statements need not call super() again.
return true
}
}
Expand All @@ -520,10 +624,8 @@ func analyzeStatements(statements []*ast.Node) bool {
case ast.KindSwitchStatement:
// Switch with super in all cases (including default)
if switchHasSuper(stmt) {
// All switch paths have super, check remaining statements
if i+1 < len(statements) {
return analyzeStatements(statements[i+1:])
}
// All switch paths already satisfy the requirement; trailing
// statements need not call super() again.
return true
}

Expand All @@ -546,12 +648,129 @@ func analyzeStatements(statements []*ast.Node) bool {
}
}
}

case ast.KindBlock:
// A nested block is transparent to control flow.
if analyzeStatements(stmt.Statements()) {
return true
}

case ast.KindLabeledStatement:
// Labels don't affect control flow reachability of the wrapped statement.
labeledStmt := stmt.AsLabeledStatement()
if labeledStmt != nil && analyzeStatements([]*ast.Node{labeledStmt.Statement}) {
return true
}

case ast.KindTryStatement:
tryStmt := stmt.AsTryStatement()
if tryStmt != nil && tryStatementHasSuper(tryStmt) {
return true
}

case ast.KindWhileStatement:
// A `while (true)` (or other always-truthy condition) loop always
// executes its body at least once.
whileStmt := stmt.AsWhileStatement()
if whileStmt != nil && isAlwaysTruthyCondition(whileStmt.Expression) &&
loopBodyGuaranteesSuper(loopBodyStatements(whileStmt.Statement)) {
return true
}

case ast.KindForStatement:
// `for (;;)` or `for (...; true; ...)` always executes its body at
// least once.
forStmt := stmt.AsForStatement()
if forStmt != nil && (forStmt.Condition == nil || isAlwaysTruthyCondition(forStmt.Condition)) &&
loopBodyGuaranteesSuper(loopBodyStatements(forStmt.Statement)) {
return true
}

case ast.KindDoStatement:
// A do-while loop always executes its body at least once,
// regardless of the condition.
doStmt := stmt.AsDoStatement()
if doStmt != nil && loopBodyGuaranteesSuper(loopBodyStatements(doStmt.Statement)) {
return true
}
}
}

return false
}

// loopBodyStatements returns the statement list making up a loop body,
// handling both block bodies (`while (x) { ... }`) and single-statement
// bodies without braces (`while (x) foo();`).
func loopBodyStatements(body *ast.Node) []*ast.Node {
if body == nil {
return nil
}
if body.Kind == ast.KindBlock {
return body.Statements()
}
return []*ast.Node{body}
}

// isAlwaysTruthyCondition reports whether a loop condition is a literal that
// always evaluates to true, meaning the loop body is guaranteed to run.
func isAlwaysTruthyCondition(expr *ast.Node) bool {
return expr != nil && expr.Kind == ast.KindTrueKeyword
}

// loopBodyGuaranteesSuper reports whether a loop body that is guaranteed to
// execute at least once is also guaranteed to call super() before it can be
// exited via break/continue. It only recognizes the common, unambiguous
// top-level pattern of statements executed in sequence; anything that might
// hide a break/continue (nested blocks, if/switch/try, nested loops, labels,
// ...) causes it to bail out conservatively (return false) rather than risk
// missing a real "super() not called" case.
func loopBodyGuaranteesSuper(stmts []*ast.Node) bool {
for _, stmt := range stmts {
if stmt == nil {
continue
}
if hasSuperCall(stmt) {
return true
}
switch stmt.Kind {
case ast.KindReturnStatement, ast.KindThrowStatement:
return true
case ast.KindBreakStatement, ast.KindContinueStatement:
return false
case ast.KindExpressionStatement, ast.KindVariableStatement, ast.KindEmptyStatement:
continue
default:
return false
}
}
return false
}

// tryStatementHasSuper reports whether a try statement guarantees super() is
// called. A finally block that calls super() on every path guarantees it
// regardless of the try/catch outcome (finally always runs). Otherwise, both
// the try block and (if present) the catch block must independently
// guarantee it - if there is no catch clause, an exception thrown before
// super() propagates out of the constructor, which is an acceptable
// terminating path (like an explicit throw).
func tryStatementHasSuper(tryStmt *ast.TryStatement) bool {
if tryStmt.FinallyBlock != nil && analyzeStatements(tryStmt.FinallyBlock.Statements()) {
return true
}
if tryStmt.TryBlock == nil || !analyzeStatements(tryStmt.TryBlock.Statements()) {
return false
}
if tryStmt.CatchClause == nil {
return true
}
catchClause := tryStmt.CatchClause.AsCatchClause()
if catchClause == nil || catchClause.Block == nil {
return false
}
return analyzeStatements(catchClause.Block.Statements())
}

// expressionHasSuper checks if an expression contains a super() call
func expressionHasSuper(expr *ast.Node) bool {
if expr == nil {
Expand Down Expand Up @@ -603,16 +822,7 @@ func statementHasSuper(stmt *ast.Node) bool {
return false
}

if stmt.Kind == ast.KindBlock {
return analyzeStatements(stmt.Statements())
}

// Return and throw statements are valid path terminators
if stmt.Kind == ast.KindReturnStatement || stmt.Kind == ast.KindThrowStatement {
return true
}

return hasSuperCall(stmt)
return analyzeStatements([]*ast.Node{stmt})
}

// switchHasSuper checks if a switch statement has super in all branches
Expand Down Expand Up @@ -718,9 +928,11 @@ var ConstructorSuperRule = rule.Rule{
} else if !analysis.allPathsHaveSuper {
// super() called in some paths but not all
ctx.ReportNode(node, buildMissingSome())
} else if analysis.allPathsHaveSuper && len(analysis.superCallLocations) > 1 {
// Check for actual duplicates (super calls that can execute sequentially)
duplicates := findDuplicateSuperCalls(body, analysis.superCallLocations)
} else {
// All paths call super(); check for calls that could
// execute more than once (inside a non-exiting loop,
// or genuinely sequential duplicates).
duplicates := findAllSuperDuplicates(body, analysis.superCallLocations)
for _, superCall := range duplicates {
// Report on the super keyword, not the call expression
superKeyword := superCall.Expression()
Expand Down
Loading
Loading