diff --git a/src/Psalm/Internal/Type/UnionTemplateHandler.php b/src/Psalm/Internal/Type/UnionTemplateHandler.php index f8a303053..d9f7ffed3 100644 --- a/src/Psalm/Internal/Type/UnionTemplateHandler.php +++ b/src/Psalm/Internal/Type/UnionTemplateHandler.php @@ -108,8 +108,7 @@ class UnionTemplateHandler } if ($atomic_type instanceof Atomic\TTemplateParam - && ($param_name_key = strpos($key, '&') ? $key : $atomic_type->param_name) - && isset($template_result->template_types[$param_name_key][$atomic_type->defining_class]) + && isset($template_result->template_types[$atomic_type->param_name][$atomic_type->defining_class]) ) { $a = self::handleTemplateParamStandin( $atomic_type, @@ -489,6 +488,38 @@ class UnionTemplateHandler $param_name_key = $key; } + $extra_types = []; + + if ($atomic_type->extra_types) { + foreach ($atomic_type->extra_types as $extra_type) { + $extra_type = self::replaceTemplateTypesWithStandins( + new \Psalm\Type\Union([$extra_type]), + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_upper_bound, + $depth + 1 + ); + + if ($extra_type->isSingle()) { + $extra_type = array_values($extra_type->getAtomicTypes())[0]; + + if ($extra_type instanceof Atomic\TNamedObject + || $extra_type instanceof Atomic\TTemplateParam + || $extra_type instanceof Atomic\TIterable + || $extra_type instanceof Atomic\TObjectWithProperties + ) { + $extra_types[] = $extra_type; + } + } + } + } + if ($replace) { $atomic_types = []; @@ -675,6 +706,10 @@ class UnionTemplateHandler ]; } + foreach ($atomic_types as $atomic_type) { + $atomic_type->extra_types = $extra_types; + } + return $atomic_types; } diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index 4e7ccb2fe..d5ebe6f7a 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -4388,6 +4388,86 @@ class ClassTemplateExtendsTest extends TestCase abstract public function foo($param): void; }' ], + 'extendAndImplementedTemplatedProperty' => [ + 'foo = $foo; + } + } + + /** @extends ATestCase */ + class BTestCase extends ATestCase { + public function getFoo(): B { + return $this->foo; + } + } + + new BTestCase(new BMock());' + ], + 'extendAndImplementedTemplatedIntersectionProperty' => [ + 'obj = $obj; + } + } + + /** @extends ATestCase */ + class BTestCase extends ATestCase { + public function getFoo(): void { + $this->obj->foo(); + } + }' + ], + 'extendAndImplementedTemplatedIntersectionReceives' => [ + 'obj = $obj; + } + } + + /** @extends ATestCase */ + class BTestCase extends ATestCase {} + + new BTestCase(new BMock());' + ], ]; }