Skip to content

Commit 99951ac

Browse files
rajyanondrejmirtes
authored andcommitted
Early termination for NeverType match expression
1 parent c564943 commit 99951ac

16 files changed

+156
-27
lines changed

src/Type/Accessory/HasMethodType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic
6262
return $otherType->isSuperTypeOf($this);
6363
}
6464

65+
if ($this->isCallable()->yes() && $otherType->isCallable()->yes()) {
66+
return TrinaryLogic::createYes();
67+
}
68+
6569
if ($otherType instanceof self) {
6670
$limit = TrinaryLogic::createYes();
6771
} else {

src/Type/CallableType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic
8383

8484
public function isSuperTypeOf(Type $type): TrinaryLogic
8585
{
86+
if ($type instanceof CompoundType && !$type instanceof self) {
87+
return $type->isSubTypeOf($this);
88+
}
89+
8690
return $this->isSuperTypeOfInternal($type, false);
8791
}
8892

src/Type/ClosureType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic
108108

109109
public function isSuperTypeOf(Type $type): TrinaryLogic
110110
{
111+
if ($type instanceof CompoundType) {
112+
return $type->isSubTypeOf($this);
113+
}
114+
111115
return $this->isSuperTypeOfInternal($type, false);
112116
}
113117

src/Type/Generic/GenericObjectType.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,15 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic
106106

107107
public function isSuperTypeOf(Type $type): TrinaryLogic
108108
{
109+
if ($type instanceof CompoundType) {
110+
return $type->isSubTypeOf($this);
111+
}
112+
109113
return $this->isSuperTypeOfInternal($type, false);
110114
}
111115

112116
private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): TrinaryLogic
113117
{
114-
if ($type instanceof CompoundType) {
115-
return $type->isSubTypeOf($this);
116-
}
117-
118118
$nakedSuperTypeOf = parent::isSuperTypeOf($type);
119119
if ($nakedSuperTypeOf->no()) {
120120
return $nakedSuperTypeOf;

src/Type/Generic/TemplateTypeTrait.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Type\GeneralizePrecision;
77
use PHPStan\Type\IntersectionType;
88
use PHPStan\Type\MixedType;
9+
use PHPStan\Type\NeverType;
910
use PHPStan\Type\SubtractableType;
1011
use PHPStan\Type\Type;
1112
use PHPStan\Type\TypeCombinator;
@@ -200,6 +201,10 @@ public function isSuperTypeOf(Type $type): TrinaryLogic
200201
return $type->isSubTypeOf($this);
201202
}
202203

204+
if ($type instanceof NeverType) {
205+
return TrinaryLogic::createYes();
206+
}
207+
203208
return $this->getBound()->isSuperTypeOf($type)
204209
->and(TrinaryLogic::createMaybe());
205210
}

src/Type/IntersectionType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic
9292
return TrinaryLogic::createYes();
9393
}
9494

95+
if ($otherType instanceof NeverType) {
96+
return TrinaryLogic::createYes();
97+
}
98+
9599
$results = [];
96100
foreach ($this->getTypes() as $innerType) {
97101
$results[] = $innerType->isSuperTypeOf($otherType);

src/Type/IterableType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic
7878

7979
public function isSuperTypeOf(Type $type): TrinaryLogic
8080
{
81+
if ($type instanceof CompoundType) {
82+
return $type->isSubTypeOf($this);
83+
}
84+
8185
return $type->isIterable()
8286
->and($this->getIterableValueType()->isSuperTypeOf($type->getIterableValueType()))
8387
->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType()));

src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
5555
}
5656

