Skip to content

Fix false positive dead catch for ReflectionMethod::invoke/invokeArgs#4985

Merged
staabm merged 8 commits intophpstan:2.1.xfrom
staabm:bug7719
Feb 18, 2026
Merged

Fix false positive dead catch for ReflectionMethod::invoke/invokeArgs#4985
staabm merged 8 commits intophpstan:2.1.xfrom
staabm:bug7719

Conversation

@staabm
Copy link
Contributor

@staabm staabm commented Feb 18, 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
@staabm staabm marked this pull request as ready for review February 18, 2026 08:59
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

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.

Maybe it's better to have ondrej opinion about something hardcoded like this ? WDYT ?

@staabm
Copy link
Contributor Author

staabm commented Feb 18, 2026

Maybe it's better to have ondrej opinion about something hardcoded like this ? WDYT ?

since there is currently no way to differentiate implicity throw points via extension I think we can hardcode it (similar to how we hardcode things into TypeSpecifier which are not achievable via extenions).

@staabm staabm merged commit 64a7ad9 into phpstan:2.1.x Feb 18, 2026
639 of 644 checks passed
@staabm staabm deleted the bug7719 branch February 18, 2026 10:23
@VincentLanglet
Copy link
Contributor

since there is currently no way to differentiate implicity throw points via extension I think we can hardcode it (similar to how we hardcode things into TypeSpecifier which are not achievable via extenions).

The other solution would be a dynamicThrowTypeExtention with explicitPoint which resolve the method called (so ReflectionMethod/Function needs to be generic of the name method/function) AND the args, in order to know whether it will throw an exception or not ; and which one.

But that's way more work.

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