ControlFlowGraph::passReturn()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 21
ccs 0
cts 11
cp 0
crap 6
rs 9.584
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Patsura Dmitry https://github.com/ovr <[email protected]>
4
 */
5
6
namespace PHPSA\ControlFlow;
7
8
use PhpParser\Node\Stmt\ClassMethod;
9
use PhpParser\Node\Stmt\Function_;
10
use PHPSA\Context;
11
use PHPSA\ControlFlow\Node;
12
13
class ControlFlowGraph
14
{
15
    /**
16
     * @var int
17
     */
18
    protected $lastBlockId = 1;
19
20
    /**
21
     * @var Block
22
     */
23
    protected $root;
24
25
    /**
26
     * @var Block[]
27
     */
28
    protected $labels;
29
30
    /**
31
     * @todo
32
     *
33
     * @var \PhpParser\Node\Stmt\Goto_[]
34
     */
35
    protected $unresolvedGotos;
36
37
    /**
38
     * @var Context
39
     */
40
    protected $context;
41
42
    /**
43
     * @param $statement
44
     */
45
    public function __construct($statement, Context $context)
46
    {
47
        $this->context = $context;
48
        $this->root = new Block($this->lastBlockId++);
49
50
        if ($statement instanceof Function_) {
51
            if ($statement->stmts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statement->stmts of type PhpParser\Node\Stmt[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
52
                $this->passNodes($statement->stmts, $this->root);
53
            }
54
        }
55
56
        if ($statement instanceof ClassMethod) {
57
            if ($statement->stmts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statement->stmts of type PhpParser\Node\Stmt[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
58
                $this->passNodes($statement->stmts, $this->root);
59
            }
60
        }
61
    }
62
63
    /**
64
     * @param \PhpParser\Node[] $nodes
65
     * @param Block $block
66
     */
67
    protected function passNodes(array $nodes, Block $block)
68
    {
69
        foreach ($nodes as $stmt) {
70
            switch (get_class($stmt)) {
71
                case \PhpParser\Node\Stmt\Goto_::class:
72
                    if (isset($this->labels[$stmt->name])) {
0 ignored issues
show
Bug introduced by
Accessing name on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
73
                        $block->addChildren(
74
                            new Node\JumpNode($this->labels[$stmt->name])
0 ignored issues
show
Bug introduced by
Accessing name on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
75
                        );
76
                    } else {
77
                        $this->unresolvedGotos[] = $stmt;
78
                    }
79
                    break;
80
                case \PhpParser\Node\Expr\Assign::class:
81
                    $this->passAssign($stmt, $block);
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Expr\Assign>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
82
                    break;
83
                case \PhpParser\Node\Stmt\Return_::class:
84
                    $block = $this->passReturn($stmt, $block);
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Stmt\Return_>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
85
                    break;
86
                case \PhpParser\Node\Stmt\Foreach_::class:
87
                    $block = $this->passForeach($stmt, $block);
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Stmt\Foreach_>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
88
                    break;
89
                case \PhpParser\Node\Stmt\For_::class:
90
                    $block = $this->passFor($stmt, $block);
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Stmt\For_>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
91
                    break;
92
                case \PhpParser\Node\Stmt\If_::class:
93
                    $block = $this->passIf($stmt, $block);
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Stmt\If_>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
94
                    break;
95
                case \PhpParser\Node\Stmt\While_::class:
96
                    $block = $this->passWhile($stmt, $this->createNewBlockIfNeeded($block));
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Stmt\While_>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
97
                    break;
98
                case \PhpParser\Node\Stmt\Do_::class:
99
                    $block = $this->passDo($stmt, $block);
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Stmt\Do_>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
100
                    break;
101
                case \PhpParser\Node\Stmt\Throw_::class:
102
                    $this->passThrow($stmt, $block);
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Stmt\Throw_>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
103
                    break;
104
                case \PhpParser\Node\Expr\Exit_::class:
105
                    $block->addChildren(new Node\ExitNode());
106
                    break;
107
                case \PhpParser\Node\Stmt\Label::class:
108
                    $block = $this->createNewBlockIfNeeded($block);
109
                    $block->label = $stmt->name;
0 ignored issues
show
Bug introduced by
Accessing name on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
110
                    $this->labels[$block->label] = $block;
111
                    break;
112
                case \PhpParser\Node\Stmt\TryCatch::class:
113
                    $block = $this->passTryCatch($stmt, $block);
0 ignored issues
show
Compatibility introduced by
$stmt of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Stmt\TryCatch>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
114
                    break;
115
                case \PhpParser\Node\Stmt\Nop::class:
116
                    // ignore commented code
117
                    break;
118
                default:
119
                    $this->context->debug('[CFG] Unimplemented ' . get_class($stmt), $stmt);
120
                    break;
121
            }
122
        }
123
    }
124
125
    /**
126
     * If current block is not empty, lets create a new one
127
     *
128
     * @param Block $block
129
     * @return Block
130
     */
131
    protected function createNewBlockIfNeeded(Block $block)
132
    {
133
        if (!$block->getChildren()) {
134
            $next = new Block($this->lastBlockId++);
135
136
            $next->setExit(
137
                $block
138
            );
139
140
            return $next;
141
        }
142
143
        return $block;
144
    }
145
146
    /**
147
     * @param \PhpParser\Node\Expr $expr
148
     * @return Node\AbstractNode
149
     */
150
    protected function passExpr(\PhpParser\Node\Expr $expr)
151
    {
152
        switch (get_class($expr)) {
153
            case \PhpParser\Node\Expr\BinaryOp\NotIdentical::class:
154
                return new Node\Expr\BinaryOp\NotIdentical();
155
156
            case \PhpParser\Node\Expr\BinaryOp\Identical::class:
157
                return new Node\Expr\BinaryOp\Identical();
158
159
            case \PhpParser\Node\Expr\BinaryOp\NotEqual::class:
160
                return new Node\Expr\BinaryOp\NotEqual();
161
162
            case \PhpParser\Node\Expr\BinaryOp\Equal::class:
163
                return new Node\Expr\BinaryOp\Equal();
164
165
            case \PhpParser\Node\Expr\BinaryOp\Smaller::class:
166
                return new Node\Expr\BinaryOp\Smaller();
167
168
            case \PhpParser\Node\Expr\BinaryOp\SmallerOrEqual::class:
169
                return new Node\Expr\BinaryOp\SmallerOrEqual();
170
171
            case \PhpParser\Node\Expr\BinaryOp\Greater::class:
172
                return new Node\Expr\BinaryOp\Greater();
173
174
            case \PhpParser\Node\Expr\BinaryOp\GreaterOrEqual::class:
175
                return new Node\Expr\BinaryOp\GreaterOrEqual();
176
177
            case \PhpParser\Node\Expr\Instanceof_::class:
178
                return new Node\Expr\InstanceOfExpr();
179
180
            default:
181
                $this->context->debug('[CFG] Unimplemented ' . get_class($expr), $expr);
182
        }
183
184
        return new Node\UnknownNode();
185
    }
186
187
    /**
188
     * @param \PhpParser\Node\Stmt\If_ $if
189
     * @param Block $block
190
     * @return Block
191
     */
192
    protected function passIf(\PhpParser\Node\Stmt\If_ $if, Block $block)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $if. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
193
    {
194
        $trueBlock = new Block($this->lastBlockId++);
195
        $this->passNodes($if->stmts, $trueBlock);
196
197
        $jumpIf = new Node\JumpIfNode($this->passExpr($if->cond), $trueBlock);
198
199
        $elseBlock = null;
200
201
        if ($if->else) {
202
            if ($if->else->stmts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $if->else->stmts of type PhpParser\Node\Stmt[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
203
                $elseBlock = new Block($this->lastBlockId++);
204
                $this->passNodes($if->else->stmts, $elseBlock);
205
206
                $jumpIf->setElse($elseBlock);
207
            }
208
        }
209
210
        $block->addChildren(
211
            $jumpIf
212
        );
213
214
        $exitBlock = new Block($this->lastBlockId++);
215
        $trueBlock->setExit($exitBlock);
216
217
        if ($elseBlock) {
218
            $elseBlock->setExit($exitBlock);
219
        }
220
221
        return $exitBlock;
222
    }
223
224
    /**
225
     * @param \PhpParser\Node\Stmt\Foreach_ $foreach
226
     * @param Block $block
227
     * @return Block
228
     */
229
    protected function passForeach(\PhpParser\Node\Stmt\Foreach_ $foreach, Block $block)
230
    {
231
        $block->setExit(
232
            $next = new Block($this->lastBlockId++)
233
        );
234
235
        $next->setExit(
236
            $loop = new Block($this->lastBlockId++)
237
        );
238
239
        $this->passNodes($foreach->stmts, $loop);
240
241
        $loop->addChildren(new Node\JumpNode($next));
242
243
        $loop->setExit(
244
            $after = new Block($this->lastBlockId++)
245
        );
246
247
        return $after;
248
    }
249
250
    /**
251
     * @param \PhpParser\Node\Stmt\For_ $for
252
     * @param Block $block
253
     * @return Block
254
     */
255
    protected function passFor(\PhpParser\Node\Stmt\For_ $for, Block $block)
256
    {
257
        $this->passNodes($for->init, $block);
258
259
        $block->setExit(
260
            $loop = new Block($this->lastBlockId++)
261
        );
262
        $this->passNodes($for->stmts, $loop);
263
264
        $loop->setExit(
265
            $after = new Block($this->lastBlockId++)
266
        );
267
        return $after;
268
    }
269
270
    /**
271
     * @param \PhpParser\Node\Stmt\Do_ $do
272
     * @param Block $block
273
     * @return Block
274
     */
275
    protected function passDo(\PhpParser\Node\Stmt\Do_ $do, Block $block)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $do. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
276
    {
277
        $loop = new Block($this->lastBlockId++);
278
        $this->passNodes($do->stmts, $loop);
279
280
        $block->setExit($loop);
281
282
        $cond = new Block($this->lastBlockId++);
283
        $loop->setExit($cond);
284
285
        $jumpIf = new Node\JumpIfNode($this->passExpr($do->cond), $loop);
286
        $cond->addChildren($jumpIf);
287
288
        $exitBlock = new Block($this->lastBlockId++);
289
        $jumpIf->setElse($exitBlock);
290
291
        return $exitBlock;
292
    }
293
294
    /**
295
     * @param \PhpParser\Node\Stmt\While_ $while
296
     * @param Block $cond
297
     * @return Block
298
     */
299
    protected function passWhile(\PhpParser\Node\Stmt\While_ $while, Block $cond)
300
    {
301
        $loop = new Block($this->lastBlockId++);
302
303
        $jumpIf = new Node\JumpIfNode($this->passExpr($while->cond), $loop);
304
        $cond->addChildren($jumpIf);
305
306
        $this->passNodes($while->stmts, $loop);
307
308
        $loop->addChildren(new Node\JumpNode($cond));
309
        //$loop->setExit($cond);
310
311
        $after = new Block($this->lastBlockId++);
312
        $jumpIf->setElse($after);
313
314
        return $after;
315
    }
316
317
    /**
318
     * @param \PhpParser\Node\Stmt\Throw_ $throw_
319
     * @param Block $block
320
     */
321
    protected function passThrow(\PhpParser\Node\Stmt\Throw_ $throw_, Block $block)
0 ignored issues
show
Unused Code introduced by
The parameter $throw_ is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
322
    {
323
        $block->addChildren(new Node\ThrowNode());
324
    }
325
326
    /**
327
     * @param \PhpParser\Node\Expr\Assign $assign
328
     * @param Block $block
329
     */
330
    protected function passAssign(\PhpParser\Node\Expr\Assign $assign, Block $block)
0 ignored issues
show
Unused Code introduced by
The parameter $assign is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
331
    {
332
        $block->addChildren(new Node\AssignNode());
333
    }
334
335
    /**
336
     * @param \PhpParser\Node\Stmt\Return_ $return
337
     * @param Block $block
338
     */
339
    protected function passReturn(\PhpParser\Node\Stmt\Return_ $return, Block $block)
340
    {
341
        if ($return->expr) {
342
            $block->addChildren(
343
                new Node\ReturnNode(
344
                    $this->passExpr(
345
                        $return->expr
346
                    )
347
                )
348
            );
349
        } else {
350
            $block->addChildren(
351
                new Node\ReturnNode()
352
            );
353
        }
354
355
        $unreachableBlock = new Block($this->lastBlockId++, true);
356
        $block->setExit($unreachableBlock);
357
358
        return $unreachableBlock;
359
    }
360
361
    /**
362
     * @param \PhpParser\Node\Stmt\TryCatch $stmt
363
     * @param Block $block
364
     * @return Block
365
     */
366
    protected function passTryCatch(\PhpParser\Node\Stmt\TryCatch $stmt, Block $block)
367
    {
368
        $try = new Block($this->lastBlockId++);
369
        $this->passNodes($stmt->stmts, $try);
370
371
        $block->setExit($try);
372
373
        if ($stmt->finally) {
374
            $finally = new Block($this->lastBlockId++);
375
            $this->passNodes($stmt->finally->stmts, $finally);
376
377
            $try->setExit($finally);
378
            return $finally;
379
        }
380
381
        return $try;
382
    }
383
384
    /**
385
     * @return Block
386
     */
387
    public function getRoot()
388
    {
389
        return $this->root;
390
    }
391
}
392