Skip to content

Simulate loop analysis for goto-label pairs#5701

Open
phpstan-bot wants to merge 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-k9brwhl
Open

Simulate loop analysis for goto-label pairs#5701
phpstan-bot wants to merge 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-k9brwhl

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

This PR treats goto as a terminating statement (like break/continue/return) and implements loop-like scope stabilization for backward goto-label pairs (where a goto jumps back to an earlier label, creating a loop).

Changes

goto as a terminating statement: A goto statement now produces an exit point carrying its scope, and marks execution as terminated. This fixes forward-goto cases where code after a goto was not recognized as unreachable, and where labels after terminating code were not recognized as reachable via goto.

Same-level backward goto stabilization: When a label at the current statement-list level has a goto targeting it from later in the same list, the statements between the label and the goto are processed in a stabilization loop (using NoopNodeCallback and enterDeep(), with generalization after GENERALIZE_AFTER_ITERATION and a cap at LOOP_SCOPE_ITERATIONS), exactly matching the existing loop analysis pattern for while/for/foreach.

Nested backward goto stabilization: When a compound statement (e.g., try-catch) contains both a label and a goto targeting it across sub-blocks (label in try body, goto in catch body), the containing statement is processed through the same stabilization loop before the real analysis pass.

Label reachability: Labels now un-terminate execution when reached via a forward goto exit point. The unreachable-statement detection (getNextUnreachableStatements) stops at Label nodes to avoid false positives.

Regression tests

  • bug-4674.php (NSRT) — forward goto type narrowing
  • bug-7711.php (NSRT) — backward goto null-check retry loop
  • bug-7734.php (NSRT) — forward goto resource narrowing
  • bug-9904.php (NSRT) — forward goto skips foreach
  • goto-label-stabilization.php (NSRT) — comprehensive stabilization tests (forward narrowing, backward loops, retry patterns, multiple gotos to same label)
  • bug-11731.php (UnreachableStatementRule) — unreachable code after goto
  • bug-12167.php (NumberComparisonOperatorsConstantConditionRule) — try-catch retry pattern with nested goto-label
  • bug-12852.php (BooleanNotConstantConditionRule) — unconditional assignment before condition in goto loop (correct always-true report, matching while-loop semantics)
  • bug-14638.php (MissingReturnRule) — Symfony cache goto retry pattern with missing return false positive

Closes phpstan/phpstan#14641
Closes phpstan/phpstan#920
Closes phpstan/phpstan#14638
Closes phpstan/phpstan#4674
Closes phpstan/phpstan#7711
Closes phpstan/phpstan#7734
Closes phpstan/phpstan#9904
Closes phpstan/phpstan#11731
Closes phpstan/phpstan#12167

Treat goto as a terminating statement with exit points, and implement
loop-like stabilization for backward goto-label pairs at both the
same statement-list level and across nested structures like try-catch.
Copy link
Copy Markdown
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Good job so far!

Comment thread src/Analyser/NodeScopeResolver.php Outdated
Comment on lines +413 to +464
$backwardGotoLabels = [];
$labelIndices = [];
foreach ($stmts as $idx => $s) {
if (!($s instanceof Node\Stmt\Label)) {
continue;
}

$labelIndices[$s->name->toString()] = $idx;
}
$nodeFinder = new NodeFinder();
if ($labelIndices !== []) {
foreach ($labelIndices as $labelName => $labelIdx) {
$stmtsAfterLabel = array_slice($stmts, $labelIdx + 1);
/** @var Goto_[] $gotos */
$gotos = $nodeFinder->findInstanceOf($stmtsAfterLabel, Goto_::class);
foreach ($gotos as $goto) {
if ($goto->name->toString() === $labelName) {
$backwardGotoLabels[$labelName] = true;
break;
}
}
}
}

$nestedBackwardGotoLabels = [];
foreach ($stmts as $idx => $s) {
if ($s instanceof Node\Stmt\Label) {
continue;
}
/** @var Node\Stmt\Label[] $nestedLabels */
$nestedLabels = $nodeFinder->findInstanceOf([$s], Node\Stmt\Label::class);
if ($nestedLabels === []) {
continue;
}
/** @var Goto_[] $nestedGotos */
$nestedGotos = $nodeFinder->findInstanceOf([$s], Goto_::class);
if ($nestedGotos === []) {
continue;
}
$nestedLabelNames = [];
foreach ($nestedLabels as $nestedLabel) {
$nestedLabelNames[$nestedLabel->name->toString()] = true;
}
foreach ($nestedGotos as $nestedGoto) {
$gotoName = $nestedGoto->name->toString();
if (!isset($nestedLabelNames[$gotoName])) {
continue;
}

$nestedBackwardGotoLabels[$idx][$gotoName] = true;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would be really nice if we were able to move this code to a richParserNodeVisitor and if it ran very efficiently when traversing the AST. I don't want to do it here because the same code might be analysed multiple times and this would waste a lot of resources. i'm talking about code between lines 413-464.

Would be nice if the attributes on AST Node would be set just once on a specific node, like Label or Goto_ so that it's not spammed on all nodes for example.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. The review feedback has been addressed with a single commit that:

  1. Created src/Parser/GotoLabelVisitor.php — a richParserNodeVisitor that runs once per file parse (not per analysis pass) and sets attributes on specific AST nodes:

    • hasBackwardGoto on Label nodes that have a backward goto targeting them
    • nestedBackwardGotoLabels on compound statement nodes (like TryCatch) that contain both a label and a goto to it
  2. Updated src/Analyser/NodeScopeResolver.php — removed the 53-line scanning block (the $backwardGotoLabels / $nestedBackwardGotoLabels computation with NodeFinder) and replaced the two consumption sites with getAttribute() calls reading from the pre-computed visitor attributes.

The attributes are only set on the specific relevant nodes (Label and compound statements), not spammed on all nodes, as the reviewer requested.

The backward goto and nested backward goto detection code was running
inside processStmtNodesInternalWithoutFlushingPendingFibers, which
meant it executed on every analysis pass including stabilization loops.
Move it to GotoLabelVisitor so it runs once per parse and stores
results as attributes on Label and compound-statement nodes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Run make cs-fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Applied make cs-fix which fixed 2 coding style issues in GotoLabelVisitor.php (converting if-body to early-continue pattern). Both make phpstan and make tests pass cleanly. Committed and pushed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants