Skip to content

Commit 88be42e

Browse files
VincentLangletphpstan-bot
authored andcommitted
Add implicit throw point for dynamic instantiation of non-final classes 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.
1 parent c6fb24e commit 88be42e

5 files changed

Lines changed: 134 additions & 4 deletions

File tree

src/Analyser/ExprHandler/NewHandler.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
184184
);
185185
}
186186

187+
if ($classReflection !== null && $constructorReflection === null && !$classReflection->isFinal() && $this->implicitThrows) {
188+
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
189+
$impurePoints[] = new ImpurePoint(
190+
$scope,
191+
$expr,
192+
'new',
193+
sprintf('instantiation of class %s', $classReflection->getDisplayName()),
194+
false,
195+
);
196+
}
197+
187198
if ($parametersAcceptor !== null) {
188199
$normalizedExpr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
189200
}

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,6 @@ public function testBug4806(): void
273273
'Dead catch - ArgumentCountError is never thrown in the try block.',
274274
65,
275275
],
276-
[
277-
'Dead catch - Throwable is never thrown in the try block.',
278-
119,
279-
],
280276
]);
281277
}
282278

@@ -798,4 +794,14 @@ public function testBug14569(): void
798794
$this->analyse([__DIR__ . '/data/bug-14569.php'], []);
799795
}
800796

797+
public function testBug6574(): void
798+
{
799+
$this->analyse([__DIR__ . '/data/bug-6574.php'], [
800+
[
801+
'Dead catch - Exception is never thrown in the try block.',
802+
83,
803+
],
804+
]);
805+
}
806+
801807
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6574;
4+
5+
interface FooInterface
6+
{
7+
}
8+
9+
interface BarInterface
10+
{
11+
public function __construct();
12+
}
13+
14+
abstract class AbstractBaz
15+
{
16+
}
17+
18+
abstract class AbstractQux
19+
{
20+
public function __construct()
21+
{
22+
}
23+
}
24+
25+
class NonFinalClass
26+
{
27+
}
28+
29+
final class FinalClass
30+
{
31+
}
32+
33+
/** @param class-string<FooInterface> $class */
34+
function interfaceWithoutConstructor(string $class): void
35+
{
36+
try {
37+
new $class();
38+
} catch (\Exception $e) {
39+
}
40+
}
41+
42+
/** @param class-string<BarInterface> $class */
43+
function interfaceWithConstructor(string $class): void
44+
{
45+
try {
46+
new $class();
47+
} catch (\Exception $e) {
48+
}
49+
}
50+
51+
/** @param class-string<AbstractBaz> $class */
52+
function abstractClassWithoutConstructor(string $class): void
53+
{
54+
try {
55+
new $class();
56+
} catch (\Exception $e) {
57+
}
58+
}
59+
60+
/** @param class-string<AbstractQux> $class */
61+
function abstractClassWithConstructor(string $class): void
62+
{
63+
try {
64+
new $class();
65+
} catch (\Exception $e) {
66+
}
67+
}
68+
69+
/** @param class-string<NonFinalClass> $class */
70+
function nonFinalClassWithoutConstructor(string $class): void
71+
{
72+
try {
73+
new $class();
74+
} catch (\Exception $e) {
75+
}
76+
}
77+
78+
/** @param class-string<FinalClass> $class */
79+
function finalClassWithoutConstructor(string $class): void
80+
{
81+
try {
82+
new $class();
83+
} catch (\Exception $e) { // dead catch - final class with no constructor
84+
}
85+
}

tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,9 @@ public function testBug14557(): void
225225
$this->analyse([__DIR__ . '/data/bug-14557-function.php'], []);
226226
}
227227

228+
public function testBug6574(): void
229+
{
230+
$this->analyse([__DIR__ . '/data/bug-6574.php'], []);
231+
}
232+
228233
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6574Pure;
4+
5+
interface FooInterface
6+
{
7+
}
8+
9+
abstract class AbstractBar
10+
{
11+
}
12+
13+
/** @param class-string<FooInterface> $class */
14+
function interfaceWithoutConstructor(string $class): void
15+
{
16+
new $class();
17+
}
18+
19+
/** @param class-string<AbstractBar> $class */
20+
function abstractClassWithoutConstructor(string $class): void
21+
{
22+
new $class();
23+
}

0 commit comments

Comments
 (0)