From 0731c4734741c8588fe0d150f0e1f80f7cd9e48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Fri, 15 Aug 2025 21:29:19 +0200 Subject: [PATCH 1/2] Indicates whether a variable is global --- src/Analyser/MutatingScope.php | 31 +++++++++-- src/Analyser/NodeScopeResolver.php | 2 +- src/Analyser/Scope.php | 2 + tests/PHPStan/Analyser/GlobalVariableTest.php | 52 +++++++++++++++++++ .../Analyser/data/global-in-class-method.php | 16 ++++++ .../Analyser/data/global-in-function.php | 12 +++++ .../Analyser/data/global-in-script.php | 8 +++ 7 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Analyser/GlobalVariableTest.php create mode 100644 tests/PHPStan/Analyser/data/global-in-class-method.php create mode 100644 tests/PHPStan/Analyser/data/global-in-function.php create mode 100644 tests/PHPStan/Analyser/data/global-in-script.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 411ad6faee..ab50317504 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -179,6 +179,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 = []; @@ -609,10 +611,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(); } @@ -653,7 +670,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(); @@ -704,7 +721,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); } @@ -4204,9 +4221,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(); @@ -4979,7 +5000,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 6f87d2c9d0..53dd70d2db 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1976,7 +1976,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 d7397399f6..74c6f3d26d 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 @@ + Date: Wed, 3 Sep 2025 19:47:26 +0200 Subject: [PATCH 2/2] Use a dedicated virtual node --- src/Analyser/MutatingScope.php | 32 ++++++++----- src/Analyser/NodeScopeResolver.php | 3 +- src/Node/Expr/GlobalVariableExpr.php | 37 +++++++++++++++ src/Node/Printer/Printer.php | 6 +++ .../ExpressionTypeResolverExtensionTest.php | 3 +- .../GlobalExpressionTypeResolverExtension.php | 47 +++++++++++++++++++ ...pe-resolver-extension-global-statement.php | 32 +++++++++++++ ...er-extension-method-call-returns-bool.php} | 0 .../expression-type-resolver-extension.neon | 4 ++ 9 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 src/Node/Expr/GlobalVariableExpr.php create mode 100644 tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php create mode 100644 tests/PHPStan/Analyser/data/expression-type-resolver-extension-global-statement.php rename tests/PHPStan/Analyser/data/{expression-type-resolver-extension.php => expression-type-resolver-extension-method-call-returns-bool.php} (100%) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index ab50317504..f84687bdd6 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -37,6 +37,7 @@ use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Node\Expr\GlobalVariableExpr; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; @@ -179,8 +180,6 @@ final class MutatingScope implements Scope private const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; - private const IS_GLOBAL_ATTRIBUTE_NAME = 'isGlobal'; - /** @var Type[] */ private array $resolvedTypes = []; @@ -618,12 +617,8 @@ public function isGlobalVariable(string $variableName): bool return true; } - $varExprString = '$' . $variableName; - if (!isset($this->expressionTypes[$varExprString])) { - return false; - } - - return $this->expressionTypes[$varExprString]->getExpr()->getAttribute(self::IS_GLOBAL_ATTRIBUTE_NAME) === true; + $globalVariableExprString = $this->getNodeKey(new GlobalVariableExpr(new Variable($variableName))); + return array_key_exists($globalVariableExprString, $this->expressionTypes); } /** @api */ @@ -824,6 +819,10 @@ public function getType(Expr $node): Type return $propertyReflection->getReadableType(); } + if ($node instanceof GlobalVariableExpr) { + return $this->getType($node->getVar()); + } + $key = $this->getNodeKey($node); if (!array_key_exists($key, $this->resolvedTypes)) { @@ -4221,13 +4220,9 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions); } - public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty, bool $isGlobal = false): self + public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty): 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(); @@ -4330,6 +4325,17 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, } } + if ($expr instanceof GlobalVariableExpr) { + foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) { + $typeFromExtension = $extension->getType($expr, $this); + if ($typeFromExtension !== null) { + $type = $typeFromExtension; + break; + } + } + $scope = $scope->specifyExpressionType($expr->getVar(), $type, $nativeType, $certainty); + } + if ($certainty->no()) { throw new ShouldNotHappenException(); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 53dd70d2db..c2b8941af0 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -89,6 +89,7 @@ use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Node\Expr\GlobalVariableExpr; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\PropertyInitializationExpr; @@ -1976,7 +1977,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { continue; } - $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes(), true); + $scope = $scope->assignExpression(new GlobalVariableExpr($var), new MixedType(), new MixedType()); $vars[] = $var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); diff --git a/src/Node/Expr/GlobalVariableExpr.php b/src/Node/Expr/GlobalVariableExpr.php new file mode 100644 index 0000000000..80a41bbe42 --- /dev/null +++ b/src/Node/Expr/GlobalVariableExpr.php @@ -0,0 +1,37 @@ +var; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_GlobalVariableExpr'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index a9fb822528..108965c684 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -9,6 +9,7 @@ use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Node\Expr\GlobalVariableExpr; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; @@ -98,4 +99,9 @@ protected function pPHPStan_Node_IssetExpr(IssetExpr $expr): string // phpcs:ign return sprintf('__phpstanIssetExpr(%s)', $this->p($expr->getExpr())); } + protected function pPHPStan_Node_GlobalVariableExpr(GlobalVariableExpr $expr): string // phpcs:ignore + { + return sprintf('__phpstanGlobalVariable(%s)', $this->p($expr->getVar())); + } + } diff --git a/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php index 6c3d707942..da4662895b 100644 --- a/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php +++ b/tests/PHPStan/Analyser/ExpressionTypeResolverExtensionTest.php @@ -10,7 +10,8 @@ class ExpressionTypeResolverExtensionTest extends TypeInferenceTestCase public static function dataFileAsserts(): iterable { - yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension-method-call-returns-bool.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension-global-statement.php'); } /** diff --git a/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php b/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php new file mode 100644 index 0000000000..21dfc9ec64 --- /dev/null +++ b/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php @@ -0,0 +1,47 @@ +getVar()->name; + + if ($variableName === 'MY_GLOBAL_BOOL') { + return new BooleanType(); + } + + if ($variableName === 'MY_GLOBAL_INT') { + return new IntegerType(); + } + + if ($variableName === 'MY_GLOBAL_STR') { + return new StringType(); + } + + if ($variableName === 'MY_GLOBAL_ARRAY') { + return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true)); + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/data/expression-type-resolver-extension-global-statement.php b/tests/PHPStan/Analyser/data/expression-type-resolver-extension-global-statement.php new file mode 100644 index 0000000000..932b6fdab6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/expression-type-resolver-extension-global-statement.php @@ -0,0 +1,32 @@ +', $MY_GLOBAL_ARRAY); // overriden by value assign expression + assertType('int', $MY_GLOBAL_INT); +}; + +$testClass = new class () { + public function foo($MY_GLOBAL_INT) { + global $MY_GLOBAL_STR; + + assertType('string', $MY_GLOBAL_STR); + assertType('mixed', $MY_GLOBAL_INT); + } +}; diff --git a/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php b/tests/PHPStan/Analyser/data/expression-type-resolver-extension-method-call-returns-bool.php similarity index 100% rename from tests/PHPStan/Analyser/data/expression-type-resolver-extension.php rename to tests/PHPStan/Analyser/data/expression-type-resolver-extension-method-call-returns-bool.php diff --git a/tests/PHPStan/Analyser/expression-type-resolver-extension.neon b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon index de0f92640b..3b591ce582 100644 --- a/tests/PHPStan/Analyser/expression-type-resolver-extension.neon +++ b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon @@ -1,5 +1,9 @@ # config for ExpressionTypeResolverExtensionTest services: + - + class: ExpressionTypeResolverExtension\GlobalExpressionTypeResolverExtension + tags: + - phpstan.broker.expressionTypeResolverExtension - class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension tags: