diff --git a/Makefile b/Makefile index ab0a439c3b..d1fd73e1dd 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,7 @@ lint: --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php \ --exclude tests/PHPStan/Rules/Properties/data/readonly-property-hooks.php \ --exclude tests/PHPStan/Rules/Properties/data/readonly-property-hooks-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/virtual-hooked-properties.php \ --exclude tests/PHPStan/Rules/Classes/data/bug-12281.php \ --exclude tests/PHPStan/Rules/Traits/data/bug-12281.php \ --exclude tests/PHPStan/Rules/Classes/data/invalid-hooked-properties.php \ diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php index c8602aef79..b567aa7f7b 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -160,4 +160,9 @@ public function getHooks(): array return $this->originalNode->hooks; } + public function isVirtual(): bool + { + return $this->classReflection->getNativeProperty($this->name)->isVirtual()->yes(); + } + } diff --git a/src/Rules/Properties/PropertyInClassRule.php b/src/Rules/Properties/PropertyInClassRule.php index 50e3880013..661fb52c4c 100644 --- a/src/Rules/Properties/PropertyInClassRule.php +++ b/src/Rules/Properties/PropertyInClassRule.php @@ -92,6 +92,17 @@ public function processNode(Node $node, Scope $scope): array } } + if ($node->isVirtual()) { + if ($node->getDefault() !== null) { + return [ + RuleErrorBuilder::message('Virtual hooked properties cannot have a default value.') + ->nonIgnorable() + ->identifier('property.virtualDefault') + ->build(), + ]; + } + } + return []; } diff --git a/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php index 6d61e5c7cc..f9c60aadd6 100644 --- a/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php @@ -177,4 +177,18 @@ public function testPhp84AndReadonlyHookedProperties(): void ]); } + public function testPhp84AndVirtualHookedProperties(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/virtual-hooked-properties.php'], [ + [ + 'Virtual hooked properties cannot have a default value.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/virtual-hooked-properties.php b/tests/PHPStan/Rules/Properties/data/virtual-hooked-properties.php new file mode 100644 index 0000000000..4c69f70d60 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/virtual-hooked-properties.php @@ -0,0 +1,22 @@ + $this->firstName; + set => $this->firstName = $value; + } + + public string $middleName { + get => $this->middleName; + set => $this->middleName = $value; + } + + public string $lastName = 'Doe' { + get => 'Smith'; + } + + public string $maidenName = 'Brown'; +}