Passed
Push — master ( 5961c9...04a23b )
by Théo
01:54
created

NameStmtPrefixer   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 320
Duplicated Lines 0 %

Test Coverage

Coverage 98.55%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 160
dl 0
loc 320
ccs 68
cts 69
cp 0.9855
rs 2
c 2
b 0
f 0
wmc 84

7 Methods

Rating   Name   Duplication   Size   Complexity  
A enterNode() 0 11 3
A arrayStartsWith() 0 11 3
A findParent() 0 13 3
A doesNameBelongToNamespace() 0 22 4
A __construct() 0 14 1
D prefixName() 0 154 58
C doesNameBelongToUseStatement() 0 63 12

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\Node;
24
use PhpParser\Node\Expr\ArrowFunction;
25
use PhpParser\Node\Expr\ClassConstFetch;
26
use PhpParser\Node\Expr\ConstFetch;
27
use PhpParser\Node\Expr\FuncCall;
28
use PhpParser\Node\Expr\Instanceof_;
29
use PhpParser\Node\Expr\New_;
30
use PhpParser\Node\Expr\StaticCall;
31
use PhpParser\Node\Expr\StaticPropertyFetch;
32
use PhpParser\Node\Name;
33
use PhpParser\Node\Name\FullyQualified;
34
use PhpParser\Node\NullableType;
35
use PhpParser\Node\Param;
36
use PhpParser\Node\Stmt\Catch_;
37
use PhpParser\Node\Stmt\Class_;
38
use PhpParser\Node\Stmt\ClassMethod;
39
use PhpParser\Node\Stmt\Function_;
40
use PhpParser\Node\Stmt\Interface_;
41
use PhpParser\Node\Stmt\Property;
42
use PhpParser\Node\Stmt\TraitUse;
43
use PhpParser\Node\Stmt\TraitUseAdaptation\Alias;
44
use PhpParser\Node\Stmt\TraitUseAdaptation\Precedence;
45
use PhpParser\Node\Stmt\Use_;
46
use PhpParser\Node\Stmt\UseUse;
47
use PhpParser\NodeVisitorAbstract;
48
use function count;
49
use function in_array;
50
use function strtolower;
51
52
/**
53
 * Prefixes names when appropriate.
54
 *
55
 * ```
56
 * new Foo\Bar();
57
 * ```.
58
 *
59
 * =>
60
 *
61
 * ```
62
 * new \Humbug\Foo\Bar();
63
 * ```
64
 *
65
 * @private
66
 */
