Skip to content

Commit 5d687dc

Browse files
committed
Improve expression resolving of superglobals
1 parent a83c3dc commit 5d687dc

File tree

6 files changed

+187
-36
lines changed

6 files changed

+187
-36
lines changed

Diff for: src/Analyser/MutatingScope.php

+40-23
Original file line numberDiff line numberDiff line change
@@ -506,10 +506,6 @@ public function afterOpenSslCall(string $openSslFunctionName): self
506506
/** @api */
507507
public function hasVariableType(string $variableName): TrinaryLogic
508508
{
509-
if ($this->isGlobalVariable($variableName)) {
510-
return TrinaryLogic::createYes();
511-
}
512-
513509
$varExprString = '$' . $variableName;
514510
if (!isset($this->expressionTypes[$varExprString])) {
515511
if ($this->canAnyVariableExist()) {
@@ -541,10 +537,6 @@ public function getVariableType(string $variableName): Type
541537
}
542538
}
543539

544-
if ($this->isGlobalVariable($variableName)) {
545-
return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true));
546-
}
547-
548540
if ($this->hasVariableType($variableName)->no()) {
549541
throw new UndefinedVariableException($this, $variableName);
550542
}
@@ -599,11 +591,6 @@ public function getMaybeDefinedVariables(): array
599591
return $variables;
600592
}
601593

602-
private function isGlobalVariable(string $variableName): bool
603-
{
604-
return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true);
605-
}
606-
607594
/** @api */
608595
public function hasConstant(Name $name): bool
609596
{
@@ -2883,18 +2870,16 @@ public function isInFunctionExists(string $functionName): bool
28832870
public function enterClass(ClassReflection $classReflection): self
28842871
{
28852872
$thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection));
2886-
$constantTypes = $this->getConstantTypes();
2887-
$constantTypes['$this'] = $thisHolder;
2888-
$nativeConstantTypes = $this->getNativeConstantTypes();
2889-
$nativeConstantTypes['$this'] = $thisHolder;
2873+
$expressionTypes = array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), ['$this' => $thisHolder]);
2874+
$nativeExpressionTypes = array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), ['$this' => $thisHolder]);
28902875

28912876
return $this->scopeFactory->create(
28922877
$this->context->enterClass($classReflection),
28932878
$this->isDeclareStrictTypes(),
28942879
null,
28952880
$this->getNamespace(),
2896-
$constantTypes,
2897-
$nativeConstantTypes,
2881+
$expressionTypes,
2882+
$nativeExpressionTypes,
28982883
[],
28992884
[],
29002885
null,
@@ -3258,8 +3243,8 @@ private function enterFunctionLike(
32583243
$this->isDeclareStrictTypes(),
32593244
$functionReflection,
32603245
$this->getNamespace(),
3261-
array_merge($this->getConstantTypes(), $expressionTypes),
3262-
array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes),
3246+
array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), $expressionTypes),
3247+
array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), $nativeExpressionTypes),
32633248
$conditionalTypes,
32643249
);
32653250
}
@@ -3272,6 +3257,8 @@ public function enterNamespace(string $namespaceName): self
32723257
$this->isDeclareStrictTypes(),
32733258
null,
32743259
$namespaceName,
3260+
$this->getSuperglobalTypes(),
3261+
$this->getNativeSuperglobalTypes(),
32753262
);
32763263
}
32773264

@@ -3548,8 +3535,8 @@ private function enterAnonymousFunctionWithoutReflection(
35483535
$this->isDeclareStrictTypes(),
35493536
$this->getFunction(),
35503537
$this->getNamespace(),
3551-
array_merge($this->getConstantTypes(), $expressionTypes),
3552-
array_merge($this->getNativeConstantTypes(), $nativeTypes),
3538+
array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), $expressionTypes),
3539+
array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), $nativeTypes),
35533540
[],
35543541
$this->inClosureBindScopeClasses,
35553542
new TrivialParametersAcceptor(),
@@ -5807,6 +5794,36 @@ public function getConstantReflection(Type $typeWithConstant, string $constantNa
58075794
return $typeWithConstant->getConstant($constantName);
58085795
}
58095796

