Resolve generic parameter defaults that reference other template parameters#5304
Conversation
|
You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x. |
8f929b8 to
dd69bbf
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes generic template default resolution when a template parameter’s default references another template parameter, ensuring defaults are substituted with the concrete provided types during PHPDoc generic parsing and class reflection mapping.
Changes:
- Resolve template defaults that reference other template parameters in
TypeNodeResolverwhen fewer type args are provided than template params. - Add defensive substitution of such defaults in
ClassReflection::typeMapFromList()/typeMapToList(). - Add an NSRT test covering
Codec<DI, EI, DO of EI = EI, EO of DI = DI>with partial and full type argument lists.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/PHPStan/Analyser/nsrt/template-default-referring-other.php | Adds coverage for template defaults referencing other template params with partial/full generic args. |
| src/Reflection/ClassReflection.php | Substitutes defaults referencing other template params when converting between type lists and template maps. |
| src/PhpDoc/TypeNodeResolver.php | Substitutes template-default types that reference other template params during generic type resolution. |
Comments suppressed due to low confidence (1)
src/PhpDoc/TypeNodeResolver.php:887
- Default-type substitution is only applied in a single pass using a
$resolveMapbuilt before any substitutions. If a template default refers to another template param whose value itself comes from a default (or from another substitution), the nested reference can remain unresolved. Consider resolving defaults iteratively (or rebuilding the resolve map as defaults are resolved) so chained references between template defaults are fully substituted.
for ($i = $providedCount; $i < count($genericTypes); $i++) {
$genericTypes[$i] = TypeTraverser::map($genericTypes[$i], static function (Type $type, callable $traverse) use ($resolveMap, $mainTypeClassName): Type {
if ($type instanceof TemplateType && $type->getScope()->getClassName() === $mainTypeClassName && isset($resolveMap[$type->getName()])) {
return $resolveMap[$type->getName()];
}
return $traverse($type);
});
}
}
if (in_array($mainTypeClassName, [
Traversable::class,
IteratorAggregate::class,
Iterator::class,
], true)) {
if (count($genericTypes) === 1) {
return new GenericObjectType($mainTypeClassName, [
new MixedType(true),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/PHPStan/Analyser/nsrt/template-default-referring-other.php
Outdated
Show resolved
Hide resolved
b2554dd to
93dd44c
Compare
93dd44c to
2461ca9
Compare
|
@ondrejmirtes Claude has considered your comment and has chosen much simpler approach. |
…meters When a template parameter has a default referencing another template parameter (e.g. `@template DO of EI = EI`), the default was left as an unresolved TemplateType instead of being substituted with the concrete type provided for the referenced parameter. The root cause is that resolveTemplateTypes returns resolved values without re-invoking the callback, so transitive TemplateType references in the standins map are never resolved. Fixed in ClassReflection::typeMapFromList — the single gateway where all type arguments enter the standins map. After building the map, any entry that is a TemplateType referencing another entry in the same class is resolved to that entry's concrete type. Co-Authored-By: Claude Code
2461ca9 to
4281e33
Compare
|
Perfect 👍 |
Summary
When a template parameter has a default referencing another template parameter (e.g.
@template DO of EI = EI), the default was left as an unresolved TemplateType instead of being substituted with the concrete type.Root cause
TemplateTypeHelper::resolveTemplateTypesreturns resolved values ($newType) without re-invoking the callback. When the standins map hasDO → TemplateType(EI)andEI → MoneyValue, resolvingDOyieldsTemplateType(EI)— but that is never resolved further toMoneyValue. This is because TypeTraverser's$traversetraverses inner types (bound, default) but does not re-invoke the callback on the type itself, so$traverse($newType)wouldn't help either.Fix
Single-point fix in
ClassReflection::typeMapFromList— the gateway where all type arguments enter the standins map (active template type map). After building the map, any entry that is a TemplateType referencing another entry in the same class scope is resolved to that entry's concrete type.Why
typeMapFromListand not elsewhere:GenericObjectType::getClassReflection()→withTypes())TypeNodeResolver,resolveTemplateTypes, orFileTypeMapperresolveTemplateTypesbehaviorExample
Test plan
tests/PHPStan/Analyser/nsrt/template-default-referring-other.phppassestemplate-defaulttests passCo-Authored-By: Claude Code