diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 411ad6faee..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; @@ -609,10 +610,21 @@ public function afterOpenSslCall(string $openSslFunctionName): self ); } + /** @api */ + public function isGlobalVariable(string $variableName): bool + { + if ($this->isSuperglobalVariable($variableName)) { + return true; + } + + $globalVariableExprString = $this->getNodeKey(new GlobalVariableExpr(new Variable($variableName))); + return array_key_exists($globalVariableExprString, $this->expressionTypes); + } + /** @api */ public function hasVariableType(string $variableName): TrinaryLogic { - if ($this->isGlobalVariable($variableName)) { + if ($this->isSuperglobalVariable($variableName)) { return TrinaryLogic::createYes(); } @@ -653,7 +665,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 +716,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); } @@ -807,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)) { @@ -4309,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(); } @@ -4979,7 +5006,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..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()); + $scope = $scope->assignExpression(new GlobalVariableExpr($var), new MixedType(), new MixedType()); $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/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/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/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/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 @@ +