Skip to content

Resolve template defaults referencing other templates in composite types - #6329

Merged
ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
calebdw:calebdw/push-ktxvvktqmorp
Sep 2, 2026
Merged

Resolve template defaults referencing other templates in composite types#6329
ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
calebdw:calebdw/push-ktxvvktqmorp

Conversation

@calebdw

@calebdw calebdw commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Hello!

ClassReflection::typeMapFromList() only substituted a sibling template reference when the default was exactly a bare TemplateType. So @template TResult = TRelated resolved correctly, but a default that merely contained the reference - = TRelated|null, = list<TRelated>, or an array shape - kept the unresolved template and leaked it into inferred types.

Traverse the type instead, substituting any nested reference to a template of the same class. Bounds are deliberately not descended into, because they may be self-referential (@template T of Foo<T>) and traversing them expands without bound.

The substitution has to apply to explicitly passed arguments as well as to $tag->getDefault(), because TypeNodeResolver already pads missing arguments with the raw defaults before this code runs.

Thanks!

@ondrejmirtes

Copy link
Copy Markdown
Member

Please follow what the CI says:

Fixture uses the mixed type on line 34 which requires PHP 8.0. Add a <?php // lint >= 8.0 comment on the first line so the fixture is skipped on older PHP versions in CI.

`ClassReflection::typeMapFromList()` only substituted a sibling template
reference when the default was exactly a bare `TemplateType`. So
`@template TResult = TRelated` resolved correctly, but a default that
merely contained the reference - `= TRelated|null`, `= list<TRelated>`,
or an array shape - kept the unresolved template and leaked it into
inferred types.

Traverse the type instead, substituting any nested reference to a
template of the same class. Bounds are deliberately not descended into,
because they may be self-referential (`@template T of Foo<T>`) and
traversing them expands without bound.

The substitution has to apply to explicitly passed arguments as well as
to `$tag->getDefault()`, because TypeNodeResolver already pads missing
arguments with the raw defaults before this code runs.
@calebdw
calebdw force-pushed the calebdw/push-ktxvvktqmorp branch from 5a56065 to db25abc Compare September 2, 2026 13:21
@ondrejmirtes

Copy link
Copy Markdown
Member

Thank you!

@ondrejmirtes
ondrejmirtes merged commit f13caf1 into phpstan:2.2.x Sep 2, 2026
783 of 791 checks passed
@calebdw
calebdw deleted the calebdw/push-ktxvvktqmorp branch September 2, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants