Skip to content

Commit ce9299c

Browse files
committed
Bleeding edge - report catch with exception that is not thrown in the try block
1 parent 79642dc commit ce9299c

File tree

7 files changed

+196
-0
lines changed

7 files changed

+196
-0
lines changed

conf/config.level4.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ rules:
1818
- PHPStan\Rules\TooWideTypehints\TooWideFunctionReturnTypehintRule
1919

2020
conditionalTags:
21+
PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule:
22+
phpstan.rules.rule: %featureToggles.preciseExceptionTracking%
2123
PHPStan\Rules\DeadCode\UnusedPrivateConstantRule:
2224
phpstan.rules.rule: %featureToggles.unusedClassElements%
2325
PHPStan\Rules\DeadCode\UnusedPrivateMethodRule:
@@ -147,6 +149,9 @@ services:
147149
tags:
148150
- phpstan.rules.rule
149151

152+
-
153+
class: PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule
154+
150155
-
151156
class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule
152157
arguments:

phpstan-baseline.neon

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ parameters:
110110
count: 1
111111
path: src/PhpDoc/Tag/VarTag.php
112112

113+
-
114+
message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#"
115+
count: 2
116+
path: src/Reflection/BetterReflection/BetterReflectionProvider.php
117+
118+
-
119+
message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\NodeCompiler\\\\Exception\\\\UnableToCompileNode\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAClassReflection\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAnInterfaceReflection is never thrown in the try block\\.$#"
120+
count: 1
121+
path: src/Reflection/BetterReflection/BetterReflectionProvider.php
122+
113123
-
114124
message: "#^Only booleans are allowed in a negated boolean, int\\|false given\\.$#"
115125
count: 1

src/Analyser/NodeScopeResolver.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
use PHPStan\File\FileReader;
5656
use PHPStan\Node\BooleanAndNode;
5757
use PHPStan\Node\BooleanOrNode;
58+
use PHPStan\Node\CatchWithUnthrownExceptionNode;
5859
use PHPStan\Node\ClassConstantsNode;
5960
use PHPStan\Node\ClassMethodsNode;
6061
use PHPStan\Node\ClassPropertiesNode;
@@ -1183,6 +1184,7 @@ private function processStmtNode(
11831184
$throwPoints = $newThrowPoints;
11841185

11851186
if (count($matchingThrowPoints) === 0) {
1187+
$nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType), $scope);
11861188
continue;
11871189
}
11881190

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node;
4+
5+
use PhpParser\Node\Stmt\Catch_;
6+
use PhpParser\NodeAbstract;
7+
use PHPStan\Type\Type;
8+
9+
class CatchWithUnthrownExceptionNode extends NodeAbstract implements VirtualNode
10+
{
11+
12+
private Catch_ $originalNode;
13+
14+
private Type $caughtType;
15+
16+
public function __construct(Catch_ $originalNode, Type $caughtType)
17+
{
18+
parent::__construct($originalNode->getAttributes());
19+
$this->originalNode = $originalNode;
20+
$this->caughtType = $caughtType;
21+
}
22+
23+
public function getOriginalNode(): Catch_
24+
{
25+
return $this->originalNode;
26+
}
27+
28+
public function getCaughtType(): Type
29+
{
30+
return $this->caughtType;
31+
}
32+
33+
public function getType(): string
34+
{
35+
return 'PHPStan_Node_CatchWithUnthrownExceptionNode';
36+
}
37+
38+
/**
39+
* @return string[]
40+
*/
41+
public function getSubNodeNames(): array
42+
{
43+
return [];
44+
}
45+
46+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\CatchWithUnthrownExceptionNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\Type\VerbosityLevel;
11+
12+
/**
13+
* @implements Rule<CatchWithUnthrownExceptionNode>
14+
*/
15+
class CatchWithUnthrownExceptionRule implements Rule
16+
{
17+
18+
public function getNodeType(): string
19+
{
20+
return CatchWithUnthrownExceptionNode::class;
21+
}
22+
23+
public function processNode(Node $node, Scope $scope): array
24+
{
25+
return [
26+
RuleErrorBuilder::message(
27+
sprintf('Dead catch - %s is never thrown in the try block.', $node->getCaughtType()->describe(VerbosityLevel::typeOnly()))
28+
)->line($node->getLine())->build(),
29+
];
30+
}
31+
32+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
class CatchWithUnthrownExceptionRuleTest extends RuleTestCase
9+
{
10+
11+
protected function getRule(): Rule
12+
{
13+
return new CatchWithUnthrownExceptionRule();
14+
}
15+
16+
public function testRule(): void
17+
{
18+
$this->analyse([__DIR__ . '/data/unthrown-exception.php'], [
19+
[
20+
'Dead catch - Throwable is never thrown in the try block.',
21+
12,
22+
],
23+
[
24+
'Dead catch - Exception is never thrown in the try block.',
25+
21,
26+
],
27+
[
28+
'Dead catch - Throwable is never thrown in the try block.',
29+
38,
30+
],
31+
[
32+
'Dead catch - RuntimeException is never thrown in the try block.',
33+
47,
34+
],
35+
]);
36+
}
37+
38+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace UnthrownException;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(): void
9+
{
10+
try {
11+
$foo = 1;
12+
} catch (\Throwable $e) {
13+
// pass
14+
}
15+
}
16+
17+
public function doBar(): void
18+
{
19+
try {
20+
$foo = 1;
21+
} catch (\Exception $e) {
22+
// pass
23+
}
24+
}
25+
26+
/** @throws \InvalidArgumentException */
27+
public function throwIae(): void
28+
{
29+
30+
}
31+
32+
public function doBaz(): void
33+
{
34+
try {
35+
$this->throwIae();
36+
} catch (\InvalidArgumentException $e) {
37+
38+
} catch (\Throwable $e) {
39+
// dead
40+
}
41+
}
42+
43+
public function doLorem(): void
44+
{
45+
try {
46+
$this->throwIae();
47+
} catch (\RuntimeException $e) {
48+
// dead
49+
} catch (\Throwable $e) {
50+
51+
}
52+
}
53+
54+
public function doIpsum(): void
55+
{
56+
try {
57+
$this->throwIae();
58+
} catch (\Throwable $e) {
59+
60+
}
61+
}
62+
63+
}

0 commit comments

Comments
 (0)