diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index fead38b71d0..8e850570012 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -870,7 +870,12 @@ public static function addContextProperties( $property_type = $property_storage->type; if (!$property_type->isMixed() - && !$property_storage->is_promoted + && (!$property_storage->is_promoted + || (strtolower($fq_class_name) !== strtolower($property_class_name) + && isset($storage->declaring_method_ids['__construct']) + && strtolower( + $storage->declaring_method_ids['__construct']->fq_class_name, + ) === strtolower($fq_class_name))) && !$property_storage->has_default && !($property_type->isNullable() && $property_type->from_docblock) ) { @@ -881,7 +886,13 @@ public static function addContextProperties( ]); } } else { - if (!$property_storage->has_default && !$property_storage->is_promoted) { + if (!$property_storage->has_default + && (!$property_storage->is_promoted + || (strtolower($fq_class_name) !== strtolower($property_class_name) + && isset($storage->declaring_method_ids['__construct']) + && strtolower( + $storage->declaring_method_ids['__construct']->fq_class_name, + ) === strtolower($fq_class_name)))) { $property_type = new Union([new TMixed()], [ 'initialized' => false, 'from_property' => true, @@ -1097,6 +1108,13 @@ private function checkPropertyInitialization( continue; } + if ($property->is_promoted + && strtolower($property_class_name) !== $fq_class_name_lc + && isset($storage->declaring_method_ids['__construct']) + && strtolower($storage->declaring_method_ids['__construct']->fq_class_name) === $fq_class_name_lc) { + $property_is_initialized = false; + } + if ($property->has_default || $property_is_initialized) { continue; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index a357afac605..e78cfd8d230 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -81,6 +81,28 @@ public static function analyze( $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); $fq_class_name = $class_storage->name; + + if ($context->collect_initializations + && isset($stmt->name->name) + && $stmt->name->name === '__construct' + && isset($class_storage->declaring_method_ids['__construct'])) { + $construct_fq_class_name = $class_storage->declaring_method_ids['__construct']->fq_class_name; + $construct_class_storage = $codebase->classlike_storage_provider->get($construct_fq_class_name); + $construct_fq_class_name = $construct_class_storage->name; + + foreach ($construct_class_storage->properties as $property_name => $property_storage) { + if ($property_storage->is_promoted + && isset($context->vars_in_scope['$this->' . $property_name])) { + $context_type = $context->vars_in_scope['$this->' . $property_name]; + $context->vars_in_scope['$this->' . $property_name] = $context_type->setProperties( + [ + 'initialized_class' => $construct_fq_class_name, + 'initialized' => true, + ], + ); + } + } + } } elseif ($context->self) { if ($stmt->class->getFirst() === 'static' && isset($context->vars_in_scope['$this'])) { $fq_class_name = (string) $context->vars_in_scope['$this']; diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index 3d066210444..a5fd313413d 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -587,6 +587,22 @@ class A { 'MixedAssignment', ], ], + 'promotedPropertyNoExtendedConstructor' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], 'propertyWithoutTypeSuppressingIssueAndAssertingNull' => [ 'code' => ' 'PropertyNotSetInConstructor', ], + 'promotedPropertyNotSetInExtendedConstructor' => [ + 'code' => ' 'PropertyNotSetInConstructor', + 'ignored_issues' => [], + 'php_version' => '8.0', + ], 'nullableTypedPropertyNoConstructor' => [ 'code' => '