Completed
Push — master ( 2cf8eb...09dcac )
by Дмитрий
04:07
created

ControlFlowGraph   F

Complexity

Total Complexity 46

Size/Duplication

Total Lines 350
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 28

Test Coverage

Coverage 95.03%

Importance

Changes 0
Metric Value
dl 0
loc 350
ccs 172
cts 181
cp 0.9503
rs 3.6
c 0
b 0
f 0
wmc 46
lcom 1
cbo 28

13 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 17 5
C passNodes() 0 54 15
A createNewBlockIfNeeded() 0 14 2
D passExpr() 0 36 10
B passIf() 0 31 4
A passFor() 0 14 1
A passDo() 0 18 1
A passWhile() 0 22 1
A passThrow() 0 4 1
A passAssign() 0 4 1
A passReturn() 0 16 2
A passTryCatch() 0 17 2
A getRoot() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ControlFlowGraph often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ControlFlowGraph, and based on these observations, apply Extract Interface, too.

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