Skip to content
53 changes: 53 additions & 0 deletions src/Node/ClassPropertiesNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@
}
$originalProperties[$property->getName()] = $property;
$is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait());
if (!$is->yes() && $classReflection->hasConstructor()) {

Check warning on line 135 in src/Node/ClassPropertiesNode.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $originalProperties[$property->getName()] = $property; $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait()); - if (!$is->yes() && $classReflection->hasConstructor()) { + if ($is->no() && $classReflection->hasConstructor()) { $constructorDeclaringClass = $classReflection->getConstructor()->getDeclaringClass(); if ($constructorDeclaringClass->getName() !== $classReflection->getName()) { $is = $this->isPromotedByConstructorChain($constructorDeclaringClass, $property->getName());

Check warning on line 135 in src/Node/ClassPropertiesNode.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $originalProperties[$property->getName()] = $property; $is = TrinaryLogic::createFromBoolean($property->isPromoted() && !$property->isPromotedFromTrait()); - if (!$is->yes() && $classReflection->hasConstructor()) { + if ($is->no() && $classReflection->hasConstructor()) { $constructorDeclaringClass = $classReflection->getConstructor()->getDeclaringClass(); if ($constructorDeclaringClass->getName() !== $classReflection->getName()) { $is = $this->isPromotedByConstructorChain($constructorDeclaringClass, $property->getName());
$constructorDeclaringClass = $classReflection->getConstructor()->getDeclaringClass();
if ($constructorDeclaringClass->getName() !== $classReflection->getName()) {
$is = $this->isPromotedByConstructorChain($constructorDeclaringClass, $property->getName());
}
}
if (!$is->yes() && $classReflection->hasNativeProperty($property->getName())) {
$propertyReflection = $classReflection->getNativeProperty($property->getName());
if ($propertyReflection->isVirtual()->yes()) {
Expand Down Expand Up @@ -419,4 +425,51 @@
return $this->propertyAssigns;
}

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();
}

}
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,22 @@ 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,
],
]);
}

}
70 changes: 70 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-13380.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php // lint >= 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;
}
Loading