Skip to content

Commit cec1be7

Browse files
committed
Support ::class on expression (PHP 8.0)
https://wiki.php.net/rfc/class_name_literal_on_object
1 parent 869f27b commit cec1be7

File tree

7 files changed

+169
-6
lines changed

7 files changed

+169
-6
lines changed

src/Analyser/MutatingScope.php

+15-4
Original file line numberDiff line numberDiff line change
@@ -1586,15 +1586,26 @@ private function resolveType(Expr $node): Type
15861586
}
15871587
$constantClassType = new ObjectType($resolvedName);
15881588
}
1589+
1590+
if (strtolower($constantName) === 'class') {
1591+
return new ConstantStringType($constantClassType->getClassName(), true);
1592+
}
15891593
} else {
15901594
$constantClassType = $this->getType($node->class);
15911595
}
15921596

1593-
if (strtolower($constantName) === 'class' && $constantClassType instanceof TypeWithClassName) {
1594-
return new ConstantStringType($constantClassType->getClassName(), true);
1595-
}
1596-
15971597
$referencedClasses = TypeUtils::getDirectClassNames($constantClassType);
1598+
if (strtolower($constantName) === 'class') {
1599+
if (count($referencedClasses) === 0) {
1600+
return new ErrorType();
1601+
}
1602+
$classTypes = [];
1603+
foreach ($referencedClasses as $referencedClass) {
1604+
$classTypes[] = new GenericClassStringType(new ObjectType($referencedClass));
1605+
}
1606+
1607+
return TypeCombinator::union(...$classTypes);
1608+
}
15981609
$types = [];
15991610
foreach ($referencedClasses as $referencedClass) {
16001611
if (!$this->reflectionProvider->hasClass($referencedClass)) {

src/Php/PhpVersion.php

+5
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,9 @@ public function supportsThrowExpression(): bool
6666
return $this->versionId >= 80000;
6767
}
6868

69+
public function supportsClassConstantOnExpression(): bool
70+
{
71+
return $this->versionId >= 80000;
72+
}
73+
6974
}

src/Rules/Classes/ClassConstantRule.php

+28-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node;
66
use PhpParser\Node\Expr\ClassConstFetch;
77
use PHPStan\Analyser\Scope;
8+
use PHPStan\Php\PhpVersion;
89
use PHPStan\Reflection\ReflectionProvider;
910
use PHPStan\Rules\ClassCaseSensitivityCheck;
1011
use PHPStan\Rules\ClassNameNodePair;
@@ -30,15 +31,19 @@ class ClassConstantRule implements \PHPStan\Rules\Rule
3031

3132
private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck;
3233

34+
private PhpVersion $phpVersion;
35+
3336
public function __construct(
3437
ReflectionProvider $reflectionProvider,
3538
RuleLevelHelper $ruleLevelHelper,
36-
ClassCaseSensitivityCheck $classCaseSensitivityCheck
39+
ClassCaseSensitivityCheck $classCaseSensitivityCheck,
40+
PhpVersion $phpVersion
3741
)
3842
{
3943
$this->reflectionProvider = $reflectionProvider;
4044
$this->ruleLevelHelper = $ruleLevelHelper;
4145
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
46+
$this->phpVersion = $phpVersion;
4247
}
4348

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

