Repository navigation
Keep union members of different template types apart in TypeCombinator and infer naked template types from what other union members leave unexplained - #6821
Open
phpstan-bot wants to merge 1 commit into
Conversation
…or` and infer naked template types from what other union members leave unexplained
- TypeCombinator::processArrayTypes(): group template-typed arrays by name
and scope and process each group (and the non-template arrays) on its own.
Previously all-template-array unions were merged under the first template's
name, so `TKey of array|TValue of array` became `TKey of array` (and
`TA of array{a: int}|TB of array{a: int}` became `TB ...`). Template
`list`s and template arrays mixed with plain arrays no longer lose the
template either.
- TypeCombinator::compareTypesInUnion(): do not merge `iterable` members when
either is a template type (`TA of iterable|TB of iterable` stayed `iterable`).
- UnionType::accepts(): when the `array|Traversable` reading of a
TemplateIterableType is not accepted, check the template itself member by
member, as isSuperTypeOf() already does (`TA|TB` accepts `TA`).
- UnionType::inferTemplateTypes(): received members that no non-naked member
infers anything from are inferred against the naked template members, so
`T|Box<T>` receiving `stdClass|Box<array{int}>` gives `T = array{int}|stdClass`
instead of dropping `stdClass`.
- Ported every change to turbo-ext (TypeCombinator.cpp, UnionType.cpp).
- Probed non-array template bounds (int, int range, string, float, bool,
object, class, mixed, constant scalars, scalar unions): these already
stayed apart.
Together these let `@template T of TKey|TValue` with
`(T is object ? T : Box<T>)` resolve T again instead of reporting
"Unable to resolve the template type T".
ondrejmirtes
force-pushed
the
create-pull-request/patch-7jx5g0i
branch
from
October 27, 2026 18:45
5c97adf to
d90e11b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Since 2.3.0, calling a method with
@template T of TKey|TValueand@param (T is object ? T : Box<T>) $valuefrom code holding(TValue is object ? TValue : Box<TValue>)reportedUnable to resolve the template type Tand anargument.typeerror. 2.2.x was already wrong here: T silently fell back to its bound, so it reported areturn.typeerror at the call site instead. That error is gone now too.There were two separate problems:
TypeCombinator::union()merged template arrays of different template types into one.TKey of array|TValue of arraybecameTKey of array. So the else branchBox<T>of the conditional becameBox<T of TKey of array>, andBox<TValue>could not match it.UnionType::inferTemplateTypes()dropped the received members meant for a naked template member. ForT|Box<T>receivingX|Box<Y>,Box<T>inferredT = Yand the nakedTwas skipped, soXwas lost.Changes
src/Type/TypeCombinator.php(processArrayTypes(), newgroupArrayTypesByTemplateType()): arrays are grouped by template type (name and scope), with non-template arrays in their own group. Each group is processed separately, so only arrays of the same template are merged. CoversTemplateArrayType,TemplateConstantArrayType(TA of array{a: int}|TB of array{a: int}used to collapse toTB), templatelists, and template arrays next to plain arrays.src/Type/TypeCombinator.php(compareTypesInUnion()): theiterablemerge (string[]|int[]→(string|int)[]) no longer applies to template types.TA of iterable|TB of iterableused to become plainiterable.src/Type/UnionType.php(accepts()): when thearray|Traversablereading of aTemplateIterableTypeis not accepted, the template is checked member by member, the same wayisSuperTypeOf()already treats it. Without this,TA|TB(bothof iterable) did not accept itself once the union was kept apart. It also fixes(TKey&iterable)|...not being accepted asTKey|TValue, which 2.2.x got wrong too.src/Type/UnionType.php(inferTemplateTypes()): after the non-naked members have inferred, any remaining received member that no non-naked member infers anything from is inferred against the naked template members.turbo-ext/src/TypeCombinator.cpp,turbo-ext/src/UnionType.cpp: the same changes in the native mirrors.int,int<0, 5>,string,float,bool,object, a class,mixed, constant scalars,int|string).Root cause
Union simplification and inference both treated template-typed members by their bound. In the array and iterable merges, two different templates with array/iterable bounds were folded into one type. The template-array code kept only the first template's name, which is unsound. The iterable merge widened to plain
iterable, which loses the template. Template inference then could not match the narrowed conditional branch. Separately, the union inference rule "a naked template member only infers when no other member does" threw away argument parts that no other member could explain.Test
tests/PHPStan/Rules/Methods/data/bug-15431.php: the issue's playground code, nearly verbatim.CallMethodsRuleTest::testBug15431expects no errors.ReturnTypeRuleTest::testBug15431expects only the legitimate error insideunboxValue(): an objectTKeymight itself be a box. That error was also reported by 2.2.x.tests/PHPStan/Rules/Methods/data/bug-15431-iterable.php/ReturnTypeRuleTest::testBug15431Iterable: template iterables in unions are accepted as their templates.tests/PHPStan/Analyser/nsrt/bug-15431.php:T|Box<T>,(T is object ? T : Box<T>)andT|array<T>inference from mixed argumentsAll new tests fail without the fix. The full suite passes with and without the turbo extension loaded, and
make phpstanis clean.Fixes phpstan/phpstan#15431
🤖 Generated with Claude Code