Skip to content

[NodeAnalyzer] Detect always terminated stmts with statements before the return - #8260

Merged
TomasVotruba merged 1 commit into
mainfrom
fix-terminated-node-analyzer-try-catch
Aug 2, 2026
Merged

[NodeAnalyzer] Detect always terminated stmts with statements before the return#8260
TomasVotruba merged 1 commit into
mainfrom
fix-terminated-node-analyzer-try-catch

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

TerminatedNodeAnalyzer bailed out whenever the second-to-last statement of a try/catch, if/else or switch branch was not itself a terminator:

if (isset($stmts[$lastKey - 1]) && ! $this->isTerminatedNode($stmts[$lastKey - 1], $node)) {
    return false;
}

That guard came from #6837, to stop a break inside a switch case being read as always terminated. It is too broad: any branch that does real work before its return trips it.

This moves the check to the Switch_ branch, where a break/continue genuinely escapes to the statement after the switch. The generic path now only looks at the last statement, as intended.

Effect

RemoveUnreachableStatementRector now sees through branches that do work before returning:

 try {
     echo 'try';
     return something();
 } catch (Exception $e) {
     echo 'catch';
     return null;
 }
-
-echo 'never executed';

Bare-return branches already worked; only the ones with preceding statements were missed.

Downstream

This also fixes ConsoleExecuteReturnIntRector in rector-symfony appending a second, unreachable return 0;:

public function execute(InputInterface $input, OutputInterface $output)
{
    try {
        $output->writeln('working');
        return 0;
    } catch (\Exception $exception) {
        $output->writeln($exception->getMessage());
        return 1;
    }
}
-    public function execute(InputInterface $input, OutputInterface $output)
+    public function execute(InputInterface $input, OutputInterface $output): int
     {
         try {
             $output->writeln('working');
             return 0;
         } catch (\Exception $exception) {
             $output->writeln($exception->getMessage());
             return 1;
         }
-        return 0;
     }

Notes

  • #6837's skip_switch_break_not_unreachable.php.inc still passes.
  • The escaping-jump lookup descends into nested loops, so a break belonging to an inner loop still counts as escaping. That stays conservative (skip rather than remove) and matches the previous behaviour; skip_switch_case_nested_loop_break.php.inc locks it in.
  • isTerminatedNode() params narrowed to Stmt, as type-perfect requires once the generic call site is gone.

…the return

TerminatedNodeAnalyzer bailed out whenever the second-to-last statement of
a try/catch, if/else or switch branch was not itself a terminator. That guard
was added in #6837 to keep a break inside a switch case from being treated as
always terminated, but it also rejected every branch doing real work before
its return.

Move the check to the Switch_ branch, where a break/continue actually escapes
to the next statement, so the generic path only looks at the last statement.
@TomasVotruba
TomasVotruba merged commit 4229d89 into main Aug 2, 2026
64 checks passed
@TomasVotruba
TomasVotruba deleted the fix-terminated-node-analyzer-try-catch branch August 2, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant