Skip to content

Commit cab84d3

Browse files
committed
first pass
1 parent 51b0ae1 commit cab84d3

File tree

10 files changed

+316
-5
lines changed

10 files changed

+316
-5
lines changed

.idea/icon.png

-23.1 KB
Binary file not shown.

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@
128128
"patches/DependencyChecker.patch",
129129
"patches/Resolver.patch"
130130
],
131-
"symfony/console": [
131+
"phpstan/phpdoc-parser": [
132+
"patches/phpdoc-parser-generic-type-aliases.patch"
133+
],
134+
"symfony/console": [
132135
"patches/OutputFormatter.patch"
133136
]
134137
}

patches/phpdoc-parser-generic-type-aliases.patch

Whitespace-only changes.

src/PhpDoc/PhpDocNodeResolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScop
522522
foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) {
523523
$alias = $typeAliasTagValue->alias;
524524
$typeNode = $typeAliasTagValue->type;
525-
$resolved[$alias] = new TypeAliasTag($alias, $typeNode, $nameScope);
525+
$resolved[$alias] = new TypeAliasTag($alias, $typeNode, $nameScope, $typeAliasTagValue->templateTypes);
526526
}
527527
}
528528

src/PhpDoc/Tag/TypeAliasTag.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\PhpDoc\Tag;
44

55
use PHPStan\Analyser\NameScope;
6+
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
67
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
78
use PHPStan\Type\TypeAlias;
89

@@ -12,10 +13,14 @@
1213
final class TypeAliasTag
1314
{
1415

16+
/**
17+
* @param TemplateTagValueNode[] $templateTagValueNodes
18+
*/
1519
public function __construct(
1620
private string $aliasName,
1721
private TypeNode $typeNode,
1822
private NameScope $nameScope,
23+
private array $templateTagValueNodes = [],
1924
)
2025
{
2126
}
@@ -30,6 +35,8 @@ public function getTypeAlias(): TypeAlias
3035
return new TypeAlias(
3136
$this->typeNode,
3237
$this->nameScope,
38+
$this->templateTagValueNodes,
39+
$this->aliasName,
3340
);
3441
}
3542

src/PhpDoc/TypeNodeResolver.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
use PHPStan\Type\Type;
108108
use PHPStan\Type\TypeAliasResolver;
109109
use PHPStan\Type\TypeAliasResolverProvider;
110+
use PHPStan\Type\TypeAlias;
110111
use PHPStan\Type\TypeCombinator;
111112
use PHPStan\Type\TypeTraverser;
112113
use PHPStan\Type\TypeUtils;
@@ -863,6 +864,13 @@ static function (string $variance): TemplateTypeVariance {
863864
return new ErrorType();
864865
}
865866

867+
// Check for a generic type alias (e.g. ProviderRequest<AppraisalFilter>) before
868+
// falling through to class-based generic resolution.
869+
$genericTypeAlias = $this->findGenericTypeAlias($typeNode->type->name, $nameScope);
870+
if ($genericTypeAlias !== null) {
871+
return $genericTypeAlias->resolveWithArgs($this, $genericTypes);
872+
}
873+
866874
$mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope);
867875
$mainTypeObjectClassNames = $mainType->getObjectClassNames();
868876
if (count($mainTypeObjectClassNames) > 1) {
@@ -1395,4 +1403,28 @@ private function getTypeAliasResolver(): TypeAliasResolver
13951403
return $this->typeAliasResolverProvider->getTypeAliasResolver();
13961404
}
13971405

1406+
/**
1407+
* Returns the TypeAlias for $name if it is a generic (parameterised) type alias
1408+
* visible in the current $nameScope, or null otherwise.
1409+
*/
1410+
private function findGenericTypeAlias(string $name, NameScope $nameScope): ?TypeAlias
1411+
{
1412+
if ($nameScope->shouldBypassTypeAliases()) {
1413+
return null;
1414+
}
1415+
1416+
$className = $nameScope->getClassNameForTypeAlias();
1417+
if ($className === null || !$this->getReflectionProvider()->hasClass($className)) {
1418+
return null;
1419+
}
1420+
1421+
$typeAliases = $this->getReflectionProvider()->getClass($className)->getTypeAliases();
1422+
if (!array_key_exists($name, $typeAliases)) {
1423+
return null;
1424+
}
1425+
1426+
$typeAlias = $typeAliases[$name];
1427+
return $typeAlias->isGeneric() ? $typeAlias : null;
1428+
}
1429+
13981430
}

src/Type/Generic/TemplateTypeScope.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ public static function createWithAnonymousFunction(): self
1212
return new self(null, null);
1313
}
1414

15+
public static function createWithTypeAlias(string $className, string $aliasName): self
16+
{
17+
return new self($className, '__typeAlias_' . $aliasName);
18+
}
19+
1520
public static function createWithFunction(string $functionName): self
1621
{
1722
return new self(null, $functionName);

src/Type/TypeAlias.php

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,34 @@
33
namespace PHPStan\Type;
44

55
use PHPStan\Analyser\NameScope;
6+
use PHPStan\PhpDoc\Tag\TemplateTag;
67
use PHPStan\PhpDoc\TypeNodeResolver;
8+
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
79
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
810
use 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

1021
final 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

Comments
 (0)