diff --git a/src/Type/Php/BackedEnumFromDynamicStaticMethodThrowTypeExtension.php b/src/Type/Php/BackedEnumFromDynamicStaticMethodThrowTypeExtension.php new file mode 100644 index 0000000000..aebe7ec5b4 --- /dev/null +++ b/src/Type/Php/BackedEnumFromDynamicStaticMethodThrowTypeExtension.php @@ -0,0 +1,56 @@ +getName() === 'from' + && $methodReflection->getDeclaringClass()->isBackedEnum(); + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + $arguments = $methodCall->getArgs(); + if (count($arguments) < 1) { + return $methodReflection->getThrowType(); + } + + $valueType = $scope->getType($arguments[0]->value); + if (!$valueType->isConstantScalarValue()->yes()) { + return $methodReflection->getThrowType(); + } + + $enumCases = $methodReflection->getDeclaringClass()->getEnumCases(); + + $backingValueTypes = []; + foreach ($enumCases as $enumCase) { + if ($enumCase->getBackingValueType() === null) { + return $methodReflection->getThrowType(); + } + + $backingValueTypes[] = $enumCase->getBackingValueType(); + } + + $backingValueType = TypeCombinator::union(...$backingValueTypes); + if (!$backingValueType->isSuperTypeOf($valueType)->yes()) { + return $methodReflection->getThrowType(); + } + + return new VoidType(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php index aee538052e..b574f5ed18 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use function sprintf; use const PHP_VERSION_ID; @@ -70,4 +71,27 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], $errors); } + #[RequiresPhp('>= 8.1')] + public function testBug13297(): void + { + $this->analyse([__DIR__ . '/data/bug-13297.php'], [ + [ + "Method Bug13297\HelloWorld::sayHello3() throws checked exception ValueError but it's missing from the PHPDoc @throws tag.", + 25, + ], + [ + "Method Bug13297\HelloWorld::sayHello3() throws checked exception TypeError but it's missing from the PHPDoc @throws tag.", + 25, + ], + [ + "Method Bug13297\HelloWorld::sayHello4() throws checked exception ValueError but it's missing from the PHPDoc @throws tag.", + 31, + ], + [ + "Method Bug13297\HelloWorld::sayHello4() throws checked exception TypeError but it's missing from the PHPDoc @throws tag.", + 31, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-13297.php b/tests/PHPStan/Rules/Exceptions/data/bug-13297.php new file mode 100644 index 0000000000..f7f98dae64 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13297.php @@ -0,0 +1,33 @@ += 8.1 + +namespace Bug13297; + +enum Foo: int { + case A = 1; + case B = 2; +} + +class HelloWorld +{ + /** @param value-of $int */ + public function sayHello(int $int): void + { + Foo::from($int); + } + + public function sayHello2(): void + { + Foo::from(1); + } + + public function sayHello3(int $int): void + { + Foo::from($int); + } + + /** @param 1|2|3 $int */ + public function sayHello4(int $int): void + { + Foo::from($int); + } +}