From 7259b5720ea49ef38b13892fa78f35a876581b1b Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:04:07 +0000 Subject: [PATCH 1/7] Fix phpstan/phpstan#14117: Variable might not be defined false positive with same condition - Fixed createConditionalExpressions in MutatingScope to preserve variables in newVariableTypes when their type matches but certainty differs between branches - This ensures conditional expressions are created to track that a variable is definitely defined when the same condition holds as when it was assigned - Removed now-unnecessary @phpstan-ignore variable.undefined comments in OptimizedDirectorySourceLocator.php that were working around this bug - New regression test in tests/PHPStan/Rules/Variables/data/bug-14117.php --- src/Analyser/MutatingScope.php | 8 +++ .../OptimizedDirectorySourceLocator.php | 4 +- .../Variables/DefinedVariableRuleTest.php | 19 +++++++ .../Rules/Variables/data/bug-14117.php | 51 +++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-14117.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 900c3f3fff..49ed3181bb 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4094,6 +4094,14 @@ private function createConditionalExpressions( continue; } + if ( + array_key_exists($exprString, $newVariableTypes) + && $newVariableTypes[$exprString]->equalTypes($holder) + && !$newVariableTypes[$exprString]->getCertainty()->equals($holder->getCertainty()) + ) { + continue; + } + unset($newVariableTypes[$exprString]); } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index e66dfea5d6..f2b0f7e3a6 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -127,7 +127,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return null; } - [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($file, $identifier); // @phpstan-ignore variable.undefined + [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($file, $identifier); $classReflection = $this->nodeToReflection($reflector, $fetchedClassNode); $this->cache->save($reflectionCacheKey, $variableCacheKey, $classReflection->exportToCache()); @@ -171,7 +171,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return null; } - [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($file, $identifier); // @phpstan-ignore variable.undefined + [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($file, $identifier); $constantReflection = $this->nodeToReflection( $reflector, $fetchedConstantNode, diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 1a9f84f7ab..ac3c9cba07 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1333,4 +1333,23 @@ public function testBug5477(): void $this->analyse([__DIR__ . '/data/bug-5477.php'], []); } + public function testBug14117(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-14117.php'], [ + [ + 'Variable $value might not be defined.', + 33, + ], + [ + 'Variable $value might not be defined.', + 49, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14117.php b/tests/PHPStan/Rules/Variables/data/bug-14117.php new file mode 100644 index 0000000000..6d672c61b3 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-14117.php @@ -0,0 +1,51 @@ + Date: Wed, 4 Mar 2026 07:27:55 +0100 Subject: [PATCH 2/7] regression test --- .../Variables/DefinedVariableRuleTest.php | 20 ++++ .../PHPStan/Rules/Variables/data/bug-8430.php | 31 +++++ .../Rules/Variables/data/bug-8430b.php | 111 ++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-8430.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-8430b.php diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index ac3c9cba07..cf7047c31a 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1323,6 +1323,26 @@ public function testBug5919(): void $this->analyse([__DIR__ . '/data/bug-5919.php'], []); } + public function testBug8430(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-8430.php'], []); + } + + public function testBug8430b(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-8430b.php'], []); + } + public function testBug5477(): void { $this->cliArgumentsVariablesRegistered = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-8430.php b/tests/PHPStan/Rules/Variables/data/bug-8430.php new file mode 100644 index 0000000000..9ec7e1c436 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8430.php @@ -0,0 +1,31 @@ + 1]; + } + if (!$a && !$b) { + echo $arr['a']; + } + } + } + + public function def(string $a, bool $b): void + { + if (!$b) { + $arr = ['a' => 1]; + } + if (!$a && !$b) { + echo $arr['a']; + } + } +} + diff --git a/tests/PHPStan/Rules/Variables/data/bug-8430b.php b/tests/PHPStan/Rules/Variables/data/bug-8430b.php new file mode 100644 index 0000000000..4622031dd7 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-8430b.php @@ -0,0 +1,111 @@ + */ + public array $c; + public string $d; +} + +class B +{ + private function abc(): void + { + } + + /** + * @param A[] $a + * @param array $b + */ + public function def(array $a, array $b, string $c, bool $d): void + { + $e = false; + $f = false; + switch ($b['repeat'] ?? null) { + case 'Y': + $e = true; + break; + case 'A': + $e = true; + $f = true; + break; + } + $g = 5; + $h = 0; + for ($i = 1; $i <= $g; $i++) { + if (!$d) { + $arr = ['a' => 1]; + } + $j = $a[$i] ?? null; + if ($j) { + /** @var array $k */ + $k = []; + if ($e) { + foreach ($j->a as $l) { + /** @var A[] $m */ + $m = $f || empty($k) ? $j->b : []; + array_push($m, ...$l); + array_push($k, $m); + } + if (empty($k)) { + array_push($k, $j->b); + } + } else { + array_push($k, $j->b); + foreach ($j->a as $l) { + array_push($k[0], ...$l); + break; + } + } + foreach ($k as $n) { + if (!$d) { + foreach ($n as $o) { + switch ($o->c['x'] ?? '') { + case 'y': + $p = $o->c[$o->d] ?? null; + if (is_array($p)) { + $this->abc(); + } + break; + case 'z': + $p = $o->c[$o->d] ?? null; + if (is_array($p)) { + $this->abc(); + } + break; + default: + $this->abc(); + break; + } + } + } + if (!empty($n)) { + $h++; + } + } + } + if (!$c && !$d) { + echo $arr['a']; + } + } + } + + public function ghi(string $a, bool $b): void + { + if (!$b) { + $arr = ['a' => 1]; + } + $this->abc(); + if (!$a && !$b) { + echo $arr['a']; + } + } +} From 2b68f499aa4067a76a03c3dc9c4dcffb641d7061 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 4 Mar 2026 07:29:21 +0100 Subject: [PATCH 3/7] regression test --- .../Variables/DefinedVariableRuleTest.php | 10 ++++++++++ .../PHPStan/Rules/Variables/data/bug-10657.php | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-10657.php diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index cf7047c31a..6611b9ea36 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1353,6 +1353,16 @@ public function testBug5477(): void $this->analyse([__DIR__ . '/data/bug-5477.php'], []); } + public function testBug10657(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-10657.php'], []); + } + public function testBug14117(): void { $this->cliArgumentsVariablesRegistered = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-10657.php b/tests/PHPStan/Rules/Variables/data/bug-10657.php new file mode 100644 index 0000000000..f9d6a508fb --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-10657.php @@ -0,0 +1,18 @@ + Date: Wed, 4 Mar 2026 07:32:01 +0100 Subject: [PATCH 4/7] added test --- .../Variables/DefinedVariableRuleTest.php | 10 ++++++++++ .../PHPStan/Rules/Variables/data/bug-6830.php | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-6830.php diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 6611b9ea36..5ed54a92a3 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1363,6 +1363,16 @@ public function testBug10657(): void $this->analyse([__DIR__ . '/data/bug-10657.php'], []); } + public function testBug6830(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-6830.php'], []); + } + public function testBug14117(): void { $this->cliArgumentsVariablesRegistered = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-6830.php b/tests/PHPStan/Rules/Variables/data/bug-6830.php new file mode 100644 index 0000000000..1a4e5a4490 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-6830.php @@ -0,0 +1,18 @@ + $bools */ +function test(array $bools): void +{ + foreach ($bools as $bool) { + if ($bool) { + $foo = 'foo'; + } + if ($bool) { + echo $foo; + } + } +} From 76fd431d5954360407315c7b81edfd638bec2bb2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 4 Mar 2026 07:34:51 +0100 Subject: [PATCH 5/7] added test --- .../Variables/DefinedVariableRuleTest.php | 4 ++++ .../PHPStan/Rules/Variables/data/bug-14117.php | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 5ed54a92a3..8835f5b702 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1389,6 +1389,10 @@ public function testBug14117(): void 'Variable $value might not be defined.', 49, ], + [ + 'Undefined variable: $value', + 65 + ] ]); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14117.php b/tests/PHPStan/Rules/Variables/data/bug-14117.php index 6d672c61b3..341ec28209 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14117.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14117.php @@ -46,6 +46,22 @@ function baz(): void { } if ($key === 3) { - echo $value; // this one SHOULD report "might not be defined" because $key === 3 doesn't guarantee either earlier block ran + echo $value; // SHOULD report "is not defined" + } +} + +function boo(): void { + $key = rand(0, 2); + + if ($key === 1) { + $value = 'test'; + } + + if ($key === 1) { + unset($value); + } + + if ($key === 1) { + echo $value; } } From 709fbd02a67677595044ac1ca4ccdff52fc61e75 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 4 Mar 2026 07:35:48 +0100 Subject: [PATCH 6/7] cheap check first --- src/Analyser/MutatingScope.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 49ed3181bb..4e510b52f4 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4096,8 +4096,8 @@ private function createConditionalExpressions( if ( array_key_exists($exprString, $newVariableTypes) - && $newVariableTypes[$exprString]->equalTypes($holder) && !$newVariableTypes[$exprString]->getCertainty()->equals($holder->getCertainty()) + && $newVariableTypes[$exprString]->equalTypes($holder) ) { continue; } From 4fcbc9771cced2303f94333d8d5498377e75374d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 4 Mar 2026 07:40:12 +0100 Subject: [PATCH 7/7] more tests --- .../Rules/Variables/DefinedVariableRuleTest.php | 8 ++++++-- tests/PHPStan/Rules/Variables/data/bug-14117.php | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 8835f5b702..d6a4ce9a4c 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1391,8 +1391,12 @@ public function testBug14117(): void ], [ 'Undefined variable: $value', - 65 - ] + 65, + ], + [ + 'Undefined variable: $value', + 81, + ], ]); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14117.php b/tests/PHPStan/Rules/Variables/data/bug-14117.php index 341ec28209..e1d7c40306 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14117.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14117.php @@ -65,3 +65,19 @@ function boo(): void { echo $value; } } + +function loo(): void { + $key = rand(0, 2); + + if ($key === 1) { + $value = 'test'; + } + + if ($key === 1 || $key === 2) { + unset($value); + } + + if ($key === 1) { + echo $value; + } +}