diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 2b19e1abe4..4837c62aae 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -177,6 +177,8 @@ final class MutatingScope implements Scope private const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; + private const IS_GLOBAL_ATTRIBUTE_NAME = 'isGlobal'; + /** @var Type[] */ private array $resolvedTypes = []; @@ -584,10 +586,25 @@ public function afterOpenSslCall(string $openSslFunctionName): self ); } + /** @api */ + public function isGlobalVariable(string $variableName): bool + { + if ($this->isSuperglobalVariable($variableName)) { + return true; + } + + $varExprString = '$' . $variableName; + if (!isset($this->expressionTypes[$varExprString])) { + return false; + } + + return $this->expressionTypes[$varExprString]->getExpr()->getAttribute(self::IS_GLOBAL_ATTRIBUTE_NAME) === true; + } + /** @api */ public function hasVariableType(string $variableName): TrinaryLogic { - if ($this->isGlobalVariable($variableName)) { + if ($this->isSuperglobalVariable($variableName)) { return TrinaryLogic::createYes(); } @@ -628,7 +645,7 @@ public function getVariableType(string $variableName): Type $varExprString = '$' . $variableName; if (!array_key_exists($varExprString, $this->expressionTypes)) { - if ($this->isGlobalVariable($variableName)) { + if ($this->isSuperglobalVariable($variableName)) { return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true)); } return new MixedType(); @@ -679,7 +696,7 @@ public function getMaybeDefinedVariables(): array return $variables; } - private function isGlobalVariable(string $variableName): bool + private function isSuperglobalVariable(string $variableName): bool { return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true); } @@ -4168,9 +4185,13 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions); } - public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty): self + public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty, bool $isGlobal = false): self { $node = new Variable($variableName); + if ($isGlobal || $this->isGlobalVariable($variableName)) { + $node->setAttribute(self::IS_GLOBAL_ATTRIBUTE_NAME, true); + } + $scope = $this->assignExpression($node, $type, $nativeType); if ($certainty->no()) { throw new ShouldNotHappenException(); @@ -4945,7 +4966,7 @@ private function createConditionalExpressions( private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array { $intersectedVariableTypeHolders = []; - $globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name); + $globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isSuperglobalVariable($node->name); $nodeFinder = new NodeFinder(); foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) { if (isset($theirVariableTypeHolders[$exprString])) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 63741b2c04..ea25e24f89 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1965,7 +1965,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { continue; } - $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); + $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes(), true); $vars[] = $var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index a33b06a377..f269cde048 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -57,6 +57,8 @@ public function getFunctionName(): ?string; public function getParentScope(): ?self; + public function isGlobalVariable(string $variableName): bool; + public function hasVariableType(string $variableName): TrinaryLogic; public function getVariableType(string $variableName): Type; diff --git a/tests/PHPStan/Analyser/GlobalVariableTest.php b/tests/PHPStan/Analyser/GlobalVariableTest.php new file mode 100644 index 0000000000..7c30d08dd5 --- /dev/null +++ b/tests/PHPStan/Analyser/GlobalVariableTest.php @@ -0,0 +1,52 @@ +assertTrue($scope->isGlobalVariable('FOO')); + $this->assertFalse($scope->isGlobalVariable('whatever')); + }); + } + + public function testGlobalVariableInFunction(): void + { + self::processFile(__DIR__ . '/data/global-in-function.php', function (Node $node, Scope $scope): void { + if (!($node instanceof Return_)) { + return; + } + + $this->assertFalse($scope->isGlobalVariable('BAR')); + $this->assertTrue($scope->isGlobalVariable('CONFIG')); + $this->assertFalse($scope->isGlobalVariable('localVar')); + }); + } + + public function testGlobalVariableInClassMethod(): void + { + self::processFile(__DIR__ . '/data/global-in-class-method.php', function (Node $node, Scope $scope): void { + if (!($node instanceof Return_)) { + return; + } + + $this->assertFalse($scope->isGlobalVariable('count')); + $this->assertTrue($scope->isGlobalVariable('GLB_A')); + $this->assertTrue($scope->isGlobalVariable('GLB_B')); + $this->assertFalse($scope->isGlobalVariable('key')); + $this->assertFalse($scope->isGlobalVariable('step')); + }); + } + +} diff --git a/tests/PHPStan/Analyser/data/global-in-class-method.php b/tests/PHPStan/Analyser/data/global-in-class-method.php new file mode 100644 index 0000000000..c1357cc15b --- /dev/null +++ b/tests/PHPStan/Analyser/data/global-in-class-method.php @@ -0,0 +1,16 @@ + $step) { + break; + } + + return false; + } +} diff --git a/tests/PHPStan/Analyser/data/global-in-function.php b/tests/PHPStan/Analyser/data/global-in-function.php new file mode 100644 index 0000000000..8836c42a35 --- /dev/null +++ b/tests/PHPStan/Analyser/data/global-in-function.php @@ -0,0 +1,12 @@ +