Completed
Pull Request — master (#335)
by Дмитрий
02:26
created

ControlFlowGraph::__construct()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.583

Importance

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