NameStmtPrefixer   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 390
Duplicated Lines 0 %

Test Coverage

Coverage 98.55%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 169
c 5
b 0
f 0
dl 0
loc 390
ccs 68
cts 69
cp 0.9855
rs 3.2
wmc 65

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A prefixConstFetchNode() 0 42 6
A enterNode() 0 9 2
A getParent() 0 13 2
A prefixFuncCallNode() 0 31 6
A isNamePrefixable() 0 11 3
A isParentNodeSupported() 0 9 3
A isPrefixableClassName() 0 10 4
A doesNameBelongToNamespace() 0 21 4
B doesNameBelongToGlobalNamespace() 0 27 9
C prefixName() 0 88 14
B doesNameHasUseStatement() 0 50 11

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\UseStmt\UseStmtCollection;
20
use Humbug\PhpScoper\PhpParser\UseStmtName;
21
use Humbug\PhpScoper\Symbol\EnrichedReflector;
22
use PhpParser\Node;
23
use PhpParser\Node\Expr\ArrowFunction;
24
use PhpParser\Node\Expr\ClassConstFetch;
25
use PhpParser\Node\Expr\Closure;
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\IntersectionType;
33
use PhpParser\Node\Name;
34
use PhpParser\Node\Name\FullyQualified;
35
use PhpParser\Node\NullableType;
36
use PhpParser\Node\Param;
37
use PhpParser\Node\Stmt\Catch_;
38
use PhpParser\Node\Stmt\Class_;
39
use PhpParser\Node\Stmt\ClassMethod;
40
use PhpParser\Node\Stmt\Function_;
41
use PhpParser\Node\Stmt\Interface_;
42
use PhpParser\Node\Stmt\Property;
43
use PhpParser\Node\Stmt\TraitUse;
44
use PhpParser\Node\Stmt\TraitUseAdaptation\Alias;
45
use PhpParser\Node\Stmt\TraitUseAdaptation\Precedence;
46
use PhpParser\Node\Stmt\Use_;
47
use PhpParser\Node\UnionType;
48
use PhpParser\NodeVisitorAbstract;
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
    private const SUPPORTED_PARENT_NODE_CLASS_NAMES = [
70
        Alias::class,
71
        ArrowFunction::class,
72
        Catch_::class,
73
        ConstFetch::class,
74 549
        Class_::class,
75 549
        ClassConstFetch::class,
76 549
        ClassMethod::class,
77 549
        Closure::class,
78
        FuncCall::class,
79
        Function_::class,
80
        Instanceof_::class,
81
        Interface_::class,
82
        New_::class,
83 548
        Param::class,
84
        Precedence::class,
85 548
        Property::class,
86 544
        StaticCall::class,
87 548
        StaticPropertyFetch::class,
88
        TraitUse::class,
89
        UnionType::class,
90
        IntersectionType::class,
91 544
    ];
92
93 544
    private string $prefix;
94
    private NamespaceStmtCollection $namespaceStatements;
95 544
    private UseStmtCollection $useStatements;
96 4
    private EnrichedReflector $enrichedReflector;
97
98
    public function __construct(
99
        string $prefix,
100 4
        NamespaceStmtCollection $namespaceStatements,
101
        UseStmtCollection $useStatements,
102
        EnrichedReflector $enrichedReflector
103
    ) {
104 544
        $this->prefix = $prefix;
105 542
        $this->namespaceStatements = $namespaceStatements;
106 541
        $this->useStatements = $useStatements;
107 540
        $this->enrichedReflector = $enrichedReflector;
108 540
    }
109 538
110 538
    public function enterNode(Node $node): Node
111 538
    {
112 538
        if (!($node instanceof Name)) {
113 538
            return $node;
114 538
        }
115 538
116 544
        return $this->prefixName(
117
            $node,
118
            self::getParent($node),
119 537
        );
120
    }
121
122
    private static function getParent(Node $name): Node
