diff --git a/src/Type/Generic/GenericClassStringType.php b/src/Type/Generic/GenericClassStringType.php index 4caf05985c..1ee3c7ee5f 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -8,12 +8,14 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -172,4 +174,24 @@ public static function __set_state(array $properties): Type return new self($properties['type']); } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ConstantStringType && $typeToRemove->isClassString()) { + $generic = $this->getGenericType(); + + if ($generic instanceof TypeWithClassName) { + $classReflection = $generic->getClassReflection(); + if ( + $classReflection !== null + && $classReflection->isFinal() + && $generic->getClassName() === $typeToRemove->getValue() + ) { + return new NeverType(); + } + } + } + + return parent::tryRemove($typeToRemove); + } + } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 85d802ca25..e26e967e03 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -962,6 +962,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5758.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-3931.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5223.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7698.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-7698.php b/tests/PHPStan/Analyser/data/bug-7698.php new file mode 100644 index 0000000000..adc10510bb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7698.php @@ -0,0 +1,41 @@ +value::class; + assertType('class-string|class-string', $class); + + if ($class === A::class) { + return; + } + + assertType('class-string', $class); + + if ($class === B::class) { + return; + } + + assertType('*NEVER*', $class); +} diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 78108d5788..380093e3b2 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -224,4 +224,13 @@ public function testBug7622(): void $this->analyse([__DIR__ . '/data/bug-7622.php'], []); } + public function testBug7698(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7698.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7698.php b/tests/PHPStan/Rules/Comparison/data/bug-7698.php new file mode 100644 index 0000000000..b06b9cdfb2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7698.php @@ -0,0 +1,51 @@ += 8.1 + +namespace Bug7698Match; + +final class A +{ +} + +final class B +{ +} + +final class C +{ +} + +final class Test +{ + public function __construct(public readonly A|B $value) + { + } +} + +function matchIt() +{ + $t = new Test(new A()); + $class = $t->value::class; + echo match ($class) { + A::class => 'A', + B::class => 'B' + }; +} + +function matchGetClassString() +{ + $t = new Test(new A()); + echo match (get_class($t->value)) { + A::class => 'A', + B::class => 'B' + }; +} + +function test(A|B|C $abc): string +{ + $class = $abc::class; + return match ($class) { + A::class => 'A', + B::class => 'B', + C::class => 'C', + }; +}