Skip to content

Commit 8746ed7

Browse files
Improve BackedEnum::from
1 parent 1f150cc commit 8746ed7

File tree

3 files changed

+114
-0
lines changed

3 files changed

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

tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\ShouldNotHappenException;
77
use PHPStan\Testing\RuleTestCase;
8+
use PHPUnit\Framework\Attributes\RequiresPhp;
9+
810
use function sprintf;
911
use const PHP_VERSION_ID;
1012

@@ -70,4 +72,27 @@ public function testRule(): void
7072
$this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], $errors);
7173
}
7274

75+
#[RequiresPhp('>= 8.0')]
76+
public function testBug13297(): void
77+
{
78+
$this->analyse([__DIR__ . '/data/bug-13297.php'], [
79+
[
80+
"Method Bug13297\HelloWorld::sayHello3() throws checked exception ValueError but it's missing from the PHPDoc @throws tag.",
81+
25,
82+
],
83+
[
84+
"Method Bug13297\HelloWorld::sayHello3() throws checked exception TypeError but it's missing from the PHPDoc @throws tag.",
85+
25,
86+
],
87+
[
88+
"Method Bug13297\HelloWorld::sayHello4() throws checked exception ValueError but it's missing from the PHPDoc @throws tag.",
89+
31,
90+
],
91+
[
92+
"Method Bug13297\HelloWorld::sayHello4() throws checked exception TypeError but it's missing from the PHPDoc @throws tag.",
93+
31,
94+
],
95+
]);
96+
}
97+
7398
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php // lint >= 8.0
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)