Skip to content

Commit 851eece

Browse files
ondrejmirtesphpstan-bot
authored andcommitted
Use lower bound types for contravariant template positions in GenericObjectType::inferTemplateTypes
- In `GenericObjectType::inferTemplateTypes()`, inferred types from contravariant template positions are now converted to lower bound types via `convertToLowerBoundTypes()`. This mirrors how `ClosureType` handles parameter types (contravariant positions). - The effective variance is determined using the same logic as `getReferencedTemplateTypes()`: explicit call-site variance takes precedence, falling back to the declared template variance. - This fixes template inference when a child interface with an invariant template extends a parent with a contravariant template. Previously, types from contravariant positions were treated as upper bounds (unioned), causing the inferred type to widen incorrectly. Now they are treated as lower bounds (intersected), preserving the narrower type from covariant positions. - Also fixes the same issue for direct use of contravariant generic types in non-variadic parameters, methods, and static methods.
1 parent 03834ec commit 851eece

File tree

3 files changed

+268
-1
lines changed

3 files changed

+268
-1
lines changed

src/Type/Generic/GenericObjectType.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,26 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
296296
$otherTypes = $ancestorClassReflection->typeMapToList($ancestorClassReflection->getActiveTemplateTypeMap());
297297
$typeMap = TemplateTypeMap::createEmpty();
298298

299+
$classReflection = $this->getClassReflection();
300+
$typeList = [];
301+
if ($classReflection !== null) {
302+
$typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap());
303+
}
304+
299305
foreach ($this->getTypes() as $i => $type) {
300306
$other = $otherTypes[$i] ?? new ErrorType();
301-
$typeMap = $typeMap->union($type->inferTemplateTypes($other));
307+
$map = $type->inferTemplateTypes($other);
308+
309+
$effectiveVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant();
310+
if ($effectiveVariance->invariant() && isset($typeList[$i]) && $typeList[$i] instanceof TemplateType) {
311+
$effectiveVariance = $typeList[$i]->getVariance();
312+
}
313+
314+
if ($effectiveVariance->contravariant()) {
315+
$map = $map->convertToLowerBoundTypes();
316+
}
317+
318+
$typeMap = $typeMap->union($map);
302319
}
303320