5797+
/** @return array<string, ExpressionTypeHolder> */
5798+
private function getSuperglobalTypes(): array
5799+
{
5800+
$superglobalTypes = [];
5801+
$exprStrings = ['$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV'];
5802+
foreach ($this->expressionTypes as $exprString => $typeHolder) {
5803+
if (!in_array($exprString, $exprStrings, true)) {
5804+
continue;
5805+
}
5806+
5807+
$superglobalTypes[$exprString] = $typeHolder;
5808+
}
5809+
return $superglobalTypes;
5810+
}
5811+
5812+
/** @return array<string, ExpressionTypeHolder> */
5813+
private function getNativeSuperglobalTypes(): array
5814+
{
5815+
$superglobalTypes = [];
5816+
$exprStrings = ['$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV'];
5817+
foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) {
5818+
if (!in_array($exprString, $exprStrings, true)) {
5819+
continue;
5820+
}
5821+
5822+
$superglobalTypes[$exprString] = $typeHolder;
5823+
}
5824+
return $superglobalTypes;
5825+
}
5826+
58105827
/**
58115828
* @return array<string, ExpressionTypeHolder>
58125829
*/

Diff for: src/Analyser/ScopeFactory.php

+21-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
namespace PHPStan\Analyser;
44

5+
use PhpParser\Node\Expr\Variable;
6+
use PHPStan\Type\ArrayType;
7+
use PHPStan\Type\BenevolentUnionType;
8+
use PHPStan\Type\IntegerType;
9+
use PHPStan\Type\MixedType;
10+
use PHPStan\Type\StringType;
11+
512
/**
613
* @api
714
*/
@@ -14,7 +21,20 @@ public function __construct(private InternalScopeFactory $internalScopeFactory)
1421

1522
public function create(ScopeContext $context): MutatingScope
1623
{
17-
return $this->internalScopeFactory->create($context);
24+
$superglobalType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true));
25+
$expressionTypes = [
26+
'$GLOBALS' => ExpressionTypeHolder::createYes(new Variable('GLOBALS'), $superglobalType),
27+
'$_SERVER' => ExpressionTypeHolder::createYes(new Variable('_SERVER'), $superglobalType),
28+
'$_GET' => ExpressionTypeHolder::createYes(new Variable('_GET'), $superglobalType),
29+
'$_POST' => ExpressionTypeHolder::createYes(new Variable('_POST'), $superglobalType),
30+
'$_FILES' => ExpressionTypeHolder::createYes(new Variable('_FILES'), $superglobalType),
31+
'$_COOKIE' => ExpressionTypeHolder::createYes(new Variable('_COOKIE'), $superglobalType),
32+
'$_SESSION' => ExpressionTypeHolder::createYes(new Variable('_SESSION'), $superglobalType),
33+
'$_REQUEST' => ExpressionTypeHolder::createYes(new Variable('_REQUEST'), $superglobalType),
34+
'$_ENV' => ExpressionTypeHolder::createYes(new Variable('_ENV'), $superglobalType),
35+
];
36+
37+
return $this->internalScopeFactory->create($context, false, null, null, $expressionTypes, $expressionTypes);
1838
}
1939

2040
}

Diff for: tests/PHPStan/Analyser/ScopeTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ public function testDefinedVariables(): void
261261
->assignVariable('a', new ConstantStringType('a'), new StringType(), TrinaryLogic::createYes())
262262
->assignVariable('b', new ConstantStringType('b'), new StringType(), TrinaryLogic::createMaybe());
263263

264-
$this->assertSame(['a'], $scope->getDefinedVariables());
264+
$this->assertSame(['GLOBALS', '_SERVER', '_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_REQUEST', '_ENV', 'a'], $scope->getDefinedVariables());
265265
}
266266

