From ac40a6fde166b1821e249a37112526fef12a9fcb Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:50:02 +0000 Subject: [PATCH 1/3] Add regression test for readonly class property modification via clone() - New test in tests/PHPStan/Rules/Properties/data/bug-14063.php covering readonly class with clone() property overrides - Tests AccessPropertiesInAssignRule correctly reports "Assign to protected(set) property" when modifying readonly class properties via clone() from outside the declaring class - Tests ReadOnlyPropertyAssignRule does not double-report (defers to AccessPropertiesInAssignRule) - The underlying fix was in BetterReflection 6.70 which correctly sets isProtectedSet() for properties in readonly classes Closes https://github.com/phpstan/phpstan/issues/14063 --- .../AccessPropertiesInAssignRuleTest.php | 19 +++++++++ .../ReadOnlyPropertyAssignRuleTest.php | 8 ++++ .../Rules/Properties/data/bug-14063.php | 39 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-14063.php diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index 6fb0fe27d5d..433dc95e795 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -207,6 +207,25 @@ public function testBug13123(): void $this->analyse([__DIR__ . '/data/bug-13123.php'], []); } + #[RequiresPhp('>= 8.5')] + public function testBug14063(): void + { + $this->analyse([__DIR__ . '/data/bug-14063.php'], [ + [ + 'Assign to protected(set) property Bug14063\Obj::$value.', + 31, + ], + [ + 'Assign to protected(set) property Bug14063\Obj::$value.', + 34, + ], + [ + 'Assign to protected(set) property Bug14063\Base::$value.', + 38, + ], + ]); + } + #[RequiresPhp('>= 8.5')] public function testCloneWith(): void { diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index 40bbb2d5c32..ae886b13b51 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -180,4 +180,12 @@ public function testCloneWith(): void $this->analyse([__DIR__ . '/data/readonly-property-assign-clone-with.php'], []); } + #[RequiresPhp('>= 8.5')] + public function testBug14063(): void + { + // readonly class properties modified via clone() from outside the class + // are reported by AccessPropertiesInAssignRule, not this rule + $this->analyse([__DIR__ . '/data/bug-14063.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14063.php b/tests/PHPStan/Rules/Properties/data/bug-14063.php new file mode 100644 index 00000000000..3d2ebf2e5ec --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-14063.php @@ -0,0 +1,39 @@ += 8.5 + +declare(strict_types = 1); + +namespace Bug14063; + +final readonly class Obj +{ + public function __construct(public string $value) {} + + public function withValue(string $newValue): self + { + return clone($this, ['value' => $newValue]); + } +} + +readonly class Base +{ + public function __construct(public string $value) {} +} + +class Child extends Base +{ + public function withValue(string $newValue): self + { + return clone($this, ['value' => $newValue]); + } +} + +$obj = new Obj('val'); +$newObj = clone($obj, ['value' => 'newVal']); + +function test(Obj $obj): void { + clone($obj, ['value' => 'newVal']); +} + +function testBase(Base $base): void { + clone($base, ['value' => 'newVal']); +} From 60538dac68450fcc36811c2397de5e28745a74ad Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 8 Apr 2026 14:57:35 +0000 Subject: [PATCH 2/3] Remove unnecessary bug-14063 test from ReadOnlyPropertyAssignRuleTest The test was asserting no errors from ReadOnlyPropertyAssignRule, which adds no value since the rule is not expected to handle this case. Co-Authored-By: Claude Opus 4.6 --- .../Rules/Properties/ReadOnlyPropertyAssignRuleTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index ae886b13b51..40bbb2d5c32 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -180,12 +180,4 @@ public function testCloneWith(): void $this->analyse([__DIR__ . '/data/readonly-property-assign-clone-with.php'], []); } - #[RequiresPhp('>= 8.5')] - public function testBug14063(): void - { - // readonly class properties modified via clone() from outside the class - // are reported by AccessPropertiesInAssignRule, not this rule - $this->analyse([__DIR__ . '/data/bug-14063.php'], []); - } - } From 90137ed1f4941a37046be4ad03da6df73148266a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 8 Apr 2026 15:51:56 +0000 Subject: [PATCH 3/3] Fix invalid test: non-readonly class cannot extend readonly class Make Child class readonly since PHP does not allow a non-readonly class to extend a readonly class. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Rules/Properties/data/bug-14063.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Properties/data/bug-14063.php b/tests/PHPStan/Rules/Properties/data/bug-14063.php index 3d2ebf2e5ec..0dc05a8c80e 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14063.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14063.php @@ -19,7 +19,7 @@ public function withValue(string $newValue): self public function __construct(public string $value) {} } -class Child extends Base +readonly class Child extends Base { public function withValue(string $newValue): self {