diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index f29a7cd9620..98bb5fbc39a 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -132,6 +132,34 @@ 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->isPromotedByConstructorChain($constructorDeclaringClass, $property->getName()); + if (!$is->yes()) { + $is = $this->isPropertyDeclaredInAncestor($constructorDeclaringClass, $property->getName()); + } + } + } + if (!$is->yes()) { + foreach ($constructors as $constructorName) { + if (strtolower($constructorName) === '__construct') { + continue; + } + if (!$classReflection->hasNativeMethod($constructorName)) { + continue; + } + $methodReflection = $classReflection->getNativeMethod($constructorName); + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->getName() === $classReflection->getName()) { + continue; + } + $is = $this->isPropertyDeclaredInAncestor($declaringClass, $property->getName()); + if ($is->yes()) { + break; + } + } + } if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) { $propertyReflection = $classReflection->getNativeProperty($property->getName()); if ($propertyReflection->isVirtual()->yes()) { @@ -419,4 +447,65 @@ public function getPropertyAssigns(): array return $this->propertyAssigns; } + private function isPropertyDeclaredInAncestor(ClassReflection $ancestorClass, string $propertyName): TrinaryLogic + { + $nativeReflection = $ancestorClass->getNativeReflection(); + if ( + $nativeReflection->hasProperty($propertyName) + && !$nativeReflection->getProperty($propertyName)->isPrivate() + && $nativeReflection->getProperty($propertyName)->getDeclaringClass()->getName() === $ancestorClass->getName() + ) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createNo(); + } + + private function isPromotedByConstructorChain(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()) { + continue; + } + + $ancestorNativeReflection = $ancestor->getNativeReflection(); + if ($ancestorNativeReflection->hasProperty($propertyName) && !$ancestorNativeReflection->getProperty($propertyName)->isPrivate()) { + 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 1aa54142672..352f31cc3fd 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -233,4 +233,30 @@ 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, + ], + [ + 'Class Bug13380\BarPrivate has an uninitialized property $prop. Give it default value or assign it in the constructor.', + 69, + ], + [ + 'Class Bug13380\BazBody has an uninitialized property $prop. Give it default value or assign it in the constructor.', + 88, + ], + [ + 'Class Bug13380\FooBodyNoInit has an uninitialized property $prop. Give it default value or assign it in the constructor.', + 99, + ], + ]); + } + } 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 00000000000..94abdacb8db --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13380.php @@ -0,0 +1,109 @@ += 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; +} + +class FooPrivate +{ + public function __construct( + private string $prop, + ){ + } +} + +class BarPrivate extends FooPrivate { + public string $prop; +} + +// Non-promoted property initialized in parent constructor body +class FooBody +{ + protected string $prop; + + public function __construct() + { + $this->prop = "1232"; + } +} + +class BarBody extends FooBody { + public string $prop; +} + +class BazBody extends FooBody { + public string $prop; + + public function __construct() + { + // Does not call parent::__construct, so $prop is uninitialized + } +} + +// Non-promoted property NOT initialized in parent constructor body +class FooBodyNoInit +{ + protected string $prop; + + public function __construct() + { + // doesn't initialize $prop + } +} + +class BarBodyNoInit extends FooBodyNoInit { + public string $prop; +}