diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f8cbcb4ff0..0d78ac8b14 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -503,6 +503,9 @@ private function processStmtNode( if ($param->getDocComment() !== null) { $phpDoc = $param->getDocComment()->getText(); } + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } $nodeCallback(new ClassPropertyNode( $param->var->name, $param->flags, @@ -513,6 +516,7 @@ private function processStmtNode( $param, false, $scope->isInTrait(), + $scope->getClassReflection()->isReadOnly(), ), $methodScope); } } @@ -657,6 +661,9 @@ private function processStmtNode( foreach ($stmt->props as $prop) { $this->processStmtNode($prop, $scope, $nodeCallback); [,,,,,,,,,,$isReadOnly, $docComment] = $this->getPhpDocs($scope, $stmt); + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } $nodeCallback( new ClassPropertyNode( $prop->name->toString(), @@ -668,6 +675,7 @@ private function processStmtNode( $prop, $isReadOnly, $scope->isInTrait(), + $scope->getClassReflection()->isReadOnly(), ), $scope, ); diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php index a7944ec834..42f47bba8b 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -23,6 +23,7 @@ public function __construct( Node $originalNode, private bool $isReadonlyByPhpDoc, private bool $isDeclaredInTrait, + private bool $isReadonlyClass, ) { parent::__construct($originalNode->getAttributes()); @@ -76,7 +77,7 @@ public function isStatic(): bool public function isReadOnly(): bool { - return (bool) ($this->flags & Class_::MODIFIER_READONLY); + return (bool) ($this->flags & Class_::MODIFIER_READONLY) || $this->isReadonlyClass; } public function isReadOnlyByPhpDoc(): bool diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 0e5f76a460..0bbe418ff5 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -584,6 +584,11 @@ public function isEnum(): bool return $this->reflection->isEnum(); } + public function isReadOnly(): bool + { + return $this->reflection->isReadOnly(); + } + public function isBackedEnum(): bool { if (!$this->reflection instanceof ReflectionEnum) { diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php index c1e4dd90bf..51776a3dda 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php @@ -89,4 +89,14 @@ public function testRule(int $phpVersionId, array $errors): void $this->analyse([__DIR__ . '/data/read-only-property.php'], $errors); } + /** + * @dataProvider dataRule + * @param mixed[] $errors + */ + public function testRuleReadonlyClass(int $phpVersionId, array $errors): void + { + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/read-only-property-readonly-class.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php b/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php new file mode 100644 index 0000000000..1c3a6dae11 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-readonly-class.php @@ -0,0 +1,24 @@ += 8.2 + +namespace ReadOnlyPropertyReadonlyClass; + +readonly class Foo +{ + + private int $foo; + private $bar; + private int $baz = 0; + +} + +readonly final class ErrorResponse +{ + public function __construct(public string $message = '') + { + } +} + +readonly class StaticReadonlyProperty +{ + private static int $foo; +}