From ee8b6205967c912c6eb01684a2af4b93b743e611 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 03:27:59 +0000 Subject: [PATCH 1/7] Fix variable definedness after always-iterating loops with break - When a loop always iterates (while(true), for(;;), do...while(true)), the only way to exit is through a break statement - Previously, break scopes were merged into an unreachable "condition became false" scope, causing variables defined before break to be reported as "might not be defined" - Now, when the loop always iterates and has break exit points, the post-loop scope is built purely from the break exit point scopes - Applied the fix consistently to while, for, and do-while loops - Added regression test for all three loop types Closes https://github.com/phpstan/phpstan/issues/11545 --- src/Analyser/NodeScopeResolver.php | 43 ++++++++++++++++--- .../Variables/DefinedVariableRuleTest.php | 10 +++++ .../Rules/Variables/data/bug-11545.php | 37 ++++++++++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-11545.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8a807ea745..9a090bbae5 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1517,8 +1517,17 @@ private function processStmtNode( } $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); - foreach ($breakExitPoints as $breakExitPoint) { - $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); + if ($alwaysIterates && count($breakExitPoints) > 0) { + $finalScope = null; + foreach ($breakExitPoints as $breakExitPoint) { + $finalScope = $finalScope === null + ? $breakExitPoint->getScope() + : $finalScope->mergeWith($breakExitPoint->getScope()); + } + } else { + foreach ($breakExitPoints as $breakExitPoint) { + $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); + } } $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes(); @@ -1624,8 +1633,18 @@ private function processStmtNode( } else { $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep()); } - foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { - $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + $breakExitPoints = $bodyScopeResult->getExitPointsByType(Break_::class); + if ($alwaysIterates && count($breakExitPoints) > 0) { + $finalScope = null; + foreach ($breakExitPoints as $breakExitPoint) { + $finalScope = $finalScope === null + ? $breakExitPoint->getScope() + : $finalScope->mergeWith($breakExitPoint->getScope()); + } + } else { + foreach ($breakExitPoints as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + } } return new InternalStatementResult( @@ -1736,8 +1755,18 @@ private function processStmtNode( $finalScope = $finalScope->filterByFalseyValue($lastCondExpr); } - foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { - $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); + if ($alwaysIterates->yes() && count($breakExitPoints) > 0) { + $finalScope = null; + foreach ($breakExitPoints as $breakExitPoint) { + $finalScope = $finalScope === null + ? $breakExitPoint->getScope() + : $finalScope->mergeWith($breakExitPoint->getScope()); + } + } else { + foreach ($breakExitPoints as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + } } if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { @@ -1753,7 +1782,7 @@ private function processStmtNode( } else { $finalScope = $finalScope->mergeWith($scope); } - } else { + } elseif (!$alwaysIterates->yes()) { if (!$this->polluteScopeWithLoopInitialAssignments) { $finalScope = $finalScope->mergeWith($scope); } diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 112a57d4cc..a39a83952c 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1257,4 +1257,14 @@ public function testBug12944(): void $this->analyse([__DIR__ . '/data/bug-12944.php'], []); } + public function testBug11545(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-11545.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-11545.php b/tests/PHPStan/Rules/Variables/data/bug-11545.php new file mode 100644 index 0000000000..5c10a8c9f4 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-11545.php @@ -0,0 +1,37 @@ + $max) { + $result = 'done'; + break; + } + ++$i; + } + print $result; +} + +function foo_for(int $max): void { + for ($i = 0;; ++$i) { + if ($i > $max) { + $result = 'done'; + break; + } + } + print $result; +} + +function foo_do_while(int $max): void { + $i = 0; + do { + if ($i > $max) { + $result = 'done'; + break; + } + ++$i; + } while (true); + print $result; +} From c6690e28042aeaf429029aa8e0b3132559ceeaa1 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 17 Feb 2026 03:34:43 +0000 Subject: [PATCH 2/7] Add regression test for #11984 Closes https://github.com/phpstan/phpstan/issues/11984 --- .../Variables/DefinedVariableRuleTest.php | 10 ++++++ .../Rules/Variables/data/bug-11984.php | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-11984.php diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index a39a83952c..c481c23717 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1267,4 +1267,14 @@ public function testBug11545(): void $this->analyse([__DIR__ . '/data/bug-11545.php'], []); } + public function testBug11984(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-11984.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-11984.php b/tests/PHPStan/Rules/Variables/data/bug-11984.php new file mode 100644 index 0000000000..da45fb4f3c --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-11984.php @@ -0,0 +1,31 @@ + + */ + public function loadFromFile(): array + { + return ['x' => 1]; + } + + /** + * @return array + */ + public function test(): array + { + while (true) { + try { + $data = $this->loadFromFile(); + + break; + } catch (\Exception $ex) { + } + } + + return $data; + } +} From 5f5765945b4dd09e583a3e678c84390ad0484255 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 17 Feb 2026 03:37:03 +0000 Subject: [PATCH 3/7] Add regression test for #10245 Closes https://github.com/phpstan/phpstan/issues/10245 --- tests/PHPStan/Analyser/nsrt/bug-10245.php | 25 ++++++++++++ .../Variables/DefinedVariableRuleTest.php | 40 +++++++++++++++++++ .../Rules/Variables/data/bug-10245.php | 36 +++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-10245.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-10245.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-10245.php b/tests/PHPStan/Analyser/nsrt/bug-10245.php new file mode 100644 index 0000000000..16fe99f193 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10245.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug10245; + +use function PHPStan\Testing\assertType; + +/** + * @throws \Exception + */ +function produceInt(): int +{ + return 1; +} + +function testTryCatchInWhileTrue(): void +{ + while (true) { + try { + $a = produceInt(); + break; + } catch (\Throwable) {} + } + + assertType('int', $a); +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index c481c23717..f566e0af2f 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1277,4 +1277,44 @@ public function testBug11984(): void $this->analyse([__DIR__ . '/data/bug-11984.php'], []); } + public function testBug10245(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-10245.php'], []); + } + + public function testBug9023(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-9023.php'], []); + } + + public function testBug5919(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-5919.php'], []); + } + + public function testBug5477(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-5477.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-10245.php b/tests/PHPStan/Rules/Variables/data/bug-10245.php new file mode 100644 index 0000000000..e522e4cca3 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10245.php @@ -0,0 +1,36 @@ + $max) { + $result = 'done'; + break; + } + ++$i; + } + print $result; +} From 6072a7f470cfce0b375a02125eb59a425647ac5e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 17 Feb 2026 03:37:10 +0000 Subject: [PATCH 4/7] Add regression test for #9023 Closes https://github.com/phpstan/phpstan/issues/9023 --- .../PHPStan/Rules/Variables/data/bug-9023.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-9023.php diff --git a/tests/PHPStan/Rules/Variables/data/bug-9023.php b/tests/PHPStan/Rules/Variables/data/bug-9023.php new file mode 100644 index 0000000000..855fe007b2 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9023.php @@ -0,0 +1,27 @@ + Date: Tue, 17 Feb 2026 03:37:18 +0000 Subject: [PATCH 5/7] Add regression test for #5919 Closes https://github.com/phpstan/phpstan/issues/5919 --- .../PHPStan/Rules/Variables/data/bug-5919.php | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-5919.php diff --git a/tests/PHPStan/Rules/Variables/data/bug-5919.php b/tests/PHPStan/Rules/Variables/data/bug-5919.php new file mode 100644 index 0000000000..a9aff77639 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-5919.php @@ -0,0 +1,48 @@ + Date: Tue, 17 Feb 2026 03:37:22 +0000 Subject: [PATCH 6/7] Add regression test for #5477 Closes https://github.com/phpstan/phpstan/issues/5477 --- tests/PHPStan/Rules/Variables/data/bug-5477.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-5477.php diff --git a/tests/PHPStan/Rules/Variables/data/bug-5477.php b/tests/PHPStan/Rules/Variables/data/bug-5477.php new file mode 100644 index 0000000000..222efd3d30 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-5477.php @@ -0,0 +1,17 @@ + Date: Tue, 17 Feb 2026 03:47:08 +0000 Subject: [PATCH 7/7] Fix CI failures [claude-ci-fix] Automated fix attempt 1 for CI failures. --- tests/PHPStan/Analyser/nsrt/bug-10245.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-10245.php b/tests/PHPStan/Analyser/nsrt/bug-10245.php index 16fe99f193..2fc5b5e78d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10245.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10245.php @@ -1,6 +1,6 @@ = 8.0 -namespace Bug10245; +namespace NsrtBug10245; use function PHPStan\Testing\assertType;