67
final class NameStmtPrefixer extends NodeVisitorAbstract
68 549
{
69
    public const PHP_SPECIAL_KEYWORDS = [
70
        'self',
71
        'static',
72
        'parent',
73
    ];
74 549
75 549
    private string $prefix;
76 549
    private Whitelist $whitelist;
77 549
    private NamespaceStmtCollection $namespaceStatements;
78
    private UseStmtCollection $useStatements;
79
    private FullyQualifiedNameResolver $nameResolver;
80
    private Reflector $reflector;
81
82
    public function __construct(
83 548
        string $prefix,
84
        Whitelist $whitelist,
85 548
        NamespaceStmtCollection $namespaceStatements,
86 544
        UseStmtCollection $useStatements,
87 548
        FullyQualifiedNameResolver $nameResolver,
88
        Reflector $reflector
89
    ) {
90
        $this->prefix = $prefix;
91 544
        $this->whitelist = $whitelist;
92
        $this->namespaceStatements = $namespaceStatements;
93 544
        $this->useStatements = $useStatements;
94
        $this->nameResolver = $nameResolver;
95 544
        $this->reflector = $reflector;
96 4
    }
97
98
    public function enterNode(Node $node): Node
99
    {
100 4
        if (!($node instanceof Name)) {
101
            return $node;
102
        }
103
104 544
        $parent = self::findParent($node);
105 542
106 541
        return null !== $parent
107 540
            ? $this->prefixName($node, $parent)
108 540
            : $node;
109 538
    }
110 538
111 538
    private static function findParent(Node $name): ?Node
112 538
    {
113 538
        $parent = ParentNodeAppender::findParent($name);
114 538
115 538
        if (null === $parent) {
116 544
            return $parent;
117
        }
118
119 537
        if (!($parent instanceof NullableType)) {
120
            return $parent;
121
        }
122
123
        return self::findParent($parent);
124 405
    }
125 349
126 298
    private function prefixName(Name $resolvedName, Node $parentNode): Node
127 236
    {
128 185
        if (false === (
129 133
            $parentNode instanceof Alias
130 131
            || $parentNode instanceof ArrowFunction
131 120
            || $parentNode instanceof Catch_
132
            || $parentNode instanceof ConstFetch
133 320
            || $parentNode instanceof Class_
134
            || $parentNode instanceof ClassConstFetch
135 9
            || $parentNode instanceof ClassMethod
136
            || $parentNode instanceof FuncCall
137
            || $parentNode instanceof Function_
138 403
            || $parentNode instanceof Instanceof_
139 4
            || $parentNode instanceof Interface_
140
            || $parentNode instanceof New_
141
            || $parentNode instanceof Param
142 403
            || $parentNode instanceof Precedence
143
            || $parentNode instanceof Property
144 403
            || $parentNode instanceof StaticCall
145
            || $parentNode instanceof StaticPropertyFetch
146 403
            || $parentNode instanceof TraitUse
147 403
        )
148
        ) {
149 23
            return $resolvedName;
150
        }
151
152
        if (
153 392
            (
154 257
                $parentNode instanceof Catch_
155
                || $parentNode instanceof ClassConstFetch
156 61
                || $parentNode instanceof New_
157
                || $parentNode instanceof FuncCall
158
                || $parentNode instanceof Instanceof_
159 355
                || $parentNode instanceof Param
160 85
                || $parentNode instanceof Property
161 21
                || $parentNode instanceof StaticCall
162
                || $parentNode instanceof StaticPropertyFetch
163
            )
164 67
            && in_array((string) $resolvedName, self::PHP_SPECIAL_KEYWORDS, true)
165 19
        ) {
166
            return $resolvedName;
167
        }
168
169
        $originalName = OriginalNameResolver::getOriginalName($resolvedName);
170 48
171 8
        if ($parentNode instanceof ConstFetch && 'null' === $originalName->toLowerString()) {
172
            return $originalName;
173
        }
174 46
175
        // Do not prefix if there is a matching use statement.
176 11
        $useStatement = $this->useStatements->findStatementForNode(
177
            $this->namespaceStatements->findNamespaceForNode($resolvedName),
178
            $resolvedName,
179
        );
180
181
        if (
182
            self::doesNameBelongToUseStatement(
183
                $originalName,
184 320
                $resolvedName,
185 76
                $parentNode,
186 37
                $useStatement,
187
                $this->whitelist,
188
            )
189 54
        ) {
190 9
            return $originalName;
191
        }
192
193
        if ($resolvedName instanceof FullyQualified
194 297
            && (
195 3
                $this->prefix === $resolvedName->getFirst() // Skip if is already prefixed
196
                || $this->whitelist->belongsToWhitelistedNamespace((string) $resolvedName)  // Skip if the namespace node is whitelisted
197
            )
198 297
        ) {
199 297
            return $resolvedName;
200 297
        }
201 297
202
        // Do not prefix if the Name is inside of the current namespace
203
        $currentNamespace = $this->namespaceStatements->getCurrentNamespaceName();
204
205
        if (
206
            self::doesNameBelongToNamespace(
207
                $originalName,
208
                $resolvedName,
209
                $currentNamespace,
210
            )
211
            || (
212
                // In the global scope
213
                $currentNamespace === null
214
                && $originalName->parts === $resolvedName->parts
215
                && !($originalName instanceof FullyQualified)
216
                && !($parentNode instanceof ConstFetch)
217
                && $resolvedName instanceof FullyQualified
218
                && !$this->whitelist->isSymbolWhitelisted($resolvedName->toString())
219
                && !$this->reflector->isFunctionInternal($resolvedName->toString())
220
                && !$this->reflector->isClassInternal($resolvedName->toString())
221
            )
222
        ) {
223
            return $originalName;
224
        }
225
226
        // Check if the class can be prefixed
227
        if (!($parentNode instanceof ConstFetch || $parentNode instanceof FuncCall)
228
            && $resolvedName instanceof FullyQualified
229
            && $this->reflector->isClassInternal($resolvedName->toString())
230
        ) {
231
            return $resolvedName;
232
        }
233
234
        if ($parentNode instanceof ConstFetch) {
235
            if ($this->whitelist->isSymbolWhitelisted($resolvedName->toString(), true)) {
236
                return $resolvedName;
237
            }
238
239
            if ($this->reflector->isConstantInternal($resolvedName->toString())) {
240
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
241
            }
242
243
            // Constants have an autoloading fallback so we cannot prefix them when the name is ambiguous
244
            // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
245
            if (false === ($resolvedName instanceof FullyQualified)) {
246
                return $resolvedName;
247
            }
248
249
            if ($this->whitelist->isGlobalWhitelistedConstant((string) $resolvedName)) {
250
                // Unlike classes & functions, whitelisted are not prefixed with aliases registered in scoper-autoload.php
251
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
252
            }
253
254
            // Continue
255
        }
256
257
        // Functions have a fallback auto-loading so we cannot prefix them when the name is ambiguous
258
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
259
        if ($parentNode instanceof FuncCall) {
260
            if ($this->reflector->isFunctionInternal($originalName->toString())) {
261
                return new FullyQualified(
262
                    $originalName->toString(),
263
                    $originalName->getAttributes(),
264
                );
265
            }
266
267
            if (!($resolvedName instanceof FullyQualified)) {
268
                return $resolvedName;
269
            }
270
        }
271
272
        if ($parentNode instanceof ClassMethod && $resolvedName->isSpecialClassName()) {
273
            return $resolvedName;
274
        }
275
276
        return FullyQualifiedFactory::concat(
277
            $this->prefix,
278
            $resolvedName->toString(),
279
            $resolvedName->getAttributes()
280
        );
281
    }
282
283
    /**
284
     * @param string[] $array
285
     * @param string[] $start
286
     */
287
    private static function arrayStartsWith(array $array, array $start): bool
288
    {
289
        $prefixLength = count($start);
290
291
        for ($index = 0; $index < $prefixLength; ++$index) {
292
            if ($array[$index] !== $start[$index]) {
293
                return false;
294
            }
295
        }
296
297
        return true;
298
    }
299
300
    private static function doesNameBelongToNamespace(
301
        Name $originalName,
302
        Name $resolvedName,
303
        ?Name $namespace
304
    ): bool
305
    {
306
        if (
307
            $namespace === null
308
            || !($resolvedName instanceof FullyQualified)
309
            // In case the original name is a FQ, we do not skip the prefixing
310
            // and keep it as FQ
311
            || $originalName instanceof FullyQualified
312
        ) {
313
            return false;
314
        }
315
316
        $originalNameFQParts = [
317
            ...$namespace->parts,
318
            ...$originalName->parts,
319
        ];
320
321
        return $originalNameFQParts === $resolvedName->parts;
322
    }
323
324
    private static function doesNameBelongToUseStatement(
325
        Name $originalName,
326
        Name $resolvedName,
327
        Node $parentNode,
328
        ?Name $useStatementName,
329
        Whitelist $whitelist
330
    ): bool
331
    {
332
        if (
333
            null === $useStatementName
334
            || !($resolvedName instanceof FullyQualified)
335
            // In case the original name is a FQ, we do not skip the prefixing
336
            // and keep it as FQ
337
            || $originalName instanceof FullyQualified
338
            // TODO: review Isolated Finder support
339
            || $resolvedName->parts === ['Isolated', 'Symfony', 'Component', 'Finder', 'Finder']
340
            || !self::arrayStartsWith($resolvedName->parts, $useStatementName->parts)
341
        ) {
342
            return false;
343
        }
344
345
        if ($parentNode instanceof ConstFetch) {
346
            // If a constant is whitelisted, it can be that letting a non FQ breaks
347
            // things. For example the whitelisted namespaced constant could be
348
            // used via a partial import (in which case it is a regular import not
349
            // a constant one) which may not be prefixed.
350
            // For this reason in this scenario we will always transform the
351
            // constant in a FQ one.
352
            // Note that this could be adjusted based on the type of the use
353
            // statement but that requires further changes as at this point we
354
            // only have the use statement name.
355
            // TODO: review this statement also check use aliases with constants or functions
356
            if ($whitelist->isGlobalWhitelistedConstant($resolvedName->toString())
357
                || $whitelist->isSymbolWhitelisted($resolvedName->toString(), true)
358
            ) {
359
                return false;
360
            }
361
362
            return null !== $useStatementName;
363
        }
364
365
        $useStatementParent = ParentNodeAppender::getParent($useStatementName);
366
367
        if (!($useStatementParent instanceof UseUse)) {
368
            return false;
369
        }
370
371
        $useStatementAlias = $useStatementParent->alias;
372
373
        if (null === $useStatementAlias) {
374
            return true;
375
        }
376
377
        // Classes and namespaces usages are case-insensitive
378
        $caseSensitiveUseStmt = !in_array(
379
            $useStatementParent->type,
380
            [Use_::TYPE_UNKNOWN, Use_::TYPE_NORMAL],
381
            true,
382
        );
383
384
        return $caseSensitiveUseStmt
385
            ? $originalName->getFirst() === $useStatementAlias->toString()
386
            : strtolower($originalName->getFirst()) === $useStatementAlias->toLowerString();
387
    }
388
}
389