Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b973b5f
first pass
shmax Apr 1, 2026
eb67c21
enforce required generic params
shmax Apr 1, 2026
6fdb88f
more tests
shmax Apr 1, 2026
b27d26e
tests for invalud usage
shmax Apr 1, 2026
4f18071
fix regression
shmax Apr 1, 2026
95abc98
add more scenarios to test fixture
shmax Apr 1, 2026
f529784
remove empty patch
shmax Apr 1, 2026
590daa6
coalesce to empty array
shmax Apr 1, 2026
b8e0c55
lint
shmax Apr 1, 2026
a94b14b
fix use function ordering
shmax Apr 1, 2026
beedafb
fix generic alias resolution: skip alias path when name resolves to a…
shmax Apr 1, 2026
e023f92
remove resolveWithDefaults: bare generic alias usage restores old Tem…
shmax Apr 1, 2026
04c8aec
fix phpstan self-analysis errors: ignore property.notFound, add null …
shmax Apr 1, 2026
0195d7d
remove ignore
shmax Apr 1, 2026
95ae9e4
add property.notFound baseline entry for PHP < 8.0 (phpdoc-parser lac…
shmax Apr 1, 2026
2fd32f3
move property.notFound baseline entry to universal phpstan-baseline.n…
shmax Apr 1, 2026
89423b3
fix generic alias bare-usage resolution and data-provider parameter s…
shmax Apr 2, 2026
6c7322d
update baseline after rebase onto 2.1.x
shmax Apr 2, 2026
2321f59
add temp patches
shmax Apr 2, 2026
5726a40
use LateResolvableType
shmax Apr 2, 2026
6426482
remove
shmax Apr 2, 2026
fb9203b
lint
shmax Apr 2, 2026
d7fe98c
fix array vs list error
shmax Apr 2, 2026
e3aebe9
remove dead code
shmax Apr 2, 2026
ef31211
fold getRawGenericTypeAliasesUsage into getNonGenericObjectTypesWithG…
shmax Apr 15, 2026
106b589
update lock hash
shmax Apr 15, 2026
45ba91b
lint
shmax Apr 15, 2026
57a2395
remove unnecessary BC coverage from generic type alias resolution
shmax Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
first pass
  • Loading branch information
shmax committed Apr 15, 2026
commit b973b5f5ddc0ec9d9ad45b3777b2903f7166b299
Binary file removed .idea/icon.png
Binary file not shown.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@
"patches/DependencyChecker.patch",
"patches/Resolver.patch"
],
"symfony/console": [
"phpstan/phpdoc-parser": [
"patches/phpdoc-parser-generic-type-aliases.patch"
],
"symfony/console": [
"patches/OutputFormatter.patch"
]
}
Expand Down
Empty file.
2 changes: 1 addition & 1 deletion src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ public function resolveTypeAliasTags(PhpDocNode $phpDocNode, NameScope $nameScop
foreach ($phpDocNode->getTypeAliasTagValues($tagName) as $typeAliasTagValue) {
$alias = $typeAliasTagValue->alias;
$typeNode = $typeAliasTagValue->type;
$resolved[$alias] = new TypeAliasTag($alias, $typeNode, $nameScope);
$resolved[$alias] = new TypeAliasTag($alias, $typeNode, $nameScope, $typeAliasTagValue->templateTypes);
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/PhpDoc/Tag/TypeAliasTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\PhpDoc\Tag;

use PHPStan\Analyser\NameScope;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\TypeAlias;

Expand All @@ -12,10 +13,14 @@
final class TypeAliasTag
{

/**
* @param TemplateTagValueNode[] $templateTagValueNodes
*/
public function __construct(
private string $aliasName,
private TypeNode $typeNode,
private NameScope $nameScope,
private array $templateTagValueNodes = [],
)
{
}
Expand All @@ -30,6 +35,8 @@ public function getTypeAlias(): TypeAlias
return new TypeAlias(
$this->typeNode,
$this->nameScope,
$this->templateTagValueNodes,
$this->aliasName,
);
}

Expand Down
32 changes: 32 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeAliasResolver;
use PHPStan\Type\TypeAliasResolverProvider;
use PHPStan\Type\TypeAlias;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
Expand Down Expand Up @@ -829,6 +830,13 @@ static function (string $variance): TemplateTypeVariance {
return new ErrorType();
}

// Check for a generic type alias (e.g. ProviderRequest<AppraisalFilter>) before
// falling through to class-based generic resolution.
$genericTypeAlias = $this->findGenericTypeAlias($typeNode->type->name, $nameScope);
if ($genericTypeAlias !== null) {
return $genericTypeAlias->resolveWithArgs($this, $genericTypes);
}

$mainType = $this->resolveIdentifierTypeNode($typeNode->type, $nameScope);
$mainTypeObjectClassNames = $mainType->getObjectClassNames();
if (count($mainTypeObjectClassNames) > 1) {
Expand Down Expand Up @@ -1361,4 +1369,28 @@ private function getTypeAliasResolver(): TypeAliasResolver
return $this->typeAliasResolverProvider->getTypeAliasResolver();
}

/**
* Returns the TypeAlias for $name if it is a generic (parameterised) type alias
* visible in the current $nameScope, or null otherwise.
*/
private function findGenericTypeAlias(string $name, NameScope $nameScope): ?TypeAlias
{
if ($nameScope->shouldBypassTypeAliases()) {
return null;
}

$className = $nameScope->getClassNameForTypeAlias();
if ($className === null || !$this->getReflectionProvider()->hasClass($className)) {
return null;
}

$typeAliases = $this->getReflectionProvider()->getClass($className)->getTypeAliases();
if (!array_key_exists($name, $typeAliases)) {
return null;
}

$typeAlias = $typeAliases[$name];
return $typeAlias->isGeneric() ? $typeAlias : null;
}

}
5 changes: 5 additions & 0 deletions src/Type/Generic/TemplateTypeScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public static function createWithAnonymousFunction(): self
return new self(null, null);
}