123
    {
124 405
        $parent = ParentNodeAppender::getParent($name);
125 349
126 298
        // The parent can be a nullable type. For example for "public ?Foo $x"
127 236
        // the parent of Name("Foo") will be NullableType.
128 185
        // In practice, we do not get any information from NullableType to
129 133
        // determine if we can prefix or not our name hence we skip it completely
130 131
        if (!($parent instanceof NullableType)) {
131 120
            return $parent;
132
        }
133 320
134
        return self::getParent($parent);
135 9
    }
136
137
    private function prefixName(Name $resolvedName, Node $parentNode): Node
138 403
    {
139 4
        if ($resolvedName->isSpecialClassName()
140
            || !self::isParentNodeSupported($parentNode)
141
        ) {
142 403
            return $resolvedName;
143
        }
144 403
145
        $originalName = OriginalNameResolver::getOriginalName($resolvedName);
146 403
147 403
        // Happens when assigning `null` as a default value for example
148
        if ($parentNode instanceof ConstFetch
149 23
            && 'null' === $originalName->toLowerString()
150
        ) {
151
            return $originalName;
152
        }
153 392
154 257
        $useStatement = $this->useStatements->findStatementForNode(
155
            $this->namespaceStatements->findNamespaceForNode($resolvedName),
156 61
            $resolvedName,
157
        );
158
159 355
        if ($this->doesNameHasUseStatement(
160 85
            $originalName,
161 21
            $resolvedName,
162
            $parentNode,
163
            $useStatement,
164 67
        )) {
165 19
            // Do not prefix if there is a matching use statement.
166
            return $originalName;
167
        }
168
169
        if ($this->isNamePrefixable($resolvedName)) {
170 48
            return $resolvedName;
171 8
        }
172
173
        // Do not prefix if the Name is inside the current namespace
174 46
        $currentNamespace = $this->namespaceStatements->getCurrentNamespaceName();
175
176 11
        if (self::doesNameBelongToNamespace(
177
            $originalName,
178
            $resolvedName,
179
            $currentNamespace,
180
        )
181
            // At this point if the name belongs to the global namespace, since
182
            // we are NOT in an excluded namespace, the current namespace will
183
            // become prefixed hence there is no need for prefixing.
184 320
            // This is however not true for exposed constants as the constants
185 76
            // cannot be aliases – they are transformed to keep their original
186 37
            // FQ name. In other words, they cannot remain untouched/non-FQ
187
            || $this->doesNameBelongToGlobalNamespace(
188
                $originalName,
189 54
                $resolvedName->toString(),
190 9
                $parentNode,
191
                $currentNamespace,
192
            )
193
        ) {
194 297
            return $originalName;
195 3
        }
196
197
        if (!$this->isPrefixableClassName($resolvedName, $parentNode)) {
198 297
            return $resolvedName;
199 297
        }
200 297
201 297
        if ($parentNode instanceof ConstFetch) {
202
            $prefixedName = $this->prefixConstFetchNode($resolvedName);
203
204
            if (null !== $prefixedName) {
205
                return $prefixedName;
206
            }
207
208
            // Continue
209
        }
210
211
        if ($parentNode instanceof FuncCall) {
212
            $prefixedName = $this->prefixFuncCallNode($originalName, $resolvedName);
213
214
            if (null !== $prefixedName) {
215
                return $prefixedName;
216
            }
217
218
            // Continue
219
        }
220
221
        return FullyQualifiedFactory::concat(
222
            $this->prefix,
223
            $resolvedName->toString(),
224
            $resolvedName->getAttributes(),
225
        );
226
    }
227
228
    private static function isParentNodeSupported(Node $parentNode): bool
229
    {
230
        foreach (self::SUPPORTED_PARENT_NODE_CLASS_NAMES as $supportedClassName) {
231
            if ($parentNode instanceof $supportedClassName) {
232
                return true;
233
            }
234
        }
235
236
        return false;
237
    }
238
239
    private function isNamePrefixable(Name $resolvedName): bool
