From 437d1f8f64b8d293970db7df03b9d4f105b20fb3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:22:10 +0000 Subject: [PATCH] Fix variable defined before loop reported as maybe undefined inside loop - Fixed intersectConditionalExpressions in MutatingScope to merge conditional expressions with matching conditions but different result types, instead of dropping them entirely - Added regression test case in tests/PHPStan/Rules/Variables/data/bug-6830.php - Root cause: during loop scope merging, conditional expression keys include the result type, so different types across iterations caused the intersection to drop the conditional linking variable definedness to the guarding condition Closes https://github.com/phpstan/phpstan/issues/6830 --- src/Analyser/MutatingScope.php | 56 ++++++++++++++++++- .../PHPStan/Rules/Variables/data/bug-6830.php | 15 +++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f1dfb1f0da..ca4d27ff3e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3431,18 +3431,70 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi } $otherHolders = $otherConditionalExpressions[$exprString]; + $allKeysMatch = true; foreach (array_keys($holders) as $key) { if (!array_key_exists($key, $otherHolders)) { - continue 2; + $allKeysMatch = false; + break; } } - $newConditionalExpressions[$exprString] = $holders; + if ($allKeysMatch) { + $newConditionalExpressions[$exprString] = $holders; + continue; + } + + // Keys don't match exactly - try matching by condition expressions + // and merge result type holders + $mergedHolders = []; + foreach ($holders as $holder) { + $conditionHolders = $holder->getConditionExpressionTypeHolders(); + foreach ($otherHolders as $otherHolder) { + if ($this->conditionExpressionHoldersMatch($conditionHolders, $otherHolder->getConditionExpressionTypeHolders())) { + $mergedTypeHolder = $holder->getTypeHolder()->and($otherHolder->getTypeHolder()); + $mergedHolder = new ConditionalExpressionHolder( + $conditionHolders, + $mergedTypeHolder, + ); + $mergedHolders[$mergedHolder->getKey()] = $mergedHolder; + break; + } + } + } + + if (count($mergedHolders) <= 0) { + continue; + } + + $newConditionalExpressions[$exprString] = $mergedHolders; } return $newConditionalExpressions; } + /** + * @param array $a + * @param array $b + */ + private function conditionExpressionHoldersMatch(array $a, array $b): bool + { + if (count($a) !== count($b)) { + return false; + } + + foreach ($a as $key => $holder) { + if (!array_key_exists($key, $b)) { + return false; + } + + if (!$holder->equals($b[$key])) { + return false; + } + } + + return true; + } + /** * @param array $newConditionalExpressions * @param array $existingConditionalExpressions diff --git a/tests/PHPStan/Rules/Variables/data/bug-6830.php b/tests/PHPStan/Rules/Variables/data/bug-6830.php index 1a4e5a4490..7306eb9a6f 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-6830.php +++ b/tests/PHPStan/Rules/Variables/data/bug-6830.php @@ -16,3 +16,18 @@ function test(array $bools): void } } } + +function test2(bool $do): void +{ + if ($do) { + $x = 9999; + } + + foreach ([1, 2, 3] as $whatever) { + if ($do) { + if ($x) { + $x = 123; + } + } + } +}