Skip to content

Commit 38740d0

Browse files
authored
Resolve generic parameter defaults that reference other template parameters
1 parent 38036bf commit 38740d0

File tree

2 files changed

+127
-1
lines changed

2 files changed

+127
-1
lines changed

src/Reflection/ClassReflection.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use PHPStan\Type\ErrorType;
4141
use PHPStan\Type\FileTypeMapper;
4242
use PHPStan\Type\Generic\GenericObjectType;
43+
use PHPStan\Type\Generic\TemplateType;
4344
use PHPStan\Type\Generic\TemplateTypeFactory;
4445
use PHPStan\Type\Generic\TemplateTypeHelper;
4546
use PHPStan\Type\Generic\TemplateTypeMap;
@@ -1736,8 +1737,16 @@ public function typeMapFromList(array $types): TemplateTypeMap
17361737

17371738
$map = [];
17381739
$i = 0;
1740+
$className = $this->getName();
17391741
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
1740-
$map[$tag->getName()] = $types[$i] ?? $tag->getDefault() ?? $tag->getBound();
1742+
$type = $types[$i] ?? $tag->getDefault() ?? $tag->getBound();
1743+
if ($type instanceof TemplateType && $type->getScope()->getClassName() === $className) {
1744+
$resolved = $map[$type->getName()] ?? null;
1745+
if ($resolved !== null && !$resolved instanceof TemplateType) {
1746+
$type = $resolved;
1747+
}
1748+
}
1749+
$map[$tag->getName()] = $type;
17411750
$i++;
17421751
}
17431752

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php // lint >= 8.1
2+
3+
namespace TemplateDefaultReferringOther;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class MoneyValue
8+
{
9+
10+
public function __construct(
11+
public readonly string $currency,
12+
public readonly int $cents,
13+
)
14+
{
15+
}
16+
17+
}
18+
19+
/**
20+
* @template-contravariant DI
21+
* @template-contravariant EI
22+
* @template-covariant DO of EI = EI
23+
* @template-covariant EO of DI = DI
24+
*/
25+
interface Codec
26+
{
27+
28+
/**
29+
* @param DI $data
30+
* @return DO
31+
*/
32+
public function decode(mixed $data): mixed;
33+
34+
/**
35+
* @param EI $data
36+
* @return EO
37+
*/
38+
public function encode(mixed $data): mixed;
39+
40+
}
41+
42+
/**
43+
* @implements Codec<
44+
* array{currency: string, cents: int},
45+
* MoneyValue,
46+
* >
47+
*/
48+
class MoneyCodec implements Codec
49+
{
50+
51+
public function decode(mixed $data): MoneyValue
52+
{
53+
return new MoneyValue($data['currency'], $data['cents']);
54+
}
55+
56+
public function encode(mixed $data): array
57+
{
58+
return [
59+
'currency' => $data->currency,
60+
'cents' => $data->cents,
61+
];
62+
}
63+
64+
}
65+
66+
/**
67+
* @implements Codec<
68+
* string,
69+
* \DateTimeInterface,
70+
* \DateTimeImmutable,
71+
* string,
72+
* >
73+
*/
74+
class DateTimeInterfaceCodec implements Codec
75+
{
76+
77+
public function decode(mixed $data): \DateTimeImmutable
78+
{
79+
return new \DateTimeImmutable($data);
80+
}
81+
82+
public function encode(mixed $data): string
83+
{
84+
return $data->format('c');
85+
}
86+
87+
}
88+
89+
/**
90+
* @param Codec<array{currency: string, cents: int}, MoneyValue> $moneyCodec
91+
* @param Codec<string, \DateTimeInterface, \DateTimeImmutable, string> $dtCodec
92+
*/
93+
function test(
94+
Codec $moneyCodec,
95+
Codec $dtCodec,
96+
string $dtString,
97+
\DateTimeInterface $dtInterface,
98+
): void
99+
{
100+
assertType('TemplateDefaultReferringOther\MoneyValue', $moneyCodec->decode(['currency' => 'CZK', 'cents' => 123]));
101+
assertType('array{currency: string, cents: int}', $moneyCodec->encode(new MoneyValue('CZK', 100)));
102+
103+
assertType('DateTimeImmutable', $dtCodec->decode($dtString));
104+
assertType('string', $dtCodec->encode($dtInterface));
105+
}
106+
107+
function testMoneyCodecDirect(MoneyCodec $codec): void
108+
{
109+
assertType('TemplateDefaultReferringOther\MoneyValue', $codec->decode(['currency' => 'CZK', 'cents' => 123]));
110+
assertType('array{currency: string, cents: int}', $codec->encode(new MoneyValue('CZK', 100)));
111+
}
112+
113+
function testDateTimeCodecDirect(DateTimeInterfaceCodec $codec): void
114+
{
115+
assertType('DateTimeImmutable', $codec->decode('2024-01-01'));
116+
assertType('string', $codec->encode(new \DateTimeImmutable()));
117+
}

0 commit comments

Comments
 (0)