Skip to content

Commit 9b6d39b

Browse files
Improve BackedEnum::from
1 parent 1f150cc commit 9b6d39b

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PhpParser\Node\Expr\StaticCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Type\DynamicMethodThrowTypeExtension;
11+
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeCombinator;
14+
use PHPStan\Type\VerbosityLevel;
15+
use PHPStan\Type\VoidType;
16+
use function count;
17+
use function in_array;
18+
19+
#[AutowiredService]
20+
final class BackedEnumFromDynamicStaticMethodThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension
21+
{
22+
23+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
24+
{
25+
return $methodReflection->getName() === 'from'
26+
&& $methodReflection->getDeclaringClass()->isBackedEnum();
27+
}
28+
29+
public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
30+
{
31+
$arguments = $methodCall->getArgs();
32+
if (count($arguments) < 1) {
33+
return $methodReflection->getThrowType();
34+
}
35+
36+
$valueType = $scope->getType($arguments[0]->value);
37+
if (!$valueType->isConstantScalarValue()->yes()) {
38+
return $methodReflection->getThrowType();
39+
}
40+
41+
$enumCases = $methodReflection->getDeclaringClass()->getEnumCases();
42+
43+
$backingValueTypes = [];
44+
foreach ($enumCases as $enumCase) {
45+
if ($enumCase->getBackingValueType() === null) {
46+
return $methodReflection->getThrowType();
47+
}
48+
49+
$backingValueTypes[] = $enumCase->getBackingValueType();
50+
}
51+
52+
$backingValueType = TypeCombinator::union(...$backingValueTypes);
53+
if (!$backingValueType->isSuperTypeOf($valueType)->yes()) {
54+
return $methodReflection->getThrowType();
55+
}
56+
57+
return new VoidType();
58+
}
59+
60+
}

tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,26 @@ public function testRule(): void
7070
$this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], $errors);
7171
}
7272

73+
public function testBug13297(): void
74+
{
75+
$this->analyse([__DIR__ . '/data/bug-13297.php'], [
76+
[
77+
"Method Bug13297\HelloWorld::sayHello3() throws checked exception ValueError but it's missing from the PHPDoc @throws tag.",
78+
25,
79+
],
80+
[
81+
"Method Bug13297\HelloWorld::sayHello3() throws checked exception TypeError but it's missing from the PHPDoc @throws tag.",
82+
25,
83+
],
84+
[
85+
"Method Bug13297\HelloWorld::sayHello4() throws checked exception ValueError but it's missing from the PHPDoc @throws tag.",
86+
31,
87+
],
88+
[
89+
"Method Bug13297\HelloWorld::sayHello4() throws checked exception TypeError but it's missing from the PHPDoc @throws tag.",
90+
31,
91+
],
92+
]);
93+
}
94+
7395
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13297;
4+
5+
enum Foo: int {
6+
case A = 1;
7+
case B = 2;
8+
}
9+
10+
class HelloWorld
11+
{
12+
/** @param value-of<Foo> $int */
13+
public function sayHello(int $int): void
14+
{
15+
Foo::from($int);
16+
}
17+
18+
public function sayHello2(): void
19+
{
20+
Foo::from(1);
21+
}
22+
23+
public function sayHello3(int $int): void
24+
{
25+
Foo::from($int);
26+
}
27+
28+
/** @param 1|2|3 $int */
29+
public function sayHello4(int $int): void
30+
{
31+
Foo::from($int);
32+
}
33+
}

0 commit comments

Comments
 (0)