Passed
Pull Request — master (#400)
by Théo
02:04
created

NameStmtPrefixer   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Test Coverage

Coverage 98.55%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 115
c 1
b 0
f 0
dl 0
loc 207
ccs 68
cts 69
cp 0.9855
rs 2.8
wmc 70

How to fix   Complexity   

Complex Class

Complex classes like NameStmtPrefixer 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.

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 NameStmtPrefixer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the humbug/php-scoper package.
7
 *
8
 * Copyright (c) 2017 Théo FIDRY <[email protected]>,
9
 *                    Pádraic Brady <[email protected]>
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace Humbug\PhpScoper\PhpParser\NodeVisitor;
16
17
use Humbug\PhpScoper\PhpParser\Node\FullyQualifiedFactory;
18
use Humbug\PhpScoper\PhpParser\NodeVisitor\NamespaceStmt\NamespaceStmtCollection;
19
use Humbug\PhpScoper\PhpParser\NodeVisitor\Resolver\FullyQualifiedNameResolver;
20
use Humbug\PhpScoper\PhpParser\NodeVisitor\UseStmt\UseStmtCollection;
21
use Humbug\PhpScoper\Reflector;
22
use Humbug\PhpScoper\Whitelist;
23
use PhpParser\NodeVisitor\NameResolver;
24
use function in_array;
25
use PhpParser\Node;
26
use PhpParser\Node\Expr\ArrowFunction;
27
use PhpParser\Node\Expr\ClassConstFetch;
28
use PhpParser\Node\Expr\ConstFetch;
29
use PhpParser\Node\Expr\FuncCall;
30
use PhpParser\Node\Expr\Instanceof_;
31
use PhpParser\Node\Expr\New_;
32
use PhpParser\Node\Expr\StaticCall;
33
use PhpParser\Node\Expr\StaticPropertyFetch;
34
use PhpParser\Node\Name;
35
use PhpParser\Node\Name\FullyQualified;
36
use PhpParser\Node\NullableType;
37
use PhpParser\Node\Param;
38
use PhpParser\Node\Stmt\Catch_;
39
use PhpParser\Node\Stmt\Class_;
40
use PhpParser\Node\Stmt\ClassMethod;
41
use PhpParser\Node\Stmt\Function_;
42
use PhpParser\Node\Stmt\Interface_;
43
use PhpParser\Node\Stmt\Property;
44
use PhpParser\NodeVisitorAbstract;
45
use function array_merge;
46
use function count;
47
use function in_array;
0 ignored issues
show
Bug introduced by
A parse error occurred: Cannot use in_array as in_array because the name is already in use
Loading history...
48
49
/**
50
 * Prefixes names when appropriate.
51
 *
52
 * ```
53
 * new Foo\Bar();
54
 * ```.
55
 *
56
 * =>
57
 *
58
 * ```
59
 * new \Humbug\Foo\Bar();
60
 * ```
61
 *
62
 * @private
63
 */
64
final class NameStmtPrefixer extends NodeVisitorAbstract
65
{
66
    public const PHP_FUNCTION_KEYWORDS = [
67
        'self',
68 549
        'static',
69
        'parent',
70
    ];
71
72
    private string $prefix;
73
    private Whitelist $whitelist;
74 549
    private NamespaceStmtCollection $namespaceStatements;
75 549
    private UseStmtCollection $useStatements;
76 549
    private FullyQualifiedNameResolver $nameResolver;
77 549
    private NameResolver $newNameResolver;
78
    private Reflector $reflector;
79
80
    public function __construct(
81
        string $prefix,
82
        Whitelist $whitelist,
83 548
        NamespaceStmtCollection $namespaceStatements,
84
        UseStmtCollection $useStatements,
85 548
        FullyQualifiedNameResolver $nameResolver,
86 544
        NameResolver $newNameResolver,
87 548
        Reflector $reflector
88
    ) {
89
        $this->prefix = $prefix;
90
        $this->whitelist = $whitelist;
91 544
        $this->namespaceStatements = $namespaceStatements;
92
        $this->useStatements = $useStatements;
93 544
        $this->nameResolver = $nameResolver;
94
        $this->newNameResolver = $newNameResolver;
95 544
        $this->reflector = $reflector;
96 4
    }
97
98
    public function enterNode(Node $node): Node
99
    {
100 4
        return ($node instanceof Name && ParentNodeAppender::hasParent($node))
101
            ? $this->prefixName($node)
102
            : $node
103
        ;
104 544
    }
105 542
106 541
    private function prefixName(Name $name): Node
107 540
    {
108 540
        $parentNode = ParentNodeAppender::getParent($name);
109 538
110 538
        if ($parentNode instanceof NullableType) {
111 538
            if (false === ParentNodeAppender::hasParent($parentNode)) {
112 538
                return $name;
113 538
            }
114 538
115 538
            $parentNode = ParentNodeAppender::getParent($parentNode);
116 544
        }
117
118
        if (false === (
119 537
            $parentNode instanceof ArrowFunction
120
                || $parentNode instanceof Catch_
121
                || $parentNode instanceof ConstFetch
122
                || $parentNode instanceof Class_
123
                || $parentNode instanceof ClassConstFetch
124 405
                || $parentNode instanceof ClassMethod
125 349
                || $parentNode instanceof FuncCall
126 298
                || $parentNode instanceof Function_
127 236
                || $parentNode instanceof Instanceof_
128 185
                || $parentNode instanceof Interface_
129 133
                || $parentNode instanceof New_
130 131
                || $parentNode instanceof Param
131 120
                || $parentNode instanceof Property
132
                || $parentNode instanceof StaticCall
133 320
                || $parentNode instanceof StaticPropertyFetch
134
        )
135 9
        ) {
136
            return $name;
137
        }
138 403
139 4
        if (
140
            (
141
                $parentNode instanceof Catch_
142 403
                || $parentNode instanceof ClassConstFetch
143
                || $parentNode instanceof New_
144 403
                || $parentNode instanceof FuncCall
145
                || $parentNode instanceof Instanceof_
146 403
                || $parentNode instanceof Param
147 403
                || $parentNode instanceof StaticCall
148
                || $parentNode instanceof StaticPropertyFetch
149 23
            )
150
            && in_array((string) $name, self::PHP_FUNCTION_KEYWORDS, true)
151
        ) {
152
            return $name;
153 392
        }
154 257
155
        if ($parentNode instanceof ConstFetch && 'null' === (string) $name) {
156 61
            return $name;
157
        }
158
159 355
        $resolvedName = $this->nameResolver->resolveName($name)->getName();
160 85
        $newResolvedName = $this->newNameResolver->getNameContext()->getResolvedName($name, Node\Stmt\Use_::TYPE_NORMAL);
161 21
162
        if ((string) $resolvedName !== (string) $newResolvedName) {
163
            $x = '';
164 67
        }
165 19
166
        // Do not prefix if there is a matching use statement.
167
        $useStatement = $this->useStatements->findStatementForNode($this->namespaceStatements->findNamespaceForNode($name), $name);
168
        if (
169
            $useStatement !== null
170 48
            && !($name instanceof FullyQualified)
171 8
            && $resolvedName->parts !== ['Isolated', 'Symfony', 'Component', 'Finder', 'Finder']
172
            && self::array_starts_with($resolvedName->parts, $useStatement->parts)
173
            && !(
174 46
                $parentNode instanceof ConstFetch
175
                && (
176 11
                    $this->whitelist->isGlobalWhitelistedConstant($resolvedName->toString())
177
                    || $this->whitelist->isSymbolWhitelisted($resolvedName->toString(), true)
178
                )
179
            )
180
            && !(
181
                $useStatement->getAttribute('parent')
182
                && $useStatement->getAttribute('parent')->alias !== null
183
                && $this->whitelist->isSymbolWhitelisted($useStatement->toString())
184 320
            )
185 76
        ) {
186 37
            return $name;
187
        }
188
189 54
        if ($this->prefix === $resolvedName->getFirst() // Skip if is already prefixed
190 9
            || $this->whitelist->belongsToWhitelistedNamespace((string) $resolvedName)  // Skip if the namespace node is whitelisted
191
        ) {
192
            return $resolvedName;
193
        }
194 297
195 3
        // Do not prefix if the Name is inside of the current namespace
196
        $namespace = $this->namespaceStatements->getCurrentNamespaceName();
197
        if (
198 297
            (
199 297
                // In a namespace
200 297
                $namespace !== null
201 297
                && array_merge($namespace->parts, $name->parts) === $resolvedName->parts
202
            )
203
            || (
204
                // In the global scope
205
                $namespace === null
206
                && $name->parts === $resolvedName->parts
207
                && !($name instanceof FullyQualified)
208
                && !($parentNode instanceof ConstFetch)
209
                && !$this->whitelist->isSymbolWhitelisted($resolvedName->toString())
210
                && !$this->reflector->isFunctionInternal($resolvedName->toString())
211
                && !$this->reflector->isClassInternal($resolvedName->toString())
212
            )
213
        ) {
214
            return $name;
215
        }
216
217
        // Check if the class can be prefixed
218
        if (false === ($parentNode instanceof ConstFetch || $parentNode instanceof FuncCall)
219
            && $this->reflector->isClassInternal($resolvedName->toString())
220
        ) {
221
            return $resolvedName;
222
        }
223
224
        if ($parentNode instanceof ConstFetch) {
225
            if ($this->whitelist->isSymbolWhitelisted($resolvedName->toString(), true)) {
226
                return $resolvedName;
227
            }
228
229
            if ($this->reflector->isConstantInternal($resolvedName->toString())) {
230
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
231
            }
232
233
            // Constants have an autoloading fallback so we cannot prefix them when the name is ambiguous
234
            // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
235
            if (false === ($resolvedName instanceof FullyQualified)) {
236
                return $resolvedName;
237
            }
238
239
            if ($this->whitelist->isGlobalWhitelistedConstant((string) $resolvedName)) {
240
                // Unlike classes & functions, whitelisted are not prefixed with aliases registered in scoper-autoload.php
241
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
242
            }
243
244
            // Continue
245
        }
246
247
        // Functions have a fallback autoloading so we cannot prefix them when the name is ambiguous
248
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
249
        if ($parentNode instanceof FuncCall) {
250
            if ($this->reflector->isFunctionInternal($resolvedName->toString())) {
251
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
252
            }
253
254
            if (false === ($resolvedName instanceof FullyQualified)) {
255
                return $resolvedName;
256
            }
257
        }
258
259
        if ($parentNode instanceof ClassMethod && $resolvedName->isSpecialClassName()) {
260
            return $name;
261
        }
262
263
        return FullyQualifiedFactory::concat(
264
            $this->prefix,
265
            $resolvedName->toString(),
266
            $resolvedName->getAttributes()
267
        );
268
    }
269
270
    private static function array_starts_with($arr, $prefix): bool
271
    {
272
        $prefixLength = count($prefix);
273
        for ($i = 0; $i < $prefixLength; ++$i) {
274
            if ($arr[$i] !== $prefix[$i]) {
275
                return false;
276
            }
277
        }
278
        return true;
279
    }
280
}
281