5757
$keyType = TypeCombinator::union(...$keyTypes);
58-
if ($keyType instanceof NeverType && !$keyType->isExplicit()) {
58+
if ($keyType instanceof NeverType) {
5959
return new ConstantArrayType([], []);
6060
}
6161

src/Type/TypeCombinator.php

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,6 @@ public static function union(Type ...$types): Type
131131
$scalarTypes = [];
132132
$hasGenericScalarTypes = [];
133133
for ($i = 0; $i < $typesCount; $i++) {
134-
if ($types[$i] instanceof NeverType) {
135-
unset($types[$i]);
136-
continue;
137-
}
138134
if ($types[$i] instanceof ConstantScalarType) {
139135
$type = $types[$i];
140136
$scalarTypes[get_class($type)][md5($type->describe(VerbosityLevel::cache()))] = $type;
@@ -378,17 +374,11 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
378374
}
379375
}
380376

381-
if (
382-
!$b instanceof ConstantArrayType
383-
&& $b->isSuperTypeOf($a)->yes()
384-
) {
377+
if ($b->isSuperTypeOf($a)->yes()) {
385378
return [null, $b];
386379
}
387380

388-
if (
389-
!$a instanceof ConstantArrayType
390-
&& $a->isSuperTypeOf($b)->yes()
391-
) {
381+
if ($a->isSuperTypeOf($b)->yes()) {
392382
return [$a, null];
393383
}
394384

src/Type/UnionType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic
116116
if (
117117
($otherType instanceof self && !$otherType instanceof TemplateUnionType)
118118
|| $otherType instanceof IterableType
119+
|| $otherType instanceof NeverType
119120
) {
120121
return $otherType->isSubTypeOf($this);
121122
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,10 @@ public function dataFileAsserts(): iterable
806806
yield from $this->gatherAssertTypes(__DIR__ . '/data/curl_getinfo_7.3.php');
807807
}
808808

809+
if (PHP_VERSION_ID >= 80000) {
810+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6251.php');
811+
}
812+
809813
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php');
810814
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6439.php');
811815
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6748.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); // lint >= 8.0
2+
3+
namespace Bug6251;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
function foo()
10+
{
11+
$var = 1;
12+
if (rand(0, 1)) {
13+
match(1) {
14+
1 => throw new \Exception(),
15+
};
16+
} else {
17+
$var = 2;
18+
}
19+
assertType('2', $var);
20+
}
21+
22+
function bar($a): void
23+
{
24+
$var = 1;
25+
if (rand(0, 1)) {
26+
match($a) {
27+
'a' => throw new \Error(),
28+
default => throw new \Exception(),
29+
};
30+
} else {
31+
$var = 2;
32+
}
33+
assertType('2', $var);
34+
}
35+
36+
function baz($a): void
37+
{
38+
$var = 1;
39+
if (rand(0, 1)) {
40+
match($a) {
41+
'a' => throw new \Error(),
42+
// throws UnhandledMatchError if not handled
43+
};
44+
} else {
45+
$var = 2;
46+
}
47+
assertType('2', $var);
48+
}
49+
50+
function buz($a): void
51+
{
52+
$var = 1;
53+
if (rand(0, 1)) {
54+
match($a) {
55+
'a' => throw new \Exception(),
56+
default => var_dump($a),
57+
};
58+
} else {
59+
$var = 2;
60+
}
61+
assertType('1|2', $var);
62+
}
63+
}

tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,17 @@ public function testRule(): void
7272
'Match expression does not handle remaining value: 3',
7373
50,
7474
],
75-
[
76-
'Match expression does not handle remaining values: 1|2|3',
77-
55,
78-
],
7975
[
8076
'Match arm comparison between 1|2 and 3 is always false.',
81-
65,
77+
61,
8278
],
8379
[
8480
'Match arm comparison between 1 and 1 is always true.',
85-
70,
81+
66,
82+
],
83+
[
84+
'Match expression does not handle remaining values: 1|2|3',
85+
78,
8686
],
8787
[
8888
'Match arm comparison between true and false is always false.',

tests/PHPStan/Rules/Comparison/data/match-expr.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ public function doFoo(int $i): void
5252
// unhandled
5353
};
5454

55-
match ($i) {
56-
// unhandled
57-
};
58-
5955
match ($i) {
6056
1, 2 => null,
6157
default => null, // OK
@@ -78,6 +74,10 @@ public function doFoo(int $i): void
7874
default => 1,
7975
1 => 2,
8076
};
77+
78+
match ($i) {
79+
// unhandled
80+
};
8181
}
8282

8383
public function doBar(\Exception $e): void

tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,19 @@ public function testModelMixin(bool $checkExplicitMixedMissingReturn): void
279279
]);
280280
}
281281

282+
public function testBug6257(): void
283+
{
284+
if (PHP_VERSION_ID < 80000) {
285+
$this->markTestSkipped('Test requires PHP 8.0.');
286+
}
287+
$this->checkExplicitMixedMissingReturn = true;
288+
$this->checkPhpDocMissingReturn = true;
289+
$this->analyse([__DIR__ . '/data/bug-6257.php'], [
290+
[
291+
'Function ReturnTypes\sometimesThrows() should always throw an exception or terminate script execution but doesn\'t do that.',
292+
27,
293+
],
294+
]);
295+
}
296+
282297
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php // lint >= 8.0
2+
3+
namespace ReturnTypes;
4+
5+
/**
6+
* @return never
7+
*/
8+
function alwaysThrow() {
9+
match(true) {
10+
true => throw new \Exception(),
11+
};
12+
}
13+
14+
/**
15+
* @return never
16+
*/
17+
function alwaysThrow2() {
18+
match(rand(0, 1)) {
19+
0 => throw new \Exception(),
20+
};
21+
}
22+
23+
/**
24+
* @return never
25+
*/
26+
function sometimesThrows() {
27+
match(rand(0, 1)) {
28+
0 => throw new \Exception(),
29+
default => 'test',
30+
};
31+
}

0 commit comments

Comments
 (0)