diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index 35536317dd6..7ce52f78d20 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -179,9 +179,12 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b): I $result = $a->equals($b); $reasons = []; if (!$result) { - if ( + $aIsSuperTypeOfB = $a->isSuperTypeOf($b); + if ($aIsSuperTypeOfB->yes() && $b->isSuperTypeOf($a)->yes()) { + $result = true; + } elseif ( $templateType->getScope()->getClassName() !== null - && $a->isSuperTypeOf($b)->yes() + && $aIsSuperTypeOfB->yes() ) { $reasons[] = sprintf( 'Template type %s on class %s is not covariant. Learn more: https://phpstan.org/blog/whats-up-with-template-covariant', diff --git a/tests/PHPStan/Analyser/nsrt/bug-14647.php b/tests/PHPStan/Analyser/nsrt/bug-14647.php new file mode 100644 index 00000000000..9df764a266c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14647.php @@ -0,0 +1,92 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14647Nsrt; + +use function PHPStan\Testing\assertType; + +/** + * @template TValue + */ +class Collection +{ + /** @param array $items */ + public function __construct(private readonly array $items) {} + + /** @return array */ + public function items(): array + { + return $this->items; + } +} + +abstract class AbstractValue +{ + final public function __construct() {} + + /** @return Collection */ + public function collect(): Collection + { + return new Collection([new static()]); + } +} + +final class Value extends AbstractValue +{ + /** @return Collection */ + #[\Override] + public function collect(): Collection + { + return parent::collect(); + } + + /** @return Collection */ + public function childCollect(): Collection + { + return new Collection([new static()]); + } + + public function testTypes(): void + { + assertType('Bug14647Nsrt\Collection', $this->collect()); + assertType('Bug14647Nsrt\Collection', $this->childCollect()); + assertType('static(Bug14647Nsrt\Value)', new static()); + } +} + +/** + * @template T + */ +class Box +{ + /** @param T $value */ + public function __construct(private readonly mixed $value) {} + + /** @return T */ + public function get(): mixed + { + return $this->value; + } +} + +final class FinalFoo +{ + /** @return Box */ + public function boxed(): Box + { + return new Box(new static()); + } + + /** @return Box */ + public static function staticBoxed(): Box + { + return new Box(new static()); + } +} + +function testFinalFoo(FinalFoo $foo): void +{ + assertType('Bug14647Nsrt\Box', $foo->boxed()); + assertType('Bug14647Nsrt\Box', FinalFoo::staticBoxed()); +} diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index a60a6a704a6..0ced1dc18ed 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1347,4 +1347,10 @@ public function testBug14553(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14553.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug14647(): void + { + $this->analyse([__DIR__ . '/data/bug-14647.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14647.php b/tests/PHPStan/Rules/Methods/data/bug-14647.php new file mode 100644 index 00000000000..22a613e7084 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14647.php @@ -0,0 +1,88 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14647; + +/** + * @template TValue + */ +class Collection +{ + /** @param array $items */ + public function __construct(private readonly array $items) {} + + /** @return array */ + public function items(): array + { + return $this->items; + } +} + +abstract class AbstractValue +{ + final public function __construct() {} + + /** @return Collection */ + public function collect(): Collection + { + return new Collection([new static()]); + } +} + +final class Value extends AbstractValue +{ + /** @return Collection */ + #[\Override] + public function collect(): Collection + { + return parent::collect(); + } + + /** @return Collection */ + public function childCollect(): Collection + { + return new Collection([new static()]); + } +} + +/** + * @template T + */ +class Box +{ + /** @param T $value */ + public function __construct(private readonly mixed $value) {} + + /** @return T */ + public function get(): mixed + { + return $this->value; + } +} + +final class FinalWithStaticMethods +{ + /** @return Box */ + public static function staticBoxed(): Box + { + return new Box(new static()); + } + + /** @return Box */ + public function instanceBoxed(): Box + { + return new Box(new static()); + } + + /** @param Box $box */ + public function acceptBox(Box $box): void + { + } + + public function test(): void + { + $this->acceptBox(new Box(new static())); + $this->acceptBox(new Box(new FinalWithStaticMethods())); + } +}