diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 1ba50bab15..7530b8b7f8 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -75,6 +75,7 @@ rules: - PHPStan\Rules\Properties\AccessPropertiesInAssignRule - PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule - PHPStan\Rules\Properties\PropertyAttributesRule + - PHPStan\Rules\Properties\ReadOnlyPropertyRule - PHPStan\Rules\Variables\UnsetRule services: diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 6bf3aa7589..cbb75add69 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -137,4 +137,9 @@ public function supportsFinalConstants(): bool return $this->versionId >= 80100; } + public function supportsReadOnlyProperties(): bool + { + return $this->versionId >= 80100; + } + } diff --git a/src/Rules/Properties/ReadOnlyPropertyRule.php b/src/Rules/Properties/ReadOnlyPropertyRule.php new file mode 100644 index 0000000000..16162f53bb --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyRule.php @@ -0,0 +1,52 @@ + + */ +class ReadOnlyPropertyRule implements Rule +{ + + private PhpVersion $phpVersion; + + public function __construct(PhpVersion $phpVersion) + { + $this->phpVersion = $phpVersion; + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->isReadOnly()) { + return []; + } + + $errors = []; + if (!$this->phpVersion->supportsReadOnlyProperties()) { + $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable()->build(); + } + + if ($node->getNativeType() === null) { + $errors[] = RuleErrorBuilder::message('Readonly property must have a native type.')->nonIgnorable()->build(); + } + + if ($node->getDefault() !== null) { + $errors[] = RuleErrorBuilder::message('Readonly property cannot have a default value.')->nonIgnorable()->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php new file mode 100644 index 0000000000..7bcd73b7c9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php @@ -0,0 +1,82 @@ + + */ +class ReadOnlyPropertyRuleTest extends RuleTestCase +{ + + /** @var int */ + private $phpVersionId; + + protected function getRule(): Rule + { + return new ReadOnlyPropertyRule(new PhpVersion($this->phpVersionId)); + } + + public function dataRule(): array + { + return [ + [ + 80000, + [ + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 8, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 9, + ], + [ + 'Readonly property must have a native type.', + 9, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 10, + ], + [ + 'Readonly property cannot have a default value.', + 10, + ], + ], + ], + [ + 80100, + [ + [ + 'Readonly property must have a native type.', + 9, + ], + [ + 'Readonly property cannot have a default value.', + 10, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param int $phpVersionId + * @param mixed[] $errors + */ + public function testRule(int $phpVersionId, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/read-only-property.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property.php b/tests/PHPStan/Rules/Properties/data/read-only-property.php new file mode 100644 index 0000000000..4182f194c4 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property.php @@ -0,0 +1,12 @@ += 8.1 + +namespace ReadOnlyProperty; + +class Foo +{ + + private readonly int $foo; + private readonly $bar; + private readonly int $baz = 0; + +}