Completed
Push — master ( c3e51a...f8b546 )
by Дмитрий
06:38
created

ControlFlowGraph   F

Complexity

Total Complexity 44

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 27

Test Coverage

Coverage 32.95%

Importance

Changes 0
Metric Value
dl 0
loc 344
ccs 58
cts 176
cp 0.3295
rs 3.9115
c 0
b 0
f 0
wmc 44
lcom 1
cbo 27

13 Methods

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