Skip to content

Commit

Permalink
Support ::class on expression (PHP 8.0)
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Oct 16, 2020
1 parent 869f27b commit cec1be7
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 6 deletions.
19 changes: 15 additions & 4 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1586,15 +1586,26 @@ private function resolveType(Expr $node): Type
}
$constantClassType = new ObjectType($resolvedName);
}

if (strtolower($constantName) === 'class') {
return new ConstantStringType($constantClassType->getClassName(), true);
}
} else {
$constantClassType = $this->getType($node->class);
}

if (strtolower($constantName) === 'class' && $constantClassType instanceof TypeWithClassName) {
return new ConstantStringType($constantClassType->getClassName(), true);
}

$referencedClasses = TypeUtils::getDirectClassNames($constantClassType);
if (strtolower($constantName) === 'class') {
if (count($referencedClasses) === 0) {
return new ErrorType();
}
$classTypes = [];
foreach ($referencedClasses as $referencedClass) {
$classTypes[] = new GenericClassStringType(new ObjectType($referencedClass));
}

return TypeCombinator::union(...$classTypes);
}
$types = [];
foreach ($referencedClasses as $referencedClass) {
if (!$this->reflectionProvider->hasClass($referencedClass)) {
Expand Down
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,9 @@ public function supportsThrowExpression(): bool
return $this->versionId >= 80000;
}

public function supportsClassConstantOnExpression(): bool
{
return $this->versionId >= 80000;
}

}
29 changes: 28 additions & 1 deletion src/Rules/Classes/ClassConstantRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PHPStan\Analyser\Scope;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\ClassNameNodePair;
Expand All @@ -30,15 +31,19 @@ class ClassConstantRule implements \PHPStan\Rules\Rule

private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck;

private PhpVersion $phpVersion;

public function __construct(
ReflectionProvider $reflectionProvider,
RuleLevelHelper $ruleLevelHelper,
ClassCaseSensitivityCheck $classCaseSensitivityCheck
ClassCaseSensitivityCheck $classCaseSensitivityCheck,
PhpVersion $phpVersion
)
{
$this->reflectionProvider = $reflectionProvider;
$this->ruleLevelHelper = $ruleLevelHelper;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
$this->phpVersion = $phpVersion;
}

public function getNodeType(): string
Expand Down Expand Up @@ -107,6 +112,10 @@ public function processNode(Node $node, Scope $scope): array
$className = $this->reflectionProvider->getClass($className)->getName();
}

if (strtolower($constantName) === 'class') {
return $messages;
}

if ($scope->isInClass() && $scope->getClassReflection()->getName() === $className) {
$classType = new ThisType($scope->getClassReflection());
} else {
Expand All @@ -125,6 +134,24 @@ static function (Type $type) use ($constantName): bool {
if ($classType instanceof ErrorType) {
return $classTypeResult->getUnknownClassErrors();
}

if (strtolower($constantName) === 'class') {
if (!$this->phpVersion->supportsClassConstantOnExpression()) {
return [
RuleErrorBuilder::message('Accessing ::class constant on an expression is supported only on PHP 8.0 and later.')
->nonIgnorable()
->build(),
];
}

if ((new StringType())->isSuperTypeOf($classType)->yes()) {
return [
RuleErrorBuilder::message('Accessing ::class constant on a dynamic string is not supported in PHP.')
->nonIgnorable()
->build(),
];
}
}
}

if ((new StringType())->isSuperTypeOf($classType)->yes()) {
Expand Down
10 changes: 10 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10176,6 +10176,15 @@ public function dataNotEmptyArray(): array
return $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php');
}

public function dataClassConstantOnExpression(): array
{
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
return [];
}

return $this->gatherAssertTypes(__DIR__ . '/data/class-constant-on-expr.php');
}

/**
* @dataProvider dataBug2574
* @dataProvider dataBug2577
Expand Down Expand Up @@ -10255,6 +10264,7 @@ public function dataNotEmptyArray(): array
* @dataProvider dataPow
* @dataProvider dataThrowExpression
* @dataProvider dataNotEmptyArray
* @dataProvider dataClassConstantOnExpression
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
23 changes: 23 additions & 0 deletions tests/PHPStan/Analyser/data/class-constant-on-expr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace ClassConstantOnExprAssertType;

use function PHPStan\Analyser\assertType;

class Foo
{

public function doFoo(
\stdClass $std,
string $string,
?\stdClass $stdOrNull,
?string $stringOrNull
): void
{
assertType('class-string<stdClass>', $std::class);
assertType('*ERROR*', $string::class);
assertType('class-string<stdClass>', $stdOrNull::class);
assertType('*ERROR*', $stringOrNull::class);
}

}
68 changes: 67 additions & 1 deletion tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Classes;

use PHPStan\Php\PhpVersion;
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
Expand All @@ -12,14 +13,18 @@
class ClassConstantRuleTest extends \PHPStan\Testing\RuleTestCase
{

/** @var int */
private $phpVersion;

protected function getRule(): Rule
{
$broker = $this->createReflectionProvider();
return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker));
return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersion));
}

public function testClassConstant(): void
{
$this->phpVersion = PHP_VERSION_ID;
$this->analyse(
[
__DIR__ . '/data/class-constant.php',
Expand Down Expand Up @@ -85,6 +90,8 @@ public function testClassConstantVisibility(): void
if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) {
$this->markTestSkipped('Test does not run on PHP 7.4 because of referencing parent:: without parent class.');
}

$this->phpVersion = PHP_VERSION_ID;
$this->analyse([__DIR__ . '/data/class-constant-visibility.php'], [
[
'Access to private constant PRIVATE_BAR of class ClassConstantVisibility\Bar.',
Expand Down Expand Up @@ -149,6 +156,7 @@ public function testClassConstantVisibility(): void

public function testClassExists(): void
{
$this->phpVersion = PHP_VERSION_ID;
$this->analyse([__DIR__ . '/data/class-exists.php'], [
[
'Class UnknownClass\Bar not found.',
Expand All @@ -168,4 +176,62 @@ public function testClassExists(): void
]);
}

public function dataClassConstantOnExpression(): array
{
return [
[
70400,
[
[
'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.',
15,
],
[
'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.',
16,
],
[
'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.',
17,
],
[
'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.',
18,
],
],
],
[
80000,
[
[
'Accessing ::class constant on a dynamic string is not supported in PHP.',
16,
],
[
'Cannot access constant class on stdClass|null.',
17,
],
[
'Cannot access constant class on string|null.',
18,
],
],
],
];
}

/**
* @dataProvider dataClassConstantOnExpression
* @param int $phpVersion
* @param mixed[] $errors
*/
public function testClassConstantOnExpression(int $phpVersion, array $errors): void
{
if (!self::$useStaticReflectionProvider) {
$this->markTestSkipped('Test requires static reflection');
}
$this->phpVersion = $phpVersion;
$this->analyse([__DIR__ . '/data/class-constant-on-expr.php'], $errors);
}

}
21 changes: 21 additions & 0 deletions tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php // lint >= 8.0

namespace ClassConstantOnExpr;

class Foo
{

public function doFoo(
\stdClass $std,
string $string,
?\stdClass $stdOrNull,
?string $stringOrNull
): void
{
echo $std::class;
echo $string::class;
echo $stdOrNull::class;
echo $stringOrNull::class;
}

}

0 comments on commit cec1be7

Please sign in to comment.