Skip to content

Commit a93e756

Browse files
phpstan-botclaude
andcommitted
Handle non-promoted properties and additional constructors for inherited initialization
When a child class redeclares a property from a parent class, also consider it initialized if the constructor-declaring ancestor class declares the same non-private property (not just promoted parameters). Also handle inherited additional constructors (e.g. setUp()) the same way. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6f20308 commit a93e756

3 files changed

Lines changed: 83 additions & 0 deletions

File tree

src/Node/ClassPropertiesNode.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,28 @@ public function getUninitializedProperties(
136136
$constructorDeclaringClass = $classReflection->getConstructor()->getDeclaringClass();
137137
if ($constructorDeclaringClass->getName() !== $classReflection->getName()) {
138138
$is = $this->isPromotedByConstructorChain($constructorDeclaringClass, $property->getName());
139+
if (!$is->yes()) {
140+
$is = $this->isPropertyDeclaredInAncestor($constructorDeclaringClass, $property->getName());
141+
}
142+
}
143+
}
144+
if (!$is->yes()) {
145+
foreach ($constructors as $constructorName) {
146+
if (strtolower($constructorName) === '__construct') {
147+
continue;
148+
}
149+
if (!$classReflection->hasNativeMethod($constructorName)) {
150+
continue;
151+
}
152+
$methodReflection = $classReflection->getNativeMethod($constructorName);
153+
$declaringClass = $methodReflection->getDeclaringClass();
154+
if ($declaringClass->getName() === $classReflection->getName()) {
155+
continue;
156+
}
157+
$is = $this->isPropertyDeclaredInAncestor($declaringClass, $property->getName());
158+
if ($is->yes()) {
159+
break;
160+
}
139161
}
140162
}
141163
if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) {
@@ -425,6 +447,20 @@ public function getPropertyAssigns(): array
425447
return $this->propertyAssigns;
426448
}
427449

450+
private function isPropertyDeclaredInAncestor(ClassReflection $ancestorClass, string $propertyName): TrinaryLogic
451+
{
452+
$nativeReflection = $ancestorClass->getNativeReflection();
453+
if (
454+
$nativeReflection->hasProperty($propertyName)
455+
&& !$nativeReflection->getProperty($propertyName)->isPrivate()
456+
&& $nativeReflection->getProperty($propertyName)->getDeclaringClass()->getName() === $ancestorClass->getName()
457+
) {
458+
return TrinaryLogic::createYes();
459+
}
460+
461+
return TrinaryLogic::createNo();
462+
}
463+
428464
private function isPromotedByConstructorChain(ClassReflection $constructorDeclaringClass, string $propertyName): TrinaryLogic
429465
{
430466
$ancestor = $constructorDeclaringClass;

tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,14 @@ public function testBug13380(): void
248248
'Class Bug13380\BarPrivate has an uninitialized property $prop. Give it default value or assign it in the constructor.',
249249
69,
250250
],
251+
[
252+
'Class Bug13380\BazBody has an uninitialized property $prop. Give it default value or assign it in the constructor.',
253+
88,
254+
],
255+
[
256+
'Class Bug13380\FooBodyNoInit has an uninitialized property $prop. Give it default value or assign it in the constructor.',
257+
99,
258+
],
251259
]);
252260
}
253261

tests/PHPStan/Rules/Properties/data/bug-13380.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,42 @@ public function __construct(
6868
class BarPrivate extends FooPrivate {
6969
public string $prop;
7070
}
71+
72+
// Non-promoted property initialized in parent constructor body
73+
class FooBody
74+
{
75+
protected string $prop;
76+
77+
public function __construct()
78+
{
79+
$this->prop = "1232";
80+
}
81+
}
82+
83+
class BarBody extends FooBody {
84+
public string $prop;
85+
}
86+
87+
class BazBody extends FooBody {
88+
public string $prop;
89+
90+
public function __construct()
91+
{
92+
// Does not call parent::__construct, so $prop is uninitialized
93+
}
94+
}
95+
96+
// Non-promoted property NOT initialized in parent constructor body
97+
class FooBodyNoInit
98+
{
99+
protected string $prop;
100+
101+
public function __construct()
102+
{
103+
// doesn't initialize $prop
104+
}
105+
}
106+
107+
class BarBodyNoInit extends FooBodyNoInit {
108+
public string $prop;
109+
}

0 commit comments

Comments
 (0)