Skip to content

Commit c574aa2

Browse files
ondrejmirtesclaude
andcommitted
Add ReflectionClassConstant constructor throw type extension
PhpStorm stubs now declare @throws \Exception on ReflectionClassConstant::__construct. Mirror the ReflectionProperty extension so that constructing it for a known class constant is not treated as throwing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W2hMwFhyMX7JHn6oVKrTPD
1 parent e2bfbf8 commit c574aa2

3 files changed

Lines changed: 136 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\Reflection\ReflectionProvider;
10+
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
11+
use PHPStan\Type\NeverType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeCombinator;
14+
use ReflectionClassConstant;
15+
use function count;
16+
17+
#[AutowiredService]
18+
final class ReflectionClassConstantConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension
19+
{
20+
21+
public function __construct(private ReflectionProvider $reflectionProvider)
22+
{
23+
}
24+
25+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
26+
{
27+
return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionClassConstant::class;
28+
}
29+
30+
public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
31+
{
32+
if (count($methodCall->getArgs()) < 2) {
33+
return $methodReflection->getThrowType();
34+
}
35+
36+
$valueType = $scope->getType($methodCall->getArgs()[0]->value);
37+
$constantType = $scope->getType($methodCall->getArgs()[1]->value);
38+
foreach ($valueType->getConstantStrings() as $constantString) {
39+
if (!$this->reflectionProvider->hasClass($constantString->getValue())) {
40+
return $methodReflection->getThrowType();
41+
}
42+
43+
$classReflection = $this->reflectionProvider->getClass($constantString->getValue());
44+
foreach ($constantType->getConstantStrings() as $constantConstantString) {
45+
if (!$classReflection->hasConstant($constantConstantString->getValue())) {
46+
return $methodReflection->getThrowType();
47+
}
48+
}
49+
50+
$valueType = TypeCombinator::remove($valueType, $constantString);
51+
}
52+
53+
if (!$valueType instanceof NeverType) {
54+
return $methodReflection->getThrowType();
55+
}
56+
57+
// Look for non constantStrings value.
58+
foreach ($constantType->getConstantStrings() as $constantConstantString) {
59+
$constantType = TypeCombinator::remove($constantType, $constantConstantString);
60+
}
61+
62+
if (!$constantType instanceof NeverType) {
63+
return $methodReflection->getThrowType();
64+
}
65+
66+
return null;
67+
}
68+
69+
}

‎tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ public function testRule(): void
141141
'Dead catch - ArithmeticError is never thrown in the try block.',
142142
762,
143143
],
144+
[
145+
'Dead catch - Exception is never thrown in the try block.',
146+
802,
147+
],
144148
]);
145149
}
146150

@@ -233,6 +237,10 @@ public function testRuleWithoutReportingUncheckedException(): void
233237
'Dead catch - Exception is never thrown in the try block.',
234238
555,
235239
],
240+
[
241+
'Dead catch - Exception is never thrown in the try block.',
242+
802,
243+
],
236244
]);
237245
}
238246

@@ -842,4 +850,15 @@ public function testBug9826(): void
842850
$this->analyse([__DIR__ . '/data/bug-9826.php'], []);
843851
}
844852

853+
#[RequiresPhp('>= 8.0.0')]
854+
public function testTriggerErrorThrowType(): void
855+
{
856+
$this->analyse([__DIR__ . '/data/trigger-error-throw-type.php'], [
857+
[
858+
'Dead catch - Exception is never thrown in the try block.',
859+
39,
860+
],
861+
]);
862+
}
863+
845864
}

‎tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,3 +790,51 @@ public function doFoo(int $int, int $negativeInt, int $positiveInt): void
790790
}
791791

792792
}
793+
794+
class TestReflectionClassConstant
795+
{
796+
public const FOO = 1;
797+
798+
public function doFoo(): void
799+
{
800+
try {
801+
new \ReflectionClassConstant(self::class, 'FOO');
802+
} catch (\Exception $e) {
803+
804+
}
805+
}
806+
807+
public function doBar(): void
808+
{
809+
try {
810+
new \ReflectionClassConstant(self::class, 'BAR');
811+
} catch (\Exception $e) {
812+
813+
}
814+
try {
815+
new \ReflectionClassConstant(\DateTime::class, 'BAR');
816+
} catch (\Exception $e) {
817+
818+
}
819+
}
820+
821+
public function doBaz(string $string): void
822+
{
823+
try {
824+
new \ReflectionClassConstant($string, $string);
825+
} catch (\Exception $e) {
826+
827+
}
828+
try {
829+
new \ReflectionClassConstant(self::class, $string);
830+
} catch (\Exception $e) {
831+
832+
}
833+
try {
834+
new \ReflectionClassConstant($string, 'FOO');
835+
} catch (\Exception $e) {
836+
837+
}
838+
}
839+
840+
}

0 commit comments

Comments
 (0)