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..3217db6930 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 = $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/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..4d3ba31003 --- /dev/null +++ b/tests/PHPStan/Analyser/data/GlobalExpressionTypeResolverExtension.php @@ -0,0 +1,41 @@ +name) + || !$scope->isGlobalVariable($expr->name) + ) { + return null; + } + + 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 new file mode 100644 index 0000000000..c1ec355184 --- /dev/null +++ b/tests/PHPStan/Analyser/data/expression-type-resolver-extension-global-statement.php @@ -0,0 +1,27 @@ +