Skip to content

Commit 8163acb

Browse files
phpstan-botclaude
andcommitted
Filter conditional expressions from foreach body in processAlwaysIterableForeachScopeWithoutPollute
When using $finalScope->conditionalExpressions, conditional expressions referencing variables only defined inside the foreach body (like $key) could leak into the outer scope. When those conditional expressions fired (e.g. inside an if block), the foreach variable would be re-introduced with Yes certainty, causing false "Foreach overwrites $key" errors. Filter conditional expressions to only keep those where both the target expression and all condition expressions reference variables that existed before the foreach loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7cca523 commit 8163acb

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

src/Analyser/MutatingScope.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3870,14 +3870,34 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope
38703870
);
38713871
}
38723872

3873+
$conditionalExpressions = [];
3874+
foreach ($finalScope->conditionalExpressions as $conditionalExprString => $holders) {
3875+
if (!isset($this->expressionTypes[$conditionalExprString])) {
3876+
continue;
3877+
}
3878+
$filteredHolders = [];
3879+
foreach ($holders as $holder) {
3880+
foreach (array_keys($holder->getConditionExpressionTypeHolders()) as $holderExprString) {
3881+
if (!isset($this->expressionTypes[$holderExprString])) {
3882+
continue 2;
3883+
}
3884+
}
3885+
$filteredHolders[] = $holder;
3886+
}
3887+
if ($filteredHolders === []) {
3888+
continue;
3889+
}
3890+
$conditionalExpressions[$conditionalExprString] = $filteredHolders;
3891+
}
3892+
38733893
return $this->scopeFactory->create(
38743894
$this->context,
38753895
$this->isDeclareStrictTypes(),
38763896
$this->getFunction(),
38773897
$this->getNamespace(),
38783898
$expressionTypes,
38793899
$nativeTypes,
3880-
$finalScope->conditionalExpressions,
3900+
$conditionalExpressions,
38813901
$this->inClosureBindScopeClasses,
38823902
$this->anonymousFunctionReflection,
38833903
$this->inFirstLevelStatement,

tests/PHPStan/Analyser/data/bug-14446.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,28 @@ function test(bool $initial): void {
2222

2323
assertType('bool', $initial);
2424
}
25+
26+
/**
27+
* @param mixed $value
28+
*/
29+
function testForeachKeyOverwrite($value): void {
30+
if (is_array($value) && $value !== []) {
31+
$hasOnlyStringKey = true;
32+
foreach (array_keys($value) as $key) {
33+
if (is_int($key)) {
34+
$hasOnlyStringKey = false;
35+
break;
36+
}
37+
}
38+
39+
assertType('bool', $hasOnlyStringKey);
40+
41+
if ($hasOnlyStringKey) {
42+
// $key should not be in scope here with polluteScopeWithAlwaysIterableForeach: false
43+
// Second foreach should not report "Foreach overwrites $key with its key variable"
44+
foreach ($value as $key => $element) {
45+
assertType('(int|string)', $key);
46+
}
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)