Skip to content

Commit 5d4844f

Browse files
committed
Fix phpstan/phpstan#14353: Falsy "Variable might not be defined" after foreach
- After a foreach that conditionally defines a variable, conditional expressions are created tracking "if array is empty, variable doesn't exist" - When isset() confirms the variable exists, specifyExpressionType sets certainty to Yes but stale conditional expressions with No certainty persist - A subsequent foreach over the same array triggers filterByTruthyValue for the "array is empty" case, activating the stale conditional and removing the variable - Fix: in specifyExpressionType, when certainty is Yes, remove conditional expressions for the variable that have No certainty (would unset it) - New regression test in tests/PHPStan/Rules/Variables/data/bug-14353.php
1 parent 14bf97d commit 5d4844f

3 files changed

Lines changed: 63 additions & 1 deletion

File tree

src/Analyser/MutatingScope.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2758,14 +2758,25 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType,
27582758
$nativeTypes = $scope->nativeExpressionTypes;
27592759
$nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty);
27602760

2761+
$conditionalExpressions = $this->conditionalExpressions;
2762+
if ($certainty->yes() && array_key_exists($exprString, $conditionalExpressions)) {
2763+
$conditionalExpressions[$exprString] = array_filter(
2764+
$conditionalExpressions[$exprString],
2765+
static fn (ConditionalExpressionHolder $holder) => !$holder->getTypeHolder()->getCertainty()->no(),
2766+
);
2767+
if ($conditionalExpressions[$exprString] === []) {
2768+
unset($conditionalExpressions[$exprString]);
2769+
}
2770+
}
2771+
27612772
$scope = $this->scopeFactory->create(
27622773
$this->context,
27632774
$this->isDeclareStrictTypes(),
27642775
$this->getFunction(),
27652776
$this->getNamespace(),
27662777
$expressionTypes,
27672778
$nativeTypes,
2768-
$this->conditionalExpressions,
2779+
$conditionalExpressions,
27692780
$this->inClosureBindScopeClasses,
27702781
$this->anonymousFunctionReflection,
27712782
$this->inFirstLevelStatement,

tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,4 +1514,13 @@ public function testBug14117(): void
15141514
]);
15151515
}
15161516

1517+
public function testBug14353(): void
1518+
{
1519+
$this->cliArgumentsVariablesRegistered = true;
1520+
$this->polluteScopeWithLoopInitialAssignments = false;
1521+
$this->checkMaybeUndefinedVariables = true;
1522+
$this->polluteScopeWithAlwaysIterableForeach = true;
1523+
$this->analyse([__DIR__ . '/data/bug-14353.php'], []);
1524+
}
1525+
15171526
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace Bug14353;
4+
5+
/** @return array<int> */
6+
function get(): array
7+
{
8+
return [1];
9+
}
10+
11+
class Test
12+
{
13+
/** @var mixed */
14+
public $data;
15+
16+
public function test(): void
17+
{
18+
$reports = [];
19+
20+
foreach (get() as $report) {
21+
$reports[$report] = $report;
22+
}
23+
24+
if (isset($this->data)) {
25+
foreach ($reports as $report_id => $report) {
26+
$report_ids[$report_id] = 1;
27+
}
28+
} else {
29+
foreach ($reports as $report_id => $report) {
30+
$report_ids[$report_id] = 1;
31+
}
32+
}
33+
34+
if (isset($report_ids)) {
35+
var_dump($report_ids);
36+
37+
foreach ($reports as $report) {}
38+
39+
var_dump($report_ids);
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)