From cec1be72aa725c23b171973e3e9b063ad3a20b87 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 16 Oct 2020 13:15:38 +0200 Subject: [PATCH] Support ::class on expression (PHP 8.0) https://wiki.php.net/rfc/class_name_literal_on_object --- src/Analyser/MutatingScope.php | 19 ++++-- src/Php/PhpVersion.php | 5 ++ src/Rules/Classes/ClassConstantRule.php | 29 +++++++- .../Analyser/NodeScopeResolverTest.php | 10 +++ .../Analyser/data/class-constant-on-expr.php | 23 +++++++ .../Rules/Classes/ClassConstantRuleTest.php | 68 ++++++++++++++++++- .../Classes/data/class-constant-on-expr.php | 21 ++++++ 7 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/class-constant-on-expr.php create mode 100644 tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 7b61e29fe6..20e700c34f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -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)) { diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index c6ca85ecd5..fd0d927363 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -66,4 +66,9 @@ public function supportsThrowExpression(): bool return $this->versionId >= 80000; } + public function supportsClassConstantOnExpression(): bool + { + return $this->versionId >= 80000; + } + } diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 5d96bde182..f7c43bae5d 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -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; @@ -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 @@ -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 { @@ -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()) { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index a6cbfb599d..2ba9d034db 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -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 @@ -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 diff --git a/tests/PHPStan/Analyser/data/class-constant-on-expr.php b/tests/PHPStan/Analyser/data/class-constant-on-expr.php new file mode 100644 index 0000000000..e4f307f9f3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/class-constant-on-expr.php @@ -0,0 +1,23 @@ +', $std::class); + assertType('*ERROR*', $string::class); + assertType('class-string', $stdOrNull::class); + assertType('*ERROR*', $stringOrNull::class); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index da9b3ff3d1..ccf450e0d8 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; @@ -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', @@ -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.', @@ -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.', @@ -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); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php b/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php new file mode 100644 index 0000000000..7e4d7b8677 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-constant-on-expr.php @@ -0,0 +1,21 @@ += 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; + } + +}