Skip to content

Commit dfe0c76

Browse files
authored
Add implicit throw point for dynamic instantiation of non-final classes without constructors (#5683)
1 parent 47759e8 commit dfe0c76

6 files changed

Lines changed: 178 additions & 5 deletions

File tree

src/Analyser/ExprHandler/NewHandler.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
8484
$parametersAcceptor = null;
8585
$constructorReflection = null;
8686
$classReflection = null;
87+
$isDynamic = false;
8788
$hasYield = false;
8889
$throwPoints = [];
8990
$impurePoints = [];
@@ -92,7 +93,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
9293
if ($expr->class instanceof Name) {
9394
$className = $scope->resolveName($expr->class);
9495

95-
[$constructorReflection, $classReflection, $parametersAcceptor, $constructorImpurePoints] = $this->processConstructorReflection($className, $expr, $scope);
96+
[$constructorReflection, $classReflection, $parametersAcceptor, $constructorImpurePoints] = $this->processConstructorReflection($className, $expr, $scope, false);
9697
$impurePoints = array_merge($impurePoints, $constructorImpurePoints);
9798

9899
if ($parametersAcceptor !== null) {
@@ -153,6 +154,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
153154
$nodeScopeResolver->processStmtNode($expr->class, $scope, $storage, $nodeCallback, StatementContext::createTopLevel());
154155
}
155156
} else {
157+
$isDynamic = true;
156158
$objectClasses = $scope->getType($expr)->getObjectClassNames();
157159
if (count($objectClasses) === 1) {
158160
$objectExprResult = $nodeScopeResolver->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, $storage, new NoopNodeCallback(), $context->enterDeep());
@@ -172,7 +174,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
172174
$throwPoints = array_merge($throwPoints, $additionalThrowPoints);
173175

174176
if ($className !== null) {
175-
[$constructorReflection, $classReflection, $parametersAcceptor, $constructorImpurePoints] = $this->processConstructorReflection($className, $expr, $scope);
177+
[$constructorReflection, $classReflection, $parametersAcceptor, $constructorImpurePoints] = $this->processConstructorReflection($className, $expr, $scope, true);
176178
$impurePoints = array_merge($impurePoints, $constructorImpurePoints);
177179
} else {
178180
$impurePoints[] = new ImpurePoint(
@@ -202,7 +204,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
202204
if ($constructorThrowPoint !== null) {
203205
$throwPoints[] = $constructorThrowPoint;
204206
}
205-
} elseif ($classReflection === null) {
207+
} elseif ($classReflection === null || ($isDynamic && $constructorReflection === null && !$classReflection->isFinal())) {
206208
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
207209
}
208210

@@ -218,7 +220,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
218220
/**
219221
* @return array{?MethodReflection, ?ClassReflection, ?ParametersAcceptor, ImpurePoint[]}
220222
*/
221-
private function processConstructorReflection(string $className, New_ $expr, MutatingScope $scope): array
223+
private function processConstructorReflection(string $className, New_ $expr, MutatingScope $scope, bool $isDynamic): array
222224
{
223225
$constructorReflection = null;
224226
$parametersAcceptor = null;
@@ -257,6 +259,14 @@ private function processConstructorReflection(string $className, New_ $expr, Mut
257259
'instantiation of unknown class',
258260
false,
259261
);
262+
} elseif ($isDynamic && !$classReflection->isFinal()) {
263+
$impurePoints[] = new ImpurePoint(
264+
$scope,
265+
$expr,
266+
'new',
267+
sprintf('instantiation of class %s', $classReflection->getDisplayName()),
268+
false,
269+
);
260270
}
261271

262272
return [$constructorReflection, $classReflection, $parametersAcceptor, $impurePoints];

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,4 +798,22 @@ public function testBug14569(): void
798798
$this->analyse([__DIR__ . '/data/bug-14569.php'], []);
799799
}
800800

801+
public function testBug6574(): void
802+
{
803+
$this->analyse([__DIR__ . '/data/bug-6574.php'], [
804+
[
805+
'Dead catch - Exception is never thrown in the try block.',
806+
97,
807+
],
808+
[
809+
'Dead catch - Exception is never thrown in the try block.',
810+
106,
811+
],
812+
[
813+
'Dead catch - Exception is never thrown in the try block.',
814+
115,
815+
],
816+
]);
817+
}
818+
801819
}

tests/PHPStan/Rules/Exceptions/data/bug-4806.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ final public function __construct()
1212
}
1313
}
1414

15-
class HasNoConstructor
15+
final class HasNoConstructor
1616
{
1717

1818
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
interface ThrowsVoidInterface
30+
{
31+
/** @throws void */
32+
public function __construct();
33+
}
34+
35+
abstract class AbstractThrowsVoid
36+
{
37+
/** @throws void */
38+
public function __construct()
39+
{
40+
}
41+
}
42+
43+
final class FinalClass
44+
{
45+
}
46+
47+
/** @param class-string<FooInterface> $class */
48+
function interfaceWithoutConstructor(string $class): void
49+
{
50+
try {
51+
new $class();
52+
} catch (\Exception $e) {
53+
}
54+
}
55+
56+
/** @param class-string<BarInterface> $class */
57+
function interfaceWithConstructor(string $class): void
58+
{
59+
try {
60+
new $class();
61+
} catch (\Exception $e) {
62+
}
63+
}
64+
65+
/** @param class-string<AbstractBaz> $class */
66+
function abstractClassWithoutConstructor(string $class): void
67+
{
68+
try {
69+
new $class();
70+
} catch (\Exception $e) {
71+
}
72+
}
73+
74+
/** @param class-string<AbstractQux> $class */
75+
function abstractClassWithConstructor(string $class): void
76+
{
77+
try {
78+
new $class();
79+
} catch (\Exception $e) {
80+
}
81+
}
82+
83+
/** @param class-string<NonFinalClass> $class */
84+
function nonFinalClassWithoutConstructor(string $class): void
85+
{
86+
try {
87+
new $class();
88+
} catch (\Exception $e) {
89+
}
90+
}
91+
92+
/** @param class-string<ThrowsVoidInterface> $class */
93+
function interfaceWithThrowsVoidConstructor(string $class): void
94+
{
95+
try {
96+
new $class();
97+
} catch (\Exception $e) { // dead catch - constructor is @throws void
98+
}
99+
}
100+
101+
/** @param class-string<AbstractThrowsVoid> $class */
102+
function abstractClassWithThrowsVoidConstructor(string $class): void
103+
{
104+
try {
105+
new $class();
106+
} catch (\Exception $e) { // dead catch - constructor is @throws void
107+
}
108+
}
109+
110+
/** @param class-string<FinalClass> $class */
111+
function finalClassWithoutConstructor(string $class): void
112+
{
113+
try {
114+
new $class();
115+
} catch (\Exception $e) { // dead catch - final class with no constructor
116+
}
117+
}

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)