From 906e8bf305f1039a4b25171a91f3b1b7a0c624a9 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:58:10 +0000 Subject: [PATCH] Fix phpstan/phpstan#6830: Variable inside loop might not be defined - Fixed intersectConditionalExpressions() in MutatingScope to merge conditional expression holders with same conditions but different result types - Previously, when a variable was conditionally defined before a loop and used inside the loop under the same condition, the conditional expression was lost during scope merging because the result type changed across iterations - Added fallback logic: when exact key matching fails, match holders by their condition expressions and merge result types via TypeCombinator::union() - Extended regression test in tests/PHPStan/Rules/Variables/data/bug-6830.php --- src/Analyser/MutatingScope.php | 54 ++++++++++++++++++- .../PHPStan/Rules/Variables/data/bug-6830.php | 28 ++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8de4e331b6..0cb8e4bd0e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4039,13 +4039,63 @@ 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; + } + } + + if ($allKeysMatch) { + $newConditionalExpressions[$exprString] = $holders; + continue; + } + + // When exact keys don't match (e.g. result types differ across loop iterations), + // try to merge holders that have the same conditions but different result types. + $mergedHolders = []; + foreach ($holders as $holder) { + $conditionHolders = $holder->getConditionExpressionTypeHolders(); + $conditionKeys = array_keys($conditionHolders); + + foreach ($otherHolders as $otherHolder) { + $otherConditionHolders = $otherHolder->getConditionExpressionTypeHolders(); + if (array_keys($otherConditionHolders) !== $conditionKeys) { + continue; + } + + $conditionsMatch = true; + foreach ($conditionHolders as $condKey => $condHolder) { + if (!$condHolder->equals($otherConditionHolders[$condKey])) { + $conditionsMatch = false; + break; + } + } + + if (!$conditionsMatch) { + continue; + } + + $ourTypeHolder = $holder->getTypeHolder(); + $otherTypeHolder = $otherHolder->getTypeHolder(); + $mergedType = TypeCombinator::union($ourTypeHolder->getType(), $otherTypeHolder->getType()); + $mergedCertainty = TrinaryLogic::maxMin($ourTypeHolder->getCertainty(), $otherTypeHolder->getCertainty()); + + $mergedConditionalExpression = new ConditionalExpressionHolder( + $conditionHolders, + new ExpressionTypeHolder($ourTypeHolder->getExpr(), $mergedType, $mergedCertainty), + ); + $mergedHolders[$mergedConditionalExpression->getKey()] = $mergedConditionalExpression; + break; } } - $newConditionalExpressions[$exprString] = $holders; + if ($mergedHolders === []) { + continue; + } + + $newConditionalExpressions[$exprString] = $mergedHolders; } return $newConditionalExpressions; diff --git a/tests/PHPStan/Rules/Variables/data/bug-6830.php b/tests/PHPStan/Rules/Variables/data/bug-6830.php index 1a4e5a4490..1c5e950d53 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-6830.php +++ b/tests/PHPStan/Rules/Variables/data/bug-6830.php @@ -16,3 +16,31 @@ 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; + } + } + } +} + +function test3(bool $do): void +{ + if ($do) { + $x = 'hello'; + } + + foreach ([1, 2, 3] as $whatever) { + if ($do) { + echo $x; + } + } +}