From 2c02e37331f9b6ad3bf04cc954b590931e470fa3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:14:32 +0000 Subject: [PATCH] Fix phpstan/phpstan#13380: Promoted property visibility change causes false uninitialized error - When a child class redeclares a parent's promoted property (e.g. to widen visibility), the property was incorrectly reported as uninitialized - Added check in ClassPropertiesNode::getUninitializedProperties() to detect when the inherited constructor's declaring class promotes the redeclared property - Added regression test with both the valid case (inherited constructor) and invalid case (own constructor without parent::__construct) --- src/Node/ClassPropertiesNode.php | 10 +++++++ .../UninitializedPropertyRuleTest.php | 10 +++++++ .../Rules/Properties/data/bug-13380.php | 26 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13380.php diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index f29a7cd9620..7a98ef0dd86 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -132,6 +132,16 @@ 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() + && $constructorDeclaringClass->hasNativeProperty($property->getName()) + && $constructorDeclaringClass->getNativeProperty($property->getName())->isPromoted() + ) { + $is = TrinaryLogic::createYes(); + } + } if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) { $propertyReflection = $classReflection->getNativeProperty($property->getName()); if ($propertyReflection->isVirtual()->yes()) { diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index 1aa54142672..3a0c6bead41 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -233,4 +233,14 @@ 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.', + 20, + ], + ]); + } + } 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..98a04c4a1a1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13380.php @@ -0,0 +1,26 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13380; + +class Foo +{ + public function __construct( + protected string $prop, + ){ + } +} + +class Bar extends Foo { + public string $prop; +} + +class Baz extends Foo { + public string $prop; + + public function __construct() + { + // Does not call parent::__construct, so $prop is uninitialized + } +}