Skip to content

Commit b70fb0f

Browse files
phpstan-botondrejmirtes
authored andcommitted
Fix sealed class-string match exhaustiveness for generic sealed hierarchies
- GenericObjectType::changeSubtractedType() now delegates to parent's sealed type logic, returning NeverType when all allowed subtypes are subtracted - This fixes match expressions on $foo::class reporting "unhandled values" for generic @phpstan-sealed class hierarchies - New regression test in tests/PHPStan/Rules/Comparison/data/bug-14412.php Closes phpstan/phpstan#14412
1 parent 429723d commit b70fb0f

File tree

4 files changed

+72
-1
lines changed

4 files changed

+72
-1
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1134,7 +1134,7 @@ parameters:
11341134
-
11351135
rawMessage: 'Doing instanceof PHPStan\Type\ObjectType is error-prone and deprecated. Use Type::isObject() or Type::getObjectClassNames() instead.'
11361136
identifier: phpstanApi.instanceofType
1137-
count: 1
1137+
count: 2
11381138
path: src/Type/Generic/GenericObjectType.php
11391139

11401140
-

src/Type/Generic/GenericObjectType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,14 @@ protected function recreate(string $className, array $types, ?Type $subtractedTy
405405

406406
public function changeSubtractedType(?Type $subtractedType): Type
407407
{
408+
$result = parent::changeSubtractedType($subtractedType);
409+
410+
// Parent handles sealed type exhaustiveness (returning NeverType when all
411+
// allowed subtypes are subtracted, or a single remaining subtype).
412+
if (!$result instanceof ObjectType || $result->getClassName() !== $this->getClassName()) {
413+
return $result;
414+
}
415+
408416
return new self($this->getClassName(), $this->types, $subtractedType, null, $this->variances);
409417
}
410418

tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,12 @@ public function testBug12241(): void
453453
$this->analyse([__DIR__ . '/data/bug-12241.php'], []);
454454
}
455455

456+
#[RequiresPhp('>= 8.0')]
457+
public function testBug14412(): void
458+
{
459+
$this->analyse([__DIR__ . '/data/bug-14412.php'], []);
460+
}
461+
456462
#[RequiresPhp('>= 8.0')]
457463
public function testBug13029(): void
458464
{
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14412;
6+
7+
/**
8+
* @template-covariant T
9+
* @phpstan-sealed BarCov|BazCov
10+
*/
11+
abstract class FooCov {}
12+
13+
/**
14+
* @template-covariant T
15+
* @extends FooCov<T>
16+
*/
17+
final class BarCov extends FooCov {}
18+
19+
/**
20+
* @template-covariant T
21+
* @extends FooCov<T>
22+
*/
23+
final class BazCov extends FooCov {}
24+
25+
/** @param FooCov<string> $foo */
26+
function testTemplateCovariant(FooCov $foo): string {
27+
return match ($foo::class) {
28+
BarCov::class => 'bar',
29+
BazCov::class => 'baz',
30+
};
31+
}
32+
33+
/**
34+
* @template T
35+
* @phpstan-sealed BarInv|BazInv
36+
*/
37+
abstract class FooInv {}
38+
39+
/**
40+
* @template T
41+
* @extends FooInv<T>
42+
*/
43+
final class BarInv extends FooInv {}
44+
45+
/**
46+
* @template T
47+
* @extends FooInv<T>
48+
*/
49+
final class BazInv extends FooInv {}
50+
51+
/** @param FooInv<covariant string> $foo */
52+
function testCovariantParam(FooInv $foo): string {
53+
return match ($foo::class) {
54+
BarInv::class => 'bar',
55+
BazInv::class => 'baz',
56+
};
57+
}

0 commit comments

Comments
 (0)