From 9a714b759e1a29d59d4c11dcd383f7ecffd67fc9 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Sun, 28 Mar 2021 23:10:38 -0400 Subject: [PATCH] Fix #5496 - ensure params extended in properties are properly fleshed out --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 87 +++++++++++-------- .../Call/ClassTemplateParamCollector.php | 4 +- .../Type/TemplateStandinTypeReplacer.php | 4 + tests/MixinAnnotationTest.php | 2 + tests/Template/ClassTemplateExtendsTest.php | 26 ++++++ 5 files changed, 88 insertions(+), 35 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 09143db63e5..1fadd038ad4 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -7,6 +7,7 @@ use Psalm\DocComment; use Psalm\Exception\DocblockParseException; use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\AtomicPropertyFetchAnalyzer; use Psalm\Internal\FileManipulation\PropertyDocblockManipulator; use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\Comparator\UnionTypeComparator; @@ -837,20 +838,28 @@ public static function addContextProperties( true ); - $template_result = new \Psalm\Internal\Type\TemplateResult( - $class_template_params ?: [], - [] - ); - if ($class_template_params) { - $fleshed_out_type = TemplateStandinTypeReplacer::replace( - $fleshed_out_type, - $template_result, + $this_object_type = self::getThisObjectType( + $storage, + $fq_class_name + ); + + if (!$this_object_type instanceof Type\Atomic\TGenericObject) { + $type_params = []; + + foreach ($class_template_params as $type_map) { + $type_params[] = clone \array_values($type_map)[0]; + } + + $this_object_type = new Type\Atomic\TGenericObject($this_object_type->value, $type_params); + } + + $fleshed_out_type = AtomicPropertyFetchAnalyzer::localizePropertyType( $codebase, - null, - null, - null, - $class_context->self + $fleshed_out_type, + $this_object_type, + $storage, + $property_class_storage ); } @@ -1796,6 +1805,34 @@ private function analyzeClassMethod( return $method_analyzer; } + private static function getThisObjectType( + ClassLikeStorage $class_storage, + string $original_fq_classlike_name + ): Type\Atomic\TNamedObject { + if ($class_storage->template_types) { + $template_params = []; + + foreach ($class_storage->template_types as $param_name => $template_map) { + $key = array_keys($template_map)[0]; + + $template_params[] = new Type\Union([ + new Type\Atomic\TTemplateParam( + $param_name, + \reset($template_map), + $key + ) + ]); + } + + return new Type\Atomic\TGenericObject( + $original_fq_classlike_name, + $template_params + ); + } + + return new Type\Atomic\TNamedObject($original_fq_classlike_name); + } + public static function analyzeClassMethodReturnType( PhpParser\Node\Stmt\ClassMethod $stmt, MethodAnalyzer $method_analyzer, @@ -1834,28 +1871,10 @@ public static function analyzeClassMethodReturnType( $class_storage = $codebase->classlike_storage_provider->get($declaring_class_name); } - if ($class_storage->template_types) { - $template_params = []; - - foreach ($class_storage->template_types as $param_name => $template_map) { - $key = array_keys($template_map)[0]; - - $template_params[] = new Type\Union([ - new Type\Atomic\TTemplateParam( - $param_name, - \reset($template_map), - $key - ) - ]); - } - - $this_object_type = new Type\Atomic\TGenericObject( - $original_fq_classlike_name, - $template_params - ); - } else { - $this_object_type = new Type\Atomic\TNamedObject($original_fq_classlike_name); - } + $this_object_type = self::getThisObjectType( + $class_storage, + $original_fq_classlike_name + ); $class_template_params = ClassTemplateParamCollector::collect( $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php index df72cb867db..afedd81cd47 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php @@ -17,7 +17,9 @@ class ClassTemplateParamCollector { /** * @param lowercase-string $method_name - * @return array>|null + * @return array>|null + * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress LessSpecificReturnStatement */ public static function collect( Codebase $codebase, diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index ecbc94e222d..8e1cb58663f 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -824,6 +824,10 @@ public static function handleTemplateParamClassStandin( bool $was_single, bool &$had_template ) : array { + if ($atomic_type->defining_class === $calling_class) { + return [$atomic_type]; + } + $atomic_types = []; if ($input_type && !$template_result->readonly) { diff --git a/tests/MixinAnnotationTest.php b/tests/MixinAnnotationTest.php index 629287c4276..b444a904e43 100644 --- a/tests/MixinAnnotationTest.php +++ b/tests/MixinAnnotationTest.php @@ -277,6 +277,7 @@ abstract class FooChild extends Foo{} /** * @psalm-suppress MissingConstructor + * @psalm-suppress PropertyNotSetInConstructor */ final class FooGrandChild extends FooChild {} @@ -510,6 +511,7 @@ abstract class FooChild extends Foo{} /** * @psalm-suppress MissingConstructor + * @psalm-suppress PropertyNotSetInConstructor */ final class FooGrandChild extends FooChild {} diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index f6af49b45f1..5d69ae6a2b7 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -4400,6 +4400,32 @@ public function __construct() [], '7.4' ], + 'extendTemplatedClassString' => [ + ' */ + protected $c; + + /** @param class-string $c */ + public function __construct(string $c) { + $this->c = $c; + } + + /** @return class-string */ + abstract public function foo(): string; + } + + /** + * @template T2 of object + * @extends ParentClass + */ + class ChildClass extends ParentClass { + public function foo(): string { + return $this->c; + } + }' + ], ]; }