Skip to content

Commit dab41aa

Browse files
committed
Improve expression resolving of superglobals
1 parent 76740fd commit dab41aa

File tree

6 files changed

+187
-36
lines changed

6 files changed

+187
-36
lines changed

src/Analyser/MutatingScope.php

Lines changed: 40 additions & 23 deletions
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
{
@@ -2886,18 +2873,16 @@ public function isInFunctionExists(string $functionName): bool
28862873
public function enterClass(ClassReflection $classReflection): self
28872874
{
28882875
$thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection));
2889-
$constantTypes = $this->getConstantTypes();
2890-
$constantTypes['$this'] = $thisHolder;
2891-
$nativeConstantTypes = $this->getNativeConstantTypes();
2892-
$nativeConstantTypes['$this'] = $thisHolder;
2876+
$expressionTypes = array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), ['$this' => $thisHolder]);
2877+
$nativeExpressionTypes = array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), ['$this' => $thisHolder]);
28932878

28942879
return $this->scopeFactory->create(
28952880
$this->context->enterClass($classReflection),
28962881
$this->isDeclareStrictTypes(),
28972882
null,
28982883
$this->getNamespace(),
2899-
$constantTypes,
2900-
$nativeConstantTypes,
2884+
$expressionTypes,
2885+
$nativeExpressionTypes,
29012886
[],
29022887
[],
29032888
null,
@@ -3264,8 +3249,8 @@ private function enterFunctionLike(
32643249
$this->isDeclareStrictTypes(),
32653250
$functionReflection,
32663251
$this->getNamespace(),
3267-
array_merge($this->getConstantTypes(), $expressionTypes),
3268-
array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes),
3252+
array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), $expressionTypes),
3253+
array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), $nativeExpressionTypes),
32693254
$conditionalTypes,
32703255
);
32713256
}
@@ -3278,6 +3263,8 @@ public function enterNamespace(string $namespaceName): self
32783263
$this->isDeclareStrictTypes(),
32793264
null,
32803265
$namespaceName,
3266+
$this->getSuperglobalTypes(),
3267+
$this->getNativeSuperglobalTypes(),
32813268
);
32823269
}
32833270

@@ -3554,8 +3541,8 @@ private function enterAnonymousFunctionWithoutReflection(
35543541
$this->isDeclareStrictTypes(),
35553542
$this->getFunction(),
35563543
$this->getNamespace(),
3557-
array_merge($this->getConstantTypes(), $expressionTypes),
3558-
array_merge($this->getNativeConstantTypes(), $nativeTypes),
3544+
array_merge($this->getSuperglobalTypes(), $this->getConstantTypes(), $expressionTypes),
3545+
array_merge($this->getNativeSuperglobalTypes(), $this->getNativeConstantTypes(), $nativeTypes),
35593546
[],
35603547
$this->inClosureBindScopeClasses,
35613548
new TrivialParametersAcceptor(),
@@ -5877,6 +5864,36 @@ public function getConstantReflection(Type $typeWithConstant, string $constantNa
58775864
return $typeWithConstant->getConstant($constantName);
58785865
}
58795866

5867+
/** @return array<string, ExpressionTypeHolder> */
5868+
private function getSuperglobalTypes(): array
5869+
{
5870+
$superglobalTypes = [];
5871+
$exprStrings = ['$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV'];
5872+
foreach ($this->expressionTypes as $exprString => $typeHolder) {
5873+
if (!in_array($exprString, $exprStrings, true)) {
5874+
continue;
5875+
}
5876+
5877+
$superglobalTypes[$exprString] = $typeHolder;
5878+
}
5879+
return $superglobalTypes;
5880+
}
5881+
5882+
/** @return array<string, ExpressionTypeHolder> */
5883+
private function getNativeSuperglobalTypes(): array
5884+
{
5885+
$superglobalTypes = [];
5886+
$exprStrings = ['$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV'];
5887+
foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) {
5888+
if (!in_array($exprString, $exprStrings, true)) {
5889+
continue;
5890+
}
5891+
5892+
$superglobalTypes[$exprString] = $typeHolder;
5893+
}
5894+
return $superglobalTypes;
5895+
}
5896+
58805897
/**
58815898
* @return array<string, ExpressionTypeHolder>
58825899
*/

src/Analyser/ScopeFactory.php

Lines changed: 21 additions & 1 deletion
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
}

tests/PHPStan/Analyser/ScopeTest.php

Lines changed: 1 addition & 1 deletion
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

tests/PHPStan/Analyser/nsrt/get-defined-vars.php

Lines changed: 10 additions & 10 deletions
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
}
Lines changed: 59 additions & 0 deletions
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);

tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php

Lines changed: 56 additions & 1 deletion
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)