From a64900aea1fe7b180bc38674ce3315a9147ee25c Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:02:59 +0000 Subject: [PATCH] Fix incorrect type narrowing of superglobal with dependent types - Superglobal-containing expressions are now excluded from being guarded in conditional expressions during scope merging - The root cause was that createConditionalExpressions() would create type dependencies linking superglobal array offsets to assigned variables, causing narrowing of the variable to incorrectly propagate back to the superglobal - New regression test in tests/PHPStan/Rules/Variables/data/bug-14421.php Closes https://github.com/phpstan/phpstan/issues/14421 --- src/Analyser/MutatingScope.php | 23 +++++++++++++++++++ .../PHPStan/Rules/Variables/IssetRuleTest.php | 7 ++++++ .../Rules/Variables/data/bug-14421.php | 20 ++++++++++++++++ 3 files changed, 50 insertions(+) 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..cf9eb75808 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3539,6 +3539,9 @@ private function createConditionalExpressions( array $mergedExpressionTypes, ): array { + $globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name); + $nodeFinder = new NodeFinder(); + $newVariableTypes = $ourExpressionTypes; foreach ($theirExpressionTypes as $exprString => $holder) { if (!array_key_exists($exprString, $mergedExpressionTypes)) { @@ -3595,6 +3598,16 @@ private function createConditionalExpressions( continue; } + $expr = $holder->getExpr(); + $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME); + if ($containsSuperGlobal === null) { + $containsSuperGlobal = $nodeFinder->findFirst($expr, $globalVariableCallback) !== null; + $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal); + } + if ($containsSuperGlobal === true) { + continue; + } + $variableTypeGuards = $typeGuards; unset($variableTypeGuards[$exprString]); @@ -3622,6 +3635,16 @@ private function createConditionalExpressions( continue; } + $expr = $mergedExprTypeHolder->getExpr(); + $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME); + if ($containsSuperGlobal === null) { + $containsSuperGlobal = $nodeFinder->findFirst($expr, $globalVariableCallback) !== null; + $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal); + } + if ($containsSuperGlobal === true) { + continue; + } + foreach ($typeGuards as $guardExprString => $guardHolder) { $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo())); $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index fd841e49b1..aef9f80458 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -526,6 +526,13 @@ public function testBug9503(): void $this->analyse([__DIR__ . '/data/bug-9503.php'], []); } + public function testBug14421(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-14421.php'], []); + } + public function testBug14393(): void { $this->treatPhpDocTypesAsCertain = true; 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 @@ +