Add implicit throw point for dynamic instantiation of non-final classes without constructors#5683
Conversation
…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 |
There was a problem hiding this comment.
Add test with an interface and a abstractclass which has throws void on the constructor
There was a problem hiding this comment.
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.', |
There was a problem hiding this comment.
rather than removing the error make the class final
There was a problem hiding this comment.
Pushed. Here's what was done to address the review:
-
bug-4806.php: Made
HasNoConstructorclassfinaland 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. -
bug-6574.php: Added two new test types —
ThrowsVoidInterface(interface with@throws voidconstructor) andAbstractThrowsVoid(abstract class with@throws voidconstructor) — 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.
…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>
Summary
When using
new $class()where$classis typed asclass-string<Interface>,class-string<AbstractClass>, orclass-string<NonFinalClass>and the referenced type has no constructor, PHPStan incorrectly reported: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
src/Analyser/ExprHandler/NewHandler.phpfor the dynamic class name branch when:implicitThrowsis enabled (consistent with constructor throw point handling)tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php:testBug6574test methodtests/PHPStan/Rules/Exceptions/data/bug-6574.phpwith test cases for:tests/PHPStan/Rules/Pure/data/bug-6574.phpand test method inPureFunctionRuleTest.phpto verify the "no side effects" false positive is also fixedRoot cause
In
NewHandler::processExpr, the dynamic class name branch (elsecase) resolvesnew $class()to a single class name and recursively processesnew ClassName(). The recursive call goes throughprocessConstructorReflectionwhich 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$classReflectionis not null (the class IS found in reflection) but$constructorReflectionIS 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 usingnew $class()with interface/abstract class-string types are not falsely flagged as having no side effectsFixes phpstan/phpstan#6574