Skip to content

Skip raw generic ObjectType method/property prototype when GenericObjectType ancestor is present in IntersectionType#5678

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

Skip raw generic ObjectType method/property prototype when GenericObjectType ancestor is present in IntersectionType#5678
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-i5tq0dv

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When an intersection type like Datagrid&DatagridInterface<Proxy> is formed (e.g. via assert($x instanceof Datagrid) on a DatagridInterface<Proxy>), calling a method like getPager() previously returned *NEVER* instead of the expected PagerInterface<Proxy>.

Root cause

IntersectionTypeMethodReflection::getVariants() intersects return types from all types in the intersection that provide the method. When a raw (unparameterized) ObjectType for a generic class is part of the intersection alongside a GenericObjectType of its ancestor:

  1. The raw ObjectType('Datagrid') resolves its template parameters to their bounds (e.g. T → ProxyQueryInterface), so getPager() returns PagerInterface<ProxyQueryInterface>
  2. The GenericObjectType('DatagridInterface', [Proxy]) resolves templates to the specific type args, so getPager() returns PagerInterface<Proxy>
  3. TypeCombinator::intersect(PagerInterface<ProxyQueryInterface>, PagerInterface<Proxy>) returns never because PagerInterface is invariant in T and ProxyQueryInterface !== Proxy

The underlying method is the same (inherited from DatagridInterface), so intersecting two different resolutions of the same method's return type is incorrect.

Changes

  • src/Type/IntersectionType.php:
    • In getUnresolvedMethodPrototype(): skip the raw ObjectType's prototype when a GenericObjectType ancestor that also declares the method is present in the intersection
    • In getUnresolvedInstancePropertyPrototype(): same fix for instance properties
    • In getUnresolvedStaticPropertyPrototype(): same fix for static properties
    • Added helper methods hasGenericAncestorWithMethodInIntersection() and hasGenericAncestorWithPropertyInIntersection() that check whether a raw generic ObjectType has a GenericObjectType ancestor in the intersection that also declares the member being accessed
  • phpstan-baseline.neon: added baseline entries for the instanceof checks on internal IntersectionType members (these are valid internal checks on concrete type components)

Analogous cases probed:

  • Instance properties: fixed (same pattern applied)
  • Static properties: fixed (same pattern applied)
  • Methods unique to the raw type (not on ancestor): verified they still resolve correctly (the skip condition requires the ancestor to also have the method)
  • Both types being GenericObjectType: verified this still works correctly (the skip only applies to raw ObjectType)
  • Multi-level inheritance (ChildDatagrid&DatagridInterface): tested and works

Test

Added tests/PHPStan/Analyser/nsrt/bug-5357.php with assertions covering:

  • The original reproducer from the issue (assert + method call)
  • Direct @param Datagrid&DatagridInterface<Proxy> annotation
  • Explicit @param Datagrid<Proxy> (already worked, sanity check)
  • Parent class generic intersection (ChildDatagrid&Datagrid<Proxy>)
  • Grandparent interface (ChildDatagrid&DatagridInterface<Proxy>)
  • Both types having explicit generics (Datagrid<Proxy>&DatagridInterface<Proxy>)
  • Method unique to the raw type not affected by the skip logic

Fixes phpstan/phpstan#5357

…ObjectType` ancestor is present in `IntersectionType`

- When an IntersectionType contains both a raw ObjectType for a generic class
  (e.g. Datagrid) and a GenericObjectType of its ancestor (e.g.
  DatagridInterface<Proxy>), skip the raw type's method/property prototype
  contribution if the ancestor also declares the member
- The raw ObjectType resolves templates to their bounds (e.g. ProxyQueryInterface),
  while the GenericObjectType ancestor resolves them to the specific type args
  (e.g. Proxy). Intersecting these two invariant generic return types produces
  `never`, which is incorrect
- Applied the same fix to instance property, static property, and method
  prototype resolution in IntersectionType
- Methods unique to the raw type (not declared on the ancestor) are still
  resolved normally

Closes phpstan/phpstan#5357
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