Test Failed
Pull Request — master (#37)
by Divine Niiquaye
03:19
created

AutowiringResolver::resolveStmt()   D

Complexity

Conditions 18
Paths 173

Size

Total Lines 56
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 18
eloc 30
c 4
b 0
f 0
nc 173
nop 3
dl 0
loc 56
rs 4.2583

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI\NodeVisitor;
19
20
use PhpParser\Node\Expr;
21
use PhpParser\Node\Stmt\{Class_, ClassMethod, Expression, Nop, Return_};
22
use PhpParser\NodeVisitorAbstract;
23
24
/**
25
 * Autowiring Enhancement for class methods stmts.
26
 *
27
 * Resolves same object id used multiple times in class methods stmts
28
 * and create a variable for the object.
29
 *
30
 * Example of how it works:
31
 *
32
 * ```php
33
 * $hello = $container->call(HelloWorld::class);
34
 * $container->set('foo', new Definition('MyWorld', [$hello]))->bind('addGreeting', [$hello]);
35
 *
36
 * // A variable is created for $hello object, then mapped to receiving nodes.
37
 * ```
38
 *
39
 * @author Divine Niiquaye Ibok <[email protected]>
40
 */
41
class AutowiringResolver extends NodeVisitorAbstract
42
{
43
    /** @var array<string,array<int,Expr\Assign> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string,array<int,Expr\Assign> at position 9 could not be parsed: Expected '>' at position 9, but found '>'.
Loading history...
44
    private array $replacement = [];
45
46
    /** @var array<string,array<string,string>> */
47
    private array $sameArgs = [];
48
49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function enterNode(\PhpParser\Node $node)
53
    {
54
        if ($node instanceof ClassMethod) {
55
            $nodeId = $node->name->toString();
56
57
            if (!isset($this->sameArgs[$nodeId])) {
58
                $methodsStmts = &$node->stmts;
59
                $this->resolveStmt($methodsStmts, $nodeId);
0 ignored issues
show
Bug introduced by
It seems like $methodsStmts can also be of type null; however, parameter $expressions of Rade\DI\NodeVisitor\Auto...Resolver::resolveStmt() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

59
                $this->resolveStmt(/** @scrutinizer ignore-type */ $methodsStmts, $nodeId);
Loading history...
60
            }
61
        }
62
63
        return parent::enterNode($node);
0 ignored issues
show
Bug introduced by
Are you sure the usage of parent::enterNode($node) targeting PhpParser\NodeVisitorAbstract::enterNode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function afterTraverse(array $nodes)
70
    {
71
        foreach ($nodes as $node) {
72
            if ($node instanceof Class_) {
73
                foreach ($node->stmts as $methodNode) {
74
                    if ($methodNode instanceof ClassMethod) {
75
                        $nodeId = $methodNode->name->toString();
76
77
                        if (isset($this->replacement[$nodeId])) {
78
                            $methodsStmts = &$methodNode->stmts;
79
                            $this->resolveStmt($methodsStmts, $nodeId, true);
0 ignored issues
show
Bug introduced by
It seems like $methodsStmts can also be of type null; however, parameter $expressions of Rade\DI\NodeVisitor\Auto...Resolver::resolveStmt() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

79
                            $this->resolveStmt(/** @scrutinizer ignore-type */ $methodsStmts, $nodeId, true);
Loading history...
80
81
                            $replacements = \array_map(fn (Expr\Assign $node) => new Expression($node), $this->replacement[$nodeId]);
82
                            $methodNode->stmts = [...\array_values($replacements), new Nop(), ...$methodsStmts];
83
                        }
84
                    }
85
                }
86
            }
87
        }
88
89
        return parent::afterTraverse($nodes);
0 ignored issues
show
Bug introduced by
Are you sure the usage of parent::afterTraverse($nodes) targeting PhpParser\NodeVisitorAbstract::afterTraverse() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
90
    }
91
92
    /**
93
     * @param array<int,Expression|Return_|Expr> $expressions
94
     */
95
    private function resolveStmt(array $expressions, string $parentId, bool $leaveNodes = false): void
96
    {
97
        foreach ($expressions as &$expression) {
98
            if ($expression instanceof Expression || $expression instanceof Return_) {
99
                $expression = $expression->expr;
100
                $fromStmt = true; // $expression is from stmt
101
102
                if ($expression instanceof Expr\Assign) {
103
                    $expression = &$expression->expr;
104
                }
105
            } elseif (\property_exists($expression, 'value') && $expression->value instanceof \PhpParser\Node) {
106
                $expression = &$expression->value;
107
            }
108
109
            if (\property_exists($expression, 'expr')) {
110
                $this->doReplacement($expression, $parentId, $leaveNodes, $expR);
111
112
                if ($expression instanceof Expr\Variable || $expR) {
113
                    continue;
114
                }
115
116
                $expression = &$expression->expr;
117
            }
118
119
            if ($expression instanceof Expr\MethodCall) {
120
                $exprVar = &$expression->var;
121
122
                if (!$expression instanceof Expr\Variable) {
123
                    $this->doReplacement($exprVar, $parentId, $leaveNodes);
124
                }
125
            }
126
127
            if ($expression instanceof Expr\CallLike) {
128
                $this->resolveStmt($expression->args, $parentId, $leaveNodes);
129
130
                if (isset($fromStmt)) {
131
                    continue;
132
                }
133
            }
134
135
            if ($expression instanceof Expr\Array_) {
136
                if (isset($this->replacement[$parentId][\spl_object_id($expression)]) && $leaveNodes) {
137
                    $removeIds = \array_intersect_key(
138
                        \array_keys($this->replacement[$parentId]),
139
                        \array_map(fn (Expr\ArrayItem $i) => \spl_object_id($i->value), $expression->items)
140
                    );
141
142
                    foreach ($removeIds as $removeId) {
143
                        unset($this->replacement[$parentId][$removeId]);
144
                    }
145
                }
146
147
                $this->resolveStmt($expression->items, $parentId, $leaveNodes);
148
            }
149
150
            $this->doReplacement($expression, $parentId, $leaveNodes);
151
        }
152
    }
153
154
    private function doReplacement(\PhpParser\Node &$nodeValue, string $parentId, bool $leaveNodes, ?bool &$replaced = false): void
155
    {
156
        if ($nodeValue instanceof Expr\Variable || $nodeValue instanceof Expr\Closure) {
157
            return; // @Todo: work in progress ...
158
        }
159
160
        $nodeId = \spl_object_id($nodeValue);
161
162
        if ($leaveNodes) {
163
            if (isset($this->replacement[$parentId][$nodeId])) {
164
                $nodeValue = $this->replacement[$parentId][$nodeId]->var;
165
            }
166
        } elseif (null === $varName = $this->sameArgs[$parentId][$nodeId] ?? null) {
167
            $this->sameArgs[$parentId][$nodeId] = 'v_' . \substr(\hash('sha256', \serialize($nodeValue)), 0, 5);
168
        } else {
169
            $this->replacement[$parentId][$nodeId] = new Expr\Assign(new Expr\Variable($varName), $nodeValue);
170
            $replaced = true;
171
        }
172
    }
173
}
174