Skip to content

Add implicit throw point for dynamic instantiation of non-final classes without constructors#5683

Open
phpstan-bot wants to merge 4 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-2gu185u
Open

Add implicit throw point for dynamic instantiation of non-final classes without constructors#5683
phpstan-bot wants to merge 4 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-2gu185u

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When using new $class() where $class is typed as class-string<Interface>, class-string<AbstractClass>, or class-string<NonFinalClass> and the referenced type has no constructor, PHPStan incorrectly reported:

  • "Dead catch - Exception is never thrown in the try block"
  • "Function returns void but does not have any side effects"

This happened because the dynamic instantiation analysis recursively processed new TypeName() which found no constructor, and therefore generated no throw points or impure points. At runtime, the actual class could be any concrete implementation with a constructor that throws.

Changes

  • Added implicit throw point and uncertain impure point in src/Analyser/ExprHandler/NewHandler.php for the dynamic class name branch when:
    • The resolved class has no constructor
    • The class is not final (so the actual runtime class could be a subclass)
    • implicitThrows is enabled (consistent with constructor throw point handling)
  • Updated tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php:
    • Removed the assertion for bug-4806 line 119 which was asserting the buggy behavior
    • Added new testBug6574 test method
  • Added tests/PHPStan/Rules/Exceptions/data/bug-6574.php with test cases for:
    • Interface without constructor (primary reported case)
    • Interface with constructor (already worked correctly)
    • Abstract class without constructor
    • Abstract class with constructor (already worked correctly)
    • Non-final class without constructor
    • Final class without constructor (correctly reports dead catch)
  • Added tests/PHPStan/Rules/Pure/data/bug-6574.php and test method in PureFunctionRuleTest.php to verify the "no side effects" false positive is also fixed

Root cause

In NewHandler::processExpr, the dynamic class name branch (else case) resolves new $class() to a single class name and recursively processes new ClassName(). The recursive call goes through processConstructorReflection which returns no throw/impure points when the class has no constructor. The subsequent throw point logic at lines 199-206 also doesn't fire because $classReflection is not null (the class IS found in reflection) but $constructorReflection IS null (no constructor). This leaves the expression with zero throw points and zero impure points.

The fix adds the missing throw/impure points specifically in the dynamic branch, recognizing that for non-final classes, the actual runtime class could have a constructor. Final classes without constructors correctly remain side-effect-free since they cannot be subclassed.

Test

  • CatchWithUnthrownExceptionRuleTest::testBug6574 - regression test covering interface, abstract class, non-final class (all without constructor, all should NOT report dead catch), and final class (should correctly report dead catch)
  • PureFunctionRuleTest::testBug6574 - regression test verifying that functions using new $class() with interface/abstract class-string types are not falsely flagged as having no side effects

Fixes phpstan/phpstan#6574

…es without constructors

- In `NewHandler::processExpr`, when a dynamic class name (`new $class()`) resolves
  to a single non-final class without a constructor, add an implicit throw point and
  an uncertain impure point. This is because the actual runtime class could be a
  subclass with a constructor that throws.
- The fix is guarded by `implicitThrows` to remain consistent with how constructors
  are handled when that setting is disabled.
- Fixes the false positive "Dead catch" for `class-string<Interface>`,
  `class-string<AbstractClass>`, and `class-string<NonFinalClass>` where the
  referenced type has no constructor.
- Also fixes the false positive "Function returns void but does not have any side
  effects" for the same scenarios.
- Updated `bug-4806` test which was asserting the buggy behavior for a non-final
  class without a constructor.
{
}

interface BarInterface
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add test with an interface and a abstractclass which has throws void on the constructor

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. I added test cases for interfaces and abstract classes with @throws void on their constructors. Both correctly report dead catch because PHPStan trusts the explicit @throws void annotation — when a constructor declares it doesn't throw, that contract is honored even for non-final types. All tests pass and static analysis is clean.

65,
],
[
'Dead catch - Throwable is never thrown in the try block.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

rather than removing the error make the class final

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pushed. Here's what was done to address the review:

  1. bug-4806.php: Made HasNoConstructor class final and restored the dead catch assertion at line 119 (instead of removing the assertion as before). A final class with no constructor correctly has no throw points.

  2. bug-6574.php: Added two new test types — ThrowsVoidInterface (interface with @throws void constructor) and AbstractThrowsVoid (abstract class with @throws void constructor) — plus test functions that verify dead catch is correctly reported for both. When a constructor explicitly declares @throws void, even non-final classes should report dead catch since the contract guarantees no exceptions.

All 12,079 tests pass and make phpstan reports no errors.

phpstan-bot and others added 3 commits May 16, 2026 20:02
…ct classes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d test cases

- Make HasNoConstructor class final in bug-4806.php so the dead catch
  assertion is preserved (instead of removing it)
- Add test cases for interface and abstract class with @throws void
  constructors, which should correctly report dead catch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants