Skip to content

Commit 44a1089

Browse files
VincentLangletphpstan-bot
authored andcommitted
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
1 parent 9e4186e commit 44a1089

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed

src/Analyser/MutatingScope.php

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3432,11 +3432,29 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi
34323432

34333433
$otherHolders = $otherConditionalExpressions[$exprString];
34343434
$intersectedHolders = [];
3435-
foreach ($holders as $key => $holder) {
3436-
if (!array_key_exists($key, $otherHolders)) {
3435+
foreach ($holders as $holder) {
3436+
$key = $holder->getKey();
3437+
if (array_key_exists($key, $otherHolders)) {
3438+
$intersectedHolders[$key] = $holder;
34373439
continue;
34383440
}
3439-
$intersectedHolders[$key] = $holder;
3441+
3442+
foreach ($otherHolders as $otherHolder) {
3443+
if (!$this->conditionExpressionTypeHoldersEqual(
3444+
$holder->getConditionExpressionTypeHolders(),
3445+
$otherHolder->getConditionExpressionTypeHolders(),
3446+
)) {
3447+
continue;
3448+
}
3449+
3450+
$mergedTypeHolder = $holder->getTypeHolder()->and($otherHolder->getTypeHolder());
3451+
$mergedHolder = new ConditionalExpressionHolder(
3452+
$holder->getConditionExpressionTypeHolders(),
3453+
$mergedTypeHolder,
3454+
);
3455+
$intersectedHolders[$mergedHolder->getKey()] = $mergedHolder;
3456+
break;
3457+
}
34403458
}
34413459

34423460
if (count($intersectedHolders) === 0) {
@@ -3449,6 +3467,28 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi
34493467
return $newConditionalExpressions;
34503468
}
34513469

3470+
/**
3471+
* @param array<string, ExpressionTypeHolder> $a
3472+
* @param array<string, ExpressionTypeHolder> $b
3473+
*/
3474+
private function conditionExpressionTypeHoldersEqual(array $a, array $b): bool
3475+
{
3476+
if (count($a) !== count($b)) {
3477+
return false;
3478+
}
3479+
3480+
foreach ($a as $exprString => $holder) {
3481+
if (!array_key_exists($exprString, $b)) {
3482+
return false;
3483+
}
3484+
if (!$holder->equals($b[$exprString])) {
3485+
return false;
3486+
}
3487+
}
3488+
3489+
return true;
3490+
}
3491+
34523492
/**
34533493
* @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressions
34543494
* @param array<string, ConditionalExpressionHolder[]> $existingConditionalExpressions

tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,16 @@ public function testBug6830(): void
13931393
$this->analyse([__DIR__ . '/data/bug-6830.php'], []);
13941394
}
13951395

1396+
public function testBug6830b(): void
1397+
{
1398+
$this->cliArgumentsVariablesRegistered = true;
1399+
$this->polluteScopeWithLoopInitialAssignments = false;
1400+
$this->checkMaybeUndefinedVariables = true;
1401+
$this->polluteScopeWithAlwaysIterableForeach = true;
1402+
1403+
$this->analyse([__DIR__ . '/data/bug-6830b.php'], []);
1404+
}
1405+
13961406
public function testBug14019(): void
13971407
{
13981408
$this->cliArgumentsVariablesRegistered = true;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bug6830b;
6+
7+
function test(bool $do): void
8+
{
9+
if ($do) {
10+
$x = 9999;
11+
}
12+
13+
foreach ([1, 2, 3] as $whatever) {
14+
if ($do) {
15+
if ($x) {
16+
$x = 123;
17+
}
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)