diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index c3ba1de2be..64b1c46396 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -339,6 +339,17 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsRes if ($isSuperType->no()) { return $isSuperType->toAcceptsResult(); } + + // A TemplateType member accepts eagerly, so lazyMaxMin's Yes may come solely from it. + // A Maybe from the holistic isSuperTypeOf means no member is a definite subtype, so + // when a TemplateType member is present the eager Yes is untrustworthy - trust the Maybe. + if ($isSuperType->maybe()) { + foreach ($this->types as $innerType) { + if ($innerType instanceof TemplateType) { + return $isSuperType->toAcceptsResult(); + } + } + } } if ($this->isOversizedArray()->yes()) { diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 59cc2e5225..21c047c061 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -451,11 +451,34 @@ public function testBug13565(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testBug13190TemplateGeneric(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = false; + // A raw TemplateType value returned as its generic application must not be a false positive. + $this->analyse([__DIR__ . '/data/bug-13190-template-generic.php'], []); + } + + #[RequiresPhp('>= 8.0.0')] public function testBug13190(): void { $this->checkNullables = true; $this->checkExplicitMixed = false; - $this->analyse([__DIR__ . '/data/bug-13190.php'], []); + $this->analyse([__DIR__ . '/data/bug-13190.php'], [ + [ + 'Function Bug13190\inbox() should return Bug13190\Box but returns (Bug13190\Box&T)|Bug13190\Box.', + 51, + ], + [ + 'Function Bug13190\inbox_concrete_impl() should return Bug13190\Box but returns (Bug13190\Box&Bug13190\IntBox)|(Bug13190\IntBox&T).', + 79, + ], + [ + 'Function Bug13190\inbox_concrete_impl() should return Bug13190\Box but returns Bug13190\BoxImpl|T of mixed>.', + 81, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Functions/data/bug-13190-template-generic.php b/tests/PHPStan/Rules/Functions/data/bug-13190-template-generic.php new file mode 100644 index 0000000000..0fd2cdb8c9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13190-template-generic.php @@ -0,0 +1,39 @@ += 8.0 +declare(strict_types = 1); + +namespace Bug13190TemplateGeneric; + +class Promise +{ +} + +interface Result +{ +} + +class ResultA implements Result +{ +} + +/** + * @template TypeConcurrent of bool + */ +abstract class Communicator +{ + /** @param TypeConcurrent $concurrent */ + public function __construct(private bool $concurrent) + { + } +} + +/** + * @template TypeCommunicator of Communicator + * @template TypeConcurrent of bool + * @param class-string $communicatorClass + * @param TypeConcurrent $concurrent + * @return TypeCommunicator + */ +function communicatorFactory(string $communicatorClass, bool $concurrent): Communicator +{ + return new $communicatorClass($concurrent); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-13190.php b/tests/PHPStan/Rules/Functions/data/bug-13190.php index 0565117ebb..186a011493 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-13190.php +++ b/tests/PHPStan/Rules/Functions/data/bug-13190.php @@ -53,3 +53,31 @@ function inbox($to_box): Box return new BoxImpl($to_box); } } + +/** + * @implements Box + */ +final class IntBox implements Box +{ + #[\Override] + public function toInner(): int + { + return 0; + } +} + +/** + * @template T + * + * @param T|Box $to_box + * + * @return Box + */ +function inbox_concrete_impl($to_box): Box +{ + if ($to_box instanceof IntBox) { + return $to_box; + } else { + return new BoxImpl($to_box); + } +}