From a2597633044656b6aefb0164448a2f29d4af274d Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:41:06 +0000 Subject: [PATCH 1/4] Fix TemplateIntersectionType being lost in TypeCombinator::intersect - TemplateIntersectionType extends IntersectionType, so the flattening logic in TypeCombinator::intersect was unwrapping it into its inner types, losing the template context - Added a TemplateType check to skip flattening for template types - New regression test in tests/PHPStan/Analyser/nsrt/bug-14348.php Closes https://github.com/phpstan/phpstan/issues/14348 --- src/Type/TypeCombinator.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-14348.php | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14348.php diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index d3442f0470..e7a0d6bdf5 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1212,7 +1212,7 @@ public static function intersect(Type ...$types): Type for ($i = 0; $i < $typesCount; $i++) { $type = $types[$i]; - if ($type instanceof IntersectionType) { + if ($type instanceof IntersectionType && !$type instanceof TemplateType) { // transform A & (B & C) to A & B & C array_splice($types, $i--, 1, $type->getTypes()); $typesCount = count($types); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14348.php b/tests/PHPStan/Analyser/nsrt/bug-14348.php new file mode 100644 index 0000000000..b0e728ea8d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14348.php @@ -0,0 +1,23 @@ + $tgs + */ + public function computeForFrontByPosition(array $tgs): void + { + assertType('T of Bug14348\PositionEntityInterface&Bug14348\TgEntityInterface (method Bug14348\HelloWorld::computeForFrontByPosition(), argument)', $tgs[0]); + } +} From a6735ff5d664de72d95262fa4ad1ece67fc53dfc Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 23 Mar 2026 10:22:08 +0000 Subject: [PATCH 2/4] Add TypeCombinatorTest::testIntersect case for TemplateIntersectionType preservation Adds a test case that verifies intersecting a TemplateIntersectionType (T of A&B) with MixedType preserves the template wrapper instead of flattening it to a plain IntersectionType. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Type/TypeCombinatorTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 9e4ab69709..6c51dbcd9b 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -42,6 +42,7 @@ use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateBenevolentUnionType; +use PHPStan\Type\Generic\TemplateIntersectionType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateObjectType; use PHPStan\Type\Generic\TemplateObjectWithoutClassType; @@ -4917,6 +4918,21 @@ public static function dataIntersect(): iterable ObjectType::class, $nonFinalClass->getDisplayName() . '=final', ]; + + // https://github.com/phpstan/phpstan/issues/14348 + yield [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new IntersectionType([new ObjectType('Iterator'), new ObjectType('Countable')]), + TemplateTypeVariance::createInvariant(), + ), + new MixedType(), + ], + TemplateIntersectionType::class, + 'T of Countable&Iterator (function a(), parameter)', + ]; } /** From 366e5d48e8a01237d7025541a72ef595892652f4 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 23 Mar 2026 10:44:55 +0000 Subject: [PATCH 3/4] Add regression tests for issues #9961 and #13577 Both issues involve template intersection types being lost during type narrowing, which is fixed by the TemplateIntersectionType preservation in TypeCombinator::intersect. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-13577.php | 35 +++++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-9961.php | 33 +++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13577.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-9961.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13577.php b/tests/PHPStan/Analyser/nsrt/bug-13577.php new file mode 100644 index 0000000000..2406bd0f95 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13577.php @@ -0,0 +1,35 @@ +b($foo); + } + + /** + * @param T $foo + */ + public function b($foo): void {} +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9961.php b/tests/PHPStan/Analyser/nsrt/bug-9961.php new file mode 100644 index 0000000000..9b3c051626 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9961.php @@ -0,0 +1,33 @@ + Date: Mon, 23 Mar 2026 12:03:56 +0100 Subject: [PATCH 4/4] more asserts --- tests/PHPStan/Analyser/nsrt/bug-9961.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-9961.php b/tests/PHPStan/Analyser/nsrt/bug-9961.php index 9b3c051626..0e3535ee27 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9961.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9961.php @@ -28,6 +28,8 @@ public function sayHello(Ia|Ic $a): mixed throw new \Exception; } + assertType('(Bug9961\A&T of T of Bug9961\Ia&Bug9961\Ib (method Bug9961\HelloWorld::sayHello(), argument) (method Bug9961\HelloWorld::sayHello(), argument))|T of Bug9961\Ic&Bug9961\Id (method Bug9961\HelloWorld::sayHello(), argument)', $a); + return $a; } }