From c15b9a2fef51b9928415b21004fe6edf33f5b73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Sun, 10 Aug 2025 09:38:53 +0200 Subject: [PATCH 1/4] Permit to define global types with `ExpressionTypeResolverExtension` --- src/Analyser/NodeScopeResolver.php | 2 +- .../ExpressionTypeResolverExtensionTest.php | 3 ++- .../GlobalExpressionTypeResolverExtension.php | 27 +++++++++++++++++++ ...pe-resolver-extension-global-statement.php | 10 +++++++ ...er-extension-method-call-returns-bool.php} | 0 .../expression-type-resolver-extension.neon | 4 +++ 6 files changed, 44 insertions(+), 2 deletions(-) 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/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 0c25c7271b..8c0a7b337a 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, $scope->getType($var), new MixedType(), TrinaryLogic::createYes()); $vars[] = $var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); 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..cf3fdb19df --- /dev/null +++ b/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php @@ -0,0 +1,27 @@ +name === 'MY_FRAMEWORK_GLOBAL') { + return new BooleanType(); + } + + 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..f196ef355f --- /dev/null +++ b/tests/PHPStan/Analyser/data/expression-type-resolver-extension-global-statement.php @@ -0,0 +1,10 @@ + Date: Sun, 10 Aug 2025 21:36:13 +0200 Subject: [PATCH 2/4] revert useless change --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8c0a7b337a..0c25c7271b 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, $scope->getType($var), new MixedType(), TrinaryLogic::createYes()); + $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); $vars[] = $var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); From 8793f30738fc769bf663b1bce2c47ccce053be8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Tue, 12 Aug 2025 23:36:37 +0200 Subject: [PATCH 3/4] Add ability to check if a variable is global in scope --- src/Analyser/DirectInternalScopeFactory.php | 2 + src/Analyser/InternalScopeFactory.php | 2 + src/Analyser/LazyInternalScopeFactory.php | 2 + src/Analyser/MutatingScope.php | 52 +++++++++++++++++-- src/Analyser/NodeScopeResolver.php | 2 + src/Analyser/Scope.php | 2 + .../GlobalExpressionTypeResolverExtension.php | 18 ++++++- ...pe-resolver-extension-global-statement.php | 21 +++++++- 8 files changed, 93 insertions(+), 8 deletions(-) diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index f65a599113..164b972631 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -56,6 +56,7 @@ public function create( bool $afterExtractCall = false, ?Scope $parentScope = null, bool $nativeTypesPromoted = false, + array $globalVariables = [], ): MutatingScope { return new MutatingScope( @@ -90,6 +91,7 @@ public function create( $afterExtractCall, $parentScope, $nativeTypesPromoted, + $globalVariables, ); } diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index 8d8daa714f..fe57d995df 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -19,6 +19,7 @@ interface InternalScopeFactory * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions * @param list $inFunctionCallsStack + * @param list $globalVariables */ public function create( ScopeContext $context, @@ -37,6 +38,7 @@ public function create( bool $afterExtractCall = false, ?Scope $parentScope = null, bool $nativeTypesPromoted = false, + array $globalVariables = [], ): MutatingScope; } diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 91d8c09a98..bb30b1694c 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -46,6 +46,7 @@ public function create( bool $afterExtractCall = false, ?Scope $parentScope = null, bool $nativeTypesPromoted = false, + array $globalVariables = [], ): MutatingScope { return new MutatingScope( @@ -80,6 +81,7 @@ public function create( $afterExtractCall, $parentScope, $nativeTypesPromoted, + $globalVariables, ); } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 57d047fbf8..e5203f4d43 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -202,6 +202,7 @@ final class MutatingScope implements Scope * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes * @param list $inFunctionCallsStack + * @param list $globalVariables */ public function __construct( private InternalScopeFactory $scopeFactory, @@ -235,6 +236,7 @@ public function __construct( private bool $afterExtractCall = false, private ?Scope $parentScope = null, private bool $nativeTypesPromoted = false, + private array $globalVariables = [], ) { if ($namespace === '') { @@ -363,6 +365,7 @@ public function rememberConstructorScope(): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -441,6 +444,7 @@ public function afterExtractCall(): self true, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -497,6 +501,7 @@ public function afterClearstatcacheCall(): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -579,13 +584,14 @@ public function afterOpenSslCall(string $openSslFunctionName): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } /** @api */ public function hasVariableType(string $variableName): TrinaryLogic { - if ($this->isGlobalVariable($variableName)) { + if ($this->isSuperGlobalVariable($variableName)) { return TrinaryLogic::createYes(); } @@ -626,7 +632,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(); @@ -635,6 +641,18 @@ public function getVariableType(string $variableName): Type return TypeUtils::resolveLateResolvableTypes($this->expressionTypes[$varExprString]->getType()); } + public function setVariableAsGlobal(string $variableName): self + { + $this->globalVariables[] = $variableName; + + return $this; + } + + public function isGlobalVariable(string $variableName): bool + { + return in_array($variableName, $this->globalVariables, true); + } + /** * @api * @return list @@ -677,7 +695,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); } @@ -2762,6 +2780,7 @@ private function promoteNativeTypes(): self $this->afterExtractCall, $this->parentScope, true, + $this->globalVariables, ); } @@ -2956,6 +2975,7 @@ public function pushInFunctionCall($reflection, ?ParameterReflection $parameter) $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -2981,6 +3001,7 @@ public function popInFunctionCall(): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -3572,6 +3593,7 @@ public function restoreThis(self $restoreThisScope): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -3635,6 +3657,7 @@ public function enterAnonymousFunction( false, $this, $this->nativeTypesPromoted, + [], ); } @@ -3743,6 +3766,7 @@ private function enterAnonymousFunctionWithoutReflection( false, $this, $this->nativeTypesPromoted, + [], ); } @@ -3811,6 +3835,7 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca $scope->afterExtractCall, $scope->parentScope, $this->nativeTypesPromoted, + [], ); } @@ -3870,6 +3895,7 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu $arrowFunctionScope->afterExtractCall, $arrowFunctionScope->parentScope, $this->nativeTypesPromoted, + [], ); } @@ -4033,6 +4059,7 @@ public function enterExpressionAssign(Expr $expr): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -4064,6 +4091,7 @@ public function exitExpressionAssign(Expr $expr): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -4106,6 +4134,7 @@ public function setAllowedUndefinedExpression(Expr $expr): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -4137,6 +4166,7 @@ public function unsetAllowedUndefinedExpression(Expr $expr): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -4284,6 +4314,7 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); if ($expr instanceof AlwaysRememberedExpr) { @@ -4393,6 +4424,7 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -4493,6 +4525,7 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -4705,6 +4738,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $scope->afterExtractCall, $scope->parentScope, $scope->nativeTypesPromoted, + $this->globalVariables, ); } @@ -4732,6 +4766,7 @@ public function addConditionalExpressions(string $exprString, array $conditional $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -4762,6 +4797,7 @@ public function exitFirstLevelStatements(): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -4799,6 +4835,9 @@ public function mergeWith(?self $otherScope): self $ourExpressionTypes, $mergedExpressionTypes, ); + + $mergedGlobalVariables = array_merge($this->globalVariables, $otherScope->globalVariables); + return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), @@ -4816,6 +4855,7 @@ public function mergeWith(?self $otherScope): self $this->afterExtractCall && $otherScope->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $mergedGlobalVariables, ); } @@ -4929,7 +4969,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])) { @@ -5020,6 +5060,7 @@ public function processFinallyScope(self $finallyScope, self $originalFinallySco $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -5115,6 +5156,7 @@ public function processClosureScope( $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -5164,6 +5206,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } @@ -5195,6 +5238,7 @@ public function generalizeWith(self $otherScope): self $this->afterExtractCall, $this->parentScope, $this->nativeTypesPromoted, + $this->globalVariables, ); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 0c25c7271b..fbec295ae3 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1966,6 +1966,8 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { } $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); + $scope->setVariableAsGlobal($var->name); + $vars[] = $var->name; } $scope = $this->processVarAnnotation($scope, $vars, $stmt); diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 1134614b2f..5e72a69d4a 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -61,6 +61,8 @@ public function hasVariableType(string $variableName): TrinaryLogic; public function getVariableType(string $variableName): Type; + public function isGlobalVariable(string $variableName): bool; + public function canAnyVariableExist(): bool; /** diff --git a/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php b/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php index cf3fdb19df..4d3ba31003 100644 --- a/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php +++ b/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php @@ -7,20 +7,34 @@ use PHPStan\Analyser\Scope; use PHPStan\Type\BooleanType; use PHPStan\Type\ExpressionTypeResolverExtension; +use PHPStan\Type\IntegerType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; class GlobalExpressionTypeResolverExtension implements ExpressionTypeResolverExtension { public function getType(Expr $expr, Scope $scope): ?Type { - if (!$expr instanceof Variable) { + if ( + !$expr instanceof Variable + || !\is_string($expr->name) + || !$scope->isGlobalVariable($expr->name) + ) { return null; } - if ($expr->name === 'MY_FRAMEWORK_GLOBAL') { + if ($expr->name === 'MY_GLOBAL_BOOL') { return new BooleanType(); } + if ($expr->name === 'MY_GLOBAL_INT') { + return new IntegerType(); + } + + if ($expr->name === 'MY_GLOBAL_STR') { + return new StringType(); + } + 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 index f196ef355f..c1ec355184 100644 --- a/tests/PHPStan/Analyser/data/expression-type-resolver-extension-global-statement.php +++ b/tests/PHPStan/Analyser/data/expression-type-resolver-extension-global-statement.php @@ -4,7 +4,24 @@ use function PHPStan\Testing\assertType; -global $MY_FRAMEWORK_GLOBAL, $ANOTHER_GLOBAL; +global $MY_GLOBAL_BOOL, $ANOTHER_GLOBAL; -assertType('bool', $MY_FRAMEWORK_GLOBAL); +assertType('bool', $MY_GLOBAL_BOOL); +assertType('mixed', $MY_GLOBAL_INT); // not declared in the global statement = no type assigned assertType('mixed', $ANOTHER_GLOBAL); + +$testFct = function ($MY_GLOBAL_BOOL) { + global $MY_GLOBAL_INT; + + assertType('mixed', $MY_GLOBAL_BOOL); + 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); + } +}; From f29758a0b0576c3e3887c42566744d58422a1be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Anne?= Date: Tue, 12 Aug 2025 23:49:50 +0200 Subject: [PATCH 4/4] use returned value --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index fbec295ae3..3217db6930 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1966,7 +1966,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { } $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes()); - $scope->setVariableAsGlobal($var->name); + $scope = $scope->setVariableAsGlobal($var->name); $vars[] = $var->name; }