267267
public function testMaybeDefinedVariables(): void

Diff for: tests/PHPStan/Analyser/nsrt/get-defined-vars.php

+10-10
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,37 @@
1010

1111
function doFoo(int $param) {
1212
$local = "foo";
13-
assertType('array{param: int, local: \'foo\'}', get_defined_vars());
14-
assertType('array{\'param\', \'local\'}', array_keys(get_defined_vars()));
13+
assertType('array{GLOBALS: array<mixed>, _SERVER: array<mixed>, _GET: array<mixed>, _POST: array<mixed>, _FILES: array<mixed>, _COOKIE: array<mixed>, _SESSION: array<mixed>, _REQUEST: array<mixed>, _ENV: array<mixed>, param: int, local: \'foo\'}', get_defined_vars());
14+
assertType('array{\'GLOBALS\', \'_SERVER\', \'_GET\', \'_POST\', \'_FILES\', \'_COOKIE\', \'_SESSION\', \'_REQUEST\', \'_ENV\', \'param\', \'local\'}', array_keys(get_defined_vars()));
1515
}
1616

1717
function doBar(int $param) {
1818
global $global;
1919
$local = "foo";
20-
assertType('array{param: int, global: mixed, local: \'foo\'}', get_defined_vars());
21-
assertType('array{\'param\', \'global\', \'local\'}', array_keys(get_defined_vars()));
20+
assertType('array{GLOBALS: array<mixed>, _SERVER: array<mixed>, _GET: array<mixed>, _POST: array<mixed>, _FILES: array<mixed>, _COOKIE: array<mixed>, _SESSION: array<mixed>, _REQUEST: array<mixed>, _ENV: array<mixed>, param: int, global: mixed, local: \'foo\'}', get_defined_vars());
21+
assertType('array{\'GLOBALS\', \'_SERVER\', \'_GET\', \'_POST\', \'_FILES\', \'_COOKIE\', \'_SESSION\', \'_REQUEST\', \'_ENV\', \'param\', \'global\', \'local\'}', array_keys(get_defined_vars()));
2222
}
2323

