diff --git a/conf/config.neon b/conf/config.neon index fb534ed239..ff8f0f16fd 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -607,6 +607,8 @@ services: - class: PHPStan\Analyser\ScopeFactory + arguments: + explicitMixedForGlobalVariables: %featureToggles.explicitMixedForGlobalVariables% - class: PHPStan\Analyser\NodeScopeResolver diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index e2b88a406d..701b06dbbf 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -34,20 +34,11 @@ public function __construct( private bool $treatPhpDocTypesAsCertain, private PhpVersion $phpVersion, private bool $explicitMixedInUnknownGenericNew, - private bool $explicitMixedForGlobalVariables, private ConstantResolver $constantResolver, ) { } - /** - * @param array $expressionTypes - * @param array $nativeExpressionTypes - * @param array $conditionalExpressions - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack - * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions - */ public function create( ScopeContext $context, bool $declareStrictTypes = false, @@ -102,7 +93,6 @@ public function create( $parentScope, $nativeTypesPromoted, $this->explicitMixedInUnknownGenericNew, - $this->explicitMixedForGlobalVariables, ); } diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index 13ed62fe68..6a19d4cc25 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -15,7 +15,7 @@ interface InternalScopeFactory * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param array $inFunctionCallsStack + * @param array $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index bd8d6d92fd..9bceec4ba3 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -22,8 +22,6 @@ class LazyInternalScopeFactory implements InternalScopeFactory private bool $explicitMixedInUnknownGenericNew; - private bool $explicitMixedForGlobalVariables; - /** * @param class-string $scopeClass */ @@ -34,18 +32,8 @@ public function __construct( { $this->treatPhpDocTypesAsCertain = $container->getParameter('treatPhpDocTypesAsCertain'); $this->explicitMixedInUnknownGenericNew = $this->container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew']; - $this->explicitMixedForGlobalVariables = $this->container->getParameter('featureToggles')['explicitMixedForGlobalVariables']; } - /** - * @param array $expressionTypes - * @param array $nativeExpressionTypes - * @param array $conditionalExpressions - * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack - * - */ public function create( ScopeContext $context, bool $declareStrictTypes = false, @@ -100,7 +88,6 @@ public function create( $parentScope, $nativeTypesPromoted, $this->explicitMixedInUnknownGenericNew, - $this->explicitMixedForGlobalVariables, ); } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index fcbe29f293..752b152396 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -188,7 +188,6 @@ public function __construct( private ?Scope $parentScope = null, private bool $nativeTypesPromoted = false, private bool $explicitMixedInUnknownGenericNew = false, - private bool $explicitMixedForGlobalVariables = false, ) { if ($namespace === '') { @@ -466,10 +465,6 @@ public function afterOpenSslCall(string $openSslFunctionName): self /** @api */ public function hasVariableType(string $variableName): TrinaryLogic { - if ($this->isGlobalVariable($variableName)) { - return TrinaryLogic::createYes(); - } - $varExprString = '$' . $variableName; if (!isset($this->expressionTypes[$varExprString])) { if ($this->canAnyVariableExist()) { @@ -500,10 +495,6 @@ public function getVariableType(string $variableName): Type } } - if ($this->isGlobalVariable($variableName)) { - return new ArrayType(new StringType(), new MixedType($this->explicitMixedForGlobalVariables)); - } - if ($this->hasVariableType($variableName)->no()) { throw new UndefinedVariableException($this, $variableName); } @@ -537,21 +528,6 @@ public function getDefinedVariables(): array return $variables; } - private function isGlobalVariable(string $variableName): bool - { - return in_array($variableName, [ - 'GLOBALS', - '_SERVER', - '_GET', - '_POST', - '_FILES', - '_COOKIE', - '_SESSION', - '_REQUEST', - '_ENV', - ], true); - } - /** @api */ public function hasConstant(Name $name): bool { @@ -2133,7 +2109,6 @@ public function doNotTreatPhpDocTypesAsCertain(): Scope $this->parentScope, false, $this->explicitMixedInUnknownGenericNew, - $this->explicitMixedForGlobalVariables, ); } @@ -2430,12 +2405,8 @@ public function enterClass(ClassReflection $classReflection): self $this->isDeclareStrictTypes(), null, $this->getNamespace(), - array_merge($this->getConstantTypes(), [ - '$this' => $thisHolder, - ]), - array_merge($this->getNativeConstantTypes(), [ - '$this' => $thisHolder, - ]), + array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), ['$this' => $thisHolder]), + array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), ['$this' => $thisHolder]), [], null, null, @@ -2671,8 +2642,8 @@ private function enterFunctionLike( $this->isDeclareStrictTypes(), $functionReflection, $this->getNamespace(), - array_merge($this->getConstantTypes(), $expressionTypes), - array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes), + array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), $expressionTypes), + array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), $nativeExpressionTypes), ); } @@ -2684,6 +2655,8 @@ public function enterNamespace(string $namespaceName): self $this->isDeclareStrictTypes(), null, $namespaceName, + $this->getSuperglobalTypes(), + $this->getNativeSuperglobalTypes(), ); } @@ -2907,8 +2880,8 @@ private function enterAnonymousFunctionWithoutReflection( $this->isDeclareStrictTypes(), $this->getFunction(), $this->getNamespace(), - array_merge($this->getConstantTypes(), $expressionTypes), - array_merge($this->getNativeConstantTypes(), $nativeTypes), + array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), $expressionTypes), + array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), $nativeTypes), [], $this->inClosureBindScopeClass, new TrivialParametersAcceptor(), @@ -4990,4 +4963,34 @@ private function getNativeConstantTypes(): array return $constantTypes; } + /** @return array */ + private function getSuperglobalTypes(): array + { + $superglobalTypes = []; + $exprStrings = ['$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV']; + foreach ($this->expressionTypes as $exprString => $typeHolder) { + if (!in_array($exprString, $exprStrings, true)) { + continue; + } + + $superglobalTypes[$exprString] = $typeHolder; + } + return $superglobalTypes; + } + + /** @return array */ + private function getNativeSuperglobalTypes(): array + { + $superglobalTypes = []; + $exprStrings = ['$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV']; + foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) { + if (!in_array($exprString, $exprStrings, true)) { + continue; + } + + $superglobalTypes[$exprString] = $typeHolder; + } + return $superglobalTypes; + } + } diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index c2401d375c..ea4c761e32 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -2,17 +2,45 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr\Variable; +use PHPStan\Type\ArrayType; +use PHPStan\Type\MixedType; +use PHPStan\Type\StringType; + /** @api */ class ScopeFactory { - public function __construct(private InternalScopeFactory $internalScopeFactory) + public function __construct( + private InternalScopeFactory $internalScopeFactory, + private bool $explicitMixedForGlobalVariables, + ) { } public function create(ScopeContext $context): MutatingScope { - return $this->internalScopeFactory->create($context); + $superglobalType = new ArrayType(new StringType(), new MixedType($this->explicitMixedForGlobalVariables)); + $expressionTypes = [ + '$GLOBALS' => ExpressionTypeHolder::createYes(new Variable('GLOBALS'), $superglobalType), + '$_SERVER' => ExpressionTypeHolder::createYes(new Variable('_SERVER'), $superglobalType), + '$_GET' => ExpressionTypeHolder::createYes(new Variable('_GET'), $superglobalType), + '$_POST' => ExpressionTypeHolder::createYes(new Variable('_POST'), $superglobalType), + '$_FILES' => ExpressionTypeHolder::createYes(new Variable('_FILES'), $superglobalType), + '$_COOKIE' => ExpressionTypeHolder::createYes(new Variable('_COOKIE'), $superglobalType), + '$_SESSION' => ExpressionTypeHolder::createYes(new Variable('_SESSION'), $superglobalType), + '$_REQUEST' => ExpressionTypeHolder::createYes(new Variable('_REQUEST'), $superglobalType), + '$_ENV' => ExpressionTypeHolder::createYes(new Variable('_ENV'), $superglobalType), + ]; + + return $this->internalScopeFactory->create( + $context, + false, + null, + null, + $expressionTypes, + $expressionTypes, + ); } } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 62e6095822..ea3ca9a52f 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -179,9 +179,9 @@ public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeS $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(PhpVersion::class), $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], - $container->getParameter('featureToggles')['explicitMixedForGlobalVariables'], $constantResolver, ), + $container->getParameter('featureToggles')['explicitMixedForGlobalVariables'], ); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index c629aed2f7..34798c2deb 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -463,6 +463,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-types.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5219.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/strval.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/superglobals.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-next.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string.php'); diff --git a/tests/PHPStan/Analyser/data/superglobals.php b/tests/PHPStan/Analyser/data/superglobals.php new file mode 100644 index 0000000000..2885d59f04 --- /dev/null +++ b/tests/PHPStan/Analyser/data/superglobals.php @@ -0,0 +1,59 @@ +', $GLOBALS); + assertType('array', $_SERVER); + assertType('array', $_GET); + assertType('array', $_POST); + assertType('array', $_FILES); + assertType('array', $_COOKIE); + assertType('array', $_SESSION); + assertType('array', $_REQUEST); + assertType('array', $_ENV); + } + + public function canBeOverwritten(): void + { + $GLOBALS = []; + assertType('array{}', $GLOBALS); + assertNativeType('array{}', $GLOBALS); + } + + public function canBePartlyOverwritten(): void + { + $GLOBALS['foo'] = 'foo'; + assertType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); + assertNativeType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS); + } + + public function canBeNarrowed(): void + { + if (isset($GLOBALS['foo'])) { + assertType("array&hasOffsetValue('foo', mixed~null)", $GLOBALS); + assertNativeType("array&hasOffset('foo')", $GLOBALS); // https://github.com/phpstan/phpstan/issues/8395 + } else { + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); + } + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); + } + +} + +function functionScope() { + assertType('array', $GLOBALS); + assertNativeType('array', $GLOBALS); +} + +assertType('array', $GLOBALS); +assertNativeType('array', $GLOBALS);