Skip to content

Commit 2461ca9

Browse files
committed
Resolve generic parameter defaults that reference other template parameters
When a template parameter has a default referencing another template parameter (e.g. `@template DO of EI = EI`), the default was left as an unresolved TemplateType instead of being substituted with the concrete type provided for the referenced parameter. The root cause is that resolveTemplateTypes returns resolved values without re-invoking the callback, so transitive TemplateType references in the standins map are never resolved. Fixed in ClassReflection::typeMapFromList — the single gateway where all type arguments enter the standins map. After building the map, any entry that is a TemplateType referencing another entry in the same class is resolved to that entry's concrete type. Co-Authored-By: Claude Code
1 parent c381cc0 commit 2461ca9

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

src/Reflection/ClassReflection.php

Lines changed: 13 additions & 0 deletions
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;
@@ -1741,6 +1742,18 @@ public function typeMapFromList(array $types): TemplateTypeMap
17411742
$i++;
17421743
}
17431744

1745+
$className = $this->getName();
1746+
foreach ($map as $name => $type) {
1747+
if (!$type instanceof TemplateType || $type->getScope()->getClassName() !== $className) {
1748+
continue;
1749+
}
1750+
$resolved = $map[$type->getName()] ?? null;
1751+
if ($resolved === null || $resolved instanceof TemplateType) {
1752+
continue;
1753+
}
1754+
$map[$name] = $resolved;
1755+
}
1756+
17441757
return new TemplateTypeMap($map);
17451758
}
17461759

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)