From 6ba5dd8056f3a14f18e0d1ac9f5ce357f8fe5667 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Fri, 1 Jul 2022 15:48:58 +0200 Subject: [PATCH 1/2] Inherit `@immutable` phpdoc --- src/Reflection/ClassReflection.php | 5 ++ ...ReadOnlyByPhpDocPropertyAssignRuleTest.php | 16 +++++++ ...dOnlyByPhpDocPropertyAssignRefRuleTest.php | 16 +++++++ ...ReadOnlyByPhpDocPropertyAssignRuleTest.php | 12 +++++ .../ReadOnlyByPhpDocPropertyRuleTest.php | 12 +++++ ...issing-readonly-property-assign-phpdoc.php | 33 +++++++++++++ .../data/read-only-property-phpdoc.php | 22 +++++++++ .../data/readonly-assign-phpdoc.php | 46 +++++++++++++++++++ .../data/readonly-assign-ref-phpdoc.php | 41 +++++++++++++++++ ...uninitialized-property-readonly-phpdoc.php | 22 +++++++++ 10 files changed, 225 insertions(+) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 1d89a72018..6471b78a53 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1047,6 +1047,11 @@ public function isImmutable(): bool $this->isImmutable = $resolvedPhpDoc !== null && $resolvedPhpDoc->isImmutable(); } + $parentClass = $this->getParentClass(); + if ($parentClass !== null && !$this->isImmutable) { + $this->isImmutable = $parentClass->isImmutable(); + } + return $this->isImmutable; } diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index 9163d2c8dc..2a478b578d 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -121,6 +121,22 @@ public function testRule(): void '@readonly property MissingReadOnlyPropertyAssignPhpDoc\FooTraitClass::$doubleAssigned is already assigned.', 192, ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\A has an uninitialized @readonly property $a. Assign it in the constructor.', + 233, + ], + [ + 'Class MissingReadOnlyPropertyAssignPhpDoc\B has an uninitialized @readonly property $b. Assign it in the constructor.', + 240, + ], + [ + 'Access to an uninitialized @readonly property MissingReadOnlyPropertyAssignPhpDoc\B::$b.', + 244, + ], + [ + '@readonly property MissingReadOnlyPropertyAssignPhpDoc\C::$c is already assigned.', + 257, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php index 3d5c9a0964..11b899c34c 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php @@ -39,6 +39,22 @@ public function testRule(): void '@readonly property ReadOnlyPropertyAssignRefPhpDoc\Immutable::$bar is assigned by reference.', 52, ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\A::$a is assigned by reference.', + 66, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\B::$b is assigned by reference.', + 79, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\A::$a is assigned by reference.', + 80, + ], + [ + '@readonly property ReadOnlyPropertyAssignRefPhpDoc\C::$c is assigned by reference.', + 93, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php index a36e7d49ef..bf86b85b71 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -92,6 +92,18 @@ public function testRule(): void '@readonly property ReadonlyPropertyAssignPhpDoc\Immutable::$foo is assigned outside of the constructor.', 227, ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\B::$b is assigned outside of the constructor.', + 259, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\A::$a is assigned outside of its declaring class.', + 260, + ], + [ + '@readonly property ReadonlyPropertyAssignPhpDoc\C::$c is assigned outside of the constructor.', + 273, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php index 09626c5100..4f2ea95c41 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyRuleTest.php @@ -28,6 +28,18 @@ public function testRule(): void '@readonly property cannot have a default value.', 21, ], + [ + '@readonly property cannot have a default value.', + 39, + ], + [ + '@readonly property cannot have a default value.', + 46, + ], + [ + '@readonly property cannot have a default value.', + 53, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php index fd9aa1072a..84e0e2c7fd 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/missing-readonly-property-assign-phpdoc.php @@ -225,3 +225,36 @@ class BarClass use BarTrait; } + +/** @immutable */ +class A +{ + + public string $a; + +} + +class B extends A +{ + + public string $b; + + public function __construct() + { + $b = $this->b; + } + +} + +class C extends B +{ + + public string $c; + + public function __construct() + { + $this->c = ''; + $this->c = ''; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc.php b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc.php index e40df8b894..358819c33a 100644 --- a/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/read-only-property-phpdoc.php @@ -31,3 +31,25 @@ public function __construct( { } } + +/** @immutable */ +class A +{ + + public string $a = ''; + +} + +class B extends A +{ + + public string $b = ''; + +} + +class C extends B +{ + + public string $c = ''; + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php index 5a1241ee53..b9b59be95e 100644 --- a/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php @@ -228,3 +228,49 @@ public function setFoo(int $foo): void } } + +/** @immutable */ +class A +{ + + /** @var string */ + public $a; + + public function __construct() { + $this->a = ''; // constructor - fine + } + +} + +class B extends A +{ + + /** @var string */ + public $b; + + public function __construct() + { + parent::__construct(); + $this->b = ''; // constructor - fine + } + + public function mod() + { + $this->b = 'input'; // setter - report + $this->a = 'input2'; // setter - report + } + +} + +class C extends B +{ + + /** @var string */ + public $c; + + public function mod() + { + $this->c = 'input'; // setter - report + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php index caa8a47318..26bec0835d 100644 --- a/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign-ref-phpdoc.php @@ -53,3 +53,44 @@ public function doFoo() } } + +/** @immutable */ +class A +{ + + /** @var string */ + public $a; + + public function mod() + { + $a = &$this->a; + } + +} + +class B extends A +{ + + /** @var string */ + public $b; + + public function mod() + { + $b = &$this->b; + $a = &$this->a; + } + +} + +class C extends B +{ + + /** @var string */ + public $c; + + public function mod() + { + $c = &$this->c; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly-phpdoc.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly-phpdoc.php index 4f1ec579a3..dc75bb9110 100644 --- a/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly-phpdoc.php @@ -41,3 +41,25 @@ public function __construct() } } + +/** @immutable */ +class A +{ + + public string $a; + +} + +class B extends A +{ + + public string $b; + +} + +class C extends B +{ + + public string $c; + +} From f65f7dd75cffb89ba8f81994639bfa2c1bd88e53 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sun, 10 Jul 2022 15:17:00 +0200 Subject: [PATCH 2/2] Cache parent immutability detection --- src/Reflection/ClassReflection.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 6471b78a53..22400e889f 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1045,11 +1045,11 @@ public function isImmutable(): bool if ($this->isImmutable === null) { $resolvedPhpDoc = $this->getResolvedPhpDoc(); $this->isImmutable = $resolvedPhpDoc !== null && $resolvedPhpDoc->isImmutable(); - } - $parentClass = $this->getParentClass(); - if ($parentClass !== null && !$this->isImmutable) { - $this->isImmutable = $parentClass->isImmutable(); + $parentClass = $this->getParentClass(); + if ($parentClass !== null && !$this->isImmutable) { + $this->isImmutable = $parentClass->isImmutable(); + } } return $this->isImmutable;