From 6f46059d7f662b15ebd9bf3636d1001b3970eb4d Mon Sep 17 00:00:00 2001 From: HawkForge <75696486+HawkForge@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:05:26 +0600 Subject: [PATCH 1/2] Keep iterating a for loop when a nested loop hides its back edge The convergence passes walk the body in deep context, where a nested loop is walked once instead of to its fixpoint. When the back edge is only reached after the nested loop has iterated more than once, the pass finds no back edge and stopped with the counter still at its initial value. The final walk then saw `$i` as `0` and reported `$i < 10` as always true. A dead back edge is still trusted when the body is replayable - the deep walk is then the same as the final one. Otherwise the pass continues from the end of the body, as 2.2 did. Closes https://github.com/phpstan/phpstan/issues/15420 --- src/Analyser/StmtHandler/ForHandler.php | 10 +++++-- ...isonOperatorsConstantConditionRuleTest.php | 5 ++++ .../Rules/Comparison/data/bug-15420.php | 26 +++++++++++++++++++ turbo-ext/src/ForHandler.cpp | 17 +++++++++--- 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-15420.php diff --git a/src/Analyser/StmtHandler/ForHandler.php b/src/Analyser/StmtHandler/ForHandler.php index e59ec2e5d15..6d14dd3e1f1 100644 --- a/src/Analyser/StmtHandler/ForHandler.php +++ b/src/Analyser/StmtHandler/ForHandler.php @@ -221,8 +221,14 @@ public function processStmt( $passFlows[] = $bodyScopeResult->getVariableFlow(); $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); if ($backEdgeScope === null) { - $bodyScope = $prevScope; - break; + if ($nodeScopeResolver->isReplayableConvergenceBody($stmt, $stmt->stmts)) { + $bodyScope = $prevScope; + break; + } + // a nested loop is walked only once in deep context, so the + // back edge it would reach on a later iteration can look dead - + // continue from the end of the body instead of stopping here + $backEdgeScope = $bodyScopeResult->getScope(); } $bodyScope = $backEdgeScope; diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 106ddee426d..ccc7e32fe7e 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -327,6 +327,11 @@ public function testBug15243(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-15243.php'], []); } + public function testBug15420(): void + { + $this->analyse([__DIR__ . '/data/bug-15420.php'], []); + } + public function testShiftLeftUnboundedRange(): void { $this->analyse([__DIR__ . '/../../Analyser/nsrt/shift-left-unbounded-range.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-15420.php b/tests/PHPStan/Rules/Comparison/data/bug-15420.php new file mode 100644 index 00000000000..503b65adfb4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-15420.php @@ -0,0 +1,26 @@ + Date: Thu, 15 Oct 2026 15:05:26 +0600 Subject: [PATCH 2/2] Bump expected turbo version --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 374d29043e6..90dd54da5cd 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = 'a0f7086'; + public const EXPECTED_EXTENSION_VERSION = '6f46059'; private static bool $active = false;