Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
use PHPStan\Node\Expr\GlobalVariableExpr;
use PHPStan\Node\Expr\NativeTypeExpr;
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
Expand Down Expand Up @@ -609,10 +610,21 @@ public function afterOpenSslCall(string $openSslFunctionName): self
);
}

/** @api */
public function isGlobalVariable(string $variableName): bool
{
if ($this->isSuperglobalVariable($variableName)) {
return true;
}

$globalVariableExprString = $this->getNodeKey(new GlobalVariableExpr(new Variable($variableName)));
return array_key_exists($globalVariableExprString, $this->expressionTypes);
}

/** @api */
public function hasVariableType(string $variableName): TrinaryLogic
{
if ($this->isGlobalVariable($variableName)) {
if ($this->isSuperglobalVariable($variableName)) {
return TrinaryLogic::createYes();
}

Expand Down Expand Up @@ -653,7 +665,7 @@ public function getVariableType(string $variableName): Type

$varExprString = '$' . $variableName;
if (!array_key_exists($varExprString, $this->expressionTypes)) {
if ($this->isGlobalVariable($variableName)) {
if ($this->isSuperglobalVariable($variableName)) {
return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true));
}
return new MixedType();
Expand Down Expand Up @@ -704,7 +716,7 @@ public function getMaybeDefinedVariables(): array
return $variables;
}

private function isGlobalVariable(string $variableName): bool
private function isSuperglobalVariable(string $variableName): bool
{
return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true);
}
Expand Down Expand Up @@ -807,6 +819,10 @@ public function getType(Expr $node): Type
return $propertyReflection->getReadableType();
}

if ($node instanceof GlobalVariableExpr) {
return $this->getType($node->getVar());
}

$key = $this->getNodeKey($node);

if (!array_key_exists($key, $this->resolvedTypes)) {
Expand Down Expand Up @@ -4309,6 +4325,17 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType,
}
}

if ($expr instanceof GlobalVariableExpr) {
foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) {
$typeFromExtension = $extension->getType($expr, $this);
if ($typeFromExtension !== null) {
$type = $typeFromExtension;
break;
}
}
$scope = $scope->specifyExpressionType($expr->getVar(), $type, $nativeType, $certainty);
}

if ($certainty->no()) {
throw new ShouldNotHappenException();
}
Expand Down Expand Up @@ -4979,7 +5006,7 @@ private function createConditionalExpressions(
private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array
{
$intersectedVariableTypeHolders = [];
$globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name);
$globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isSuperglobalVariable($node->name);
$nodeFinder = new NodeFinder();
foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) {
if (isset($theirVariableTypeHolders[$exprString])) {
Expand Down
3 changes: 2 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
use PHPStan\Node\Expr\GlobalVariableExpr;
use PHPStan\Node\Expr\NativeTypeExpr;
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
use PHPStan\Node\Expr\PropertyInitializationExpr;
Expand Down Expand Up @@ -1976,7 +1977,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
continue;
}

$scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes());
$scope = $scope->assignExpression(new GlobalVariableExpr($var), new MixedType(), new MixedType());
$vars[] = $var->name;
}
$scope = $this->processVarAnnotation($scope, $vars, $stmt);
Expand Down
2 changes: 2 additions & 0 deletions src/Analyser/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public function getFunctionName(): ?string;

public function getParentScope(): ?self;

public function isGlobalVariable(string $variableName): bool;

public function hasVariableType(string $variableName): TrinaryLogic;

public function getVariableType(string $variableName): Type;
Expand Down
37 changes: 37 additions & 0 deletions src/Node/Expr/GlobalVariableExpr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace PHPStan\Node\Expr;

use Override;
use PhpParser\Node\Expr;
use PHPStan\Node\VirtualNode;

final class GlobalVariableExpr extends Expr implements VirtualNode
{

public function __construct(private Expr $var)
{
parent::__construct([]);
}

public function getVar(): Expr
{
return $this->var;
}

#[Override]
public function getType(): string
{
return 'PHPStan_Node_GlobalVariableExpr';
}

/**
* @return string[]
*/
#[Override]
public function getSubNodeNames(): array
{
return [];
}

}
6 changes: 6 additions & 0 deletions src/Node/Printer/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
use PHPStan\Node\Expr\GlobalVariableExpr;
use PHPStan\Node\Expr\NativeTypeExpr;
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
Expand Down Expand Up @@ -98,4 +99,9 @@ protected function pPHPStan_Node_IssetExpr(IssetExpr $expr): string // phpcs:ign
return sprintf('__phpstanIssetExpr(%s)', $this->p($expr->getExpr()));
}

