diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index eb79d3a1770..45816900120 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1597,6 +1597,14 @@ private function analyzeClassMethod( if ($return_type && $class_storage->template_type_extends) { $generic_params = []; + $declaring_method_id = $codebase->methods->getDeclaringMethodId($analyzed_method_id); + + if ($declaring_method_id) { + $declaring_class_name = explode('::', $declaring_method_id)[0]; + + $class_storage = $codebase->classlike_storage_provider->get($declaring_class_name); + } + $class_template_params = MethodCallAnalyzer::getClassTemplateParams( $codebase, $class_storage, diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index afe2b84ad81..4deb4090d49 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -5,6 +5,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\CommentAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\TraitAnalyzer; use Psalm\Internal\Analyzer\TypeAnalyzer; @@ -161,6 +162,31 @@ public static function analyze( $local_return_type = $source->getLocalReturnType($storage->return_type); + if ($storage instanceof \Psalm\Storage\MethodStorage) { + list($fq_class_name, $method_name) = explode('::', $cased_method_id); + + if ($fq_class_name !== $context->self) { + $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); + + $found_generic_params = MethodCallAnalyzer::getClassTemplateParams( + $codebase, + $class_storage, + $fq_class_name, + strtolower($method_name), + null, + null + ); + + if ($found_generic_params) { + $local_return_type = clone $local_return_type; + + $local_return_type->replaceTemplateTypesWithArgTypes( + $found_generic_params + ); + } + } + } + if ($local_return_type->isGenerator() && $storage->has_yield) { return null; } diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index 2814ca7386a..67df229b2a4 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -886,7 +886,7 @@ public function rewind(): void {} public function valid(): bool { return false; } }', ], - 'traitUse' => [ + 'traitUseNotExtended' => [ ' [ + ' + */ + interface ICollection extends \IteratorAggregate { + /** @return \Traversable */ + public function getIterator(); + } + + class Collection implements ICollection { + /** @var array */ + private $data; + public function __construct(array $data) { + $this->data = $data; + } + /** @psalm-suppress LessSpecificImplementedReturnType */ + public function getIterator(): \Traversable { + return new \ArrayIterator($this->data); + } + } + + /** @var ICollection */ + $c = new Collection(["a" => 1]); + + foreach ($c->getIterator() as $k => $v) { atan($v); strlen($k); }', + ], ]; } diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 0c8798a9ea3..a265a20be28 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -84,7 +84,7 @@ public function bar() { * @template T as object */ class Foo { - /** @var T::class */ + /** @var class-string */ public $T; /** @@ -322,11 +322,11 @@ function takesCollectionOfItems(ArrayCollection $i): void {} ], 'noRepeatedTypeException' => [ ' */ private $type; @@ -334,14 +334,14 @@ class Foo private $items; /** - * @param class-string $type - * @template-typeof T $type + * @param class-string $type */ public function __construct(string $type) { if (!in_array($type, [A::class, B::class], true)) { throw new \InvalidArgumentException; } + $this->type = $type; $this->items = []; } @@ -372,6 +372,7 @@ public function add($item): void */ private function ensureFoo(array $items): Foo { + /** @var class-string */ $type = $items[0] instanceof A ? A::class : B::class; return new Foo($type); } @@ -441,36 +442,6 @@ public function getIterator(): \Traversable { foreach ($c as $k => $v) { atan($v); strlen($k); }', ], - 'templatedInterfaceGetIteratorIteration' => [ - ' */ - public function getIterator(); - } - - class Collection implements ICollection { - /** @var array */ - private $data; - public function __construct(array $data) { - $this->data = $data; - } - /** @psalm-suppress LessSpecificImplementedReturnType */ - public function getIterator(): \Traversable { - return new \ArrayIterator($this->data); - } - } - - /** @var ICollection */ - $c = new Collection(["a" => 1]); - - foreach ($c->getIterator() as $k => $v) { atan($v); strlen($k); }', - ], 'allowTemplatedIntersectionToExtend' => [ ' $obj * - * @return array + * @return array */ public function appendProperty(string $obj): array {