Skip to content

Commit e05ff36

Browse files
committed
Improve expression resolving of superglobals
1 parent e140197 commit e05ff36

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
@@ -513,10 +513,6 @@ public function afterOpenSslCall(string $openSslFunctionName): self
513513
/** @api */
514514
public function hasVariableType(string $variableName): TrinaryLogic
515515
{
516-
if ($this->isGlobalVariable($variableName)) {
517-
return TrinaryLogic::createYes();
518-
}
519-
520516
$varExprString = '$' . $variableName;
521517
if (!isset($this->expressionTypes[$varExprString])) {
522518
if ($this->canAnyVariableExist()) {
@@ -548,10 +544,6 @@ public function getVariableType(string $variableName): Type
548544
}
549545
}
550546

551-
if ($this->isGlobalVariable($variableName)) {
552-
return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true));
553-
}
554-
555547
if ($this->hasVariableType($variableName)->no()) {
556548
throw new UndefinedVariableException($this, $variableName);
557549
}
@@ -606,11 +598,6 @@ public function getMaybeDefinedVariables(): array
606598
return $variables;
607599
}
608600

609-
private function isGlobalVariable(string $variableName): bool
610-
{
611-
return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true);
612-
}
613-
614601
/** @api */
615602
public function hasConstant(Name $name): bool
616603
{
@@ -2893,18 +2880,16 @@ public function isInFunctionExists(string $functionName): bool
28932880
public function enterClass(ClassReflection $classReflection): self
28942881
{
28952882
$thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection));
2896-
$constantTypes = $this->getConstantTypes();
2897-
$constantTypes['$this'] = $thisHolder;
2898-
$nativeConstantTypes = $this->getNativeConstantTypes();
2899-
$nativeConstantTypes['$this'] = $thisHolder;
2883+
$expressionTypes = array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), ['$this' => $thisHolder]);
2884+
$nativeExpressionTypes = array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), ['$this' => $thisHolder]);
29002885

29012886
return $this->scopeFactory->create(
29022887
$this->context->enterClass($classReflection),
29032888
$this->isDeclareStrictTypes(),
29042889
null,
29052890
$this->getNamespace(),
2906-
$constantTypes,
2907-
$nativeConstantTypes,
2891+
$expressionTypes,
2892+
$nativeExpressionTypes,
29082893
[],
29092894
[],
29102895
null,
@@ -3298,8 +3283,8 @@ private function enterFunctionLike(
32983283
$this->isDeclareStrictTypes(),
32993284
$functionReflection,
33003285
$this->getNamespace(),
3301-
array_merge($this->getConstantTypes(), $expressionTypes),
3302-
array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes),
3286+
array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), $expressionTypes),
3287+
array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), $nativeExpressionTypes),
33033288
$conditionalTypes,
33043289
);
33053290
}
@@ -3312,6 +3297,8 @@ public function enterNamespace(string $namespaceName): self
33123297
$this->isDeclareStrictTypes(),
33133298
null,
33143299
$namespaceName,
3300+
$this->getSuperglobalTypes(),
3301+
$this->getNativeSuperglobalTypes(),
33153302
);
33163303
}
33173304

@@ -3588,8 +3575,8 @@ private function enterAnonymousFunctionWithoutReflection(
35883575
$this->isDeclareStrictTypes(),
35893576
$this->getFunction(),
35903577
$this->getNamespace(),
3591-
array_merge($this->getConstantTypes(), $expressionTypes),
3592-
array_merge($this->getNativeConstantTypes(), $nativeTypes),
3578+
array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), $expressionTypes),
3579+
array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), $nativeTypes),
35933580
[],
35943581
$this->inClosureBindScopeClasses,
35953582
new TrivialParametersAcceptor(),
@@ -5987,6 +5974,36 @@ public function getConstantReflection(Type $typeWithConstant, string $constantNa
59875974
return $typeWithConstant->getConstant($constantName);
59885975
}
59895976

5977+
/** @return array<string, ExpressionTypeHolder> */
5978+
private function getSuperglobalTypes(): array
5979+
{
5980+
$superglobalTypes = [];
5981+
$exprStrings = ['$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV'];
5982+
foreach ($this->expressionTypes as $exprString => $typeHolder) {
5983+
if (!in_array($exprString, $exprStrings, true)) {
5984+
continue;
5985+
}
5986+
5987+
$superglobalTypes[$exprString] = $typeHolder;
5988+
}
5989+
return $superglobalTypes;
5990+
}
5991+
5992+
/** @return array<string, ExpressionTypeHolder> */
5993+
private function getNativeSuperglobalTypes(): array
5994+
{
5995+
$superglobalTypes = [];
5996+
$exprStrings = ['$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV'];
5997+
foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) {
5998+
if (!in_array($exprString, $exprStrings, true)) {
5999+
continue;
6000+
}
6001+
6002+
$superglobalTypes[$exprString] = $typeHolder;
6003+
}
6004+
return $superglobalTypes;
6005+
}
6006+
59906007
/**
59916008
* @return array<string, ExpressionTypeHolder>
59926009
*/

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)