Completed
Pull Request — master (#293)
by Дмитрий
03:17
created

ControlFlowGraph   F

Complexity

Total Complexity 44

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 26

Test Coverage

Coverage 32.57%

Importance

Changes 0
Metric Value
dl 0
loc 334
ccs 57
cts 175
cp 0.3257
rs 3.9115
c 0
b 0
f 0
wmc 44
lcom 1
cbo 26

13 Methods

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