diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 12f0d540ba..3388d0e4a0 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -1234,6 +1234,38 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $myTypes = $this->types; } + // When every member of the union is a template type (e.g. T1|T2) and the received + // type only matches some of them, the unmatched template types cannot be inferred + // from this argument. Bind them to never so the union narrows to the matched members + // instead of widening each unmatched template to its bound. + if (count($myTypes) > 1) { + $templateMembers = []; + foreach ($myTypes as $type) { + if (!$type instanceof TemplateType) { + $templateMembers = []; + break; + } + $templateMembers[] = $type; + } + + if (count($templateMembers) > 0) { + $matchedTypes = TemplateTypeMap::createEmpty(); + $unmatchedNames = []; + foreach ($templateMembers as $type) { + $inferred = $type->inferTemplateTypes($receivedType); + if ($inferred->isEmpty()) { + $unmatchedNames[] = $type->getName(); + continue; + } + $matchedTypes = $matchedTypes->union($inferred); + } + + if (!$matchedTypes->isEmpty() && count($unmatchedNames) > 0) { + return $matchedTypes->union(new TemplateTypeMap(array_fill_keys($unmatchedNames, new NeverType(true)))); + } + } + } + foreach ($myTypes as $type) { if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) { continue; diff --git a/tests/PHPStan/Analyser/nsrt/bug-2579.php b/tests/PHPStan/Analyser/nsrt/bug-2579.php new file mode 100644 index 0000000000..f3572b4c4c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2579.php @@ -0,0 +1,42 @@ +analyse([__DIR__ . '/data/bug-14720.php'], []); } + public function testBug2579(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-2579.php'], [ + [ + 'Call to an undefined method Bug2579Methods\A1::bar().', + 30, + ], + [ + 'Call to an undefined method Bug2579Methods\B1::foo().', + 31, + ], + ]); + } + public function testClosureBind(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/bug-2579.php b/tests/PHPStan/Rules/Methods/data/bug-2579.php new file mode 100644 index 0000000000..eafd8419b4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-2579.php @@ -0,0 +1,32 @@ +foo(); // should pass + (f(new B1()))->bar(); // should pass + + (f(new A1()))->bar(); // should fail + (f(new B1()))->foo(); // should fail +}