From 86a188a1f48f29011cec00309cfa2d17418ee0ad Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 05:14:35 +0000 Subject: [PATCH 1/4] Fix false positive dead catch for ReflectionMethod::invoke/invokeArgs - Added ReflectionMethodInvokeMethodThrowTypeExtension to declare that ReflectionMethod::invoke(), ReflectionMethod::invokeArgs(), ReflectionFunction::invoke(), and ReflectionFunction::invokeArgs() can throw any Throwable, since they execute arbitrary user code - New regression test in tests/PHPStan/Rules/Exceptions/data/bug-7719.php - The root cause was that BetterReflection's adapter declared @throws ReflectionException on these methods, causing PHPStan to treat ReflectionException as the only possible thrown type Closes https://github.com/phpstan/phpstan/issues/7719 --- phpstan-baseline.neon | 6 ++ ...onMethodInvokeMethodThrowTypeExtension.php | 32 +++++++++++ .../CatchWithUnthrownExceptionRuleTest.php | 5 ++ .../Rules/Exceptions/data/bug-7719.php | 56 +++++++++++++++++++ tmp/cache/.gitignore | 2 - 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-7719.php delete mode 100644 tmp/cache/.gitignore diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f7b1c64ee0..0d26969976 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1641,6 +1641,12 @@ parameters: count: 1 path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php + - + rawMessage: 'Method PHPStan\Type\Php\ReflectionMethodInvokeMethodThrowTypeExtension::getThrowTypeFromMethodCall() never returns null so it can be removed from the return type.' + identifier: return.unusedType + count: 1 + path: src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php + - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php b/src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php new file mode 100644 index 0000000000..00685e8d72 --- /dev/null +++ b/src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php @@ -0,0 +1,32 @@ +getDeclaringClass()->getName(), [ReflectionMethod::class, ReflectionFunction::class], true) + && in_array($methodReflection->getName(), ['invoke', 'invokeArgs'], true); + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + return new ObjectType(Throwable::class); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 272c010d97..4be8be7913 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -604,6 +604,11 @@ public function testBug9568(): void $this->analyse([__DIR__ . '/data/bug-9568.php'], []); } + public function testBug7719(): void + { + $this->analyse([__DIR__ . '/data/bug-7719.php'], []); + } + #[RequiresPhp('>= 8.4')] public function testPropertyHooks(): void { diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-7719.php b/tests/PHPStan/Rules/Exceptions/data/bug-7719.php new file mode 100644 index 0000000000..5da5979c90 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-7719.php @@ -0,0 +1,56 @@ +invokeArgs($endpoint, ['id' => 2]); + } catch (\RuntimeException $e) { + echo $e->getMessage(); + die; + } + var_dump($methodResponse); + } + + public function sayHelloWithInvoke(Endpoint $endpoint, string $methodName): void + { + try { + $methodResponse = (new \ReflectionMethod($endpoint, $methodName))->invoke($endpoint, 2); + } catch (\RuntimeException $e) { + echo $e->getMessage(); + die; + } + var_dump($methodResponse); + } + + public function sayHelloWithFunction(string $functionName): void + { + try { + $result = (new \ReflectionFunction($functionName))->invokeArgs([1, 2]); + } catch (\RuntimeException $e) { + echo $e->getMessage(); + die; + } + var_dump($result); + } + + public function sayHelloWithFunctionInvoke(string $functionName): void + { + try { + $result = (new \ReflectionFunction($functionName))->invoke(1, 2); + } catch (\RuntimeException $e) { + echo $e->getMessage(); + die; + } + var_dump($result); + } +} diff --git a/tmp/cache/.gitignore b/tmp/cache/.gitignore deleted file mode 100644 index 125e34294b..0000000000 --- a/tmp/cache/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.* From 1cf7ba9a95168d0ea1651d63f0eafc132203b9bf Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 17 Feb 2026 05:22:47 +0000 Subject: [PATCH 2/4] Add regression test for #9267 Closes https://github.com/phpstan/phpstan/issues/9267 Co-Authored-By: Claude Opus 4.6 --- .../CatchWithUnthrownExceptionRuleTest.php | 5 ++++ .../Rules/Exceptions/data/bug-9267.php | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-9267.php diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 4be8be7913..e9c8b70b38 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -609,6 +609,11 @@ public function testBug7719(): void $this->analyse([__DIR__ . '/data/bug-7719.php'], []); } + public function testBug9267(): void + { + $this->analyse([__DIR__ . '/data/bug-9267.php'], []); + } + #[RequiresPhp('>= 8.4')] public function testPropertyHooks(): void { diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-9267.php b/tests/PHPStan/Rules/Exceptions/data/bug-9267.php new file mode 100644 index 0000000000..6ce0f7ef0c --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-9267.php @@ -0,0 +1,30 @@ +invokeArgs(new C, array()); + } + catch (FooException $e) { + print "CAUGHT FOO!\n"; + } +} + +function baz(\ReflectionMethod $r): void { + try { + $r->invoke(new C); + } + catch (FooException $e) { + print "CAUGHT FOO!\n"; + } +} From 8c5b0b38e4befd964012b7098e817c4d8a8a4e64 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:24:51 +0000 Subject: [PATCH 3/4] Use @throws stubs instead of DynamicMethodThrowTypeExtension Replace the DynamicMethodThrowTypeExtension with @throws \Throwable annotations in ReflectionMethod.stub and a new ReflectionFunction.stub. This is a simpler approach that achieves the same result. Co-authored-by: Markus Staab --- conf/config.neon | 1 + phpstan-baseline.neon | 6 ---- ...onMethodInvokeMethodThrowTypeExtension.php | 32 ------------------- stubs/ReflectionFunction.stub | 16 ++++++++++ stubs/ReflectionMethod.stub | 10 ++++++ 5 files changed, 27 insertions(+), 38 deletions(-) delete mode 100644 src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php create mode 100644 stubs/ReflectionFunction.stub diff --git a/conf/config.neon b/conf/config.neon index 19056af1ab..c0bd1d45f3 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -125,6 +125,7 @@ parameters: - ../stubs/Redis.stub - ../stubs/ReflectionAttribute.stub - ../stubs/ReflectionClassConstant.stub + - ../stubs/ReflectionFunction.stub - ../stubs/ReflectionFunctionAbstract.stub - ../stubs/ReflectionMethod.stub - ../stubs/ReflectionParameter.stub diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0d26969976..f7b1c64ee0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1641,12 +1641,6 @@ parameters: count: 1 path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php - - - rawMessage: 'Method PHPStan\Type\Php\ReflectionMethodInvokeMethodThrowTypeExtension::getThrowTypeFromMethodCall() never returns null so it can be removed from the return type.' - identifier: return.unusedType - count: 1 - path: src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php b/src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php deleted file mode 100644 index 00685e8d72..0000000000 --- a/src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php +++ /dev/null @@ -1,32 +0,0 @@ -getDeclaringClass()->getName(), [ReflectionMethod::class, ReflectionFunction::class], true) - && in_array($methodReflection->getName(), ['invoke', 'invokeArgs'], true); - } - - public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type - { - return new ObjectType(Throwable::class); - } - -} diff --git a/stubs/ReflectionFunction.stub b/stubs/ReflectionFunction.stub new file mode 100644 index 0000000000..3b2a530dba --- /dev/null +++ b/stubs/ReflectionFunction.stub @@ -0,0 +1,16 @@ + Date: Tue, 17 Feb 2026 11:34:08 +0000 Subject: [PATCH 4/4] Restore tmp/cache/.gitignore Co-authored-by: Markus Staab --- tmp/cache/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tmp/cache/.gitignore diff --git a/tmp/cache/.gitignore b/tmp/cache/.gitignore new file mode 100644 index 0000000000..125e34294b --- /dev/null +++ b/tmp/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.*