240
    {
241
        if (!$resolvedName->isFullyQualified()) {
242
            return false;
243
        }
244
245
        $isAlreadyPrefixed = $this->prefix === $resolvedName->getFirst();
246
247
        return (
248
            $isAlreadyPrefixed
249
            || $this->enrichedReflector->belongsToExcludedNamespace((string) $resolvedName)
250
        );
251
    }
252
253
    private static function doesNameBelongToNamespace(
254
        Name $originalName,
255
        Name $resolvedName,
256
        ?Name $namespaceName
257
    ): bool {
258
        if (
259
            $namespaceName === null
260
            || !$resolvedName->isFullyQualified()
261
            // In case the original name is a FQ, we do not skip the prefixing
262
            // and keep it as FQ
263
            || $originalName->isFullyQualified()
264
        ) {
265
            return false;
266
        }
267
268
        $originalNameFQParts = [
269
            ...$namespaceName->parts,
270
            ...$originalName->parts,
271
        ];
272
273
        return $originalNameFQParts === $resolvedName->parts;
274
    }
275
276
    private function doesNameBelongToGlobalNamespace(
277
        Name $originalName,
278
        string $resolvedName,
279
        Node $parentNode,
280
        ?Name $namespaceName
281
    ): bool {
282
        return null === $namespaceName
283
            && !$originalName->isFullyQualified()
284
285
            // See caller as to why we cannot allow constants to keep their
286
            // original non FQ names
287
            && !($parentNode instanceof ConstFetch)
288
289
            // If exposed we cannot keep the original non-FQCN UNLESS belongs
290
            // to the global namespace for the reasons mentioned in the caller
291
            && (
292
                !$this->enrichedReflector->isExposedClass($resolvedName)
293
                || $this->enrichedReflector->isExposedClassFromGlobalNamespace($resolvedName)
294
            )
295
            // If excluded we cannot keep the non-FQCN
296
            && !$this->enrichedReflector->isClassExcluded($resolvedName)
297
298
            && (
299
                !$this->enrichedReflector->isExposedFunction($resolvedName)
300
                || $this->enrichedReflector->isExposedFunctionFromGlobalNamespace($resolvedName)
301
            )
302
            && !$this->enrichedReflector->isFunctionExcluded($resolvedName);
303
    }
304
305
    private function doesNameHasUseStatement(
306
        Name $originalName,
307
        Name $resolvedName,
308
        Node $parentNode,
309
        ?Name $useStatementName
310
    ): bool {
311
        if (null === $useStatementName
312
            || !$resolvedName->isFullyQualified()
313
            // In case the original name is a FQ, we do not skip the prefixing
314
            // and keep it as FQ
315
            || $originalName->isFullyQualified()
316
            // TODO: review Isolated Finder support
317
            || $resolvedName->parts === ['Isolated', 'Symfony', 'Component', 'Finder', 'Finder']
318
        ) {
319
            return false;
320
        }
321
322
        $useStmt = new UseStmtName($useStatementName);
323
324
        if (!$useStmt->contains($resolvedName)) {
325
            return false;
326
        }
327
328
        [$useStmtAlias, $useStmtType] = $useStmt->getUseStmtAliasAndType();
329
330
        if ($parentNode instanceof ConstFetch) {
331
            $isExposedConstant = $this->enrichedReflector->isExposedConstant($resolvedName->toString());
332
333
            // If a constant is exposed, it can be that letting a non FQ breaks
334
            // things. For example the exposed namespaced constant could be
335
            // used via a partial import (in which case it is a regular import not
336
            // a constant one) which may not be prefixed.
337
            return ($isExposedConstant && Use_::TYPE_CONSTANT === $useStmtType)
338
                || !$isExposedConstant;
339
        }
340
341
        if (null === $useStmtAlias) {
342
            return true;
343
        }
344
345
        // Classes and namespaces usages are case-insensitive
346
        $caseSensitiveUseStmt = !in_array(
347
            $useStmtType,
348
            [Use_::TYPE_UNKNOWN, Use_::TYPE_NORMAL],
349
            true,
350
        );
351
352
        return $caseSensitiveUseStmt
353
            ? $originalName->getFirst() === $useStmtAlias
354
            : strtolower($originalName->getFirst()) === strtolower($useStmtAlias);
355
    }
