diff --git a/conf/config.neon b/conf/config.neon index f623528506a..e93a0a9b877 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -241,6 +241,8 @@ parameters: - ZEND_DEBUG_BUILD - ZEND_THREAD_SAFE - E_ALL # different on PHP 8.4 + readonlySuperglobals: false + superglobalTypes: [] customRulesetUsed: null editorUrl: null editorUrlTitle: null diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 154054ccb1f..58381d84ce5 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -177,6 +177,8 @@ parametersSchema: resultCacheSkipIfOlderThanDays: int() resultCacheChecksProjectExtensionFilesDependencies: bool() dynamicConstantNames: arrayOf(string()) + readonlySuperglobals: bool() + superglobalTypes: arrayOf(string(), anyOf('GLOBALS', '_SERVER', '_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_REQUEST', '_ENV')) customRulesetUsed: schema(bool(), nullable()) rootDir: string() tmpDir: string() diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index c0a9007c0c7..1eaa9878ed2 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -45,6 +45,7 @@ public function __construct( private ConfiguredPhpVersionRangeHelper $configuredPhpVersionRangeHelper, private $nodeCallback, private ConstantResolver $constantResolver, + private SuperglobalTypeResolver $superglobalTypeResolver, private bool $createsNodeCallbackScopes = false, ?ExpressionResultStorageStack $expressionResultStorageStack = null, ) @@ -92,6 +93,7 @@ public function create( $this->phpVersion, $this->attributeReflectionFactory, $this->configuredPhpVersionRangeHelper, + $this->superglobalTypeResolver, $this->nodeCallback, ); @@ -145,6 +147,7 @@ private function withFlavor(bool $createsNodeCallbackScopes): self $this->configuredPhpVersionRangeHelper, $this->nodeCallback, $this->constantResolver, + $this->superglobalTypeResolver, $createsNodeCallbackScopes, $this->expressionResultStorageStack, ); diff --git a/src/Analyser/DirectInternalScopeFactoryFactory.php b/src/Analyser/DirectInternalScopeFactoryFactory.php index 4e778139644..5088a21cbed 100644 --- a/src/Analyser/DirectInternalScopeFactoryFactory.php +++ b/src/Analyser/DirectInternalScopeFactoryFactory.php @@ -34,6 +34,7 @@ public function __construct( private AttributeReflectionFactory $attributeReflectionFactory, private ConfiguredPhpVersionRangeHelper $configuredPhpVersionRangeHelper, private ConstantResolver $constantResolver, + private SuperglobalTypeResolver $superglobalTypeResolver, ) { } @@ -57,6 +58,7 @@ public function create(?callable $nodeCallback): DirectInternalScopeFactory $this->configuredPhpVersionRangeHelper, $nodeCallback, $this->constantResolver, + $this->superglobalTypeResolver, ); } diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index c40ea12abbc..b12ac30f2be 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -92,6 +92,7 @@ public function create( $this->container->getByType(PhpVersion::class), $this->container->getByType(AttributeReflectionFactory::class), $this->container->getByType(ConfiguredPhpVersionRangeHelper::class), + $this->container->getByType(SuperglobalTypeResolver::class), $this->nodeCallback, ); diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 85867200a20..0bf23cd7e5e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -83,7 +83,6 @@ use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; @@ -795,7 +794,7 @@ public function getVariableType(string $variableName): Type $varExprString = '$' . $variableName; if (!array_key_exists($varExprString, $this->expressionTypes)) { if ($this->isGlobalVariable($variableName)) { - return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true)); + return $this->services->superglobalTypeResolver->getType($variableName, $this->nativeTypesPromoted); } return new MixedType(); } diff --git a/src/Analyser/ScopeServices.php b/src/Analyser/ScopeServices.php index 72bbc767679..cad047f84c2 100644 --- a/src/Analyser/ScopeServices.php +++ b/src/Analyser/ScopeServices.php @@ -43,6 +43,7 @@ public function __construct( public readonly PhpVersion $phpVersion, public readonly AttributeReflectionFactory $attributeReflectionFactory, public readonly ConfiguredPhpVersionRangeHelper $configuredPhpVersionRangeHelper, + public readonly SuperglobalTypeResolver $superglobalTypeResolver, public readonly mixed $nodeCallback, ) { diff --git a/src/Analyser/SuperglobalTypeResolver.php b/src/Analyser/SuperglobalTypeResolver.php new file mode 100644 index 00000000000..a0bd51eb9e3 --- /dev/null +++ b/src/Analyser/SuperglobalTypeResolver.php @@ -0,0 +1,111 @@ +`, since a script can write + * anything into it. A project that treats superglobals as a contract opts in to + * narrower types: `readonlySuperglobals` assumes the shapes PHP populates them + * with (READONLY_SUPERGLOBAL_TYPES), and `superglobalTypes` declares the type of + * any superglobal, taking precedence over those shapes. Either type stays sound + * only while what is written into the superglobal fits it, which + * SuperglobalAssignTypeRule checks. + * + * Either type is a PHPDoc type: PHP itself does not enforce it, so the native + * type stays the default and, with `treatPhpDocTypesAsCertain: false`, a + * defensive check like `is_string($_GET['id'])` is not reported as always true. + */ +#[AutowiredService] +final class SuperglobalTypeResolver +{ + + /** + * Request input is strings, nested into arrays by brackets in a name + * (`?a[]=1&a[b][]=2`); the nesting is unbounded, so the type stops at two levels. + * `$_SERVER` is strings too, the request variables, headers and environment, + * except for the few entries PHP adds itself; `argc` is 0 and `argv` empty on + * the web when `register_argc_argv` is on and there is no query string. + * `REQUEST_TIME` and `REQUEST_TIME_FLOAT` are set for every request, and every + * bundled SAPI sets `PHP_SELF`, if only to an empty string, unless + * `variables_order` leaves out `S` and `$_SERVER` stays empty. Any other key is + * up to the SAPI, the web server or the client: `SCRIPT_FILENAME` comes from + * the web server's configuration, and a header from the request. + * The keys are not narrowed to strings: like any array key, a numeric name + * (`?1=a`, an environment variable named `123`) becomes an int. + * `$_SESSION` and `$GLOBALS` are left out: what they hold is up to the application. + */ + private const READONLY_SUPERGLOBAL_TYPES = [ + '_GET' => self::REQUEST_INPUT_TYPE, + '_POST' => self::REQUEST_INPUT_TYPE, + '_COOKIE' => self::REQUEST_INPUT_TYPE, + '_REQUEST' => self::REQUEST_INPUT_TYPE, + '_FILES' => 'array, size: int<0, max>}|array{name: array, full_path?: array, type: array, tmp_name: array, error: array, size: array}>', + '_SERVER' => 'array{PHP_SELF: string, REQUEST_TIME: int, REQUEST_TIME_FLOAT: float, argc?: int<0, max>, argv?: list, ...}', + '_ENV' => 'array', + ]; + + private const REQUEST_INPUT_TYPE = 'array>>'; + + /** @var array */ + private array $typeStrings; + + private ?Type $defaultType = null; + + /** @var array */ + private array $resolvedTypes = []; + + /** + * @param array $superglobalTypes + */ + public function __construct( + #[AutowiredParameter] + bool $readonlySuperglobals, + #[AutowiredParameter] + array $superglobalTypes, + private Container $container, + ) + { + $this->typeStrings = $readonlySuperglobals ? $superglobalTypes + self::READONLY_SUPERGLOBAL_TYPES : $superglobalTypes; + } + + public function getType(string $variableName, bool $nativeTypesPromoted): Type + { + $contractType = $nativeTypesPromoted ? null : $this->getContractType($variableName); + + return $contractType ?? $this->defaultType ??= new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true)); + } + + /** + * The type the project holds the superglobal to, declared or assumed; + * null when it has none and anything may be written into it. + */ + public function getContractType(string $variableName): ?Type + { + if (!array_key_exists($variableName, $this->typeStrings)) { + return null; + } + + // fetched lazily: TypeStringResolver depends on services that depend on the scope factory + return $this->resolvedTypes[$variableName] ??= $this->container->getByType(TypeStringResolver::class)->resolve( + $this->typeStrings[$variableName], + new NameScope(null, [], className: null), + ); + } + +} diff --git a/src/Rules/Variables/SuperglobalAssignTypeRule.php b/src/Rules/Variables/SuperglobalAssignTypeRule.php new file mode 100644 index 00000000000..133179ecd86 --- /dev/null +++ b/src/Rules/Variables/SuperglobalAssignTypeRule.php @@ -0,0 +1,81 @@ + + */ +#[RegisteredRule(level: 3)] +final class SuperglobalAssignTypeRule implements Rule +{ + + public function __construct( + private SuperglobalTypeResolver $superglobalTypeResolver, + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return VariableAssignNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $variable = $node->getVariable(); + if (!is_string($variable->name) || !in_array($variable->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + return []; + } + + $superglobalType = $this->superglobalTypeResolver->getContractType($variable->name); + if ($superglobalType === null) { + return []; + } + + $assignedValueType = $scope->getType($node->getAssignedExpr()); + + // what the superglobal held before this write was either accepted or already reported, + // so only a value this write newly brings in is reported, not every later write + $acceptingType = TypeCombinator::union($superglobalType, $scope->getType(new Variable($variable->name))); + $accepts = $this->ruleLevelHelper->accepts($acceptingType, $assignedValueType, $scope->isDeclareStrictTypes()); + if ($accepts->result) { + return []; + } + + $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($superglobalType, $assignedValueType); + + return [ + RuleErrorBuilder::message(sprintf( + 'Superglobal $%s (%s) does not accept %s.', + $variable->name, + $superglobalType->describe($verbosityLevel), + $assignedValueType->describe($verbosityLevel), + )) + ->identifier('assign.superglobalType') + ->acceptsReasonsTip($accepts->reasons) + ->build(), + ]; + } + +} diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index f7947ba02f4..ef3cbe51d38 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -7,6 +7,7 @@ use PHPStan\Analyser\DirectInternalScopeFactoryFactory; use PHPStan\Analyser\Error; use PHPStan\Analyser\ScopeFactory; +use PHPStan\Analyser\SuperglobalTypeResolver; use PHPStan\Analyser\TypeSpecifier; use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; @@ -130,6 +131,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider $container->getByType(AttributeReflectionFactory::class), $container->getByType(ConfiguredPhpVersionRangeHelper::class), $constantResolver, + $container->getByType(SuperglobalTypeResolver::class), ), ); } diff --git a/src/Testing/PHPUnit/ContainerInitializer.php b/src/Testing/PHPUnit/ContainerInitializer.php index 8f29c59aa90..33930e6d679 100644 --- a/src/Testing/PHPUnit/ContainerInitializer.php +++ b/src/Testing/PHPUnit/ContainerInitializer.php @@ -3,14 +3,16 @@ namespace PHPStan\Testing\PHPUnit; use PHPStan\DependencyInjection\InvalidIgnoredErrorExceptionTest; +use PHPStan\DependencyInjection\InvalidSuperglobalTypesTest; +use function in_array; final class ContainerInitializer { public static function initialize(string $testClassName): void { - // This test expects an exception during container initialization - if ($testClassName === InvalidIgnoredErrorExceptionTest::class) { + // These tests expect an exception during container initialization + if (in_array($testClassName, [InvalidIgnoredErrorExceptionTest::class, InvalidSuperglobalTypesTest::class], true)) { return; } diff --git a/tests/PHPStan/Analyser/ReadonlySuperglobalsTypesTest.php b/tests/PHPStan/Analyser/ReadonlySuperglobalsTypesTest.php new file mode 100644 index 00000000000..ec06bd8e63e --- /dev/null +++ b/tests/PHPStan/Analyser/ReadonlySuperglobalsTypesTest.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/readonly-superglobals.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/SuperglobalTypesTest.php b/tests/PHPStan/Analyser/SuperglobalTypesTest.php new file mode 100644 index 00000000000..37fa0bf27ec --- /dev/null +++ b/tests/PHPStan/Analyser/SuperglobalTypesTest.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/superglobal-types.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/readonly-superglobals.php b/tests/PHPStan/Analyser/data/readonly-superglobals.php new file mode 100644 index 00000000000..d9dc38be7bb --- /dev/null +++ b/tests/PHPStan/Analyser/data/readonly-superglobals.php @@ -0,0 +1,42 @@ +|string>|string>', $_GET); + assertType('array|string>|string>', $_POST); + assertType('array|string>|string>', $_REQUEST); + assertType('array, full_path?: array, type: array, tmp_name: array, error: array, size: array}|array{name: string, full_path?: string, type: string, tmp_name: string, error: int<0, 8>, size: int<0, max>}>', $_FILES); + assertType('array{PHP_SELF: string, REQUEST_TIME: int, REQUEST_TIME_FLOAT: float, argc?: int<0, max>, argv?: list, ...}', $_SERVER); + assertType('array', $_SESSION); + assertType('array', $GLOBALS); + + assertNativeType('array', $_GET); + assertNativeType('array', $_FILES); + assertNativeType('array', $_SERVER); +} + +function serverOffsets(): void +{ + assertType('string|null', $_SERVER['REQUEST_URI'] ?? null); + assertType('int<0, max>|null', $_SERVER['argc'] ?? null); + assertType('list|null', $_SERVER['argv'] ?? null); + assertType('string', $_SERVER['PHP_SELF']); + assertType('string|null', $_SERVER['SCRIPT_FILENAME'] ?? null); + assertType('int', $_SERVER['REQUEST_TIME']); + assertType('float', $_SERVER['REQUEST_TIME_FLOAT']); + + if (isset($_SERVER['HTTP_HOST'])) { + assertType('string', $_SERVER['HTTP_HOST']); + } +} + +function configuredTypesTakePrecedence(): void +{ + assertType('array', $_COOKIE); + assertType('array', $_ENV); +} diff --git a/tests/PHPStan/Analyser/data/superglobal-types.php b/tests/PHPStan/Analyser/data/superglobal-types.php new file mode 100644 index 00000000000..49f15e97b6a --- /dev/null +++ b/tests/PHPStan/Analyser/data/superglobal-types.php @@ -0,0 +1,57 @@ +|string>|string>', $_GET); + assertType('array', $_COOKIE); + assertType('array', $_POST); + assertType('array', $_SERVER); + assertType('array', $_FILES); + assertType('array', $_ENV); + + assertNativeType('array', $_GET); + assertNativeType('array', $_COOKIE); +} + +function offsets(): void +{ + assertType('array|string>|string', $_GET['id']); + assertType('array|string>|string|null', $_GET['id'] ?? null); + assertNativeType('mixed', $_GET['id']); + + if (isset($_GET['id'])) { + assertType('array|string>|string', $_GET['id']); + } + + if (isset($_GET['id']) && is_string($_GET['id'])) { + assertType('string', $_GET['id']); + } + + assertType('string|null', $_COOKIE['session'] ?? null); +} + +function writes(): void +{ + $_GET['id'] = 1; + assertType('1', $_GET['id']); + + $_GET = []; + assertType('array{}', $_GET); +} + +function invalidatedByCalls(): void +{ + $_GET['id'] = 1; + assertType('1', $_GET['id']); + doSomething(); + assertType('array|string>|string>', $_GET); +} + +function doSomething(): void +{ +} diff --git a/tests/PHPStan/Analyser/readonly-superglobals.neon b/tests/PHPStan/Analyser/readonly-superglobals.neon new file mode 100644 index 00000000000..47be013e0ae --- /dev/null +++ b/tests/PHPStan/Analyser/readonly-superglobals.neon @@ -0,0 +1,5 @@ +parameters: + readonlySuperglobals: true + superglobalTypes: + _COOKIE: 'array' + _ENV: 'array' diff --git a/tests/PHPStan/Analyser/superglobal-types.neon b/tests/PHPStan/Analyser/superglobal-types.neon new file mode 100644 index 00000000000..1916b059d3c --- /dev/null +++ b/tests/PHPStan/Analyser/superglobal-types.neon @@ -0,0 +1,4 @@ +parameters: + superglobalTypes: + _GET: 'array>>' + _COOKIE: 'array' diff --git a/tests/PHPStan/DependencyInjection/InvalidSuperglobalTypesTest.php b/tests/PHPStan/DependencyInjection/InvalidSuperglobalTypesTest.php new file mode 100644 index 00000000000..809729a9768 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/InvalidSuperglobalTypesTest.php @@ -0,0 +1,51 @@ + + */ + public static function dataValidateSuperglobalTypes(): iterable + { + $expected = static fn (string $key, string $given): string => "The key of item 'parameters\u{a0}›\u{a0}superglobalTypes\u{a0}›\u{a0}" . $key . "' expects to be 'GLOBALS'|'_SERVER'|'_GET'|'_POST'|'_FILES'|'_COOKIE'|'_SESSION'|'_REQUEST'|'_ENV', " . $given . ' given.'; + yield [ + __DIR__ . '/invalidSuperglobalTypes/typo.neon', + $expected('_GTE', "'_GTE'"), + ]; + yield [ + __DIR__ . '/invalidSuperglobalTypes/dollar-sign.neon', + $expected('$_GET', '\'$_GET\''), + ]; + yield [ + __DIR__ . '/invalidSuperglobalTypes/list.neon', + $expected('0', '0'), + ]; + } + + #[DataProvider('dataValidateSuperglobalTypes')] + public function testValidateSuperglobalTypes(string $file, string $expectedMessage): void + { + self::$configFile = $file; + $this->expectExceptionMessage($expectedMessage); + self::getContainer(); + } + + public static function getAdditionalConfigFiles(): array + { + $files = parent::getAdditionalConfigFiles(); + if (self::$configFile !== null) { + $files[] = self::$configFile; + } + + return $files; + } + +} diff --git a/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/dollar-sign.neon b/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/dollar-sign.neon new file mode 100644 index 00000000000..157ace19e69 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/dollar-sign.neon @@ -0,0 +1,3 @@ +parameters: + superglobalTypes: + $_GET: 'array' diff --git a/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/list.neon b/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/list.neon new file mode 100644 index 00000000000..5484c25da8d --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/list.neon @@ -0,0 +1,3 @@ +parameters: + superglobalTypes: + - 'array' diff --git a/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/typo.neon b/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/typo.neon new file mode 100644 index 00000000000..bc0b03adab5 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidSuperglobalTypes/typo.neon @@ -0,0 +1,3 @@ +parameters: + superglobalTypes: + _GTE: 'array' diff --git a/tests/PHPStan/Rules/Variables/SuperglobalAssignTypeRuleTest.php b/tests/PHPStan/Rules/Variables/SuperglobalAssignTypeRuleTest.php new file mode 100644 index 00000000000..9a45a9c7c97 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/SuperglobalAssignTypeRuleTest.php @@ -0,0 +1,66 @@ + + */ +class SuperglobalAssignTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new SuperglobalAssignTypeRule( + self::getContainer()->getByType(SuperglobalTypeResolver::class), + self::getContainer()->getByType(RuleLevelHelper::class), + ); + } + + public function testRule(): void + { + $request = 'array|string>|string>'; + $server = 'array{PHP_SELF: string, REQUEST_TIME: int, REQUEST_TIME_FLOAT: float, argc?: int<0, max>, argv?: list, ...}'; + $this->analyse([__DIR__ . '/data/superglobal-assign-type.php'], [ + [ + sprintf('Superglobal $_GET (%s) does not accept array|string>|int|string>.', $request), + 23, + ], + [ + sprintf('Superglobal $_POST (%s) does not accept array|int|string>|string>.', $request), + 25, + ], + [ + 'Superglobal $_ENV (array) does not accept array.', + 26, + ], + [ + sprintf('Superglobal $_SERVER (%s) does not accept array{PHP_SELF: string, REQUEST_TIME: int, REQUEST_TIME_FLOAT: float, argc?: int<0, max>, argv?: list, SERVER_PORT: int, ...}.', $server), + 27, + 'Unsealed array value type string does not accept extra offset \'SERVER_PORT\' with value type int.', + ], + [ + // the second int written on the next line is not reported again + sprintf('Superglobal $_POST (%s) does not accept array|string>|int|string>.', $request), + 32, + ], + [ + sprintf('Superglobal $_GET (%s) does not accept string.', $request), + 43, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/superglobal-assign-type.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Variables/SuperglobalAssignTypeRuleWithoutTypesTest.php b/tests/PHPStan/Rules/Variables/SuperglobalAssignTypeRuleWithoutTypesTest.php new file mode 100644 index 00000000000..9064c713c43 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/SuperglobalAssignTypeRuleWithoutTypesTest.php @@ -0,0 +1,29 @@ + + */ +class SuperglobalAssignTypeRuleWithoutTypesTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new SuperglobalAssignTypeRule( + self::getContainer()->getByType(SuperglobalTypeResolver::class), + self::getContainer()->getByType(RuleLevelHelper::class), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/superglobal-assign-type.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/superglobal-assign-type.php b/tests/PHPStan/Rules/Variables/data/superglobal-assign-type.php new file mode 100644 index 00000000000..3de72f35a05 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/superglobal-assign-type.php @@ -0,0 +1,51 @@ + $strings + * @param list $ints + */ +function nonConformingWrites(array $strings, array $ints, int $int): void +{ + $_GET['message'] = $int; + $_POST['ids'] = $strings; + $_POST['ids'] = $ints; + $_ENV['FOO'] = 1; + $_SERVER['SERVER_PORT'] = $int; +} + +function reportedOnce(int $int, string $string): void +{ + $_POST['link_id'] = $int; + $_POST['comment_parent'] = $int; + $_POST['title'] = $string; + unset($_POST['posted']); +} + +/** + * @param list $strings + */ +function foreachTarget(array $strings): void +{ + foreach ($strings as $_GET) { + } +} + +function untypedSuperglobals(int $int): void +{ + $GLOBALS['foo'] = $int; + $_SESSION['user'] = $int; +} diff --git a/tests/PHPStan/Rules/Variables/superglobal-assign-type.neon b/tests/PHPStan/Rules/Variables/superglobal-assign-type.neon new file mode 100644 index 00000000000..e810b3f38f0 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/superglobal-assign-type.neon @@ -0,0 +1,2 @@ +parameters: + readonlySuperglobals: true diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index 1138b46ed9a..5b8d05e6952 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -339,6 +339,7 @@ enum : uint32_t PT_MSS_PHP_VERSION, PT_MSS_ATTRIBUTE_REFLECTION_FACTORY, PT_MSS_CONFIGURED_PHP_VERSION_RANGE_HELPER, + PT_MSS_SUPERGLOBAL_TYPE_RESOLVER, PT_MSS_NODE_CALLBACK, PT_MSS_COUNT, }; @@ -357,6 +358,7 @@ static const char *const pt_mss_names[PT_MSS_COUNT] = { "phpVersion", "attributeReflectionFactory", "configuredPhpVersionRangeHelper", + "superglobalTypeResolver", "nodeCallback", }; @@ -1709,22 +1711,11 @@ class MutatingScope zval *holder = zend_symtable_find(Z_ARRVAL_P(slot(PT_MS_PROP_EXPRESSION_TYPES).raw()), varExprString.get()); if (holder == NULL) { if (isGlobalVariable(variableName)) { - /* new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true)) */ - zval integer, string, mixed, benevolent, array; - if (UNEXPECTED(!pt_integer_type_new(&integer))) return zv::Val(); - if (UNEXPECTED(!pt_string_type_new(&string))) { - zval_ptr_dtor(&integer); - return zv::Val(); - } - zv::Arr members = zv::Arr::create(2); - members.push(zv::Val::adopt(integer)); - members.push(zv::Val::adopt(string)); - if (UNEXPECTED(!pt_benevolent_union_type_new(&benevolent, members.raw()))) return zv::Val(); - zv::Val benevolentVal = zv::Val::adopt(benevolent); - if (UNEXPECTED(!pt_mixed_type_new(&mixed, true))) return zv::Val(); - zv::Val mixedVal = zv::Val::adopt(mixed); - if (UNEXPECTED(!pt_array_type_new(&array, benevolentVal.raw(), mixedVal.raw()))) return zv::Val(); - return zv::Val::adopt(array); + /* $this->services->superglobalTypeResolver->getType($variableName, $this->nativeTypesPromoted) */ + zv::Ref superglobalTypeResolver = service(PT_MSS_SUPERGLOBAL_TYPE_RESOLVER); + if (UNEXPECTED(!superglobalTypeResolver.isObject())) return uninitializedProperty("superglobalTypeResolver"); + zv::Args args{variableName, slot(PT_MS_PROP_NATIVE_TYPES_PROMOTED).raw()}; + return pt_type_call(superglobalTypeResolver.asObject(), PT_LC("gettype"), 2, args); } return newMixedType(false); }