Accept union of GenericObjectTypes by merging type parameters in GenericObjectType::accepts()#5546
Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Closed
Conversation
…enericObjectType::accepts()` - When `GenericObjectType::accepts()` receives a union type and the per-member invariant check fails, attempt to merge the union members into a single GenericObjectType by unioning each type parameter position, then re-check. - This allows `Container<A>|Container<B>` to be accepted by `Container<A|B>` in the accepts context (function/method call parameter checking). - Skip the merge when any union member's type parameters reference unresolved template types, preserving correct rejection for cases like bug-5298 where `WorldProviderManagerEntry<T of WorldProvider>` should not collapse. - Also handles subclass unions and multiple template parameters. - Tested for function calls, method calls, and static method calls.
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
When a union of generic objects like
Container<TypeA>|Container<TypeB>is passed to a function expectingContainer<T>(whereT of TypeAorB), PHPStan infersT = TypeA|TypeBand then rejects the argument becauseContainer<TypeA>is not a subtype ofContainer<TypeA|TypeB>under invariant generics. This PR makesGenericObjectType::accepts()handle this pattern by merging the union members' type parameters.Changes
GenericObjectType::accepts()insrc/Type/Generic/GenericObjectType.phpto try a fallback when a union type is not accepted by the per-member check: merge all union members into a singleGenericObjectTypeby unioning their type parameters at each position, then re-check acceptance.mergeUnionMembers()private helper that:ObjectTypes with an ancestor matching$this's class nameGenericObjectTypes with the correct parameter countGenericObjectTypeornullif merge is not applicablephpstan-baseline.neonto account for newinstanceofcheckstests/PHPStan/Rules/Functions/data/bug-3136.php): union of two generics, three generics, subclass unions, multiple template parameterstests/PHPStan/Rules/Methods/data/bug-3136.php)Root cause
When
Container<TypeA>|Container<TypeB>is passed to a functionrun(Container<T> $container), PHPStan's template inference unions the type parameters intoT = TypeA|TypeB. The resolved parameter type becomesContainer<TypeA|TypeB>. The acceptance check then tests each union member individually:Container<TypeA|TypeB>.accepts(Container<TypeA>)fails because invariant generics require exact type parameter equality (TypeA|TypeB ≠ TypeA).The fix recognizes that when all members of a union are generic objects of the same class, their type parameters can be merged (unioned) to form a single generic object that exactly matches the expected type. The merge is restricted to concrete types only — when any member contains unresolved template types, the merge is skipped to avoid incorrect type collapsing.
Analogous cases probed
GenericObjectType::accepts()is used for all type acceptance contexts. Test added.SubContainer<A>|SubContainer<B>passed toContainer<T>— handled viagetAncestorWithClassName. Test added.Pair<K1, V1>|Pair<K2, V2>passed toPair<TKey, TValue>— handled naturally by per-position merging. Test added.isSuperTypeOf: NOT changed — the fix only applies to theaccepts()path (used for argument checking), notisSuperTypeOf()(used for strict type relationships). This is intentional and consistent with the existing$acceptsContextdistinction inisSuperTypeOfInternal.testBug5298).Test
CallToFunctionParametersRuleTest::testBug3136— tests the original issue (union of generic objects passed to a generic function parameter) plus subclass unions and multi-param genericsCallMethodsRuleTest::testBug3136— tests instance method and static method calls with the same patternFixes phpstan/phpstan#3136