Skip to content

Commit 3f712be

Browse files
committed
Bleeding edge - rule for detecting overwriting exit points in finally
1 parent 00a2eea commit 3f712be

File tree

6 files changed

+200
-0
lines changed

6 files changed

+200
-0
lines changed

conf/config.level4.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ rules:
2020
conditionalTags:
2121
PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule:
2222
phpstan.rules.rule: %featureToggles.preciseExceptionTracking%
23+
PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule:
24+
phpstan.rules.rule: %featureToggles.preciseExceptionTracking%
2325
PHPStan\Rules\DeadCode\UnusedPrivateConstantRule:
2426
phpstan.rules.rule: %featureToggles.unusedClassElements%
2527
PHPStan\Rules\DeadCode\UnusedPrivateMethodRule:
@@ -152,6 +154,9 @@ services:
152154
-
153155
class: PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule
154156

157+
-
158+
class: PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule
159+
155160
-
156161
class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule
157162
arguments:

src/Analyser/NodeScopeResolver.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
use PHPStan\Node\ClassStatementsGatherer;
6464
use PHPStan\Node\ClosureReturnStatementsNode;
6565
use PHPStan\Node\ExecutionEndNode;
66+
use PHPStan\Node\FinallyExitPointsNode;
6667
use PHPStan\Node\FunctionReturnStatementsNode;
6768
use PHPStan\Node\InArrowFunctionNode;
6869
use PHPStan\Node\InClassMethodNode;
@@ -1288,6 +1289,12 @@ private function processStmtNode(
12881289
$throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints());
12891290
$finallyScope = $finallyResult->getScope();
12901291
$finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope);
1292+
if (count($finallyResult->getExitPoints()) > 0) {
1293+
$nodeCallback(new FinallyExitPointsNode(
1294+
$finallyResult->getExitPoints(),
1295+
$exitPoints
1296+
), $scope);
1297+
}
12911298
$exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints());
12921299
}
12931300

src/Node/FinallyExitPointsNode.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node;
4+
5+
use PhpParser\NodeAbstract;
6+
use PHPStan\Analyser\StatementExitPoint;
7+
8+
class FinallyExitPointsNode extends NodeAbstract implements VirtualNode
9+
{
10+
11+
/** @var StatementExitPoint[] */
12+
private array $finallyExitPoints;
13+
14+
/** @var StatementExitPoint[] */
15+
private array $tryCatchExitPoints;
16+
17+
/**
18+
* @param StatementExitPoint[] $finallyExitPoints
19+
* @param StatementExitPoint[] $tryCatchExitPoints
20+
*/
21+
public function __construct(array $finallyExitPoints, array $tryCatchExitPoints)
22+
{
23+
parent::__construct([]);
24+
$this->finallyExitPoints = $finallyExitPoints;
25+
$this->tryCatchExitPoints = $tryCatchExitPoints;
26+
}
27+
28+
/**
29+
* @return StatementExitPoint[]
30+
*/
31+
public function getFinallyExitPoints(): array
32+
{
33+
return $this->finallyExitPoints;
34+
}
35+
36+
/**
37+
* @return StatementExitPoint[]
38+
*/
39+
public function getTryCatchExitPoints(): array
40+
{
41+
return $this->tryCatchExitPoints;
42+
}
43+
44+
public function getType(): string
45+
{
46+
return 'PHPStan_Node_FinallyExitPointsNode';
47+
}
48+
49+
/**
50+
* @return string[]
51+
*/
52+
public function getSubNodeNames(): array
53+
{
54+
return [];
55+
}
56+
57+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\FinallyExitPointsNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
11+
/**
12+
* @implements Rule<FinallyExitPointsNode>
13+
*/
14+
class OverwrittenExitPointByFinallyRule implements Rule
15+
{
16+
17+
public function getNodeType(): string
18+
{
19+
return FinallyExitPointsNode::class;
20+
}
21+
22+
public function processNode(Node $node, Scope $scope): array
23+
{
24+
if (count($node->getTryCatchExitPoints()) === 0) {
25+
return [];
26+
}
27+
28+
$errors = [];
29+
foreach ($node->getTryCatchExitPoints() as $exitPoint) {
30+
$errors[] = RuleErrorBuilder::message(sprintf('This %s is overwritten by a different one in the finally block below.', $this->describeExitPoint($exitPoint->getStatement())))->line($exitPoint->getStatement()->getLine())->build();
31+
}
32+
33+
foreach ($node->getFinallyExitPoints() as $exitPoint) {
34+
$errors[] = RuleErrorBuilder::message(sprintf('The overwriting %s is on this line.', $this->describeExitPoint($exitPoint->getStatement())))->line($exitPoint->getStatement()->getLine())->build();
35+
}
36+
37+
return $errors;
38+
}
39+
40+
private function describeExitPoint(Node\Stmt $stmt): string
41+
{
42+
if ($stmt instanceof Node\Stmt\Return_) {
43+
return 'return';
44+
}
45+
46+
if ($stmt instanceof Node\Stmt\Throw_) {
47+
return 'throw';
48+
}
49+
50+
if ($stmt instanceof Node\Stmt\Continue_) {
51+
return 'continue';
52+
}
53+
54+
if ($stmt instanceof Node\Stmt\Break_) {
55+
return 'break';
56+
}
57+
58+
return 'exit point';
59+
}
60+
61+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
/**
9+
* @extends RuleTestCase<OverwrittenExitPointByFinallyRule>
10+
*/
11+
class OverwrittenExitPointByFinallyRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new OverwrittenExitPointByFinallyRule();
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/overwritten-exit-point.php'], [
22+
[
23+
'This return is overwritten by a different one in the finally block below.',
24+
11,
25+
],
26+
[
27+
'This return is overwritten by a different one in the finally block below.',
28+
13,
29+
],
30+
[
31+
'The overwriting return is on this line.',
32+
15,
33+
],
34+
]);
35+
}
36+
37+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace OverwrittenExitPoint;
4+
5+
function (): string {
6+
try {
7+
if (rand(0, 1)) {
8+
throw new \Exception();
9+
}
10+
11+
return 'foo';
12+
} catch (\Exception $e) {
13+
return 'bar';
14+
} finally {
15+
return 'baz';
16+
}
17+
};
18+
19+
function (): string {
20+
try {
21+
22+
} finally {
23+
return 'foo';
24+
}
25+
};
26+
27+
function (): string {
28+
try {
29+
return 'foo';
30+
} finally {
31+
32+
}
33+
};

0 commit comments

Comments
 (0)