From ec87b766ffbfb0d9a18c24a07e19b99da5d080cd Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:34:16 +0000 Subject: [PATCH 1/3] Fix false positive undefined variable when breaking out of infinite loop - In while(true) loops, the only way to exit is via break, so the post-loop scope should be constructed solely from break exit point scopes - Previously, break scopes were merged with the "normal exit" scope (filterByFalseyValue), which degraded variable certainty to "maybe" for variables only defined in the break path - New regression test in tests/PHPStan/Rules/Variables/data/bug-9023.php Closes https://github.com/phpstan/phpstan/issues/9023 --- src/Analyser/NodeScopeResolver.php | 12 ++++++-- .../Variables/DefinedVariableRuleTest.php | 10 +++++++ .../PHPStan/Rules/Variables/data/bug-9023.php | 29 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-9023.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8a807ea745..016f25763b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1517,8 +1517,16 @@ private function processStmtNode( } $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); - foreach ($breakExitPoints as $breakExitPoint) { - $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); + if ($alwaysIterates && count($breakExitPoints) > 0) { + $breakScope = null; + foreach ($breakExitPoints as $breakExitPoint) { + $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope()); + } + $finalScope = $breakScope; + } else { + foreach ($breakExitPoints as $breakExitPoint) { + $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); + } } $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes(); diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 112a57d4cc..e18f042703 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 testBug9023(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/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..5966ffb020 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9023.php @@ -0,0 +1,29 @@ + Date: Mon, 16 Feb 2026 20:44:29 +0000 Subject: [PATCH 2/3] 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 e18f042703..482150b0b8 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1267,4 +1267,14 @@ public function testBug9023(): void $this->analyse([__DIR__ . '/data/bug-9023.php'], []); } + public function testBug11984(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $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 43c3377281df724928c4b4a9f300b902592ee21a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 20:46:29 +0000 Subject: [PATCH 3/3] Add regression tests for #11545, #10245, #5919, #5477 Closes https://github.com/phpstan/phpstan/issues/11545 Closes https://github.com/phpstan/phpstan/issues/10245 Closes https://github.com/phpstan/phpstan/issues/5919 Closes https://github.com/phpstan/phpstan/issues/5477 --- .../Variables/DefinedVariableRuleTest.php | 49 ++++++++++++++++++ .../Rules/Variables/data/bug-10245.php | 36 +++++++++++++ .../Rules/Variables/data/bug-11545.php | 45 +++++++++++++++++ .../PHPStan/Rules/Variables/data/bug-5477.php | 16 ++++++ .../PHPStan/Rules/Variables/data/bug-5919.php | 50 +++++++++++++++++++ 5 files changed, 196 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-10245.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-11545.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-5477.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-5919.php diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 482150b0b8..ccf3735539 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1277,4 +1277,53 @@ public function testBug11984(): void $this->analyse([__DIR__ . '/data/bug-11984.php'], []); } + public function testBug11545(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-11545.php'], [ + [ + 'Variable $result might not be defined.', + 24, + ], + [ + 'Variable $result might not be defined.', + 36, + ], + ]); + } + + public function testBug10245(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-10245.php'], []); + } + + public function testBug5919(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-5919.php'], []); + } + + public function testBug5477(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $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..f8c3e61156 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10245.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug10245; + +/** + * @throws \Exception + */ +function produceInt(): int +{ + return 1; +} + +function testTryCatchInWhileTrue(): void +{ + while (true) { + try { + $a = produceInt(); + break; + } catch (\Throwable) {} + } + + echo $a; +} + +function testIfBreakInWhileTrue(int $max): void +{ + $i = 0; + while (true) { + if ($i > $max) { + $result = 'done'; + break; + } + ++$i; + } + print $result; +} 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..8a118ce1e9 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-11545.php @@ -0,0 +1,45 @@ + $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; +} + +function bar(int $max): void { + while (true) { + $result = 'done'; + break; + } + print $result; +} 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..ad2e35b043 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-5477.php @@ -0,0 +1,16 @@ +