356
357
    private function isPrefixableClassName(
358
        Name $resolvedName,
359
        Node $parentNode
360
    ): bool {
361
        $isClassNode = $parentNode instanceof ConstFetch || $parentNode instanceof FuncCall;
362
363
        return (
364
            $isClassNode
365
            || !$resolvedName->isFullyQualified()
366
            || !$this->enrichedReflector->isClassExcluded($resolvedName->toString())
367
        );
368
    }
369
370
    /**
371
     * @return Name|null Returns the name to use (prefixed or not). Otherwise
372
     *                   it was not possible to resolve the name and the name
373
     *                   will end up being prefixed the "regular" way (prefix
374
     *                   added)
375
     */
376
    private function prefixConstFetchNode(Name $resolvedName): ?Name
377
    {
378
        $resolvedNameString = $resolvedName->toString();
379
380
        if ($resolvedName->isFullyQualified()) {
381
            return $this->enrichedReflector->isExposedConstant($resolvedNameString)
382
                ? $resolvedName
383
                : null;
384
        }
385
386
        // Constants have an auto-loading fallback, so as a rule we cannot
387
        // prefix them when the name is ambiguous.
388
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
389
        //
390
        // HOWEVER. However. There is _very_ high chances that if a user
391
        // explicitly register a constant to be exposed or that the constant
392
        // is internal that it is the constant in question and not the one
393
        // relative to the namespace.
394
        // Indeed it would otherwise mean that the user has for example Acme\FOO
395
        // and \FOO in the codebase AND decide to expose \FOO.
396
        // It is not only unlikely but sketchy, hence should not be an issue
397
        // in practice.
398
399
        // We distinguish exposed from internal here as internal are a much safer
400
        // bet.
401
        if ($this->enrichedReflector->isConstantInternal($resolvedNameString)) {
402
            return new FullyQualified(
403
                $resolvedNameString,
404
                $resolvedName->getAttributes(),
405
            );
406
        }
407
408
        if ($this->enrichedReflector->isExposedConstant($resolvedNameString)) {
409
            return $this->enrichedReflector->isExposedConstantFromGlobalNamespace($resolvedNameString)
410
                ? $resolvedName
411
                : new FullyQualified(
412
                    $resolvedNameString,
413
                    $resolvedName->getAttributes(),
414
                );
415
        }
416
417
        return $resolvedName;
418
    }
419
420
    /**
421
     * @return Name|null Returns the name to use (prefixed or not). Otherwise
422
     *                   it was not possible to resolve the name and the name
423
     *                   will end up being prefixed the "regular" way (prefix
424
     *                   added)
425
     */
426
    private function prefixFuncCallNode(Name $originalName, Name $resolvedName): ?Name
427
    {
428
        // Functions have a fallback auto-loading so we cannot prefix them when
429
        // the name is ambiguous
430
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
431
        //
432
        // See prefixConstFetchNode() for more details as to why we can still
433
        // take the risk under some circumstances.
434
        $resolvedNameString = $resolvedName->toString();
435
436
        if ($resolvedName->isFullyQualified()) {
437
            return $this->enrichedReflector->isFunctionExcluded($resolvedNameString)
438
                ? $resolvedName
439
                : null;
440
        }
441
442
        if ($this->enrichedReflector->isFunctionInternal($resolvedNameString)) {
443
            return new FullyQualified(
444
                $originalName->toString(),
445
                $originalName->getAttributes(),
446
            );
447
        }
448
449
        if ($this->enrichedReflector->isExposedFunction($resolvedNameString)) {
450
            // TODO: should be able to find a case for it
451
            return $this->enrichedReflector->isExposedFunctionFromGlobalNamespace($resolvedNameString)
452
                ? $resolvedName
453
                : null;
454
        }
455
456
        return $resolvedName;
457
    }
458
}
459