Skip to content

Commit c1c8d4d

Browse files
committed
use LateResolvableType
1 parent 5080e33 commit c1c8d4d

File tree

7 files changed

+292
-24
lines changed

7 files changed

+292
-24
lines changed

src/PhpDoc/TypeNodeResolver.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
use PHPStan\Type\ThisType;
104104
use PHPStan\Type\Type;
105105
use PHPStan\Type\TypeAlias;
106+
use PHPStan\Type\GenericTypeAliasType;
106107
use PHPStan\Type\TypeAliasResolver;
107108
use PHPStan\Type\TypeAliasResolverProvider;
108109
use PHPStan\Type\TypeCombinator;
@@ -849,7 +850,8 @@ static function (string $variance): TemplateTypeVariance {
849850
return new ErrorType();
850851
}
851852

852-
return $genericTypeAlias->resolveWithArgs($this, $genericTypes);
853+
$appType = $genericTypeAlias->createApplicationType($this, $genericTypes);
854+
return $appType->isResolvable() ? $appType->resolve() : $appType;
853855
}
854856

855857
$mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope);

src/Rules/Classes/LocalTypeAliasesCheck.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ public function checkInTraitDefinitionContext(ClassReflection $reflection): arra
223223

224224
foreach ($this->missingTypehintCheck->getRawGenericTypeAliasesUsage($resolvedType) as [$innerAliasName, $missingParams]) {
225225
if ($innerAliasName === $aliasName) {
226-
continue; // alias body contains its own template type placeholders — not a raw usage
226+
continue; // skip self-referential alias bodies (circular aliases are already reported separately)
227227
}
228228
$errors[] = RuleErrorBuilder::message(sprintf(
229229
'%s %s has type alias %s with generic type alias %s but does not specify its types: %s',

src/Rules/MissingTypehintCheck.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use PHPStan\Type\MixedType;
2323
use PHPStan\Type\ObjectType;
2424
use PHPStan\Type\Type;
25+
use PHPStan\Type\GenericTypeAliasType;
2526
use PHPStan\Type\TypeTraverser;
2627
use Traversable;
2728
use function array_filter;
@@ -179,20 +180,21 @@ public function getRawGenericTypeAliasesUsage(Type $type): array
179180
/** @var array<string, list<string>> $found */
180181
$found = [];
181182
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$found): Type {
182-
if ($type instanceof TemplateType) {
183-
$aliasName = $type->getScope()->getTypeAliasName();
184-
if ($aliasName !== null && $type->getDefault() === null) {
185-
$found[$aliasName][] = $type->getName();
183+
if ($type instanceof GenericTypeAliasType) {
184+
$missing = $type->getMissingRequiredParamNames();
185+
if ($missing !== []) {
186+
$found[$type->getAliasName()] = $missing;
186187
}
187-
return $type;
188188
}
189+
189190
return $traverse($type);
190191
});
191192

192193
$result = [];
193194
foreach ($found as $aliasName => $paramNames) {
194195
$result[] = [$aliasName, implode(', ', array_unique($paramNames))];
195196
}
197+
196198
return $result;
197199
}
198200

src/Type/GenericTypeAliasType.php

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
6+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
7+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8+
use PHPStan\Type\Generic\TemplateTypeHelper;
9+
use PHPStan\Type\Generic\TemplateTypeMap;
10+
use PHPStan\Type\Generic\TemplateTypeVariance;
11+
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
12+
use PHPStan\Type\Traits\LateResolvableTypeTrait;
13+
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
14+
use function array_map;
15+
use function array_merge;
16+
use function array_unique;
17+
use function count;
18+
use function implode;
19+
use function sprintf;
20+
21+
/**
22+
* Represents a generic @phpstan-type alias applied to concrete (or partially-resolved)
23+
* type arguments. For example, {@code Filter<int>} where {@code @phpstan-type Filter<TItem>} is
24+
* declared expands lazily to the alias body with TItem substituted.
25+
*
26+
* Mirrors the role of GenericObjectType for classes: GenericObjectType is a class constructor
27+
* applied to type args; GenericTypeAliasType is a type alias applied to type args.
28+
*
29+
* Implements LateResolvableType so TypeUtils::resolveLateResolvableTypes() expands it at the
30+
* right moment without leaking TemplateType placeholders to the rest of the type system.
31+
*/
32+
final class GenericTypeAliasType implements CompoundType, LateResolvableType
33+
{
34+
35+
use LateResolvableTypeTrait;
36+
use NonGeneralizableTypeTrait;
37+
38+
/**
39+
* @param list<string> $paramNames Ordered parameter names from the alias declaration.
40+
* @param list<Type> $args Supplied type arguments (may be shorter than paramNames
41+
* when trailing params are covered by defaults).
42+
* @param list<Type|null> $defaults Per-param declared default type; null when the param has no default.
43+
* @param list<Type> $boundFallbacks Per-param bound type used when both arg and default are absent.
44+
*/
45+
public function __construct(
46+
private readonly string $aliasName,
47+
private readonly Type $resolvedBody,
48+
private readonly array $paramNames,
49+
private readonly array $args,
50+
private readonly array $defaults,
51+
private readonly array $boundFallbacks,
52+
)
53+
{
54+
}
55+
56+
public function getAliasName(): string
57+
{
58+
return $this->aliasName;
59+
}
60+
61+
/**
62+
* Returns the names of required params (no declared default) that were not supplied as args.
63+
* A non-empty list means this is a "raw" usage of a generic alias that should be reported.
64+
*
65+
* @return list<string>
66+
*/
67+
public function getMissingRequiredParamNames(): array
68+
{
69+
$missing = [];
70+
foreach ($this->paramNames as $i => $name) {
71+
if (!isset($this->args[$i]) && $this->defaults[$i] === null) {
72+
$missing[] = $name;
73+
}
74+
}
75+
76+
return $missing;
77+
}
78+
79+
public function getReferencedClasses(): array
80+
{
81+
$classes = $this->resolvedBody->getReferencedClasses();
82+
foreach ($this->args as $arg) {
83+
$classes = array_merge($classes, $arg->getReferencedClasses());
84+
}
85+
86+
return array_unique($classes);
87+
}
88+
89+
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
90+
{
91+
$refs = [];
92+
foreach ($this->args as $arg) {
93+
$refs = array_merge($refs, $arg->getReferencedTemplateTypes($positionVariance));
94+
}
95+
96+
return $refs;
97+
}
98+
99+
public function equals(Type $type): bool
100+
{
101+
if (!$type instanceof self) {
102+
return false;
103+
}
104+
105+
if ($this->aliasName !== $type->aliasName || count($this->args) !== count($type->args)) {
106+
return false;
107+
}
108+
109+
foreach ($this->args as $i => $arg) {
110+
if (!$arg->equals($type->args[$i])) {
111+
return false;
112+
}
113+
}
114+
115+
return true;
116+
}
117+
118+
public function describe(VerbosityLevel $level): string
119+
{
120+
if ($this->args === []) {
121+
return $this->aliasName;
122+
}
123+
124+
return sprintf(
125+
'%s<%s>',
126+
$this->aliasName,
127+
implode(', ', array_map(static fn (Type $t) => $t->describe($level), $this->args)),
128+
);
129+
}
130+
131+
public function isResolvable(): bool
132+
{
133+
foreach ($this->args as $arg) {
134+
if (TypeUtils::containsTemplateType($arg)) {
135+
return false;
136+
}
137+
}
138+
139+
foreach ($this->paramNames as $i => $name) {
140+
if (!isset($this->args[$i]) && $this->defaults[$i] === null) {
141+
return false;
142+
}
143+
}
144+
145+
return true;
146+
}
147+
148+
protected function getResult(): Type
149+
{
150+
$map = [];
151+
foreach ($this->paramNames as $i => $name) {
152+
$map[$name] = $this->args[$i] ?? $this->defaults[$i] ?? $this->boundFallbacks[$i];
153+
}
154+
155+
return TemplateTypeHelper::resolveTemplateTypes(
156+
$this->resolvedBody,
157+
new TemplateTypeMap($map),
158+
TemplateTypeVarianceMap::createEmpty(),
159+
TemplateTypeVariance::createInvariant(),
160+
);
161+
}
162+
163+
/**
164+
* @param callable(Type): Type $cb
165+
*/
166+
public function traverse(callable $cb): Type
167+
{
168+
$newArgs = array_map($cb, $this->args);
169+
170+
foreach ($this->args as $i => $arg) {
171+
if ($arg !== $newArgs[$i]) {
172+
return new self(
173+
$this->aliasName,
174+
$this->resolvedBody,
175+
$this->paramNames,
176+
$newArgs,
177+
$this->defaults,
178+
$this->boundFallbacks,
179+
);
180+
}
181+
}
182+
183+
return $this;
184+
}
185+
186+
public function traverseSimultaneously(Type $right, callable $cb): Type
187+
{
188+
if (!$right instanceof self) {
189+
return $this;
190+
}
191+
192+
$newArgs = [];
193+
$changed = false;
194+
foreach ($this->args as $i => $arg) {
195+
$newArg = isset($right->args[$i]) ? $cb($arg, $right->args[$i]) : $arg;
196+
if ($newArg !== $arg) {
197+
$changed = true;
198+
}
199+
200+
$newArgs[] = $newArg;
201+
}
202+
203+
if (!$changed) {
204+
return $this;
205+
}
206+
207+
return new self(
208+
$this->aliasName,
209+
$this->resolvedBody,
210+
$this->paramNames,
211+
$newArgs,
212+
$this->defaults,
213+
$this->boundFallbacks,
214+
);
215+
}
216+
217+
public function toPhpDocNode(): TypeNode
218+
{
219+
if ($this->args === []) {
220+
return new IdentifierTypeNode($this->aliasName);
221+
}
222+
223+
return new GenericTypeNode(
224+
new IdentifierTypeNode($this->aliasName),
225+
array_map(static fn (Type $t) => $t->toPhpDocNode(), $this->args),
226+
);
227+
}
228+
229+
}
230+

src/Type/TypeAlias.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use function array_map;
1818
use function array_values;
1919
use function count;
20+
use function array_fill;
2021

2122
final class TypeAlias
2223
{
@@ -61,6 +62,11 @@ public function resolve(TypeNodeResolver $typeNodeResolver): Type
6162
return $this->resolvedType = $typeNodeResolver->resolve($this->typeNode, $nameScope);
6263
}
6364

65+
public function getAliasName(): string
66+
{
67+
return $this->aliasName;
68+
}
69+
6470
/** Whether this alias was declared with type parameters (e.g. @phpstan-type Foo<T>). */
6571
public function isGeneric(): bool
6672
{
@@ -75,6 +81,39 @@ public function getTemplateTagValueNodes(): array
7581
return $this->templateTagValueNodes;
7682
}
7783

84+
/**
85+
* Creates a GenericTypeAliasType for this alias with the given type arguments.
86+
*
87+
* @param list<Type> $args Concrete or partially-resolved type arguments in parameter order.
88+
*/
89+
public function createApplicationType(TypeNodeResolver $typeNodeResolver, array $args): GenericTypeAliasType
90+
{
91+
$resolvedBody = $this->resolve($typeNodeResolver);
92+
93+
$paramNames = [];
94+
$defaults = [];
95+
$boundFallbacks = [];
96+
97+
foreach (array_values($this->templateTagValueNodes) as $tvn) {
98+
$paramNames[] = $tvn->name;
99+
$defaults[] = $tvn->default !== null
100+
? $typeNodeResolver->resolve($tvn->default, $this->nameScope)
101+
: null;
102+
$boundFallbacks[] = $tvn->bound !== null
103+
? $typeNodeResolver->resolve($tvn->bound, $this->nameScope)
104+
: new MixedType(true);
105+
}
106+
107+
return new GenericTypeAliasType(
108+
$this->aliasName,
109+
$resolvedBody,
110+
$paramNames,
111+
$args,
112+
$defaults,
113+
$boundFallbacks,
114+
);
115+
}
116+
78117
/**
79118
* Resolves the alias body substituting concrete $args for each declared template parameter.
80119
*
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php declare(strict_types = 1);
2+
3+
// @deprecated Use GenericTypeAliasType instead.
4+
class_alias(\PHPStan\Type\GenericTypeAliasType::class, \PHPStan\Type\TypeAliasApplicationType::class);
5+

src/Type/UsefulTypeAliasResolver.php

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -115,24 +115,14 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope):
115115
try {
116116
$unresolvedAlias = $localTypeAliases[$aliasName];
117117

118-
// For a generic alias used bare (no type args provided), check whether every
119-
// declared template param has a default value. When they all do, we can
120-
// resolve immediately to a fully-concrete type by passing an empty args list to
121-
// resolveWithArgs() — which then falls back to each param's declared default.
122-
// When at least one param has no default, we keep the raw TemplateType
123-
// placeholders so that MissingTypehintCheck::getRawGenericTypeAliasesUsage()
124-
// can detect the bare-usage error ("does not specify its types: T").
118+
// For a generic alias used bare (no type args provided), build a GenericTypeAliasType
119+
// with empty args. If all params have declared defaults, isResolvable() will be true and
120+
// the type is immediately expanded to the concrete default form. When at least one param
121+
// has no default, the GenericTypeAliasType stays unresolved so that
122+
// MissingTypehintCheck::getRawGenericTypeAliasesUsage() can detect the bare-usage error.
125123
if ($unresolvedAlias->isGeneric()) {
126-
$allHaveDefaults = true;
127-
foreach ($unresolvedAlias->getTemplateTagValueNodes() as $tvn) {
128-
if ($tvn->default === null) {
129-
$allHaveDefaults = false;
130-
break;
131-
}
132-
}
133-
$resolvedAliasType = $allHaveDefaults
134-
? $unresolvedAlias->resolveWithArgs($this->typeNodeResolver, [])
135-
: $unresolvedAlias->resolve($this->typeNodeResolver);
124+
$appType = $unresolvedAlias->createApplicationType($this->typeNodeResolver, []);
125+
$resolvedAliasType = $appType->isResolvable() ? $appType->resolve() : $appType;
136126
} else {
137127
$resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver);
138128
}

0 commit comments

Comments
 (0)