From dc3292ac7d0fbd8df4fef713f27d733cd58f1f64 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 16 May 2025 09:02:46 +0200 Subject: [PATCH 1/4] EnforceReadonlyPublicProperty: exclude hooked interface properties --- src/Rule/EnforceReadonlyPublicPropertyRule.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Rule/EnforceReadonlyPublicPropertyRule.php b/src/Rule/EnforceReadonlyPublicPropertyRule.php index d67a959..09fce35 100644 --- a/src/Rule/EnforceReadonlyPublicPropertyRule.php +++ b/src/Rule/EnforceReadonlyPublicPropertyRule.php @@ -42,10 +42,10 @@ public function processNode(Node $node, Scope $scope): array return []; } - $classReflection = $scope->getClassReflection(); + $classReflection = $node->getClassReflection(); - if ($classReflection === null) { - return []; + if ($classReflection->isInterface()) { + return []; // unable to mark hooked properties on interfaces as readonly } if (($classReflection->getNativeReflection()->getModifiers() & 65_536) !== 0) { // readonly class, since PHP 8.2 From e2798706cac2d9a9fcd724ad9f4b3e59d2119c20 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 16 May 2025 09:13:50 +0200 Subject: [PATCH 2/4] Any hooked propety cannot be readonly, tests --- .../EnforceReadonlyPublicPropertyRule.php | 6 +-- .../EnforceReadonlyPublicPropertyRuleTest.php | 6 +++ .../code-84.php | 52 +++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php diff --git a/src/Rule/EnforceReadonlyPublicPropertyRule.php b/src/Rule/EnforceReadonlyPublicPropertyRule.php index 09fce35..7d74463 100644 --- a/src/Rule/EnforceReadonlyPublicPropertyRule.php +++ b/src/Rule/EnforceReadonlyPublicPropertyRule.php @@ -38,16 +38,12 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$node->isPublic() || $node->isReadOnly()) { + if (!$node->isPublic() || $node->isReadOnly() || $node->hasHooks()) { return []; } $classReflection = $node->getClassReflection(); - if ($classReflection->isInterface()) { - return []; // unable to mark hooked properties on interfaces as readonly - } - 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..01ca4c0 100644 --- a/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php +++ b/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php @@ -20,6 +20,12 @@ protected function getRule(): Rule return new EnforceReadonlyPublicPropertyRule($this->phpVersion); } + public function testPhp84(): void + { + $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..af8a29d --- /dev/null +++ b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php @@ -0,0 +1,52 @@ + $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; } +} + From e56291f061715f81f4dfd6ead07307abad3c5d10 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 16 May 2025 09:18:26 +0200 Subject: [PATCH 3/4] Do not run test on old PHP --- tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php b/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php index 01ca4c0..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 @@ -22,6 +23,10 @@ protected function getRule(): Rule 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'); } From 31b9247bf7f9bbfb4444f701530ccae1822164a4 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 16 May 2025 09:33:50 +0200 Subject: [PATCH 4/4] Improve test with iface children --- .../data/EnforceReadonlyPublicPropertyRule/code-84.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php index af8a29d..2323d44 100644 --- a/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php +++ b/tests/Rule/data/EnforceReadonlyPublicPropertyRule/code-84.php @@ -50,3 +50,10 @@ 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; +}