From 986cbdfb2fd0c3de71b31ac3f212bd499d2131e1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 26 Mar 2024 18:08:11 +0100 Subject: [PATCH] Detect mismatch between readonly/non-readonly class parent --- .../ExistingClassInClassExtendsRule.php | 18 ++++++++ .../ExistingClassInClassExtendsRuleTest.php | 26 ++++++++++++ .../Classes/data/extends-readonly-class.php | 41 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 tests/PHPStan/Rules/Classes/data/extends-readonly-class.php diff --git a/src/Rules/Classes/ExistingClassInClassExtendsRule.php b/src/Rules/Classes/ExistingClassInClassExtendsRule.php index 9d6e6ceeb..b24e62eb5 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -81,6 +81,24 @@ public function processNode(Node $node, Scope $scope): array $reflection->getDisplayName(), ))->build(); } + + if ($reflection->isClass()) { + if ($node->isReadonly()) { + if (!$reflection->isReadOnly()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends non-readonly class %s.', + $currentClassName !== null ? sprintf('Readonly class %s', $currentClassName) : 'Anonymous readonly class', + $reflection->getDisplayName(), + ))->nonIgnorable()->build(); + } + } elseif ($reflection->isReadOnly()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends readonly class %s.', + $currentClassName !== null ? sprintf('Non-readonly class %s', $currentClassName) : 'Anonymous non-readonly class', + $reflection->getDisplayName(), + ))->nonIgnorable()->build(); + } + } } return $messages; diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php index 48ef41b69..57d99fe61 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php @@ -119,4 +119,30 @@ public function testPhpstanInternalClass(): void ]); } + public function testReadonly(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('This test needs PHP 8.2'); + } + + $this->analyse([__DIR__ . '/data/extends-readonly-class.php'], [ + [ + 'Readonly class ExtendsReadOnlyClass\Foo extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', + 25, + ], + [ + 'Non-readonly class ExtendsReadOnlyClass\Bar extends readonly class ExtendsReadOnlyClass\ReadonlyClass.', + 30, + ], + [ + 'Anonymous non-readonly class extends readonly class ExtendsReadOnlyClass\ReadonlyClass.', + 35, + ], + [ + 'Anonymous readonly class extends non-readonly class ExtendsReadOnlyClass\Nonreadonly.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php new file mode 100644 index 000000000..56e9901dc --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php @@ -0,0 +1,41 @@ += 8.2 + +namespace ExtendsReadOnlyClass; + +class Nonreadonly +{ + +} + +readonly class ReadonlyClass +{ + +} + +class Lorem extends Nonreadonly // ok +{ + +} + +readonly class Ipsum extends ReadonlyClass // ok +{ + +} + +readonly class Foo extends Nonreadonly // not ok +{ + +} + +class Bar extends ReadonlyClass // not ok +{ + +} + +new class extends ReadonlyClass { // not ok + +}; + +new readonly class extends Nonreadonly { // not ok + +};