diff --git a/src/Rules/Variables/UnsetRule.php b/src/Rules/Variables/UnsetRule.php index dc75eb024e..8240e35e95 100644 --- a/src/Rules/Variables/UnsetRule.php +++ b/src/Rules/Variables/UnsetRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; @@ -17,6 +18,12 @@ final class UnsetRule implements Rule { + public function __construct( + private PropertyReflectionFinder $propertyReflectionFinder, + ) + { + } + public function getNodeType(): string { return Node\Stmt\Unset_::class; @@ -69,6 +76,33 @@ private function canBeUnset(Node $node, Scope $scope): ?IdentifierRuleError } return $this->canBeUnset($node->var, $scope); + } elseif ( + $node instanceof Node\Expr\PropertyFetch + && $node->name instanceof Node\Identifier + ) { + $foundPropertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $scope); + if ($foundPropertyReflection === null) { + return null; + } + + $propertyReflection = $foundPropertyReflection->getNativeReflection(); + if ($propertyReflection === null) { + return null; + } + + if ($propertyReflection->isReadOnly() || $propertyReflection->isReadOnlyByPhpDoc()) { + return RuleErrorBuilder::message( + sprintf( + 'Cannot unset %s %s::$%s property.', + $propertyReflection->isReadOnly() ? 'readonly' : '@readonly', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $foundPropertyReflection->getName(), + ), + ) + ->line($node->getStartLine()) + ->identifier($propertyReflection->isReadOnly() ? 'unset.readOnlyProperty' : 'unset.readOnlyPropertyByPhpDoc') + ->build(); + } } return null; diff --git a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php index 036d09c974..0b5861c1c0 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -13,7 +14,7 @@ class UnsetRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnsetRule(); + return new UnsetRule(self::getContainer()->getByType(PropertyReflectionFinder::class)); } public function testUnsetRule(): void @@ -91,4 +92,34 @@ public function testBug4565(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-4565.php'], []); } + public function testBug12421(): void + { + $this->analyse([__DIR__ . '/data/bug-12421.php'], [ + [ + 'Cannot unset readonly Bug12421\NativeReadonlyClass::$y property.', + 11, + ], + [ + 'Cannot unset readonly Bug12421\NativeReadonlyProperty::$y property.', + 15, + ], + [ + 'Cannot unset @readonly Bug12421\PhpdocReadonlyClass::$y property.', + 19, + ], + [ + 'Cannot unset @readonly Bug12421\PhpdocReadonlyProperty::$y property.', + 23, + ], + [ + 'Cannot unset @readonly Bug12421\PhpdocImmutableClass::$y property.', + 27, + ], + [ + 'Cannot unset readonly Bug12421\NativeReadonlyProperty::$y property.', + 34, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-12421.php b/tests/PHPStan/Rules/Variables/data/bug-12421.php new file mode 100644 index 0000000000..eb0c53f755 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12421.php @@ -0,0 +1,108 @@ += 8.2 + +namespace Bug12421; + +function doFoo() { + $x = new RegularProperty(); + unset($x->y); + var_dump($x->y); + + $x = new NativeReadonlyClass(); + unset($x->y); + var_dump($x->y); + + $x = new NativeReadonlyProperty(); + unset($x->y); + var_dump($x->y); + + $x = new PhpdocReadonlyClass(); + unset($x->y); + var_dump($x->y); + + $x = new PhpdocReadonlyProperty(); + unset($x->y); + var_dump($x->y); + + $x = new PhpdocImmutableClass(); + unset($x->y); + var_dump($x->y); + + $x = new \stdClass(); + unset($x->y); + + $x = new NativeReadonlyPropertySubClass(); + unset($x->y); + var_dump($x->y); +} + +readonly class NativeReadonlyClass +{ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +class NativeReadonlyProperty +{ + public readonly Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +/** @readonly */ +class PhpdocReadonlyClass +{ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +class PhpdocReadonlyProperty +{ + /** @readonly */ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +/** @immutable */ +class PhpdocImmutableClass +{ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +class RegularProperty +{ + public Y $y; + + public function __construct() + { + $this->y = new Y(); + } +} + +class NativeReadonlyPropertySubClass extends NativeReadonlyProperty +{ +} + +class Y +{ +} +