2424
function doConditional(int $param) {
2525
$local = "foo";
2626
if(true) {
2727
$conditional = "bar";
28-
assertType('array{param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars());
28+
assertType('array{GLOBALS: array<mixed>, _SERVER: array<mixed>, _GET: array<mixed>, _POST: array<mixed>, _FILES: array<mixed>, _COOKIE: array<mixed>, _SESSION: array<mixed>, _REQUEST: array<mixed>, _ENV: array<mixed>, param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars());
2929
} else {
3030
$other = "baz";
31-
assertType('array{param: int, local: \'foo\', other: \'baz\'}', get_defined_vars());
31+
assertType('array{GLOBALS: array<mixed>, _SERVER: array<mixed>, _GET: array<mixed>, _POST: array<mixed>, _FILES: array<mixed>, _COOKIE: array<mixed>, _SESSION: array<mixed>, _REQUEST: array<mixed>, _ENV: array<mixed>, param: int, local: \'foo\', other: \'baz\'}', get_defined_vars());
3232
}
33-
assertType('array{param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars());
33+
assertType('array{GLOBALS: array<mixed>, _SERVER: array<mixed>, _GET: array<mixed>, _POST: array<mixed>, _FILES: array<mixed>, _COOKIE: array<mixed>, _SESSION: array<mixed>, _REQUEST: array<mixed>, _ENV: array<mixed>, param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars());
3434
}
3535

3636
function doRandom(int $param) {
3737
$local = "foo";
3838
if(rand(0, 1)) {
3939
$random1 = "bar";
40-
assertType('array{param: int, local: \'foo\', random1: \'bar\'}', get_defined_vars());
40+
assertType('array{GLOBALS: array<mixed>, _SERVER: array<mixed>, _GET: array<mixed>, _POST: array<mixed>, _FILES: array<mixed>, _COOKIE: array<mixed>, _SESSION: array<mixed>, _REQUEST: array<mixed>, _ENV: array<mixed>, param: int, local: \'foo\', random1: \'bar\'}', get_defined_vars());
4141
} else {
4242
$random2 = "baz";
43-
assertType('array{param: int, local: \'foo\', random2: \'baz\'}', get_defined_vars());
43+
assertType('array{GLOBALS: array<mixed>, _SERVER: array<mixed>, _GET: array<mixed>, _POST: array<mixed>, _FILES: array<mixed>, _COOKIE: array<mixed>, _SESSION: array<mixed>, _REQUEST: array<mixed>, _ENV: array<mixed>, param: int, local: \'foo\', random2: \'baz\'}', get_defined_vars());
4444
}
45-
assertType('array{param: int, local: \'foo\', random2?: \'baz\', random1?: \'bar\'}', get_defined_vars());
45+
assertType('array{GLOBALS: array<mixed>, _SERVER: array<mixed>, _GET: array<mixed>, _POST: array<mixed>, _FILES: array<mixed>, _COOKIE: array<mixed>, _SESSION: array<mixed>, _REQUEST: array<mixed>, _ENV: array<mixed>, param: int, local: \'foo\', random2?: \'baz\', random1?: \'bar\'}', get_defined_vars());
4646
}

Diff for: tests/PHPStan/Analyser/nsrt/superglobals.php

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Superglobals;
4+
5+
use function PHPStan\Testing\assertNativeType;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Superglobals
9+
{
10+
11+
public function originalTypes(): void
12+
{
13+
assertType('array<mixed>', $GLOBALS);
14+
assertType('array<mixed>', $_SERVER);
15+
assertType('array<mixed>', $_GET);
16+
assertType('array<mixed>', $_POST);
17+
assertType('array<mixed>', $_FILES);
18+
assertType('array<mixed>', $_COOKIE);
19+
assertType('array<mixed>', $_SESSION);
20+
assertType('array<mixed>', $_REQUEST);
21+
assertType('array<mixed>', $_ENV);
22+
}
23+
24+
public function canBeOverwritten(): void
25+
{
26+
$GLOBALS = [];
27+
assertType('array{}', $GLOBALS);
28+
assertNativeType('array{}', $GLOBALS);
29+
}
30+
31+
public function canBePartlyOverwritten(): void
32+
{
33+
$GLOBALS['foo'] = 'foo';
34+
assertType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS);
35+
assertNativeType("non-empty-array&hasOffsetValue('foo', 'foo')", $GLOBALS);
36+
}
37+
38+
public function canBeNarrowed(): void
39+
{
40+
if (isset($GLOBALS['foo'])) {
41+
assertType("non-empty-array&hasOffsetValue('foo', mixed~null)", $GLOBALS);
42+
assertNativeType("non-empty-array<mixed>&hasOffset('foo')", $GLOBALS); // https://github.com/phpstan/phpstan/issues/8395
43+
} else {
44+
assertType('array<mixed>', $GLOBALS);
45+
assertNativeType('array<mixed>', $GLOBALS);
46+
}
47+
assertType('array', $GLOBALS);
48+
assertNativeType('array<mixed>', $GLOBALS);
49+
}
50+
51+
}
52+
53+
function functionScope() {
54+
assertType('array<mixed>', $GLOBALS);
55+
assertNativeType('array<mixed>', $GLOBALS);
56+
}
57+
58+
assertType('array<mixed>', $GLOBALS);
59+
assertNativeType('array<mixed>', $GLOBALS);

Diff for: tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php

+56-1
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,51 @@ public function testRuleInPhpStanNamespace(): void
2121
{
2222
$this->analyse([__DIR__ . '/data/debug-scope.php'], [
2323
[
24-
'Scope is empty',
24+
implode("\n", [
25+
'$GLOBALS (Yes): array<mixed>',
26+
'$_SERVER (Yes): array<mixed>',
27+
'$_GET (Yes): array<mixed>',
28+
'$_POST (Yes): array<mixed>',
29+
'$_FILES (Yes): array<mixed>',
30+
'$_COOKIE (Yes): array<mixed>',
31+
'$_SESSION (Yes): array<mixed>',
32+
'$_REQUEST (Yes): array<mixed>',
33+
'$_ENV (Yes): array<mixed>',
34+
'native $GLOBALS (Yes): array<mixed>',
35+
'native $_SERVER (Yes): array<mixed>',
36+
'native $_GET (Yes): array<mixed>',
37+
'native $_POST (Yes): array<mixed>',
38+
'native $_FILES (Yes): array<mixed>',
39+
'native $_COOKIE (Yes): array<mixed>',
40+
'native $_SESSION (Yes): array<mixed>',
41+
'native $_REQUEST (Yes): array<mixed>',
42+
'native $_ENV (Yes): array<mixed>',
43+
]),
2544
7,
2645
],
2746
[
2847
implode("\n", [
48+
'$GLOBALS (Yes): array<mixed>',
49+
'$_SERVER (Yes): array<mixed>',
50+
'$_GET (Yes): array<mixed>',
51+
'$_POST (Yes): array<mixed>',
52+
'$_FILES (Yes): array<mixed>',
53+
'$_COOKIE (Yes): array<mixed>',
54+
'$_SESSION (Yes): array<mixed>',
55+
'$_REQUEST (Yes): array<mixed>',
56+
'$_ENV (Yes): array<mixed>',
2957
'$a (Yes): int',
3058
'$b (Yes): int',
3159
'$debug (Yes): bool',
60+
'native $GLOBALS (Yes): array<mixed>',
61+
'native $_SERVER (Yes): array<mixed>',
62+
'native $_GET (Yes): array<mixed>',
63+
'native $_POST (Yes): array<mixed>',
64+
'native $_FILES (Yes): array<mixed>',
65+
'native $_COOKIE (Yes): array<mixed>',
66+
'native $_SESSION (Yes): array<mixed>',
67+
'native $_REQUEST (Yes): array<mixed>',
68+
'native $_ENV (Yes): array<mixed>',
3269
'native $a (Yes): int',
3370
'native $b (Yes): int',
3471
'native $debug (Yes): bool',
@@ -37,10 +74,28 @@ public function testRuleInPhpStanNamespace(): void
3774
],
3875
[
3976
implode("\n", [
77+
'$GLOBALS (Yes): array<mixed>',
78+
'$_SERVER (Yes): array<mixed>',
79+
'$_GET (Yes): array<mixed>',
80+
'$_POST (Yes): array<mixed>',
81+
'$_FILES (Yes): array<mixed>',
82+
'$_COOKIE (Yes): array<mixed>',
83+
'$_SESSION (Yes): array<mixed>',
84+
'$_REQUEST (Yes): array<mixed>',
85+
'$_ENV (Yes): array<mixed>',
4086
'$a (Yes): int',
4187
'$b (Yes): int',
4288
'$debug (Yes): bool',
4389
'$c (Maybe): 1',
90+
'native $GLOBALS (Yes): array<mixed>',
91+
'native $_SERVER (Yes): array<mixed>',
92+
'native $_GET (Yes): array<mixed>',
93+
'native $_POST (Yes): array<mixed>',
94+
'native $_FILES (Yes): array<mixed>',
95+
'native $_COOKIE (Yes): array<mixed>',
96+
'native $_SESSION (Yes): array<mixed>',
97+
'native $_REQUEST (Yes): array<mixed>',
98+
'native $_ENV (Yes): array<mixed>',
4499
'native $a (Yes): int',
45100
'native $b (Yes): int',
46101
'native $debug (Yes): bool',

0 commit comments

Comments
 (0)