Skip to content

Fix #7719: ReflectionMethod with invokeArgs: Dead catch - exception is never thrown in the try block.#4978

Open
phpstan-bot wants to merge 4 commits into2.1.xfrom
create-pull-request/patch-0g2tzvq
Open

Fix #7719: ReflectionMethod with invokeArgs: Dead catch - exception is never thrown in the try block.#4978
phpstan-bot wants to merge 4 commits into2.1.xfrom
create-pull-request/patch-0g2tzvq

Conversation

@phpstan-bot
Copy link
Collaborator

@phpstan-bot phpstan-bot commented Feb 17, 2026

Summary

PHPStan incorrectly reported "Dead catch - RuntimeException is never thrown in the try block" when catching exceptions around ReflectionMethod::invoke() and ReflectionMethod::invokeArgs() calls. These methods execute arbitrary user code via reflection and can throw any exception, not just ReflectionException.

Changes

  • Added src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php — a DynamicMethodThrowTypeExtension that declares ReflectionMethod::invoke(), ReflectionMethod::invokeArgs(), ReflectionFunction::invoke(), and ReflectionFunction::invokeArgs() can throw Throwable
  • Added regression test tests/PHPStan/Rules/Exceptions/data/bug-7719.php covering all four methods
  • Added test method testBug7719() to CatchWithUnthrownExceptionRuleTest.php
  • Updated phpstan-baseline.neon with one new entry for the extension file

Root cause

The BetterReflection library's adapter class (vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionMethod.php) has @throws ReflectionException annotations on invoke() and invokeArgs(). PHPStan reads these throw type annotations and concludes that only ReflectionException can be thrown by these methods. However, in real PHP, ReflectionMethod::invoke() and invokeArgs() execute arbitrary user code that can throw any exception. The fix adds a dynamic throw type extension that overrides this to return Throwable, correctly indicating that any exception type is possible.

Test

The regression test in tests/PHPStan/Rules/Exceptions/data/bug-7719.php creates four scenarios:

  1. ReflectionMethod::invokeArgs() with a dynamic method name — catch RuntimeException should not be reported as dead
  2. ReflectionMethod::invoke() with a dynamic method name — same
  3. ReflectionFunction::invokeArgs() with a dynamic function name — same
  4. ReflectionFunction::invoke() with a dynamic function name — same

All four expect no errors (empty expected errors array).

Fixes phpstan/phpstan#7719

Closes phpstan/phpstan#9267

phpstan-bot and others added 2 commits February 17, 2026 05:14
- 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 phpstan/phpstan#7719
Closes phpstan/phpstan#9267

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm
Copy link
Contributor

staabm commented Feb 17, 2026

@phpstan-bot instead of implementing a DynamicMethodThrowTypeExtension, declare a @throws type in ReflectionMethod stubs

@phpstan-bot
Copy link
Collaborator Author

phpstan-bot commented Feb 17, 2026

Claude finished @staabm's task in 4m 36s —— View job


Implemented stub-based approach for @throws types

  • Understand current implementation
  • Find ReflectionMethod/ReflectionFunction stubs
  • Add @throws \Throwable to ReflectionMethod.stub for invoke() and invokeArgs()
  • Create ReflectionFunction.stub with @throws \Throwable for invoke() and invokeArgs()
  • Register ReflectionFunction.stub in conf/config.neon
  • Remove DynamicMethodThrowTypeExtension class
  • Remove baseline entry for the extension file
  • Commit and push changes

Summary

Replaced the DynamicMethodThrowTypeExtension approach with @throws \Throwable annotations in stubs, as suggested:

  • stubs/ReflectionMethod.stub — added invoke() and invokeArgs() with @throws \Throwable
  • stubs/ReflectionFunction.stub — new file with invoke() and invokeArgs() with @throws \Throwable
  • conf/config.neon — registered the new ReflectionFunction.stub
  • Deleted src/Type/Php/ReflectionMethodInvokeMethodThrowTypeExtension.php and its baseline entry
  • Existing regression tests (bug-7719.php, bug-9267.php) are unchanged and should continue to pass

Note: I was unable to run tests locally due to sandbox restrictions. CI will validate the changes.

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 <staabm@users.noreply.github.com>
@staabm
Copy link
Contributor

staabm commented Feb 17, 2026

@phpstan-bot discard the change to tmp/cache/.gitignore

@phpstan-bot
Copy link
Collaborator Author

phpstan-bot commented Feb 17, 2026

Claude finished @staabm's task in 29s —— View job


Discarding change to tmp/cache/.gitignore

  • Restore tmp/cache/.gitignore from base branch
  • Commit and push

Restored tmp/cache/.gitignore to its original state.

Co-authored-by: Markus Staab <staabm@users.noreply.github.com>
Copy link
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified the tests failling without the fix locally.

Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this will create lot of false positive on the overside for someone using missingCheckedExceptionInThrows (I personally do it a lot, don't you @staabm ?).

It will require (useless) try/catch or adding @throws every where every time invoke is used, even if we know the method/function won't throw any exception.

It would be better if the throwPoint could be Implicit rather than Explicit in order to avoid both the DeadCatch and reporting missingType.checkedException

Best would be to have a dynamicReturnTypeExtension/dynamicThrowTypeExtension, but this would require changing those class to be generic I think

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants