From 7068e2245213a481078d24135094c1f925cff8c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:03:54 +0000 Subject: [PATCH 1/4] Fix conditional return type on $this not using subtracted type - When a method has a conditional return type like `@return ($this is self::A ? int : null)`, and `$this` has a subtracted type (e.g. `$this(X~X::A)` from narrowing), the conditional was not properly resolved because the subtracted type was not propagated - The root cause was in `StaticType::transformStaticType` which transforms `StaticType` instances in return types but did not propagate the calling type's subtracted type - Added subtracted type propagation in `StaticType::transformStaticType` to match the behavior of `CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType` - New regression test in tests/PHPStan/Analyser/nsrt/bug-12244.php Closes https://github.com/phpstan/phpstan/issues/12244 --- src/Type/StaticType.php | 7 +++++ tests/PHPStan/Analyser/nsrt/bug-12244.php | 32 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12244.php diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 0243c96256..a7bd9932f2 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -369,6 +369,13 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop $type = new self($type->getClassReflection(), $type->getSubtractedType()); } + if ($this->getSubtractedType() !== null && $type->getSubtractedType() === null) { + $type = $type->changeSubtractedType($this->getSubtractedType()); + if (!$type instanceof StaticType) { + return $traverse($type); + } + } + if (!$isFinal || $type instanceof ThisType) { return $traverse($type); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12244.php b/tests/PHPStan/Analyser/nsrt/bug-12244.php new file mode 100644 index 0000000000..ba792e4625 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12244.php @@ -0,0 +1,32 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug12244; + +use function PHPStan\Testing\assertType; + +enum X: string { + case A = 'a'; + case B = 'b'; + case C = 'c'; + + /** @return ($this is self::A ? int : null) */ + public function get(): ?int { + return ($this === self::A) ? 123 : null; + } + + public function doSomething(): void { + if ($this !== self::A) { + assertType('$this(Bug12244\X~Bug12244\X::A)', $this); + assertType('null', $this->get()); + } + } + + public static function doSomethingFor(X $x): void { + if ($x !== self::A) { + assertType('Bug12244\X~Bug12244\X::A', $x); + assertType('null', $x->get()); + } + } +} From f8715871b88c858d4c2e3ac61980536c57b3e4bf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 28 Feb 2026 16:57:27 +0100 Subject: [PATCH 2/4] Simplify --- src/Type/StaticType.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index a7bd9932f2..2f1e27ee22 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -369,8 +369,8 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop $type = new self($type->getClassReflection(), $type->getSubtractedType()); } - if ($this->getSubtractedType() !== null && $type->getSubtractedType() === null) { - $type = $type->changeSubtractedType($this->getSubtractedType()); + if ($this->getSubtractedType() !== null) { + $type = $type->subtract($this->getSubtractedType()); if (!$type instanceof StaticType) { return $traverse($type); } From 9e286724dcc14f6d180750385d938323dc6301c7 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 1 Mar 2026 12:39:48 +0100 Subject: [PATCH 3/4] Add test --- tests/PHPStan/Analyser/nsrt/bug-12244.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12244.php b/tests/PHPStan/Analyser/nsrt/bug-12244.php index ba792e4625..abaf912ec9 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12244.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12244.php @@ -20,6 +20,12 @@ public function doSomething(): void { if ($this !== self::A) { assertType('$this(Bug12244\X~Bug12244\X::A)', $this); assertType('null', $this->get()); + } else { + assertType('int', $this->get()); + } + + if ($this !== self::B && $this !== self::C) { + assertType('int', $this->get()); } } @@ -27,6 +33,12 @@ public static function doSomethingFor(X $x): void { if ($x !== self::A) { assertType('Bug12244\X~Bug12244\X::A', $x); assertType('null', $x->get()); + } else { + assertType('int', $x->get()); + } + + if ($x !== self::B && $x !== self::C) { + assertType('int', $x->get()); } } } From 32abd995e72aa1581a7143682ddfc0bd48fd35e4 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 1 Mar 2026 12:41:32 +0100 Subject: [PATCH 4/4] Add test --- tests/PHPStan/Analyser/nsrt/bug-12244.php | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12244.php b/tests/PHPStan/Analyser/nsrt/bug-12244.php index abaf912ec9..aeb0383fa1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12244.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12244.php @@ -42,3 +42,36 @@ public static function doSomethingFor(X $x): void { } } } + +enum Y: string { + case A = 'a'; + case B = 'b'; + case C = 'c'; + + /** @param-out ($this is self::A ? int : null) $i */ + public function get(?int &$i): void { + ($this === self::A) ? $i=null : $i=123; + } + + public function doSomething(): void { + $i = 0; + if ($this !== self::A) { + $this->get($i); + assertType('null', $i); // null + } else { + $this->get($i); + assertType('int', $i); // int + } + } + + public static function doSomethingFor(Y $x): void { + $i = 0; + if ($x !== self::A) { + $x->get($i); + assertType('null', $i); // null + } else { + $x->get($i); + assertType('int', $i); // int + } + } +}