Skip to content

Commit 105dc08

Browse files
Improve BackedEnum::from
1 parent 1f150cc commit 105dc08

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\ShouldNotHappenException;
77
use PHPStan\Testing\RuleTestCase;
8+
use PHPUnit\Framework\Attributes\RequiresPhp;
89
use function sprintf;
910
use const PHP_VERSION_ID;
1011

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

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