diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php index 1946ed558..071a8a5fc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/PropertyAssignmentAnalyzer.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt\PropertyProperty; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\PropertyFetchAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\TypeAnalyzer; use Psalm\Internal\FileManipulation\FileManipulationBuffer; @@ -600,12 +601,12 @@ class PropertyAssignmentAnalyzer } } - $class_storage = $codebase->classlike_storage_provider->get($declaring_property_class); + $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class); $property_storage = null; - if (isset($class_storage->properties[$prop_name])) { - $property_storage = $class_storage->properties[$prop_name]; + if (isset($declaring_class_storage->properties[$prop_name])) { + $property_storage = $declaring_class_storage->properties[$prop_name]; if ($property_storage->deprecated) { if (IssueBuffer::accepts( @@ -683,7 +684,7 @@ class PropertyAssignmentAnalyzer )) { // fall through } - } elseif ($class_storage->mutation_free) { + } elseif ($declaring_class_storage->mutation_free) { $visitor = new \Psalm\Internal\TypeVisitor\ImmutablePropertyAssignmentVisitor( $statements_analyzer, $stmt @@ -725,7 +726,7 @@ class PropertyAssignmentAnalyzer $class_property_type, $fq_class_name, $lhs_type_part, - $class_storage->parent_class + $declaring_class_storage->parent_class ); $class_property_type = \Psalm\Internal\Codebase\Methods::localizeType( @@ -735,6 +736,18 @@ class PropertyAssignmentAnalyzer $declaring_property_class ); + if ($lhs_type_part instanceof Type\Atomic\TGenericObject) { + $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); + + $class_property_type = PropertyFetchAnalyzer::localizePropertyType( + $codebase, + $class_property_type, + $lhs_type_part, + $class_storage, + $declaring_class_storage + ); + } + $assignment_value_type = \Psalm\Internal\Codebase\Methods::localizeType( $codebase, $assignment_value_type, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 3b879a094..543bcba92 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -2079,7 +2079,7 @@ class CallAnalyzer * @return array> * @param array> $existing_template_types */ - private static function getTemplateTypesForCall( + public static function getTemplateTypesForCall( ClassLikeStorage $declaring_class_storage = null, ClassLikeStorage $calling_class_storage = null, array $existing_template_types = [] diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/PropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/PropertyFetchAnalyzer.php index bb5c4ef63..9f441a4df 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/PropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/PropertyFetchAnalyzer.php @@ -6,6 +6,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\CodeLocation; @@ -28,6 +29,7 @@ use Psalm\Issue\UndefinedThisPropertyFetch; use Psalm\Issue\UninitializedProperty; use Psalm\IssueBuffer; use Psalm\Type; +use Psalm\Storage\ClassLikeStorage; use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; @@ -825,32 +827,13 @@ class PropertyFetchAnalyzer ); if ($lhs_type_part instanceof TGenericObject) { - if ($class_storage->template_types) { - $class_template_params = []; - - $reversed_class_template_types = array_reverse(array_keys($class_storage->template_types)); - - $provided_type_param_count = count($lhs_type_part->type_params); - - foreach ($reversed_class_template_types as $i => $type_name) { - if (isset($lhs_type_part->type_params[$provided_type_param_count - 1 - $i])) { - $class_template_params[$type_name][$declaring_class_storage->name] = [ - $lhs_type_part->type_params[$provided_type_param_count - 1 - $i], - 0 - ]; - } else { - $class_template_params[$type_name][$declaring_class_storage->name] = [ - Type::getMixed(), - 0 - ]; - } - } - - $class_property_type->replaceTemplateTypesWithArgTypes( - $class_template_params, - $codebase - ); - } + $class_property_type = self::localizePropertyType( + $codebase, + $class_property_type, + $lhs_type_part, + $class_storage, + $declaring_class_storage + ); } } @@ -919,6 +902,47 @@ class PropertyFetchAnalyzer } } + public static function localizePropertyType( + \Psalm\Codebase $codebase, + Type\Union $class_property_type, + TGenericObject $lhs_type_part, + ClassLikeStorage $calling_class_storage, + ClassLikeStorage $declaring_class_storage + ) : Type\Union { + $template_types = CallAnalyzer::getTemplateTypesForCall( + $declaring_class_storage, + $calling_class_storage, + $calling_class_storage->template_types ?: [] + ); + + if ($template_types) { + $reversed_class_template_types = array_reverse(array_keys($template_types)); + + $provided_type_param_count = count($lhs_type_part->type_params); + + foreach ($reversed_class_template_types as $i => $type_name) { + if (isset($lhs_type_part->type_params[$provided_type_param_count - 1 - $i])) { + $template_types[$type_name][$declaring_class_storage->name] = [ + $lhs_type_part->type_params[$provided_type_param_count - 1 - $i], + 0 + ]; + } else { + $template_types[$type_name][$declaring_class_storage->name] = [ + Type::getMixed(), + 0 + ]; + } + } + + $class_property_type->replaceTemplateTypesWithArgTypes( + $template_types, + $codebase + ); + } + + return $class_property_type; + } + private static function processTaints( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\PropertyFetch $stmt, diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 7756c132b..1d6f40888 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -1201,6 +1201,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse $storage->has_docblock_issues = true; } } else { + /** @psalm-suppress PropertyTypeCoercion due to a Psalm bug */ $storage->template_types[$template_name][$fq_classlike_name] = [Type::getMixed()]; } diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 77f949eda..524d3f798 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -327,7 +327,7 @@ class ClassLikeStorage public $overridden_property_ids = []; /** - * @var array>|null + * @var array>|null */ public $template_types; diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index 519224052..8c004ec34 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -3507,6 +3507,60 @@ class ClassTemplateExtendsTest extends TestCase } }' ], + 'setInheritedTemplatedPropertyOutsideClass' => [ + 'value = $value; + } + } + + /** @extends Watcher */ + class IntWatcher extends Watcher {} + + $watcher = new IntWatcher(0); + $watcher->value = 10;' + ], + 'setRetemplatedPropertyOutsideClass' => [ + 'value = $value; + } + } + + /** + * @template T as scalar + * @extends Watcher + */ + class Watcher2 extends Watcher {} + + /** @psalm-var Watcher2 $watcher */ + $watcher = new Watcher2(0); + $watcher->value = 10;' + ], ]; } diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index f2ebba417..26b8a0c7e 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -2527,6 +2527,29 @@ class ClassTemplateTest extends TestCase if (null !== $a->filter) {}' ], + 'setTemplatedPropertyOutsideClass' => [ + 'value = $value; + } + } + + /** @psalm-var Watcher $watcher */ + $watcher = new Watcher(0); + $watcher->value = 0;' + ], ]; }