Completed
Push — master ( 8f2461...8419d7 )
by Дмитрий
09:16 queued 06:39
created

ControlFlowGraph::passForeach()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 20
ccs 10
cts 10
cp 1
crap 1
rs 9.6
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 52
    public function __construct($statement, Context $context)
46
    {
47 52
        $this->context = $context;
48 52
        $this->root = new Block($this->lastBlockId++);
49
50 52
        if ($statement instanceof Function_) {
51 6
            if ($statement->stmts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statement->stmts of type PhpParser\Node[] 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 5
                $this->passNodes($statement->stmts, $this->root);
53
            }
54
        }
55
56 52
        if ($statement instanceof ClassMethod) {
57 52
            if ($statement->stmts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statement->stmts of type PhpParser\Node[] 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 51
                $this->passNodes($statement->stmts, $this->root);
59
            }
60
        }
61 52
    }
62
63
    /**
64
     * @param \PhpParser\Node[] $nodes
65
     * @param Block $block
66
     */
67 51
    protected function passNodes(array $nodes, Block $block)
68
    {
69 51
        foreach ($nodes as $stmt) {
70 51
            switch (get_class($stmt)) {
71 51
                case \PhpParser\Node\Stmt\Goto_::class:
72 1
                    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 1
                        $block->addChildren(
74 1
                            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 1
                    break;
80 51
                case \PhpParser\Node\Expr\Assign::class:
81 26
                    $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 26
                    break;
83 48
                case \PhpParser\Node\Stmt\Return_::class:
84 37
                    $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 37
                    break;
86 29
                case \PhpParser\Node\Stmt\Foreach_::class:
87 2
                    $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 2
                    break;
89 29
                case \PhpParser\Node\Stmt\For_::class:
90 4
                    $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 4
                    break;
92 29
                case \PhpParser\Node\Stmt\If_::class:
93 6
                    $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 6
                    break;
95 27
                case \PhpParser\Node\Stmt\While_::class:
96 3
                    $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 3
                    break;
98 27
                case \PhpParser\Node\Stmt\Do_::class:
99 3
                    $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 3
                    break;
101 27
                case \PhpParser\Node\Stmt\Throw_::class:
102 1
                    $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 1
                    break;
104 27
                case \PhpParser\Node\Expr\Exit_::class:
105 1
                    $block->addChildren(new Node\ExitNode());
106 1
                    break;
107 26
                case \PhpParser\Node\Stmt\Label::class:
108 1
                    $block = $this->createNewBlockIfNeeded($block);
109 1
                    $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 1
                    $this->labels[$block->label] = $block;
111 1
                    break;
112 25
                case \PhpParser\Node\Stmt\TryCatch::class:
113 2
                    $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 2
                    break;
115 25
                case \PhpParser\Node\Stmt\Nop::class:
116
                    // ignore commented code
117 1
                    break;
118
                default:
119 25
                    $this->context->debug('[CFG] Unimplemented ' . get_class($stmt), $stmt);
120 51
                    break;
121
            }
122
        }
123 51
    }
124
125
    /**
126
     * If current block is not empty, lets create a new one
127
     *
128
     * @param Block $block
129
     * @return Block
130
     */
131 4
    protected function createNewBlockIfNeeded(Block $block)
132
    {
133 4
        if (!$block->getChildren()) {
134 2
            $next = new Block($this->lastBlockId++);
135
136 2
            $next->setExit(
137 2
                $block
138
            );
139
140 2
            return $next;
141
        }
142
143 2
        return $block;
144
    }
145
146
    /**
147
     * @param \PhpParser\Node\Expr $expr
148
     * @return Node\AbstractNode
149
     */
150 38
    protected function passExpr(\PhpParser\Node\Expr $expr)
151
    {
152 38
        switch (get_class($expr)) {
153 38
            case \PhpParser\Node\Expr\BinaryOp\NotIdentical::class:
154 1
                return new Node\Expr\BinaryOp\NotIdentical();
155
156 38
            case \PhpParser\Node\Expr\BinaryOp\Identical::class:
157 1
                return new Node\Expr\BinaryOp\Identical();
158
159 38
            case \PhpParser\Node\Expr\BinaryOp\NotEqual::class:
160 1
                return new Node\Expr\BinaryOp\NotEqual();
161
162 38
            case \PhpParser\Node\Expr\BinaryOp\Equal::class:
163 4
                return new Node\Expr\BinaryOp\Equal();
164
165 38
            case \PhpParser\Node\Expr\BinaryOp\Smaller::class:
166 1
                return new Node\Expr\BinaryOp\Smaller();
167
168 38
            case \PhpParser\Node\Expr\BinaryOp\SmallerOrEqual::class:
169 1
                return new Node\Expr\BinaryOp\SmallerOrEqual();
170
171 38
            case \PhpParser\Node\Expr\BinaryOp\Greater::class:
172 2
                return new Node\Expr\BinaryOp\Greater();
173
174 38
            case \PhpParser\Node\Expr\BinaryOp\GreaterOrEqual::class:
175 1
                return new Node\Expr\BinaryOp\GreaterOrEqual();
176
177 38
            case \PhpParser\Node\Expr\Instanceof_::class:
178
                return new Node\Expr\InstanceOfExpr();
179
180
            default:
181 38
                $this->context->debug('[CFG] Unimplemented ' . get_class($expr), $expr);
182
        }
183
184 38
        return new Node\UnknownNode();
185
    }
186
187
    /**
188
     * @param \PhpParser\Node\Stmt\If_ $if
189
     * @param Block $block
190
     * @return Block
191
     */
192 6
    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 6
        $trueBlock = new Block($this->lastBlockId++);
195 6
        $this->passNodes($if->stmts, $trueBlock);
196
197 6
        $jumpIf = new Node\JumpIfNode($this->passExpr($if->cond), $trueBlock);
198
199 6
        $elseBlock = null;
200
201 6
        if ($if->else) {
202 1
            if ($if->else->stmts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $if->else->stmts of type PhpParser\Node[] 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 1
                $elseBlock = new Block($this->lastBlockId++);
204 1
                $this->passNodes($if->else->stmts, $elseBlock);
205
206 1
                $jumpIf->setElse($elseBlock);
207
            }
208
        }
209
210 6
        $block->addChildren(
211 6
            $jumpIf
212
        );
213
214 6
        $exitBlock = new Block($this->lastBlockId++);
215 6
        $trueBlock->setExit($exitBlock);
216
217 6
        if ($elseBlock) {
218 1
            $elseBlock->setExit($exitBlock);
219
        }
220
221 6
        return $exitBlock;
222
    }
