From 6dd1f21d6ce521324628d8caf0e2d6d833810531 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 13 Jan 2026 16:05:25 +0100 Subject: [PATCH] Resolve templates that reference other templates in a better way --- src/PhpDoc/Tag/TemplateTag.php | 14 ++++++++++++ src/Type/FileTypeMapper.php | 27 ++++++++++++++++++------ src/Type/Generic/TemplateTypeFactory.php | 5 +++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/PhpDoc/Tag/TemplateTag.php b/src/PhpDoc/Tag/TemplateTag.php index bafa555833..efcbe10a10 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -4,6 +4,7 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; +use PHPStan\Type\TypeTraverser; /** * @api @@ -41,4 +42,17 @@ public function getVariance(): TemplateTypeVariance return $this->variance; } + /** + * @param callable(Type $type, callable(Type): Type $traverse): Type $callback + */ + public function changeType(callable $callback): self + { + return new self( + $this->name, + TypeTraverser::map($this->bound, $callback), + $this->default !== null ? TypeTraverser::map($this->default, $callback) : null, + $this->variance, + ); + } + } diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index ad3d2ea68f..2423a4a4d9 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -25,6 +25,7 @@ use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateStrictMixedType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -283,13 +284,25 @@ public function getNameScope( continue; } - $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope); - $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); - $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap, $templateTags); - $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope); - $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); - $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap, $templateTags); - $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope); + $templatePhpDocNodes = $parent->getTemplatePhpDocNodes(); + $temporaryTemplateTypeMap = new TemplateTypeMap(array_map(static fn (array $tag) => TemplateTypeFactory::create($templateTypeScope, $tag[1]->name, new StrictMixedType(), TemplateTypeVariance::createInvariant()), $templatePhpDocNodes)); + + $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($templatePhpDocNodes, $nameScope->withTemplateTypeMap($temporaryTemplateTypeMap, [])); + $temporaryTemplateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + foreach ($templateTags as $k => $templateTag) { + $templateTags[$k] = $templateTag->changeType(static function (Type $type, $traverse) use ($temporaryTemplateTypeMap): Type { + if ($type instanceof TemplateStrictMixedType) { + $newType = $temporaryTemplateTypeMap->getType($type->getName()); + if ($newType === null) { + throw new ShouldNotHappenException(); + } + + return $newType; + } + + return $traverse($type); + }); + } $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); foreach (array_keys($templateTags) as $name) { $templateType = $templateTypeMap->getType($name); diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index fb3b2149bd..782c934886 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -19,6 +19,7 @@ use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StrictMixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; @@ -87,6 +88,10 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound, $default); } + if ($bound instanceof StrictMixedType && ($boundClass === StrictMixedType::class || $bound instanceof TemplateType)) { + return new TemplateStrictMixedType($scope, $strategy, $variance, $name, $bound, $default); + } + if ($bound instanceof MixedType && ($boundClass === MixedType::class || $bound instanceof TemplateType)) { return new TemplateMixedType($scope, $strategy, $variance, $name, $bound, $default); }