From bb4c8536ecd16f6aa3f851712fee7bb77e1a0107 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:38:57 +0000 Subject: [PATCH 1/4] Fix static return type resolution on union class types - 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 https://github.com/phpstan/phpstan/issues/11687 --- src/Type/UnionType.php | 6 +- tests/PHPStan/Analyser/nsrt/bug-11687.php | 60 +++++++++++++++++++ .../Analyser/nsrt/static-late-binding.php | 2 +- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11687.php diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index c5c9f65415..4fd4bfe8ce 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -581,7 +581,11 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce continue; } - $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this); + $prototype = $type->getUnresolvedMethodPrototype($methodName, $scope); + if ($this instanceof TemplateType) { + $prototype = $prototype->withCalledOnType($this); + } + $methodPrototypes[] = $prototype; } $methodsCount = count($methodPrototypes); diff --git a/tests/PHPStan/Analyser/nsrt/bug-11687.php b/tests/PHPStan/Analyser/nsrt/bug-11687.php new file mode 100644 index 0000000000..0455d3b2a2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11687.php @@ -0,0 +1,60 @@ +retStatic()); assertType('bool', X::retStatic()); - assertType('bool|StaticLateBinding\A|StaticLateBinding\X', $clUnioned::retStatic()); // should be bool|StaticLateBinding\A https://github.com/phpstan/phpstan/issues/11687 + assertType('bool|StaticLateBinding\A', $clUnioned::retStatic()); // should be bool|StaticLateBinding\A https://github.com/phpstan/phpstan/issues/11687 assertType('StaticLateBinding\A', A::retStatic(...)()); assertType('StaticLateBinding\B', B::retStatic(...)()); From 4f29d70cb0bbfe64cb5bc3c676397680649b33af Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 20:45:22 +0000 Subject: [PATCH 2/4] Add regression test for #12562 Closes https://github.com/phpstan/phpstan/issues/12562 Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-12562.php | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12562.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12562.php b/tests/PHPStan/Analyser/nsrt/bug-12562.php new file mode 100644 index 0000000000..c2fd5f9a32 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12562.php @@ -0,0 +1,38 @@ += 8.0 + +namespace Bug12562; + +use function PHPStan\Testing\assertType; + +class UserV1 +{ + public function toV2(): UserV2 + { + return new UserV2(); + } +} + +class UserV2 +{ + public function toV2(): self + { + return $this; + } +} + +class UserV2a extends UserV2 +{ + /** + * @return $this + */ + public function toV2(): self + { + return $this; + } +} + +function doSomething(UserV1|UserV2 $user, UserV1|UserV2a $user2): void +{ + assertType('Bug12562\UserV2', $user->toV2()); + assertType('Bug12562\UserV2', $user2->toV2()); +} From 23c263ed74e3b58045f42a56d2e04baeb2368425 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 21:37:38 +0000 Subject: [PATCH 3/4] Fix CI failures [claude-ci-fix] Automated fix attempt 1 for CI failures. --- src/Type/UnionType.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 4fd4bfe8ce..ae596c76b7 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -584,6 +584,22 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $prototype = $type->getUnresolvedMethodPrototype($methodName, $scope); if ($this instanceof TemplateType) { $prototype = $prototype->withCalledOnType($this); + } else { + $declaringClassType = new ObjectType($prototype->getNakedMethod()->getDeclaringClass()->getName()); + $typesForCalledOn = []; + foreach ($this->types as $innerType) { + if (!$innerType->hasMethod($methodName)->yes()) { + continue; + } + if (!$declaringClassType->isSuperTypeOf($innerType)->yes()) { + continue; + } + + $typesForCalledOn[] = $innerType; + } + if (count($typesForCalledOn) > 1) { + $prototype = $prototype->withCalledOnType(TypeCombinator::union(...$typesForCalledOn)); + } } $methodPrototypes[] = $prototype; } From fd69ed7e9f38087c0c10eaeba62d936c023333a4 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 22:05:01 +0000 Subject: [PATCH 4/4] Fix CI failures [claude-ci-fix] Automated fix attempt 2 for CI failures. --- src/Type/UnionType.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index ae596c76b7..4fd4bfe8ce 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -584,22 +584,6 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $prototype = $type->getUnresolvedMethodPrototype($methodName, $scope); if ($this instanceof TemplateType) { $prototype = $prototype->withCalledOnType($this); - } else { - $declaringClassType = new ObjectType($prototype->getNakedMethod()->getDeclaringClass()->getName()); - $typesForCalledOn = []; - foreach ($this->types as $innerType) { - if (!$innerType->hasMethod($methodName)->yes()) { - continue; - } - if (!$declaringClassType->isSuperTypeOf($innerType)->yes()) { - continue; - } - - $typesForCalledOn[] = $innerType; - } - if (count($typesForCalledOn) > 1) { - $prototype = $prototype->withCalledOnType(TypeCombinator::union(...$typesForCalledOn)); - } } $methodPrototypes[] = $prototype; }