Skip to content

Commit 1bffa1d

Browse files
phpstan-botclaude
andcommitted
Report uninitialized property when inherited constructor doesn't promote it
When an intermediate class overrides the constructor without a matching parameter for the promoted property, the ancestor walk now stops instead of incorrectly assuming the property is initialized. Uses constructor parameter matching as a heuristic to determine if parent::__construct is likely called. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5e9dd7c commit 1bffa1d

File tree

3 files changed

+66
-11
lines changed

3 files changed

+66
-11
lines changed

src/Node/ClassPropertiesNode.php

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,7 @@ public function getUninitializedProperties(
135135
if (!$is->yes() && $classReflection->hasConstructor()) {
136136
$constructorDeclaringClass = $classReflection->getConstructor()->getDeclaringClass();
137137
if ($constructorDeclaringClass->getName() !== $classReflection->getName()) {
138-
$ancestor = $constructorDeclaringClass;
139-
while ($ancestor !== null) {
140-
if (
141-
$ancestor->hasNativeProperty($property->getName())
142-
&& $ancestor->getNativeProperty($property->getName())->isPromoted()
143-
) {
144-
$is = TrinaryLogic::createYes();
145-
break;
146-
}
147-
$ancestor = $ancestor->getParentClass();
148-
}
138+
$is = $this->isPropertyInitializedByConstructorChain($constructorDeclaringClass, $property->getName());
149139
}
150140
}
151141
if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) {
@@ -435,4 +425,46 @@ public function getPropertyAssigns(): array
435425
return $this->propertyAssigns;
436426
}
437427

428+
private function isPropertyInitializedByConstructorChain(ClassReflection $constructorDeclaringClass, string $propertyName): TrinaryLogic
429+
{
430+
$ancestor = $constructorDeclaringClass;
431+
do {
432+
$ancestorConstructor = $ancestor->getNativeReflection()->getConstructor();
433+
if ($ancestorConstructor !== null) {
434+
foreach ($ancestorConstructor->getParameters() as $param) {
435+
if ($param->getName() === $propertyName && $param->isPromoted()) {
436+
return TrinaryLogic::createYes();
437+
}
438+
}
439+
}
440+
441+
$parent = $ancestor->getParentClass();
442+
if ($parent === null) {
443+
break;
444+
}
445+
446+
$hasOwnConstructor = $ancestor->hasConstructor()
447+
&& $ancestor->getConstructor()->getDeclaringClass()->getName() === $ancestor->getName();
448+
449+
if ($hasOwnConstructor) {
450+
$hasMatchingParam = false;
451+
if ($ancestorConstructor !== null) {
452+
foreach ($ancestorConstructor->getParameters() as $param) {
453+
if ($param->getName() === $propertyName) {
454+
$hasMatchingParam = true;
455+
break;
456+
}
457+
}
458+
}
459+
if (!$hasMatchingParam) {
460+
break;
461+
}
462+
}
463+
464+
$ancestor = $parent;
465+
} while (true);
466+
467+
return TrinaryLogic::createNo();
468+
}
469+
438470
}

tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ public function testBug13380(): void
240240
'Class Bug13380\Baz has an uninitialized property $prop. Give it default value or assign it in the constructor.',
241241
33,
242242
],
243+
[
244+
'Class Bug13380\Baz3 has an uninitialized property $prop. Give it default value or assign it in the constructor.',
245+
57,
246+
],
243247
]);
244248
}
245249

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,22 @@ public function __construct()
3737
// Does not call parent::__construct, so $prop is uninitialized
3838
}
3939
}
40+
41+
class Foo3
42+
{
43+
public function __construct(
44+
protected string $prop,
45+
){
46+
}
47+
}
48+
49+
class Bar3 extends Foo3
50+
{
51+
public function __construct()
52+
{
53+
}
54+
}
55+
56+
class Baz3 extends Bar3 {
57+
public string $prop;
58+
}

0 commit comments

Comments
 (0)