public static function createWithTypeAlias(string $className, string $aliasName): self
{
return new self($className, '__typeAlias_' . $aliasName);
}

public static function createWithFunction(string $functionName): self
{
return new self(null, $functionName);
Expand Down
117 changes: 114 additions & 3 deletions src/Type/TypeAlias.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,34 @@
namespace PHPStan\Type;

use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDoc\TypeNodeResolver;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Generic\TemplateTypeFactory;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use function array_map;
use function array_values;
use function count;

final class TypeAlias
{

private ?Type $resolvedType = null;

/**
* @param TemplateTagValueNode[] $templateTagValueNodes
*/
public function __construct(
private TypeNode $typeNode,
private NameScope $nameScope,
private array $templateTagValueNodes = [],
private string $aliasName = '',
)
{
}
Expand All @@ -26,12 +42,107 @@ public static function invalid(): self
return $self;
}

/**
* Returns the type with TemplateType placeholders for any declared template params.
* For non-generic aliases this is the fully-resolved concrete type.
*/
public function resolve(TypeNodeResolver $typeNodeResolver): Type
{
return $this->resolvedType ??= $typeNodeResolver->resolve(
$this->typeNode,
$this->nameScope,
if ($this->resolvedType !== null) {
return $this->resolvedType;
}

$nameScope = $this->nameScope;

if (count($this->templateTagValueNodes) > 0) {
$nameScope = $this->buildNameScopeWithTemplates($typeNodeResolver, $nameScope);
}

return $this->resolvedType = $typeNodeResolver->resolve($this->typeNode, $nameScope);
}

/** Whether this alias was declared with type parameters (e.g. @phpstan-type Foo<T>). */
public function isGeneric(): bool
{
return count($this->templateTagValueNodes) > 0;
}

/**
* @return TemplateTagValueNode[]
*/
public function getTemplateTagValueNodes(): array
{
return $this->templateTagValueNodes;
}

/**
* Resolves the alias body substituting concrete $args for each declared template parameter.
*
* @param Type[] $args Concrete types in the same order as the declared template params.
*/
public function resolveWithArgs(TypeNodeResolver $typeNodeResolver, array $args): Type
{
$resolvedType = $this->resolve($typeNodeResolver);

if (count($this->templateTagValueNodes) === 0) {
return $resolvedType;
}

// Map each template param name to the supplied arg (or its declared default / upper bound).
$templateTypeMapTypes = [];
foreach (array_values($this->templateTagValueNodes) as $i => $templateTagValueNode) {
if (isset($args[$i])) {
$templateTypeMapTypes[$templateTagValueNode->name] = $args[$i];
} else {
$bound = $templateTagValueNode->bound !== null
? $typeNodeResolver->resolve($templateTagValueNode->bound, $this->nameScope)
: new MixedType(true);
$default = $templateTagValueNode->default !== null
? $typeNodeResolver->resolve($templateTagValueNode->default, $this->nameScope)
: null;
$templateTypeMapTypes[$templateTagValueNode->name] = $default ?? $bound;
}
}

return TemplateTypeHelper::resolveTemplateTypes(
$resolvedType,
new TemplateTypeMap($templateTypeMapTypes),
TemplateTypeVarianceMap::createEmpty(),
TemplateTypeVariance::createInvariant(),
);
}

/**
* Builds a NameScope augmented with TemplateType placeholders for each declared template param,
* so the alias body can reference them (e.g. `TFilter` resolves to a TemplateType).
*/
private function buildNameScopeWithTemplates(TypeNodeResolver $typeNodeResolver, NameScope $nameScope): NameScope
{
$templateTags = [];
foreach ($this->templateTagValueNodes as $templateTagValueNode) {
$templateTags[$templateTagValueNode->name] = new TemplateTag(
$templateTagValueNode->name,
$templateTagValueNode->bound !== null
? $typeNodeResolver->resolve($templateTagValueNode->bound, $nameScope)
: new MixedType(true),
$templateTagValueNode->default !== null
? $typeNodeResolver->resolve($templateTagValueNode->default, $nameScope)
: null,
TemplateTypeVariance::createInvariant(),
);
}

$className = $nameScope->getClassNameForTypeAlias();
$templateTypeScope = ($className !== null && $this->aliasName !== '')
? TemplateTypeScope::createWithTypeAlias($className, $this->aliasName)
: TemplateTypeScope::createWithAnonymousFunction();

$templateTypeMap = new TemplateTypeMap(array_map(
static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag),
$templateTags,
));

return $nameScope->withTemplateTypeMap($templateTypeMap, $templateTags);
}

}
Loading