Completed
Pull Request — master (#293)
by Дмитрий
05:04 queued 01:16
created

ControlFlowGraph   F

Complexity

Total Complexity 44

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 26

Test Coverage

Coverage 32.57%

Importance

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