diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index dc5bf1ab83..90227fbcf6 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -63,6 +63,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Helper\GetTemplateTypeType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -670,6 +671,19 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if (count($genericTypes) === 1) { return TypeUtils::toBenevolentUnion($genericTypes[0]); } + return new ErrorType(); + } elseif ($mainTypeName === 'template-type') { + if (count($genericTypes) === 3) { + $result = []; + foreach ($genericTypes[1]->getObjectClassNames() as $ancestorClassName) { + foreach ($genericTypes[2]->getConstantStrings() as $templateTypeName) { + $result[] = new GetTemplateTypeType($genericTypes[0], $ancestorClassName, $templateTypeName->getValue()); + } + } + + return TypeCombinator::union(...$result); + } + return new ErrorType(); } diff --git a/src/Type/Helper/GetTemplateTypeType.php b/src/Type/Helper/GetTemplateTypeType.php new file mode 100644 index 0000000000..00ea2ab97b --- /dev/null +++ b/src/Type/Helper/GetTemplateTypeType.php @@ -0,0 +1,83 @@ +type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('template-type<%s, %s, %s>', $this->type->describe($level), $this->ancestorClassName, $this->templateTypeName); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getTemplateType($this->ancestorClassName, $this->templateTypeName); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type, $this->ancestorClassName, $this->templateTypeName); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): Type + { + return new self( + $properties['type'], + $properties['ancestorClassName'], + $properties['templateTypeName'], + ); + } + +} diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 63ff28dbb8..5e78218f5a 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1176,6 +1176,12 @@ public function testBug5091(): void $this->assertNoErrors($errors); } + public function testDiscussion9053(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/discussion-9053.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/data/discussion-9053.php b/tests/PHPStan/Analyser/data/discussion-9053.php new file mode 100644 index 0000000000..fb9cca5cff --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-9053.php @@ -0,0 +1,110 @@ + + */ +class Model implements ModelInterface +{ + /** + * @var Child[] + */ + public array $children; + + public function getChildren(): array + { + return $this->children; + } +} + +/** + * @template-covariant T of ModelInterface + */ +interface ChildInterface { + /** + * @return T + */ + public function getModel(): ModelInterface; +} + + +/** + * @implements ChildInterface + */ +class Child implements ChildInterface +{ + public function __construct(private Model $model) + { + } + + public function getModel(): Model + { + return $this->model; + } +} + +/** + * @template-covariant T of ModelInterface + */ +class Helper +{ + /** + * @param T $model + */ + public function __construct(private ModelInterface $model) + {} + + /** + * @return template-type + */ + public function getFirstChildren(): ChildInterface + { + $firstChildren = $this->model->getChildren()[0] ?? null; + + if (!$firstChildren) { + throw new \RuntimeException('No first child found.'); + } + + return $firstChildren; + } +} + +class Other { + /** + * @template TChild of ChildInterface + * @template TModel of ModelInterface + * @param Helper $helper + * @return TChild + */ + public function getFirstChildren(Helper $helper): ChildInterface { + $child = $helper->getFirstChildren(); + assertType('TChild of Discussion9053\ChildInterface (method Discussion9053\Other::getFirstChildren(), argument)', $child); + + return $child; + } +} + +function (): void { + $model = new Model(); + $helper = new Helper($model); + assertType('Discussion9053\Helper', $helper); + $child = $helper->getFirstChildren(); + assertType('Discussion9053\Child', $child); + + $other = new Other(); + $child2 = $other->getFirstChildren($helper); + assertType('Discussion9053\Child', $child2); +};