From 167347f0c73be94c572fb40eb7b6a8fd13a3ce8c Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:10:46 +0000 Subject: [PATCH 1/3] Treat trait-declared properties as initialized when promoted by an inherited constructor - In ClassPropertiesNode::getUninitializedProperties(), mark a property as initialized when its native reflection is promoted and declared in a parent class. When a child class re-declares a property through a trait, PHP resolves the property to the parent's promoted constructor parameter, so it is always initialized by the inherited constructor. - Fixes the false positive for the native readonly rule (property.uninitializedReadonly), and the same fix flows through the shared getUninitializedProperties() to the @readonly PHPDoc rule (property.uninitializedReadonlyByPhpDoc) and the plain uninitialized property rule (property.uninitialized). - Genuinely uninitialized trait properties with no promoting ancestor still report correctly. --- src/Node/ClassPropertiesNode.php | 7 +++ ...ReadOnlyByPhpDocPropertyAssignRuleTest.php | 11 +++++ .../MissingReadOnlyPropertyAssignRuleTest.php | 11 +++++ .../UninitializedPropertyRuleTest.php | 6 +++ .../Properties/data/bug-14983-phpdoc.php | 31 +++++++++++++ .../data/bug-14983-uninitialized.php | 23 ++++++++++ .../Rules/Properties/data/bug-14983.php | 43 +++++++++++++++++++ 7 files changed, 132 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-14983-phpdoc.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-14983-uninitialized.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-14983.php diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index e9b7b2b51aa..103a0289559 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -138,6 +138,13 @@ public function getUninitializedProperties( continue; } + if ( + $propertyReflection->isPromoted() + && $propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName() + ) { + $is = TrinaryLogic::createYes(); + } + foreach ($extensions as $extension) { if (!$extension->isInitialized($propertyReflection, $property->getName())) { continue; diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index 9668ddbe6d6..868e3d77e04 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -154,4 +154,15 @@ public function testBug13856(): void $this->analyse([__DIR__ . '/data/bug-13856-phpdoc.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug14983(): void + { + $this->analyse([__DIR__ . '/data/bug-14983-phpdoc.php'], [ + [ + 'Class Bug14983PhpDoc\UninitializedFromTrait has an uninitialized @readonly property $property. Assign it in the constructor.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php index 2a023b0e7c4..1f6332b72dd 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -152,6 +152,17 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug14983(): void + { + $this->analyse([__DIR__ . '/data/bug-14983.php'], [ + [ + 'Class Bug14983\UninitializedFromTrait has an uninitialized readonly property $property. Assign it in the constructor.', + 8, + ], + ]); + } + #[RequiresPhp('>= 8.1.0')] public function testBug7119(): void { diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index 0d9827885ec..d1b137e35cd 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -233,4 +233,10 @@ public function testBug12547(): void $this->analyse([__DIR__ . '/data/bug-12547.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug14983(): void + { + $this->analyse([__DIR__ . '/data/bug-14983-uninitialized.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14983-phpdoc.php b/tests/PHPStan/Rules/Properties/data/bug-14983-phpdoc.php new file mode 100644 index 00000000000..8a6a1b3c8ea --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-14983-phpdoc.php @@ -0,0 +1,31 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug14983PhpDoc; + +trait TraitA { + /** @readonly */ + protected string $property; +} + +trait TraitB { + use TraitA; +} + +class ClassA { + use TraitA; + + public function __construct( + /** @readonly */ + protected string $property, + ) {} +} + +class ClassB extends ClassA { + use TraitB; +} + +class UninitializedFromTrait { + use TraitA; +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-14983-uninitialized.php b/tests/PHPStan/Rules/Properties/data/bug-14983-uninitialized.php new file mode 100644 index 00000000000..3377daac2c9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-14983-uninitialized.php @@ -0,0 +1,23 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug14983Uninitialized; + +trait TraitA { + protected string $property; +} + +trait TraitB { + use TraitA; +} + +class ClassA { + use TraitA; + + public function __construct(protected string $property) {} +} + +class ClassB extends ClassA { + use TraitB; +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-14983.php b/tests/PHPStan/Rules/Properties/data/bug-14983.php new file mode 100644 index 00000000000..d5bef96b228 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-14983.php @@ -0,0 +1,43 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14983; + +trait TraitA { + protected readonly string $property; +} + +trait TraitB { + use TraitA; +} + +class ClassA { + use TraitA; + + public function __construct(protected readonly string $property) {} +} + +class ClassB extends ClassA { + use TraitB; +} + +trait GrandTrait { + use TraitA; +} + +class GrandParent1 { + use TraitA; + + public function __construct(protected readonly string $property) {} +} + +class Parent1 extends GrandParent1 {} + +class Child1 extends Parent1 { + use GrandTrait; +} + +class UninitializedFromTrait { + use TraitA; +} From 89c6ebd79d33ea7ea8e1d8b9d001a2480facd0ba Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 22 Jul 2026 11:45:45 +0000 Subject: [PATCH 2/3] Exercise nested trait in the genuinely-uninitialized case of bug-14983 Co-Authored-By: Claude Opus 4.8 --- tests/PHPStan/Rules/Properties/data/bug-14983.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Properties/data/bug-14983.php b/tests/PHPStan/Rules/Properties/data/bug-14983.php index d5bef96b228..23366cfe6d7 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14983.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14983.php @@ -39,5 +39,5 @@ class Child1 extends Parent1 { } class UninitializedFromTrait { - use TraitA; + use GrandTrait; } From 03b5bf67dc3ee77021de2dcf8d0f9cde16071610 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 22 Jul 2026 15:07:39 +0000 Subject: [PATCH 3/3] Treat inherited trait-declared properties as initialized regardless of promotion The parent constructor may initialize the property via a plain body assignment rather than promotion, so consult only the native reflection declaring class: when it differs from the analysed class the property is inherited and its initialization is the ancestor's responsibility. Co-Authored-By: Claude Opus 4.8 --- src/Node/ClassPropertiesNode.php | 5 +---- .../Rules/Properties/data/bug-14983-phpdoc.php | 12 ++++++++++++ .../Properties/data/bug-14983-uninitialized.php | 12 ++++++++++++ tests/PHPStan/Rules/Properties/data/bug-14983.php | 12 ++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index 103a0289559..754be7bc6e6 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -138,10 +138,7 @@ public function getUninitializedProperties( continue; } - if ( - $propertyReflection->isPromoted() - && $propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName() - ) { + if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { $is = TrinaryLogic::createYes(); } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14983-phpdoc.php b/tests/PHPStan/Rules/Properties/data/bug-14983-phpdoc.php index 8a6a1b3c8ea..0596c5e8083 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14983-phpdoc.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14983-phpdoc.php @@ -26,6 +26,18 @@ class ClassB extends ClassA { use TraitB; } +class BodyInitClassA { + use TraitA; + + public function __construct(string $property) { + $this->property = $property; + } +} + +class BodyInitClassB extends BodyInitClassA { + use TraitB; +} + class UninitializedFromTrait { use TraitA; } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14983-uninitialized.php b/tests/PHPStan/Rules/Properties/data/bug-14983-uninitialized.php index 3377daac2c9..cc0cbc3400b 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14983-uninitialized.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14983-uninitialized.php @@ -21,3 +21,15 @@ public function __construct(protected string $property) {} class ClassB extends ClassA { use TraitB; } + +class BodyInitClassA { + use TraitA; + + public function __construct(string $property) { + $this->property = $property; + } +} + +class BodyInitClassB extends BodyInitClassA { + use TraitB; +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-14983.php b/tests/PHPStan/Rules/Properties/data/bug-14983.php index 23366cfe6d7..6cdeb0b52d1 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14983.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14983.php @@ -22,6 +22,18 @@ class ClassB extends ClassA { use TraitB; } +class BodyInitClassA { + use TraitA; + + public function __construct(string $property) { + $this->property = $property; + } +} + +class BodyInitClassB extends BodyInitClassA { + use TraitB; +} + trait GrandTrait { use TraitA; }