diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index e2a9d32d27..4955c9ca0b 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -40,6 +40,7 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -1736,8 +1737,16 @@ public function typeMapFromList(array $types): TemplateTypeMap $map = []; $i = 0; + $className = $this->getName(); foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $map[$tag->getName()] = $types[$i] ?? $tag->getDefault() ?? $tag->getBound(); + $type = $types[$i] ?? $tag->getDefault() ?? $tag->getBound(); + if ($type instanceof TemplateType && $type->getScope()->getClassName() === $className) { + $resolved = $map[$type->getName()] ?? null; + if ($resolved !== null && !$resolved instanceof TemplateType) { + $type = $resolved; + } + } + $map[$tag->getName()] = $type; $i++; } diff --git a/tests/PHPStan/Analyser/nsrt/template-default-referring-other.php b/tests/PHPStan/Analyser/nsrt/template-default-referring-other.php new file mode 100644 index 0000000000..8868c97f75 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/template-default-referring-other.php @@ -0,0 +1,117 @@ += 8.1 + +namespace TemplateDefaultReferringOther; + +use function PHPStan\Testing\assertType; + +class MoneyValue +{ + + public function __construct( + public readonly string $currency, + public readonly int $cents, + ) + { + } + +} + +/** + * @template-contravariant DI + * @template-contravariant EI + * @template-covariant DO of EI = EI + * @template-covariant EO of DI = DI + */ +interface Codec +{ + + /** + * @param DI $data + * @return DO + */ + public function decode(mixed $data): mixed; + + /** + * @param EI $data + * @return EO + */ + public function encode(mixed $data): mixed; + +} + +/** + * @implements Codec< + * array{currency: string, cents: int}, + * MoneyValue, + * > + */ +class MoneyCodec implements Codec +{ + + public function decode(mixed $data): MoneyValue + { + return new MoneyValue($data['currency'], $data['cents']); + } + + public function encode(mixed $data): array + { + return [ + 'currency' => $data->currency, + 'cents' => $data->cents, + ]; + } + +} + +/** + * @implements Codec< + * string, + * \DateTimeInterface, + * \DateTimeImmutable, + * string, + * > + */ +class DateTimeInterfaceCodec implements Codec +{ + + public function decode(mixed $data): \DateTimeImmutable + { + return new \DateTimeImmutable($data); + } + + public function encode(mixed $data): string + { + return $data->format('c'); + } + +} + +/** + * @param Codec $moneyCodec + * @param Codec $dtCodec + */ +function test( + Codec $moneyCodec, + Codec $dtCodec, + string $dtString, + \DateTimeInterface $dtInterface, +): void +{ + assertType('TemplateDefaultReferringOther\MoneyValue', $moneyCodec->decode(['currency' => 'CZK', 'cents' => 123])); + assertType('array{currency: string, cents: int}', $moneyCodec->encode(new MoneyValue('CZK', 100))); + + assertType('DateTimeImmutable', $dtCodec->decode($dtString)); + assertType('string', $dtCodec->encode($dtInterface)); +} + +function testMoneyCodecDirect(MoneyCodec $codec): void +{ + assertType('TemplateDefaultReferringOther\MoneyValue', $codec->decode(['currency' => 'CZK', 'cents' => 123])); + assertType('array{currency: string, cents: int}', $codec->encode(new MoneyValue('CZK', 100))); +} + +function testDateTimeCodecDirect(DateTimeInterfaceCodec $codec): void +{ + assertType('DateTimeImmutable', $codec->decode('2024-01-01')); + assertType('string', $codec->encode(new \DateTimeImmutable())); +}