304321
return $typeMap;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12444;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template-covariant T
11+
*/
12+
interface Covariant {}
13+
14+
/**
15+
* @template T of object
16+
* @param class-string<T> $class
17+
* @return Covariant<T>
18+
*/
19+
function covariant(string $class): Covariant
20+
{
21+
throw new \Exception();
22+
}
23+
24+
/**
25+
* @template-contravariant T
26+
*/
27+
interface Contravariant {}
28+
29+
/**
30+
* @template T of object
31+
* @param class-string<T> $class
32+
* @return Contravariant<T>
33+
*/
34+
function contravariant(string $class): Contravariant
35+
{
36+
throw new \Exception();
37+
}
38+
39+
/**
40+
* @template T
41+
* @extends Covariant<T>
42+
* @extends Contravariant<T>
43+
*/
44+
interface Invariant extends Covariant, Contravariant {}
45+
46+
/**
47+
* @template T of object
48+
* @param class-string<T> $class
49+
* @return Invariant<T>
50+
*/
51+
function invariant(string $class): Invariant
52+
{
53+
throw new \Exception();
54+
}
55+
56+
/**
57+
* @template T
58+
* @param T $value
59+
* @param Covariant<T> ...$covariants
60+
* @return T
61+
*/
62+
function testCovariant(mixed $value, Covariant ...$covariants): mixed
63+
{
64+
return $value;
65+
}
66+
67+
/**
68+
* @template T
69+
* @param T $value
70+
* @param Contravariant<T> ...$contravariants
71+
* @return T
72+
*/
73+
function testContravariant(mixed $value, Contravariant ...$contravariants): mixed
74+
{
75+
return $value;
76+
}
77+
78+
// Contravariant with direct Contravariant args
79+
$r3 = testContravariant(
80+
new \RuntimeException(),
81+
contravariant(\Throwable::class),
82+
contravariant(\Exception::class),
83+
);
84+
assertType('RuntimeException', $r3);
85+
86+
// Contravariant with Invariant args (extending Contravariant) - this is the reported bug
87+
$r4 = testContravariant(
88+
new \RuntimeException(),
89+
invariant(\Throwable::class),
90+
invariant(\Exception::class),
91+
);
92+
assertType('RuntimeException', $r4);
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12444b;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template-contravariant T
11+
*/
12+
interface Contra {}
13+
14+
/**
15+
* @template T
16+
* @extends Contra<T>
17+
*/
18+
interface Inv extends Contra {}
19+
20+
/**
21+
* @template T of object
22+
* @param class-string<T> $class
23+
* @return Contra<T>
24+
*/
25+
function contra(string $class): Contra
26+
{
27+
throw new \Exception();
28+
}
29+
30+
/**
31+
* @template T of object
32+
* @param class-string<T> $class
33+
* @return Inv<T>
34+
*/
35+
function inv(string $class): Inv
36+
{
37+
throw new \Exception();
38+
}
39+
40+
// Non-variadic: two separate contravariant params
41+
/**
42+
* @template T
43+
* @param T $value
44+
* @param Contra<T> $a
45+
* @param Contra<T> $b
46+
* @return T
47+
*/
48+
function testTwoParams(mixed $value, Contra $a, Contra $b): mixed
49+
{
50+
return $value;
51+
}
52+
53+
// Non-variadic with direct Contra
54+
$r1 = testTwoParams(
55+
new \RuntimeException(),
56+
contra(\Throwable::class),
57+
contra(\Exception::class),
58+
);
59+
assertType('RuntimeException', $r1);
60+
61+
// Non-variadic with Inv (extending Contra)
62+
$r2 = testTwoParams(
63+
new \RuntimeException(),
64+
inv(\Throwable::class),
65+
inv(\Exception::class),
66+
);
67+
assertType('RuntimeException', $r2);
68+
69+
// Mixed variance: function with both covariant and contravariant template params
70+
/**
71+
* @template-covariant Out
72+
* @template-contravariant In
73+
*/
74+
interface Func
75+
{
76+
}
77+
78+
/**
79+
* @template Out
80+
* @template In
81+
* @extends Func<Out, In>
82+
*/
83+
interface InvFunc extends Func {}
84+
85+
/**
86+
* @template T
87+
* @param Func<T, T> $fn
88+
* @param T $value
89+
* @return T
90+
*/
91+
function applyFunc(Func $fn, mixed $value): mixed
92+
{
93+
return $value;
94+
}
95+
96+
/**
97+
* @param Func<\Exception, \Throwable> $fn
98+
*/
99+
function testMixedVariance(Func $fn): void
100+
{
101+
$r = applyFunc($fn, new \RuntimeException());
102+
assertType('Exception', $r);
103+
}
104+
105+
/**
106+
* @param InvFunc<\Exception, \Throwable> $fn
107+
*/
108+
function testMixedVarianceWithInv(InvFunc $fn): void
109+
{
110+
$r = applyFunc($fn, new \RuntimeException());
111+
assertType('Exception', $r);
112+
}
113+
114+
// Method on a class (vs function)
115+
class Container
116+
{
117+
/**
118+
* @template T
119+
* @param T $value
120+
* @param Contra<T> ...$contras
121+
* @return T
122+
*/
123+
public function test(mixed $value, Contra ...$contras): mixed
124+
{
125+
return $value;
126+
}
127+
128+
/**
129+
* @template T
130+
* @param T $value
131+
* @param Contra<T> ...$contras
132+
* @return T
133+
*/
134+
public static function testStatic(mixed $value, Contra ...$contras): mixed
135+
{
136+
return $value;
137+
}
138+
}
139+
140+
function testMethod(): void
141+
{
142+
$c = new Container();
143+
// Method with Inv args
144+
$r = $c->test(
145+
new \RuntimeException(),
146+
inv(\Throwable::class),
147+
inv(\Exception::class),
148+
);
149+
assertType('RuntimeException', $r);
150+
151+
// Static method with Inv args
152+
$r2 = Container::testStatic(
153+
new \RuntimeException(),
154+
inv(\Throwable::class),
155+
inv(\Exception::class),
156+
);
157+
assertType('RuntimeException', $r2);
158+
}

0 commit comments

Comments
 (0)