diff --git a/src/Rule/EnforceReadonlyPublicPropertyRule.php b/src/Rule/EnforceReadonlyPublicPropertyRule.php index d67a959..7d74463 100644 --- a/src/Rule/EnforceReadonlyPublicPropertyRule.php +++ b/src/Rule/EnforceReadonlyPublicPropertyRule.php @@ -38,15 +38,11 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$node->isPublic() || $node->isReadOnly()) { + if (!$node->isPublic() || $node->isReadOnly() || $node->hasHooks()) { return []; } - $classReflection = $scope->getClassReflection(); - - if ($classReflection === null) { - return []; - } + $classReflection = $node->getClassReflection(); if (($classReflection->getNativeReflection()->getModifiers() & 65_536) !== 0) { // readonly class, since PHP 8.2 return []; diff --git a/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php b/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php index 2a7d27b..3ae718b 100644 --- a/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php +++ b/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use ShipMonk\PHPStan\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -20,6 +21,16 @@ protected function getRule(): Rule return new EnforceReadonlyPublicPropertyRule($this->phpVersion); } + public function testPhp84(): void + { + if (PHP_VERSION_ID < 8_00_00) { + self::markTestSkipped('PHP7 parser fails with property hooks'); + } + + $this->phpVersion = $this->createPhpVersion(80_400); + $this->analyseFile(__DIR__ . '/data/EnforceReadonlyPublicPropertyRule/code-84.php'); + } + public function testPhp81(): void { $this->phpVersion = $this->createPhpVersion(80_100); diff --git a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php new file mode 100644 index 0000000..2323d44 --- /dev/null +++ b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php @@ -0,0 +1,59 @@ + $this->hooked; } + + public readonly string $publicReadonly; + + protected string $protected; + + private string $private; + +} + +class MyClass { + + use MyTrait; + + public ?int $foo; // error: Public property `foo` not marked as readonly. + + public ?string $classHooked { set => strtolower($value); } + + public readonly int $bar; + + protected int $baz; + + private int $bag; + +} + +readonly class MyReadonlyClass { + + public ?int $foo; + + public readonly int $bar; + + public ?string $hooked { get => $this->hooked; } + + protected int $baz; + + private int $bag; + +} + +interface MyInterface { + public string $key { get; } +} + +class ImplementingClass implements MyInterface { + public string $key; // error: Public property `key` not marked as readonly. +} + +class ImplementingClass2 implements MyInterface { + public readonly string $key; +}