From 44a1089197862825e5e64d4db0d9ad0289bb5c13 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:46:30 +0000 Subject: [PATCH] Fix phpstan/phpstan#6830: Variable inside loop might not be defined - Modified intersectConditionalExpressions() in MutatingScope to match conditional expressions by their condition part when exact key match fails, then merge result type holders via and() - Added conditionExpressionTypeHoldersEqual() helper method - New regression test in tests/PHPStan/Rules/Variables/data/bug-6830b.php - Root cause: conditional expression keys include result types, which change across loop iterations when variables are reassigned, causing the intersection to drop them entirely --- src/Analyser/MutatingScope.php | 46 +++++++++++++++++-- .../Variables/DefinedVariableRuleTest.php | 10 ++++ .../Rules/Variables/data/bug-6830b.php | 20 ++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-6830b.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 88a11de133..f5cbf37d35 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3432,11 +3432,29 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi $otherHolders = $otherConditionalExpressions[$exprString]; $intersectedHolders = []; - foreach ($holders as $key => $holder) { - if (!array_key_exists($key, $otherHolders)) { + foreach ($holders as $holder) { + $key = $holder->getKey(); + if (array_key_exists($key, $otherHolders)) { + $intersectedHolders[$key] = $holder; continue; } - $intersectedHolders[$key] = $holder; + + foreach ($otherHolders as $otherHolder) { + if (!$this->conditionExpressionTypeHoldersEqual( + $holder->getConditionExpressionTypeHolders(), + $otherHolder->getConditionExpressionTypeHolders(), + )) { + continue; + } + + $mergedTypeHolder = $holder->getTypeHolder()->and($otherHolder->getTypeHolder()); + $mergedHolder = new ConditionalExpressionHolder( + $holder->getConditionExpressionTypeHolders(), + $mergedTypeHolder, + ); + $intersectedHolders[$mergedHolder->getKey()] = $mergedHolder; + break; + } } if (count($intersectedHolders) === 0) { @@ -3449,6 +3467,28 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi return $newConditionalExpressions; } + /** + * @param array $a + * @param array $b + */ + private function conditionExpressionTypeHoldersEqual(array $a, array $b): bool + { + if (count($a) !== count($b)) { + return false; + } + + foreach ($a as $exprString => $holder) { + if (!array_key_exists($exprString, $b)) { + return false; + } + if (!$holder->equals($b[$exprString])) { + return false; + } + } + + return true; + } + /** * @param array $newConditionalExpressions * @param array $existingConditionalExpressions diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 2dde59f142..0d43017614 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1393,6 +1393,16 @@ public function testBug6830(): void $this->analyse([__DIR__ . '/data/bug-6830.php'], []); } + public function testBug6830b(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-6830b.php'], []); + } + public function testBug14019(): void { $this->cliArgumentsVariablesRegistered = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-6830b.php b/tests/PHPStan/Rules/Variables/data/bug-6830b.php new file mode 100644 index 0000000000..39d5e9d375 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-6830b.php @@ -0,0 +1,20 @@ +