Skip to content

Accept union of GenericObjectTypes by merging type parameters in GenericObjectType::accepts()#5546

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-holw8oh
Closed

Accept union of GenericObjectTypes by merging type parameters in GenericObjectType::accepts()#5546
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-holw8oh

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a union of generic objects like Container<TypeA>|Container<TypeB> is passed to a function expecting Container<T> (where T of TypeAorB), PHPStan infers T = TypeA|TypeB and then rejects the argument because Container<TypeA> is not a subtype of Container<TypeA|TypeB> under invariant generics. This PR makes GenericObjectType::accepts() handle this pattern by merging the union members' type parameters.

Changes

  • Modified GenericObjectType::accepts() in src/Type/Generic/GenericObjectType.php to try a fallback when a union type is not accepted by the per-member check: merge all union members into a single GenericObjectType by unioning their type parameters at each position, then re-check acceptance.
  • Added mergeUnionMembers() private helper that:
    • Verifies all union members are ObjectTypes with an ancestor matching $this's class name
    • Verifies all ancestors are GenericObjectTypes with the correct parameter count
    • Skips merge when any type parameter references unresolved template types (preserves correct rejection in cases like bug-5298)
    • Returns merged GenericObjectType or null if merge is not applicable
  • Updated phpstan-baseline.neon to account for new instanceof checks
  • Added regression tests covering:
    • Function calls (tests/PHPStan/Rules/Functions/data/bug-3136.php): union of two generics, three generics, subclass unions, multiple template parameters
    • Method calls and static method calls (tests/PHPStan/Rules/Methods/data/bug-3136.php)

Root cause

When Container<TypeA>|Container<TypeB> is passed to a function run(Container<T> $container), PHPStan's template inference unions the type parameters into T = TypeA|TypeB. The resolved parameter type becomes Container<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

  • Method calls: Covered by the fix since GenericObjectType::accepts() is used for all type acceptance contexts. Test added.
  • Static method calls: Same mechanism. Test added.
  • Subclass relationships: SubContainer<A>|SubContainer<B> passed to Container<T> — handled via getAncestorWithClassName. Test added.
  • Multiple template parameters: Pair<K1, V1>|Pair<K2, V2> passed to Pair<TKey, TValue> — handled naturally by per-position merging. Test added.
  • isSuperTypeOf: NOT changed — the fix only applies to the accepts() path (used for argument checking), not isSuperTypeOf() (used for strict type relationships). This is intentional and consistent with the existing $acceptsContext distinction in isSuperTypeOfInternal.
  • Template type parameters: The fix correctly skips when union members contain template types (verified by existing testBug5298).

Test

  • CallToFunctionParametersRuleTest::testBug3136 — tests the original issue (union of generic objects passed to a generic function parameter) plus subclass unions and multi-param generics
  • CallMethodsRuleTest::testBug3136 — tests instance method and static method calls with the same pattern
  • Verified all 11927 existing tests continue to pass
  • Verified PHPStan self-analysis passes

Fixes phpstan/phpstan#3136

…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.
@staabm staabm deleted the create-pull-request/patch-holw8oh branch April 26, 2026 19:53
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