diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 95f20d2528..482aae9d64 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -6,6 +6,8 @@ conditionalTags: phpstan.rules.rule: %featureToggles.nodeConnectingVisitorRule% PHPStan\Rules\Properties\MissingReadOnlyByPhpDocPropertyAssignRule: phpstan.rules.rule: %featureToggles.readOnlyByPhpDoc% + PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyRule: + phpstan.rules.rule: %featureToggles.readOnlyByPhpDoc% PHPStan\Rules\Properties\UninitializedPropertyRule: phpstan.rules.rule: %checkUninitializedProperties% PHPStan\Rules\Methods\ConsistentConstructorRule: @@ -188,6 +190,9 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyRule + - class: PHPStan\Rules\Properties\UninitializedPropertyRule diff --git a/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php new file mode 100644 index 0000000000..96e793b776 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyByPhpDocPropertyRule.php @@ -0,0 +1,36 @@ + + */ +class ReadOnlyByPhpDocPropertyRule implements Rule +{ + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->isReadOnlyByPhpDoc()) { + return []; + } + + $errors = []; + if ($node->getDefault() !== null) { + $errors[] = RuleErrorBuilder::message('@readonly property cannot have a default value.')->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php new file mode 100644 index 0000000000..94f906d60c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php @@ -0,0 +1,34 @@ + + */ +class ReadOnlyByPhpDocPropertyRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyByPhpDocPropertyRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/read-only-property-phpdoc.php'], [ + [ + '@readonly property cannot have a default value.', + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc.php b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc.php new file mode 100644 index 0000000000..bb730d8e01 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc.php @@ -0,0 +1,33 @@ += 8.1 + +namespace ReadOnlyPropertyPhpDoc; + +class Foo +{ + + /** + * @readonly + * @var int + */ + private $foo; + + /** @readonly */ + private $bar; + + /** + * @readonly + * @var int + */ + private $baz = 0; + +} + +final class ErrorResponse +{ + public function __construct( + /** @readonly */ + public string $message = '' + ) + { + } +}