From 5d4844f687fbb6b94b936ef58b743129d23f5b21 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:23:48 +0000 Subject: [PATCH] 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 --- src/Analyser/MutatingScope.php | 13 +++++- .../Variables/DefinedVariableRuleTest.php | 9 ++++ .../Rules/Variables/data/bug-14353.php | 42 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-14353.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 42fe96957d..abfebca2e9 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2758,6 +2758,17 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $nativeTypes = $scope->nativeExpressionTypes; $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); + $conditionalExpressions = $this->conditionalExpressions; + if ($certainty->yes() && array_key_exists($exprString, $conditionalExpressions)) { + $conditionalExpressions[$exprString] = array_filter( + $conditionalExpressions[$exprString], + static fn (ConditionalExpressionHolder $holder) => !$holder->getTypeHolder()->getCertainty()->no(), + ); + if ($conditionalExpressions[$exprString] === []) { + unset($conditionalExpressions[$exprString]); + } + } + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), @@ -2765,7 +2776,7 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $this->getNamespace(), $expressionTypes, $nativeTypes, - $this->conditionalExpressions, + $conditionalExpressions, $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index a05ca24efd..273f489a09 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1514,4 +1514,13 @@ public function testBug14117(): void ]); } + public function testBug14353(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-14353.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14353.php b/tests/PHPStan/Rules/Variables/data/bug-14353.php new file mode 100644 index 0000000000..9e202d273d --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-14353.php @@ -0,0 +1,42 @@ + */ +function get(): array +{ + return [1]; +} + +class Test +{ + /** @var mixed */ + public $data; + + public function test(): void + { + $reports = []; + + foreach (get() as $report) { + $reports[$report] = $report; + } + + if (isset($this->data)) { + foreach ($reports as $report_id => $report) { + $report_ids[$report_id] = 1; + } + } else { + foreach ($reports as $report_id => $report) { + $report_ids[$report_id] = 1; + } + } + + if (isset($report_ids)) { + var_dump($report_ids); + + foreach ($reports as $report) {} + + var_dump($report_ids); + } + } +}