Skip to content
89 changes: 89 additions & 0 deletions src/Node/ClassPropertiesNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,34 @@
}
$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()) {

Check warning on line 139 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": @@ @@ $constructorDeclaringClass = $classReflection->getConstructor()->getDeclaringClass(); if ($constructorDeclaringClass->getName() !== $classReflection->getName()) { $is = $this->isPromotedByConstructorChain($constructorDeclaringClass, $property->getName()); - if (!$is->yes()) { + if ($is->no()) { $is = $this->isPropertyDeclaredInAncestor($constructorDeclaringClass, $property->getName()); } }

Check warning on line 139 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": @@ @@ $constructorDeclaringClass = $classReflection->getConstructor()->getDeclaringClass(); if ($constructorDeclaringClass->getName() !== $classReflection->getName()) { $is = $this->isPromotedByConstructorChain($constructorDeclaringClass, $property->getName()); - if (!$is->yes()) { + if ($is->no()) { $is = $this->isPropertyDeclaredInAncestor($constructorDeclaringClass, $property->getName()); } }
$is = $this->isPropertyDeclaredInAncestor($constructorDeclaringClass, $property->getName());
}
}
}
if (!$is->yes()) {

Check warning on line 144 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": @@ @@ } } } - if (!$is->yes()) { + if ($is->no()) { foreach ($constructors as $constructorName) { if (strtolower($constructorName) === '__construct') { continue;

Check warning on line 144 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": @@ @@ } } } - if (!$is->yes()) { + if ($is->no()) { foreach ($constructors as $constructorName) { if (strtolower($constructorName) === '__construct') { continue;
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()) {
Expand Down Expand Up @@ -419,4 +447,65 @@
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();
}

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

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

// 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;
}
Loading