115+
if (strtolower($constantName) === 'class') {
116+
return $messages;
117+
}
118+
110119
if ($scope->isInClass() && $scope->getClassReflection()->getName() === $className) {
111120
$classType = new ThisType($scope->getClassReflection());
112121
} else {
@@ -125,6 +134,24 @@ static function (Type $type) use ($constantName): bool {
125134
if ($classType instanceof ErrorType) {
126135
return $classTypeResult->getUnknownClassErrors();
127136
}
137+
138+
if (strtolower($constantName) === 'class') {
139+
if (!$this->phpVersion->supportsClassConstantOnExpression()) {
140+
return [
141+
RuleErrorBuilder::message('Accessing ::class constant on an expression is supported only on PHP 8.0 and later.')
142+
->nonIgnorable()
143+
->build(),
144+
];
145+
}
146+
147+
if ((new StringType())->isSuperTypeOf($classType)->yes()) {
148+
return [
149+
RuleErrorBuilder::message('Accessing ::class constant on a dynamic string is not supported in PHP.')
150+
->nonIgnorable()
151+
->build(),
152+
];
153+
}
154+
}
128155
}
129156

130157
if ((new StringType())->isSuperTypeOf($classType)->yes()) {

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -10176,6 +10176,15 @@ public function dataNotEmptyArray(): array
1017610176
return $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php');
1017710177
}
1017810178

10179+
public function dataClassConstantOnExpression(): array
10180+
{
10181+
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
10182+
return [];
10183+
}
10184+
10185+
return $this->gatherAssertTypes(__DIR__ . '/data/class-constant-on-expr.php');
10186+
}
10187+
1017910188
/**
1018010189
* @dataProvider dataBug2574
1018110190
* @dataProvider dataBug2577
@@ -10255,6 +10264,7 @@ public function dataNotEmptyArray(): array
1025510264
* @dataProvider dataPow
1025610265
* @dataProvider dataThrowExpression
1025710266
* @dataProvider dataNotEmptyArray
10267+
* @dataProvider dataClassConstantOnExpression
1025810268
* @param string $assertType
1025910269
* @param string $file
1026010270
* @param mixed ...$args
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace ClassConstantOnExprAssertType;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
class Foo
8+
{
9+
10+
public function doFoo(
11+
\stdClass $std,
12+
string $string,
13+
?\stdClass $stdOrNull,
14+
?string $stringOrNull
15+
): void
16+
{
17+
assertType('class-string<stdClass>', $std::class);
18+
assertType('*ERROR*', $string::class);
19+
assertType('class-string<stdClass>', $stdOrNull::class);
20+
assertType('*ERROR*', $stringOrNull::class);
21+
}
22+
23+
}

tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php

+67-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Rules\Classes;
44

5+
use PHPStan\Php\PhpVersion;
56
use PHPStan\Rules\ClassCaseSensitivityCheck;
67
use PHPStan\Rules\Rule;
78
use PHPStan\Rules\RuleLevelHelper;
@@ -12,14 +13,18 @@
1213
class ClassConstantRuleTest extends \PHPStan\Testing\RuleTestCase
1314
{
1415

16+
/** @var int */
17+
private $phpVersion;
18+
1519
protected function getRule(): Rule
1620
{
1721
$broker = $this->createReflectionProvider();
18-
return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker));
22+
return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersion));
1923
}
2024

2125
public function testClassConstant(): void
2226
{
27+
$this->phpVersion = PHP_VERSION_ID;
2328
$this->analyse(
2429
[
2530
__DIR__ . '/data/class-constant.php',
@@ -85,6 +90,8 @@ public function testClassConstantVisibility(): void
8590
if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70400) {
8691
$this->markTestSkipped('Test does not run on PHP 7.4 because of referencing parent:: without parent class.');
8792
}
93+
94+
$this->phpVersion = PHP_VERSION_ID;
8895
$this->analyse([__DIR__ . '/data/class-constant-visibility.php'], [
8996
[
9097
'Access to private constant PRIVATE_BAR of class ClassConstantVisibility\Bar.',
@@ -149,6 +156,7 @@ public function testClassConstantVisibility(): void
149156

150157
public function testClassExists(): void
151158
{
159+
$this->phpVersion = PHP_VERSION_ID;
152160
$this->analyse([__DIR__ . '/data/class-exists.php'], [
153161
[
154162
'Class UnknownClass\Bar not found.',
@@ -168,4 +176,62 @@ public function testClassExists(): void
168176
]);
169177
}
170178

179+
public function dataClassConstantOnExpression(): array
180+
{
181+
return [
182+
[
183+
70400,
184+
[
185+
[
186+
'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.',
187+
15,
188+
],
189+
[
190+
'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.',
191+
16,
192+
],
193+
[
194+
'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.',
195+
17,
196+
],
197+
[
198+
'Accessing ::class constant on an expression is supported only on PHP 8.0 and later.',
199+
18,
200+
],
201+
],
202+
],
203+
[
204+
80000,
205+
[
206+
[
207+
'Accessing ::class constant on a dynamic string is not supported in PHP.',
208+
16,
209+
],
210+
[
211+
'Cannot access constant class on stdClass|null.',
212+
17,
213+
],
214+
[
215+
'Cannot access constant class on string|null.',
216+
18,
217+
],
218+
],
219+
],
220+
];
221+
}
222+
223+
/**
224+
* @dataProvider dataClassConstantOnExpression
225+
* @param int $phpVersion
226+
* @param mixed[] $errors
227+
*/
228+
public function testClassConstantOnExpression(int $phpVersion, array $errors): void
229+
{
230+
if (!self::$useStaticReflectionProvider) {
231+
$this->markTestSkipped('Test requires static reflection');
232+
}
233+
$this->phpVersion = $phpVersion;
234+
$this->analyse([__DIR__ . '/data/class-constant-on-expr.php'], $errors);
235+
}
236+
171237
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php // lint >= 8.0
2+
3+
namespace ClassConstantOnExpr;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(
9+
\stdClass $std,
10+
string $string,
11+
?\stdClass $stdOrNull,
12+
?string $stringOrNull
13+
): void
14+
{
15+
echo $std::class;
16+
echo $string::class;
17+
echo $stdOrNull::class;
18+
echo $stringOrNull::class;
19+
}
20+
21+
}

0 commit comments

Comments
 (0)