diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index f29a7cd962..914e396bca 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -132,6 +132,12 @@ public function getUninitializedProperties( } $originalProperties[$property->getName()] = $property; $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait()); + if (!$is->yes() && $classReflection->hasConstructor()) { + $constructorDeclaringClass = $classReflection->getConstructor()->getDeclaringClass(); + if ($constructorDeclaringClass->getName() !== $classReflection->getName()) { + $is = $this->isPropertyInitializedByConstructorChain($constructorDeclaringClass, $property->getName()); + } + } if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) { $propertyReflection = $classReflection->getNativeProperty($property->getName()); if ($propertyReflection->isVirtual()->yes()) { @@ -419,4 +425,46 @@ public function getPropertyAssigns(): array return $this->propertyAssigns; } + private function isPropertyInitializedByConstructorChain(ClassReflection $constructorDeclaringClass, string $propertyName): TrinaryLogic + { + $ancestor = $constructorDeclaringClass; + do { + $ancestorConstructor = $ancestor->getNativeReflection()->getConstructor(); + if ($ancestorConstructor !== null) { + foreach ($ancestorConstructor->getParameters() as $param) { + if ($param->getName() === $propertyName && $param->isPromoted()) { + return TrinaryLogic::createYes(); + } + } + } + + $parent = $ancestor->getParentClass(); + if ($parent === null) { + break; + } + + $hasOwnConstructor = $ancestor->hasConstructor() + && $ancestor->getConstructor()->getDeclaringClass()->getName() === $ancestor->getName(); + + if ($hasOwnConstructor) { + $hasMatchingParam = false; + if ($ancestorConstructor !== null) { + foreach ($ancestorConstructor->getParameters() as $param) { + if ($param->getName() === $propertyName) { + $hasMatchingParam = true; + break; + } + } + } + if (!$hasMatchingParam) { + break; + } + } + + $ancestor = $parent; + } while (true); + + return TrinaryLogic::createNo(); + } + } diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index 1aa5414267..f1bdc9dc04 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -233,4 +233,18 @@ public function testBug12547(): void $this->analyse([__DIR__ . '/data/bug-12547.php'], []); } + public function testBug13380(): void + { + $this->analyse([__DIR__ . '/data/bug-13380.php'], [ + [ + 'Class Bug13380\Baz has an uninitialized property $prop. Give it default value or assign it in the constructor.', + 33, + ], + [ + 'Class Bug13380\Baz3 has an uninitialized property $prop. Give it default value or assign it in the constructor.', + 57, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-13380.php b/tests/PHPStan/Rules/Properties/data/bug-13380.php new file mode 100644 index 0000000000..6be2ea5d81 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13380.php @@ -0,0 +1,58 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13380; + +class Foo +{ + public function __construct( + protected string $prop, + ){ + } +} + +class Bar extends Foo { + public string $prop; +} + +class Bar2 extends Foo +{ + public function __construct( + string $prop, + ){ + parent::__construct($prop); + } +} + +class Baz2 extends Bar2 { + public string $prop; +} + +class Baz extends Foo { + public string $prop; + + public function __construct() + { + // Does not call parent::__construct, so $prop is uninitialized + } +} + +class Foo3 +{ + public function __construct( + protected string $prop, + ){ + } +} + +class Bar3 extends Foo3 +{ + public function __construct() + { + } +} + +class Baz3 extends Bar3 { + public string $prop; +}