Repository navigation
Expand file tree
/
Copy pathTerminatedNodeAnalyzer.php
More file actions
217 lines (176 loc) · 6.23 KB
/
Copy pathTerminatedNodeAnalyzer.php
File metadata and controls
217 lines (176 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
<?php
declare(strict_types=1);
namespace Rector\NodeAnalyzer;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Exit_;
use PhpParser\Node\Expr\Throw_;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Continue_;
use PhpParser\Node\Stmt\Else_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Finally_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Goto_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\InlineHTML;
use PhpParser\Node\Stmt\Label;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Nop;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\TryCatch;
use PhpParser\NodeVisitor;
use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser;
use Rector\PhpParser\Node\FileNode;
final readonly class TerminatedNodeAnalyzer
{
/**
* @var array<class-string<Node>>
*/
private const array TERMINABLE_NODES = [Return_::class, Break_::class, Continue_::class];
/**
* @var array<class-string<Node>>
*/
private const array TERMINABLE_NODES_BY_ITS_STMTS = [TryCatch::class, If_::class, Switch_::class];
/**
* @var array<class-string<Node>>
*/
private const array ALLOWED_CONTINUE_CURRENT_STMTS = [InlineHTML::class, Nop::class];
public function __construct(
private SimpleCallableNodeTraverser $simpleCallableNodeTraverser
) {
}
/**
* @param StmtsAware $stmtsAware
*/
public function isAlwaysTerminated(Node $stmtsAware, Stmt $node, Stmt $currentStmt): bool
{
if (in_array($currentStmt::class, self::ALLOWED_CONTINUE_CURRENT_STMTS, true)) {
return false;
}
if (($stmtsAware instanceof FileNode || $stmtsAware instanceof Namespace_) && ($currentStmt instanceof ClassLike || $currentStmt instanceof Function_)) {
return false;
}
if (! in_array($node::class, self::TERMINABLE_NODES_BY_ITS_STMTS, true)) {
return $this->isTerminatedNode($node, $currentStmt);
}
if ($node instanceof TryCatch) {
return $this->isTerminatedInLastStmtsTryCatch($node);
}
if ($node instanceof If_) {
return $this->isTerminatedInLastStmtsIf($node);
}
/** @var Switch_ $node */
return $this->isTerminatedInLastStmtsSwitch($node);
}
private function isTerminatedNode(Stmt $previousNode, Stmt $currentStmt): bool
{
if (in_array($previousNode::class, self::TERMINABLE_NODES, true)) {
return true;
}
if ($previousNode instanceof Expression && ($previousNode->expr instanceof Exit_ || $previousNode->expr instanceof Throw_)) {
return true;
}
if ($previousNode instanceof Goto_) {
return ! $currentStmt instanceof Label;
}
return false;
}
private function isTerminatedInLastStmtsSwitch(Switch_ $switch): bool
{
if ($switch->cases === []) {
return false;
}
// a break/continue jumps out of the Switch_, so the next stmt is still executable
if ($this->hasEscapingJump($switch)) {
return false;
}
$hasDefault = false;
foreach ($switch->cases as $key => $case) {
if (! $case->cond instanceof Expr) {
$hasDefault = true;
}
if ($case->stmts === [] && isset($switch->cases[$key + 1])) {
continue;
}
if (! $this->isTerminatedInLastStmts($case->stmts)) {
return false;
}
}
return $hasDefault;
}
private function hasEscapingJump(Switch_ $switch): bool
{
$hasEscapingJump = false;
foreach ($switch->cases as $case) {
$this->simpleCallableNodeTraverser->traverseNodesWithCallable(
$case->stmts,
static function (Node $node) use (&$hasEscapingJump): ?int {
// nested scopes bring their own jump targets
if ($node instanceof FunctionLike || $node instanceof ClassLike) {
return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
if (! $node instanceof Break_ && ! $node instanceof Continue_ && ! $node instanceof Goto_) {
return null;
}
$hasEscapingJump = true;
return NodeVisitor::STOP_TRAVERSAL;
}
);
if ($hasEscapingJump) {
return true;
}
}
return false;
}
private function isTerminatedInLastStmtsTryCatch(TryCatch $tryCatch): bool
{
if ($tryCatch->finally instanceof Finally_ && $this->isTerminatedInLastStmts($tryCatch->finally->stmts)) {
return true;
}
foreach ($tryCatch->catches as $catch) {
if (! $this->isTerminatedInLastStmts($catch->stmts)) {
return false;
}
}
return $this->isTerminatedInLastStmts($tryCatch->stmts);
}
private function isTerminatedInLastStmtsIf(If_ $if): bool
{
// Without ElseIf_[] and Else_, after If_ is possibly executable
if ($if->elseifs === [] && ! $if->else instanceof Else_) {
return false;
}
foreach ($if->elseifs as $elseif) {
if (! $this->isTerminatedInLastStmts($elseif->stmts)) {
return false;
}
}
if (! $this->isTerminatedInLastStmts($if->stmts)) {
return false;
}
if (! $if->else instanceof Else_) {
return false;
}
return $this->isTerminatedInLastStmts($if->else->stmts);
}
/**
* @param Stmt[] $stmts
*/
private function isTerminatedInLastStmts(array $stmts): bool
{
if ($stmts === []) {
return false;
}
$lastKey = array_key_last($stmts);
$lastNode = $stmts[$lastKey];
if ($lastNode instanceof Expression) {
return $lastNode->expr instanceof Exit_ || $lastNode->expr instanceof Throw_;
}
return $lastNode instanceof Return_;
}
}