Skip to content

Commit 59ded7b

Browse files
phpstan-botondrejmirtes
authored andcommitted
Do not let a union member absorb a received never when inferring template types
- `UnionType::inferTemplateTypes()` skips the absorption check for a received `never`: every member is a supertype of `never`, so which member "explains" it is meaningless and absorbing it left the union's naked template unbound. `P<TRejected>|TRejected` receiving `Closure(Throwable): never` now binds `TRejected = never` again instead of reporting `argument.templateType`. - The absorption introduced in dc41d1e for `T|null` receiving `null` and `T|int|float` receiving `int` is unchanged. - Analogous case fixed: `IterableType::inferTemplateTypes()` no longer takes a received `never` apart via `getIterableKeyType()`/`getIterableValueType()`. `NeverType::isIterable()` is `yes` while `isArray()`/`isObject()` are `no`, so `iterable<T>` was the only container binding `T` to an *implicit* never, which downstream reads as unresolvable (`iterable<T>|T` produced `P<*NEVER*>` plus "contains unresolvable type"). It now infers nothing from `never`, like `array<T>` and `Traversable<T>` already did. - Probed and found already correct: `IntersectionType` and `BenevolentUnionType` have no absorption loop; `array<T>|T`, `Traversable<T>|T`, `T|null`, `T|string` and `T|int|float` receiving `never`; unions nested in `P<P<T>|T>` and `array{P<T>|T}` are fixed by the same change.
1 parent c4c035e commit 59ded7b

8 files changed

Lines changed: 300 additions & 1 deletion

File tree

src/Type/IterableType.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,10 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
449449
return $receivedType->inferTemplateTypesOn($this);
450450
}
451451