protected function pPHPStan_Node_GlobalVariableExpr(GlobalVariableExpr $expr): string // phpcs:ignore
{
return sprintf('__phpstanGlobalVariable(%s)', $this->p($expr->getVar()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class ExpressionTypeResolverExtensionTest extends TypeInferenceTestCase

public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension.php');
yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension-method-call-returns-bool.php');
yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension-global-statement.php');
}

/**
Expand Down
52 changes: 52 additions & 0 deletions tests/PHPStan/Analyser/GlobalVariableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Node;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Testing\TypeInferenceTestCase;

class GlobalVariableTest extends TypeInferenceTestCase
{

public function testGlobalVariableInScript(): void
{
self::processFile(__DIR__ . '/data/global-in-script.php', function (Node $node, Scope $scope): void {
if (!($node instanceof Return_)) {
return;
}

$this->assertTrue($scope->isGlobalVariable('FOO'));
$this->assertFalse($scope->isGlobalVariable('whatever'));
});
}

public function testGlobalVariableInFunction(): void
{
self::processFile(__DIR__ . '/data/global-in-function.php', function (Node $node, Scope $scope): void {
if (!($node instanceof Return_)) {
return;
}

$this->assertFalse($scope->isGlobalVariable('BAR'));
$this->assertTrue($scope->isGlobalVariable('CONFIG'));
$this->assertFalse($scope->isGlobalVariable('localVar'));
});
}

public function testGlobalVariableInClassMethod(): void
{
self::processFile(__DIR__ . '/data/global-in-class-method.php', function (Node $node, Scope $scope): void {
if (!($node instanceof Return_)) {
return;
}

$this->assertFalse($scope->isGlobalVariable('count'));
$this->assertTrue($scope->isGlobalVariable('GLB_A'));
$this->assertTrue($scope->isGlobalVariable('GLB_B'));
$this->assertFalse($scope->isGlobalVariable('key'));
$this->assertFalse($scope->isGlobalVariable('step'));
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace ExpressionTypeResolverExtension;

use PHPStan\Analyser\Scope;
use PHPStan\Node\Expr\GlobalVariableExpr;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\ExpressionTypeResolverExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PhpParser\Node\Expr;

class GlobalExpressionTypeResolverExtension implements ExpressionTypeResolverExtension {

public function getType(Expr $expr, Scope $scope): ?Type
{

if (!$expr instanceof GlobalVariableExpr) {
return null;
}

$variableName = $expr->getVar()->name;

if ($variableName === 'MY_GLOBAL_BOOL') {
return new BooleanType();
}

if ($variableName === 'MY_GLOBAL_INT') {
return new IntegerType();
}

if ($variableName === 'MY_GLOBAL_STR') {
return new StringType();
}

if ($variableName === 'MY_GLOBAL_ARRAY') {
return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true));
}

return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

// test file for ExpressionTypeResolverExtensionTest

use function PHPStan\Testing\assertType;

global $MY_GLOBAL_BOOL, $ANOTHER_GLOBAL;

assertType('bool', $MY_GLOBAL_BOOL);
assertType('mixed', $MY_GLOBAL_INT); // not declared in the global statement = no type assigned
assertType('mixed', $ANOTHER_GLOBAL);

$testFct = function ($MY_GLOBAL_BOOL) {
/** @var float $MY_GLOBAL_STR */
global $MY_GLOBAL_INT, $MY_GLOBAL_STR, $MY_GLOBAL_ARRAY;

$MY_GLOBAL_ARRAY = new ArrayIterator([1, 2, 3]);

assertType('mixed', $MY_GLOBAL_BOOL); // not declared in the global statement = no type assigned
assertType('float', $MY_GLOBAL_STR); // overriden by PHPDoc
assertType('ArrayIterator<int, int>', $MY_GLOBAL_ARRAY); // overriden by value assign expression
assertType('int', $MY_GLOBAL_INT);
};

$testClass = new class () {
public function foo($MY_GLOBAL_INT) {
global $MY_GLOBAL_STR;

assertType('string', $MY_GLOBAL_STR);
assertType('mixed', $MY_GLOBAL_INT);
}
};
16 changes: 16 additions & 0 deletions tests/PHPStan/Analyser/data/global-in-class-method.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

class ClassForGlobalTest
{

public function doSomething(int $count = 3): bool
{
global $GLB_A, $GLB_B;

foreach ([1, 2, 3] as $key => $step) {
break;
}

return false;
}
}
12 changes: 12 additions & 0 deletions tests/PHPStan/Analyser/data/global-in-function.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

global $BAR;

function globalTest(string $BAR): void
{
global $CONFIG;

$localVar = true;

return;
}
8 changes: 8 additions & 0 deletions tests/PHPStan/Analyser/data/global-in-script.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

global $FOO;

$FOO = "bar";
$whatever = 15;

return;
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# config for ExpressionTypeResolverExtensionTest
services:
-
class: ExpressionTypeResolverExtension\GlobalExpressionTypeResolverExtension
tags:
- phpstan.broker.expressionTypeResolverExtension
-
class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension
tags:
Expand Down
Loading