From 02b2e3d0885528724a1af91a1758b8f36e2167f4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 20 Nov 2023 15:24:58 +0100 Subject: [PATCH] ReadOnlyClassRule --- conf/config.level0.neon | 1 + src/Php/PhpVersion.php | 10 ++++ src/Rules/Classes/ReadOnlyClassRule.php | 58 +++++++++++++++++++ .../Rules/Classes/ReadOnlyClassRuleTest.php | 42 ++++++++++++++ .../Rules/Classes/data/readonly-class.php | 20 +++++++ 5 files changed, 131 insertions(+) create mode 100644 src/Rules/Classes/ReadOnlyClassRule.php create mode 100644 tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php create mode 100644 tests/PHPStan/Rules/Classes/data/readonly-class.php diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 4f896f2995..c57c70d473 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -56,6 +56,7 @@ rules: - PHPStan\Rules\Classes\LocalTypeTraitAliasesRule - PHPStan\Rules\Classes\NewStaticRule - PHPStan\Rules\Classes\NonClassAttributeClassRule + - PHPStan\Rules\Classes\ReadOnlyClassRule - PHPStan\Rules\Classes\TraitAttributeClassRule - PHPStan\Rules\Constants\DynamicClassConstantFetchRule - PHPStan\Rules\Constants\FinalConstantRule diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index d52fea9a16..062b76d76e 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -262,4 +262,14 @@ public function supportsDynamicClassConstantFetch(): bool return $this->versionId >= 80300; } + public function supportsReadOnlyClasses(): bool + { + return $this->versionId >= 80200; + } + + public function supportsReadOnlyAnonymousClasses(): bool + { + return $this->versionId >= 80300; + } + } diff --git a/src/Rules/Classes/ReadOnlyClassRule.php b/src/Rules/Classes/ReadOnlyClassRule.php new file mode 100644 index 0000000000..21755c623c --- /dev/null +++ b/src/Rules/Classes/ReadOnlyClassRule.php @@ -0,0 +1,58 @@ + + */ +class ReadOnlyClassRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$classReflection->isReadOnly()) { + return []; + } + if ($classReflection->isAnonymous()) { + if ($this->phpVersion->supportsReadOnlyAnonymousClasses()) { + return []; + } + + return [ + RuleErrorBuilder::message('Anonymous readonly classes are supported only on PHP 8.3 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + + if ($this->phpVersion->supportsReadOnlyClasses()) { + return []; + } + + return [ + RuleErrorBuilder::message('Readonly classes are supported only on PHP 8.2 and later.') + ->identifier('classConstant.nativeTypeNotSupported') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php b/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php new file mode 100644 index 0000000000..79a43ec853 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ReadOnlyClassRuleTest.php @@ -0,0 +1,42 @@ + + */ +class ReadOnlyClassRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new ReadOnlyClassRule(self::getContainer()->getByType(PhpVersion::class)); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80200) { + $errors = [ + [ + 'Readonly classes are supported only on PHP 8.2 and later.', + 5, + ], + ]; + } elseif (PHP_VERSION_ID < 80300) { + $errors = [ + [ + 'Anonymous readonly classes are supported only on PHP 8.3 and later.', + 15, + ], + ]; + } + $this->analyse([__DIR__ . '/data/readonly-class.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/readonly-class.php b/tests/PHPStan/Rules/Classes/data/readonly-class.php new file mode 100644 index 0000000000..5f26eacd5d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/readonly-class.php @@ -0,0 +1,20 @@ += 8.3 + +namespace ReadonlyClass; + +readonly class Foo +{ + +} + +class Bar +{ + + public function doFoo(): void + { + $c = new readonly class () { + + }; + } + +}