From abdc52ed9acb3cbfd45e5cbbee1324cdb09d0056 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:08:05 +0000 Subject: [PATCH] Fix incorrect type narrowing of superglobal with dependent types - Extended conditional expression skip logic in MutatingScope to handle superglobal variables not present in the other branch's expression types - Added exprContainsSuperGlobal() helper method to detect superglobal expressions for reuse across merge logic - New regression test in tests/PHPStan/Rules/Variables/data/bug-14421.php - Root cause: superglobals always exist but aren't tracked in expressionTypes unless narrowed, so the dependent type skip check failed to recognize them --- src/Analyser/MutatingScope.php | 22 +++++++++++++++++-- .../PHPStan/Rules/Variables/IssetRuleTest.php | 7 ++++++ .../Rules/Variables/data/bug-14421.php | 20 +++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-14421.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a43ba35dda..87276600a9 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -840,6 +840,20 @@ private function isGlobalVariable(string $variableName): bool return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true); } + private function exprContainsSuperGlobal(Expr $expr): bool + { + $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME); + if ($containsSuperGlobal !== null) { + return $containsSuperGlobal; + } + + $nodeFinder = new NodeFinder(); + $containsSuperGlobal = $nodeFinder->findFirst($expr, fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name)) !== null; + $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal); + + return $containsSuperGlobal; + } + /** @api */ public function hasConstant(Name $name): bool { @@ -3603,9 +3617,13 @@ private function createConditionalExpressions( } foreach ($variableTypeGuards as $guardExprString => $guardHolder) { + $exprExistsInTheirs = array_key_exists($exprString, $theirExpressionTypes) + && $theirExpressionTypes[$exprString]->getCertainty()->yes(); + if (!$exprExistsInTheirs) { + $exprExistsInTheirs = $this->exprContainsSuperGlobal($holder->getExpr()); + } if ( - array_key_exists($exprString, $theirExpressionTypes) - && $theirExpressionTypes[$exprString]->getCertainty()->yes() + $exprExistsInTheirs && array_key_exists($guardExprString, $theirExpressionTypes) && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() && !$guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType())->no() diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index fd841e49b1..0f57cd0fd3 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -562,4 +562,11 @@ public function testBug14393(): void ]); } + public function testBug14421(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-14421.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14421.php b/tests/PHPStan/Rules/Variables/data/bug-14421.php new file mode 100644 index 0000000000..eafdc7068a --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-14421.php @@ -0,0 +1,20 @@ +