diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 94510cd3a6..653aa46927 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3612,6 +3612,77 @@ private function createConditionalExpressions( } if (count($typeGuards) === 0) { + // Look for variables whose certainty changed from YES (in our scope) to + // MAYBE (in merged scope) because they don't exist in their scope. + // For each such variable, look for potential type guards among variables + // that were removed from newVariableTypes (because merged type == their + // type) but whose type differs between our and their scope. + // This handles cases like: if (isset($a['key'])) { $b = ...; } + // where $b should be conditionally defined based on $a's narrowed type. + $certaintyChangedVars = []; + foreach ($newVariableTypes as $exprString => $holder) { + if (!$holder->getCertainty()->yes()) { + continue; + } + if (!array_key_exists($exprString, $mergedExpressionTypes)) { + continue; + } + if ($mergedExpressionTypes[$exprString]->getCertainty()->yes()) { + continue; + } + if (!$holder->getExpr() instanceof Variable) { + continue; + } + $certaintyChangedVars[$exprString] = $holder; + } + + if (count($certaintyChangedVars) > 0) { + $recoveredTypeGuards = []; + foreach ($ourExpressionTypes as $exprString => $holder) { + if ($holder->getExpr() instanceof VirtualNode) { + continue; + } + if (array_key_exists($exprString, $newVariableTypes)) { + continue; + } + if (!array_key_exists($exprString, $mergedExpressionTypes)) { + continue; + } + if (!$holder->getCertainty()->yes()) { + continue; + } + if ( + array_key_exists($exprString, $theirExpressionTypes) + && !$theirExpressionTypes[$exprString]->getCertainty()->yes() + ) { + continue; + } + if ($mergedExpressionTypes[$exprString]->equalTypes($holder)) { + continue; + } + $recoveredTypeGuards[$exprString] = $holder; + } + + foreach ($certaintyChangedVars as $exprString => $holder) { + foreach ($recoveredTypeGuards as $guardExprString => $guardHolder) { + if ($guardExprString === $exprString) { + continue; + } + if ( + array_key_exists($exprString, $theirExpressionTypes) + && $theirExpressionTypes[$exprString]->getCertainty()->yes() + && array_key_exists($guardExprString, $theirExpressionTypes) + && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() + && !$guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType())->no() + ) { + continue; + } + $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], $holder); + $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; + } + } + } + return $conditionalExpressions; } diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 207070c762..e11cb299b0 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1517,4 +1517,13 @@ public function testBug11218(): void $this->analyse([__DIR__ . '/data/bug-11218.php'], []); } + public function testBug9426(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9426.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-9426.php b/tests/PHPStan/Rules/Variables/data/bug-9426.php new file mode 100644 index 0000000000..5bb3880e98 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9426.php @@ -0,0 +1,20 @@ +