Repository navigation
Expand file tree
/
Copy pathTerminatedNodeAnalyzer.php
More file actions
317 lines (258 loc) · 9.42 KB
/
Copy pathTerminatedNodeAnalyzer.php
File metadata and controls
317 lines (258 loc) · 9.42 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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
<?php
declare(strict_types=1);
namespace Rector\NodeAnalyzer;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Exit_;
use PhpParser\Node\Expr\Throw_;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Scalar\Int_;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Continue_;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\Else_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Finally_;
use PhpParser\Node\Stmt\For_;
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\Node\Stmt\While_;
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;
}
// an infinite loop with no break never falls through to the next stmt
if ($node instanceof While_ || $node instanceof Do_ || $node instanceof For_) {
return $this->isTerminatedInfiniteLoop($node);
}
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 isTerminatedInfiniteLoop(While_|Do_|For_ $loop): bool
{
if (! $this->isInfiniteLoopCondition($loop)) {
return false;
}
// a break/goto escaping the loop makes the following stmt reachable again;
// stay conservative and treat any break/goto in the body as escaping
return ! $this->hasBreakOrGoto($loop->stmts);
}
private function isInfiniteLoopCondition(While_|Do_|For_ $loop): bool
{
if ($loop instanceof For_) {
// "for (;;)" has no condition and loops forever
if ($loop->cond === []) {
return true;
}
$lastCond = end($loop->cond);
return $lastCond instanceof Expr && $this->isAlwaysTrue($lastCond);
}
return $this->isAlwaysTrue($loop->cond);
}
private function isAlwaysTrue(Expr $expr): bool
{
if ($expr instanceof ConstFetch) {
return $expr->name->toLowerString() === 'true';
}
return $expr instanceof Int_ && $expr->value !== 0;
}
/**
* @param Stmt[] $stmts
*/
private function hasBreakOrGoto(array $stmts): bool
{
$hasBreakOrGoto = false;
$this->simpleCallableNodeTraverser->traverseNodesWithCallable(
$stmts,
static function (Node $node) use (&$hasBreakOrGoto): ?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 Goto_) {
return null;
}
$hasBreakOrGoto = true;
return NodeVisitor::STOP_TRAVERSAL;
}
);
return $hasBreakOrGoto;
}
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;
}
}
// catching less than \Throwable lets an Error escape the try,
// so the try/catch is not guaranteed to end control flow here
if ($tryCatch->catches !== [] && ! $this->hasThrowableCatch($tryCatch)) {
return false;
}
return $this->isTerminatedInLastStmts($tryCatch->stmts);
}
private function hasThrowableCatch(TryCatch $tryCatch): bool
{
foreach ($tryCatch->catches as $catch) {
foreach ($catch->types as $type) {
if ($type->toString() === 'Throwable') {
return true;
}
}
}
return false;
}
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
{
// trailing comments are parsed as Nop stmts, skip them to reach the real last stmt
while ($stmts !== [] && end($stmts) instanceof Nop) {
array_pop($stmts);
}
if ($stmts === []) {
return false;
}
$stmt = end($stmts);
if ($stmt instanceof Expression) {
return $stmt->expr instanceof Exit_ || $stmt->expr instanceof Throw_;
}
// an infinite loop with no break never falls through to the next stmt
if ($stmt instanceof While_ || $stmt instanceof Do_ || $stmt instanceof For_) {
return $this->isTerminatedInfiniteLoop($stmt);
}
return $stmt instanceof Return_;
}
}