452-
if (!$receivedType->isIterable()->yes()) {
452+
// never is iterable, but taking its key and value type apart would bind
453+
// the templates to an implicit never that is later read as unresolved.
454+
// ArrayType and Traversable<T> infer nothing from never either.
455+
if (!$receivedType->isIterable()->yes() || $receivedType instanceof NeverType) {
453456
return TemplateTypeMap::createEmpty();
454457
}
455458

src/Type/UnionType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,10 +1376,16 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
13761376
// absorbed by that member: it tells nothing about the other members,
13771377
// so T|null receiving null must not bind T. Only what no member
13781378
// absorbs is inferred against the remaining members below.
1379+
// never is the exception: every member is its supertype, so which
1380+
// member absorbs it is meaningless - it always has to be inferred.
13791381
$types = TemplateTypeMap::createEmpty();
13801382
$receivedTypes = $receivedType instanceof UnionType ? $receivedType->getTypes() : [$receivedType];
13811383
$remainingReceivedTypes = [];
13821384
foreach ($receivedTypes as $receivedInnerType) {
1385+
if ($receivedInnerType instanceof NeverType) {
1386+
$remainingReceivedTypes[] = $receivedInnerType;
1387+
continue;
1388+
}
13831389
foreach ($this->types as $type) {
13841390
if ($type->isSuperTypeOf($receivedInnerType)->yes()) {
13851391
$types = $types->union($type->inferTemplateTypes($receivedInnerType));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15167;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/** @template-covariant T */
8+
interface P
9+
{
10+
11+
/**
12+
* @template TRejected
13+
* @param ?(callable(\Throwable): (P<TRejected>|TRejected)) $onRejected
14+
* @return P<T|TRejected>
15+
*/
16+
public function catch(?callable $onRejected = null): P;
17+
18+
/**
19+
* @template TR
20+
* @param callable(\Throwable): (P<TR>|TR) $onRejected
21+
* @return P<T|TR>
22+
*/
23+
public function catchNotNullable(callable $onRejected): P;
24+
25+
}
26+
27+
/** @param P<int> $p */
28+
function f(P $p): void
29+
{
30+
$throwing = static function (\Throwable $e): void {
31+
throw $e;
32+
};
33+
$notThrowing = static function (\Throwable $e): void {
34+
};
35+
36+
assertType('static-Closure(Throwable): never', $throwing);
37+
assertType('static-Closure(Throwable): void', $notThrowing);
38+
39+
assertType('Bug15167\\P<int>', $p->catch($throwing));
40+
41+
assertType('Bug15167\\P<int>', $p->catchNotNullable($throwing));
42+
43+
assertType('Bug15167\\P<int|void>', $p->catch($notThrowing));
44+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace NeverIntoUnionTemplate;
4+
5+
use Traversable;
6+
use function PHPStan\Testing\assertType;
7+
8+
/** @template-covariant T */
9+
interface P
10+
{
11+
12+
}
13+
14+
/**
15+
* @template T
16+
* @param callable(): (T|null) $cb
17+
* @return P<T>
18+
*/
19+
function tOrNull(callable $cb): P
20+
{
21+
throw new \Exception();
22+
}
23+
24+
/**
25+
* @template T
26+
* @param callable(): (T|int|float) $cb
27+
* @return P<T>
28+
*/
29+
function tOrIntFloat(callable $cb): P
30+
{
31+
throw new \Exception();
32+
}
33+
34+
/**
35+
* @template T
36+
* @param callable(): (array<T>|T) $cb
37+
* @return P<T>
38+
*/
39+
function arrayOrT(callable $cb): P
40+
{
41+
throw new \Exception();
42+
}
43+
44+
/**
45+
* @template T
46+
* @param callable(): (iterable<T>|T) $cb
47+
* @return P<T>
48+
*/
49+
function iterableOrT(callable $cb): P
50+
{
51+
throw new \Exception();
52+
}
53+
54+
/**
55+
* @template T
56+
* @param callable(): (Traversable<T>|T) $cb
57+
* @return P<T>
58+
*/
59+
function traversableOrT(callable $cb): P
60+
{
61+
throw new \Exception();
62+
}
63+
64+
/**
65+
* @template T
66+
* @param callable(): (P<T>|T) $cb
67+
* @return P<T>
68+
*/
69+
function pOrT(callable $cb): P
70+
{
71+
throw new \Exception();
72+
}
73+
74+
/**
75+
* @template T
76+
* @param callable(): iterable<T> $cb
77+
* @return P<T>
78+
*/
79+
function iterable(callable $cb): P
80+
{
81+
throw new \Exception();
82+
}
83+
84+
/**
85+
* @template T
86+
* @param P<P<T>|T> $p
87+
* @return P<T>
88+
*/
89+
function nestedInGeneric(P $p): P
90+
{
91+
throw new \Exception();
92+
}
93+
94+
/**
95+
* @template T
96+
* @param array{P<T>|T} $a
97+
* @return P<T>
98+
*/
99+
function nestedInArrayShape(array $a): P
100+
{
101+
throw new \Exception();
102+
}
103+
104+
/**
105+
* @param P<never> $pNever
106+
* @param array{never} $aNever
107+
*/
108+
function test(P $pNever, array $aNever): void
109+
{
110+
$throwing = static function (): void {
111+
throw new \Exception();
112+
};
113+
114+
assertType('NeverIntoUnionTemplate\\P<never>', tOrNull($throwing));
115+
assertType('NeverIntoUnionTemplate\\P<never>', tOrIntFloat($throwing));
116+
assertType('NeverIntoUnionTemplate\\P<never>', arrayOrT($throwing));
117+
assertType('NeverIntoUnionTemplate\\P<never>', iterableOrT($throwing));
118+
assertType('NeverIntoUnionTemplate\\P<never>', traversableOrT($throwing));
119+
assertType('NeverIntoUnionTemplate\\P<never>', pOrT($throwing));
120+
121+
// no union involved: consistent with array<T> and Traversable<T>, which
122+
// infer nothing from never either
123+
assertType('NeverIntoUnionTemplate\\P<mixed>', iterable($throwing));
124+
125+
assertType('NeverIntoUnionTemplate\\P<never>', nestedInGeneric($pNever));
126+
assertType('NeverIntoUnionTemplate\\P<never>', nestedInArrayShape($aNever));
127+
128+
$returningNull = static fn () => null;
129+
$returningInt = static fn (): int => 1;
130+
131+
assertType('NeverIntoUnionTemplate\\P<mixed>', tOrNull($returningNull));
132+
assertType('NeverIntoUnionTemplate\\P<mixed>', tOrIntFloat($returningInt));
133+
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3099,6 +3099,11 @@ public function testBug13114(bool $checkExplicitMixed, bool $checkImplicitMixed)
30993099
]);
31003100
}
31013101

3102+
public function testBug15167(): void
3103+
{
3104+
$this->analyse([__DIR__ . '/data/bug-15167.php'], []);
3105+
}
3106+
31023107
public function testBug15168(): void
31033108
{
31043109
$this->analyse([__DIR__ . '/data/bug-15168.php'], []);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15167Functions;
4+
5+
use Traversable;
6+
7+
/** @template-covariant T */
8+
interface P
9+
{
10+
11+
}
12+
13+
/**
14+
* @template T
15+
* @param callable(): (array<T>|T) $cb
16+
* @return P<T>
17+
*/
18+
function arrayOrT(callable $cb): P
19+
{
20+
throw new \Exception();
21+
}
22+
23+
/**
24+
* @template T
25+
* @param callable(): (iterable<T>|T) $cb
26+
* @return P<T>
27+
*/
28+
function iterableOrT(callable $cb): P
29+
{
30+
throw new \Exception();
31+
}
32+
33+
/**
34+
* @template T
35+
* @param callable(): (Traversable<T>|T) $cb
36+
* @return P<T>
37+
*/
38+
function traversableOrT(callable $cb): P
39+
{
40+
throw new \Exception();
41+
}
42+
43+
/**
44+
* @template T
45+
* @param callable(): (P<T>|T) $cb
46+
* @return P<T>
47+
*/
48+
function pOrT(callable $cb): P
49+
{
50+
throw new \Exception();
51+
}
52+
53+
function test(): void
54+
{
55+
$throwing = static function (): void {
56+
throw new \Exception();
57+
};
58+
59+
arrayOrT($throwing);
60+
iterableOrT($throwing);
61+
traversableOrT($throwing);
62+
pOrT($throwing);
63+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4345,6 +4345,14 @@ public function testBug15002(): void
43454345
$this->analyse([__DIR__ . '/data/bug-15002.php'], []);
43464346
}
43474347

4348+
public function testBug15167(): void
4349+
{
4350+
$this->checkThisOnly = false;
4351+
$this->checkNullables = true;
4352+
$this->checkUnionTypes = true;
4353+
$this->analyse([__DIR__ . '/data/bug-15167.php'], []);
4354+
}
4355+
43484356
#[RequiresPhp('>= 8.1.0')]
43494357
public function testBug8441(): void
43504358
{
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15167Methods;
4+
5+
/** @template-covariant T */
6+
interface P
7+
{
8+
9+
/**
10+
* @template TRejected
11+
* @param ?(callable(\Throwable): (P<TRejected>|TRejected)) $onRejected
12+
* @return P<T|TRejected>
13+
*/
14+
public function catch(?callable $onRejected = null): P;
15+
16+
/**
17+
* @template TR
18+
* @param callable(\Throwable): (P<TR>|TR) $onRejected
19+
* @return P<T|TR>
20+
*/
21+
public function catchNotNullable(callable $onRejected): P;
22+
23+
}
24+
25+
/** @param P<int> $p */
26+
function f(P $p): void
27+
{
28+
$throwing = static function (\Throwable $e): void {
29+
throw $e;
30+
};
31+
$notThrowing = static function (\Throwable $e): void {
32+
};
33+
34+
$p->catch($throwing);
35+
$p->catchNotNullable($throwing);
36+
$p->catch($notThrowing);
37+
}

0 commit comments

Comments
 (0)