223
224
    /**
225
     * @param \PhpParser\Node\Stmt\Foreach_ $foreach
226
     * @param Block $block
227
     * @return Block
228
     */
229 2
    protected function passForeach(\PhpParser\Node\Stmt\Foreach_ $foreach, Block $block)
230
    {
231 2
        $block->setExit(
232 2
            $next = new Block($this->lastBlockId++)
233
        );
234
235 2
        $next->setExit(
236 2
            $loop = new Block($this->lastBlockId++)
237
        );
238
239 2
        $this->passNodes($foreach->stmts, $loop);
240
241 2
        $loop->addChildren(new Node\JumpNode($next));
242
243 2
        $loop->setExit(
244 2
            $after = new Block($this->lastBlockId++)
245
        );
246
247 2
        return $after;
248
    }
249
250
    /**
251
     * @param \PhpParser\Node\Stmt\For_ $for
252
     * @param Block $block
253
     * @return Block
254
     */
255 4
    protected function passFor(\PhpParser\Node\Stmt\For_ $for, Block $block)
256
    {
257 4
        $this->passNodes($for->init, $block);
258
259 4
        $block->setExit(
260 4
            $loop = new Block($this->lastBlockId++)
261
        );
262 4
        $this->passNodes($for->stmts, $loop);
263
264 4
        $loop->setExit(
265 4
            $after = new Block($this->lastBlockId++)
266
        );
267 4
        return $after;
268
    }
269
270
    /**
271
     * @param \PhpParser\Node\Stmt\Do_ $do
272
     * @param Block $block
273
     * @return Block
274
     */
275 3
    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 3
        $loop = new Block($this->lastBlockId++);
278 3
        $this->passNodes($do->stmts, $loop);
279
280 3
        $block->setExit($loop);
281
282 3
        $cond = new Block($this->lastBlockId++);
283 3
        $loop->setExit($cond);
284
285 3
        $jumpIf = new Node\JumpIfNode($this->passExpr($do->cond), $loop);
286 3
        $cond->addChildren($jumpIf);
287
288 3
        $exitBlock = new Block($this->lastBlockId++);
289 3
        $jumpIf->setElse($exitBlock);
290
291 3
        return $exitBlock;
292
    }
293
294
    /**
295
     * @param \PhpParser\Node\Stmt\While_ $while
296
     * @param Block $cond
297
     * @return Block
298
     */
299 3
    protected function passWhile(\PhpParser\Node\Stmt\While_ $while, Block $cond)
300
    {
301 3
        $loop = new Block($this->lastBlockId++);
302
303 3
        $jumpIf = new Node\JumpIfNode($this->passExpr($while->cond), $loop);
304 3
        $cond->addChildren($jumpIf);
305
306 3
        $this->passNodes($while->stmts, $loop);
307
308 3
        $loop->addChildren(new Node\JumpNode($cond));
309
        //$loop->setExit($cond);
310
311 3
        $after = new Block($this->lastBlockId++);
312 3
        $jumpIf->setElse($after);
313
314 3
        return $after;
315
    }
316
317
    /**
318
     * @param \PhpParser\Node\Stmt\Throw_ $throw_
319
     * @param Block $block
320
     */
321 1
    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 1
        $block->addChildren(new Node\ThrowNode());
324 1
    }
325
326
    /**
327
     * @param \PhpParser\Node\Expr\Assign $assign
328
     * @param Block $block
329
     */
330 26
    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 26
        $block->addChildren(new Node\AssignNode());
333 26
    }
334
335
    /**
336
     * @param \PhpParser\Node\Stmt\Return_ $return
337
     * @param Block $block
338
     */
339 37
    protected function passReturn(\PhpParser\Node\Stmt\Return_ $return, Block $block)
340
    {
341 37
        if ($return->expr) {
342 37
            $block->addChildren(
343 37
                new Node\ReturnNode(
344 37
                    $this->passExpr(
345 37
                        $return->expr
346
                    )
347
                )
348
            );
349
        } else {
350 1
            $block->addChildren(
351 1
                new Node\ReturnNode()
352
            );
353
        }
354
355 37
        $unreachableBlock = new Block($this->lastBlockId++, true);
356 37
        $block->setExit($unreachableBlock);
357
358 37
        return $unreachableBlock;
359
    }
360
361
    /**
362
     * @param \PhpParser\Node\Stmt\TryCatch $stmt
363
     * @param Block $block
364
     * @return Block
365
     */
366 2
    protected function passTryCatch(\PhpParser\Node\Stmt\TryCatch $stmt, Block $block)
367
    {
368 2
        $try = new Block($this->lastBlockId++);
369 2
        $this->passNodes($stmt->stmts, $try);
370
371 2
        $block->setExit($try);
372
373 2
        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 2
        return $try;
382
    }
383
384
    /**
385
     * @return Block
386
     */
387
    public function getRoot()
388
    {
389
        return $this->root;
390
    }
391
}
392