33namespace PHPStan \Type ;
44
55use PHPStan \Analyser \NameScope ;
6+ use PHPStan \PhpDoc \Tag \TemplateTag ;
67use PHPStan \PhpDoc \TypeNodeResolver ;
8+ use PHPStan \PhpDocParser \Ast \PhpDoc \TemplateTagValueNode ;
79use PHPStan \PhpDocParser \Ast \Type \IdentifierTypeNode ;
810use PHPStan \PhpDocParser \Ast \Type \TypeNode ;
11+ use PHPStan \Type \Generic \TemplateTypeFactory ;
12+ use PHPStan \Type \Generic \TemplateTypeHelper ;
13+ use PHPStan \Type \Generic \TemplateTypeMap ;
14+ use PHPStan \Type \Generic \TemplateTypeScope ;
15+ use PHPStan \Type \Generic \TemplateTypeVariance ;
16+ use PHPStan \Type \Generic \TemplateTypeVarianceMap ;
17+ use function array_map ;
18+ use function array_values ;
19+ use function count ;
920
1021final class TypeAlias
1122{
1223
1324 private ?Type $ resolvedType = null ;
1425
26+ /**
27+ * @param TemplateTagValueNode[] $templateTagValueNodes
28+ */
1529 public function __construct (
1630 private TypeNode $ typeNode ,
1731 private NameScope $ nameScope ,
32+ private array $ templateTagValueNodes = [],
33+ private string $ aliasName = '' ,
1834 )
1935 {
2036 }
@@ -26,12 +42,107 @@ public static function invalid(): self
2642 return $ self ;
2743 }
2844
45+ /**
46+ * Returns the type with TemplateType placeholders for any declared template params.
47+ * For non-generic aliases this is the fully-resolved concrete type.
48+ */
2949 public function resolve (TypeNodeResolver $ typeNodeResolver ): Type
3050 {
31- return $ this ->resolvedType ??= $ typeNodeResolver ->resolve (
32- $ this ->typeNode ,
33- $ this ->nameScope ,
51+ if ($ this ->resolvedType !== null ) {
52+ return $ this ->resolvedType ;
53+ }
54+
55+ $ nameScope = $ this ->nameScope ;
56+
57+ if (count ($ this ->templateTagValueNodes ) > 0 ) {
58+ $ nameScope = $ this ->buildNameScopeWithTemplates ($ typeNodeResolver , $ nameScope );
59+ }
60+
61+ return $ this ->resolvedType = $ typeNodeResolver ->resolve ($ this ->typeNode , $ nameScope );
62+ }
63+
64+ /** Whether this alias was declared with type parameters (e.g. @phpstan-type Foo<T>). */
65+ public function isGeneric (): bool
66+ {
67+ return count ($ this ->templateTagValueNodes ) > 0 ;
68+ }
69+
70+ /**
71+ * @return TemplateTagValueNode[]
72+ */
73+ public function getTemplateTagValueNodes (): array
74+ {
75+ return $ this ->templateTagValueNodes ;
76+ }
77+
78+ /**
79+ * Resolves the alias body substituting concrete $args for each declared template parameter.
80+ *
81+ * @param Type[] $args Concrete types in the same order as the declared template params.
82+ */
83+ public function resolveWithArgs (TypeNodeResolver $ typeNodeResolver , array $ args ): Type
84+ {
85+ $ resolvedType = $ this ->resolve ($ typeNodeResolver );
86+
87+ if (count ($ this ->templateTagValueNodes ) === 0 ) {
88+ return $ resolvedType ;
89+ }
90+
91+ // Map each template param name to the supplied arg (or its declared default / upper bound).
92+ $ templateTypeMapTypes = [];
93+ foreach (array_values ($ this ->templateTagValueNodes ) as $ i => $ templateTagValueNode ) {
94+ if (isset ($ args [$ i ])) {
95+ $ templateTypeMapTypes [$ templateTagValueNode ->name ] = $ args [$ i ];
96+ } else {
97+ $ bound = $ templateTagValueNode ->bound !== null
98+ ? $ typeNodeResolver ->resolve ($ templateTagValueNode ->bound , $ this ->nameScope )
99+ : new MixedType (true );
100+ $ default = $ templateTagValueNode ->default !== null
101+ ? $ typeNodeResolver ->resolve ($ templateTagValueNode ->default , $ this ->nameScope )
102+ : null ;
103+ $ templateTypeMapTypes [$ templateTagValueNode ->name ] = $ default ?? $ bound ;
104+ }
105+ }
106+
107+ return TemplateTypeHelper::resolveTemplateTypes (
108+ $ resolvedType ,
109+ new TemplateTypeMap ($ templateTypeMapTypes ),
110+ TemplateTypeVarianceMap::createEmpty (),
111+ TemplateTypeVariance::createInvariant (),
34112 );
35113 }
36114
115+ /**
116+ * Builds a NameScope augmented with TemplateType placeholders for each declared template param,
117+ * so the alias body can reference them (e.g. `TFilter` resolves to a TemplateType).
118+ */
119+ private function buildNameScopeWithTemplates (TypeNodeResolver $ typeNodeResolver , NameScope $ nameScope ): NameScope
120+ {
121+ $ templateTags = [];
122+ foreach ($ this ->templateTagValueNodes as $ templateTagValueNode ) {
123+ $ templateTags [$ templateTagValueNode ->name ] = new TemplateTag (
124+ $ templateTagValueNode ->name ,
125+ $ templateTagValueNode ->bound !== null
126+ ? $ typeNodeResolver ->resolve ($ templateTagValueNode ->bound , $ nameScope )
127+ : new MixedType (true ),
128+ $ templateTagValueNode ->default !== null
129+ ? $ typeNodeResolver ->resolve ($ templateTagValueNode ->default , $ nameScope )
130+ : null ,
131+ TemplateTypeVariance::createInvariant (),
132+ );
133+ }
134+
135+ $ className = $ nameScope ->getClassNameForTypeAlias ();
136+ $ templateTypeScope = ($ className !== null && $ this ->aliasName !== '' )
137+ ? TemplateTypeScope::createWithTypeAlias ($ className , $ this ->aliasName )
138+ : TemplateTypeScope::createWithAnonymousFunction ();
139+
140+ $ templateTypeMap = new TemplateTypeMap (array_map (
141+ static fn (TemplateTag $ tag ): Type => TemplateTypeFactory::fromTemplateTag ($ templateTypeScope , $ tag ),
142+ $ templateTags ,
143+ ));
144+
145+ return $ nameScope ->withTemplateTypeMap ($ templateTypeMap , $ templateTags );
146+ }
147+
37148}
0 commit comments