Skip to content

Fix #11687: Method call on unioned class type analysis is broken#4960

Open
phpstan-bot wants to merge 4 commits into2.1.xfrom
create-pull-request/patch-8y7mxaj
Open

Fix #11687: Method call on unioned class type analysis is broken#4960
phpstan-bot wants to merge 4 commits into2.1.xfrom
create-pull-request/patch-8y7mxaj

Conversation

@phpstan-bot
Copy link
Collaborator

@phpstan-bot phpstan-bot commented Feb 16, 2026

Summary

When calling a static method that returns static on a union of class types (e.g., $clUnioned::retStatic() where $clUnioned is class-string<A>|class-string<X>), PHPStan incorrectly resolved the static return type to the entire union instead of just the class that declares the method. This caused A::retStatic() (returning static) to resolve as A|X instead of just A.

Changes

  • Modified src/Type/UnionType.php in getUnresolvedMethodPrototype(): stopped calling withCalledOnType($this) for non-template union types. Each member type now keeps its own called-on type, so static resolves correctly per-class.
  • Template union types (TemplateUnionType, TemplateBenevolentUnionType) still pass the full union as the called-on type, preserving template type identity (e.g., T of DateTime|DateTimeImmutable stays as the template type rather than being split).
  • Updated expected assertion in tests/PHPStan/Analyser/nsrt/static-late-binding.php from bool|StaticLateBinding\A|StaticLateBinding\X to bool|StaticLateBinding\A.
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-11687.php.

Root cause

In UnionType::getUnresolvedMethodPrototype(), each member type's method prototype had its calledOnType overridden with the full union via ->withCalledOnType($this). Later, in CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType(), when a StaticType was encountered in a method's return type, it was replaced with $this->calledOnType — the full union. This meant A::retStatic() returning static(A) would become A|X instead of just A.

The fix conditionally applies withCalledOnType($this) only when the union is a TemplateType (i.e., TemplateUnionType or TemplateBenevolentUnionType), where preserving the full union/template identity is correct. For plain unions, each member keeps its own type as the called-on type.

Test

Added tests/PHPStan/Analyser/nsrt/bug-11687.php which tests:

  • A::retStaticConst() returns int
  • X::retStaticConst() returns bool
  • $clUnioned::retStaticConst() returns bool|int (correct, no static involved)
  • A::retStatic() returns A (uses @return static)
  • X::retStatic() returns bool
  • $clUnioned::retStatic() returns bool|A (previously was bool|A|X)

Fixes phpstan/phpstan#11687

Closes phpstan/phpstan#12562

phpstan-bot and others added 4 commits February 16, 2026 20:38
- In UnionType::getUnresolvedMethodPrototype(), stopped overriding each
  member's calledOnType with the full union for non-template unions
- Template union types (TemplateUnionType, TemplateBenevolentUnionType)
  still propagate the union as calledOnType to preserve template identity
- Updated existing test assertion in static-late-binding.php
- Added regression test tests/PHPStan/Analyser/nsrt/bug-11687.php

Closes phpstan/phpstan#11687
Closes phpstan/phpstan#12562

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Automated fix attempt 1 for CI failures.
Automated fix attempt 2 for CI failures.
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.

1 participant