Passed
Push — master ( 25d678...f406c5 )
by Théo
03:58 queued 01:42
created

NameStmtPrefixer::doesNameBelongToUseStatement()   B

Complexity

Conditions 11
Paths 6

Size

Total Lines 51
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 22
c 1
b 0
f 0
nc 6
nop 5
dl 0
loc 51
ccs 0
cts 0
cp 0
crap 132
rs 7.3166

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 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 UnexpectedValueException;
49
use function count;
50
use function get_class;
51
use function in_array;
52
use function Safe\sprintf;
53
use function strtolower;
54
55
/**
56
 * Prefixes names when appropriate.
57
 *
58
 * ```
59
 * new Foo\Bar();
60
 * ```.
61
 *
62
 * =>
63
 *
64
 * ```
65
 * new \Humbug\Foo\Bar();
66
 * ```
67
 *
68 549
 * @private
69
 */
70
final class NameStmtPrefixer extends NodeVisitorAbstract
71
{
72
    public const PHP_SPECIAL_KEYWORDS = [
73
        'self',
74 549
        'static',
75 549
        'parent',
76 549
    ];
77 549
78
    private string $prefix;
79
    private Whitelist $whitelist;
80
    private NamespaceStmtCollection $namespaceStatements;
81
    private UseStmtCollection $useStatements;
82
    private FullyQualifiedNameResolver $nameResolver;
83 548
    private Reflector $reflector;
84
85 548
    public function __construct(
86 544
        string $prefix,
87 548
        Whitelist $whitelist,
88
        NamespaceStmtCollection $namespaceStatements,
89
        UseStmtCollection $useStatements,
90
        FullyQualifiedNameResolver $nameResolver,
91 544
        Reflector $reflector
92
    ) {
93 544
        $this->prefix = $prefix;
94
        $this->whitelist = $whitelist;
95 544
        $this->namespaceStatements = $namespaceStatements;
96 4
        $this->useStatements = $useStatements;
97
        $this->nameResolver = $nameResolver;
98
        $this->reflector = $reflector;
99
    }
100 4
101
    public function enterNode(Node $node): Node
102
    {
103
        if (!($node instanceof Name)) {
104 544
            return $node;
105 542
        }
106 541
107 540
        $parent = self::findParent($node);
108 540
109 538
        return null !== $parent
110 538
            ? $this->prefixName($node, $parent)
111 538
            : $node;
112 538
    }
113 538
114 538
    private static function findParent(Node $name): ?Node
115 538
    {
116 544
        $parent = ParentNodeAppender::findParent($name);
117
118
        if (null === $parent) {
119 537
            return $parent;
120
        }
121
122
        if (!($parent instanceof NullableType)) {
123
            return $parent;
124 405
        }
125 349
126 298
        return self::findParent($parent);
127 236
    }
128 185
129 133
    private function prefixName(Name $resolvedName, Node $parentNode): Node
130 131
    {
131 120
        if (false === (
132
            $parentNode instanceof Alias
133 320
            || $parentNode instanceof ArrowFunction
134
            || $parentNode instanceof Catch_
135 9
            || $parentNode instanceof ConstFetch
136
            || $parentNode instanceof Class_
137
            || $parentNode instanceof ClassConstFetch
138 403
            || $parentNode instanceof ClassMethod
139 4
            || $parentNode instanceof FuncCall
140
            || $parentNode instanceof Function_
141
            || $parentNode instanceof Instanceof_
142 403
            || $parentNode instanceof Interface_
143
            || $parentNode instanceof New_
144 403
            || $parentNode instanceof Param
145
            || $parentNode instanceof Precedence
146 403
            || $parentNode instanceof Property
147 403
            || $parentNode instanceof StaticCall
148
            || $parentNode instanceof StaticPropertyFetch
149 23
            || $parentNode instanceof TraitUse
150
        )
151
        ) {
152
            return $resolvedName;
153 392
        }
154 257
155
        if (
156 61
            (
157
                $parentNode instanceof Catch_
158
                || $parentNode instanceof ClassConstFetch
159 355
                || $parentNode instanceof New_
160 85
                || $parentNode instanceof FuncCall
161 21
                || $parentNode instanceof Instanceof_
162
                || $parentNode instanceof Param
163
                || $parentNode instanceof Property
164 67
                || $parentNode instanceof StaticCall
165 19
                || $parentNode instanceof StaticPropertyFetch
166
            )
167
            && in_array((string) $resolvedName, self::PHP_SPECIAL_KEYWORDS, true)
168
        ) {
169
            return $resolvedName;
170 48
        }
171 8
172
        $originalName = OriginalNameResolver::getOriginalName($resolvedName);
173
174 46
        if ($parentNode instanceof ConstFetch && 'null' === $originalName->toLowerString()) {
175
            return $originalName;
176 11
        }
177
178
        // Do not prefix if there is a matching use statement.
179
        $useStatement = $this->useStatements->findStatementForNode(
180
            $this->namespaceStatements->findNamespaceForNode($resolvedName),
181
            $resolvedName,
182
        );
183
184 320
        if (
185 76
            self::doesNameBelongToUseStatement(
186 37
                $originalName,
187
                $resolvedName,
188
                $parentNode,
189 54
                $useStatement,
190 9
                $this->whitelist,
191
            )
192
        ) {
193
            return $originalName;
194 297
        }
195 3
196
        if ($resolvedName instanceof FullyQualified
197
            && (
198 297
                $this->prefix === $resolvedName->getFirst() // Skip if is already prefixed
199 297
                || $this->whitelist->belongsToWhitelistedNamespace((string) $resolvedName)  // Skip if the namespace node is whitelisted
200 297
            )
201 297
        ) {
202
            return $resolvedName;
203
        }
204
205
        // Do not prefix if the Name is inside of the current namespace
206
        $currentNamespace = $this->namespaceStatements->getCurrentNamespaceName();
207
208
        if (
209
            self::doesNameBelongToNamespace(
210
                $originalName,
211
                $resolvedName,
212
                $currentNamespace,
213
            )
214
            || (
215
                // In the global scope
216
                $currentNamespace === null
217
                && $originalName->parts === $resolvedName->parts
218
                && !($originalName instanceof FullyQualified)
219
                && !($parentNode instanceof ConstFetch)
220
                && $resolvedName instanceof FullyQualified
221
                && !$this->whitelist->isSymbolWhitelisted($resolvedName->toString())
222
                && !$this->reflector->isFunctionInternal($resolvedName->toString())
223
                && !$this->reflector->isClassInternal($resolvedName->toString())
224
            )
225
        ) {
226
            return $originalName;
227
        }
228
229
        // Check if the class can be prefixed
230
        if (!($parentNode instanceof ConstFetch || $parentNode instanceof FuncCall)
231
            && $resolvedName instanceof FullyQualified
232
            && $this->reflector->isClassInternal($resolvedName->toString())
233
        ) {
234
            return $resolvedName;
235
        }
236
237
        if ($parentNode instanceof ConstFetch) {
238
            if ($this->whitelist->isSymbolWhitelisted($resolvedName->toString(), true)) {
239
                return $resolvedName;
240
            }
241
242
            if ($this->reflector->isConstantInternal($resolvedName->toString())) {
243
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
244
            }
245
246
            // Constants have an autoloading fallback so we cannot prefix them when the name is ambiguous
247
            // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
248
            if (false === ($resolvedName instanceof FullyQualified)) {
249
                return $resolvedName;
250
            }
251
252
            if ($this->whitelist->isGlobalWhitelistedConstant((string) $resolvedName)) {
253
                // Unlike classes & functions, whitelisted are not prefixed with aliases registered in scoper-autoload.php
254
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
255
            }
256
257
            // Continue
258
        }
259
260
        // Functions have a fallback auto-loading so we cannot prefix them when the name is ambiguous
261
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
262
        if ($parentNode instanceof FuncCall) {
263
            if ($this->reflector->isFunctionInternal($originalName->toString())) {
264
                return new FullyQualified(
265
                    $originalName->toString(),
266
                    $originalName->getAttributes(),
267
                );
268
            }
269
270
            if (!($resolvedName instanceof FullyQualified)) {
271
                return $resolvedName;
272
            }
273
        }
274
275
        if ($parentNode instanceof ClassMethod && $resolvedName->isSpecialClassName()) {
276
            return $resolvedName;
277
        }
278
279
        return FullyQualifiedFactory::concat(
280
            $this->prefix,
281
            $resolvedName->toString(),
282
            $resolvedName->getAttributes()
283
        );
284
    }
285
286
    /**
287
     * @param string[] $array
288
     * @param string[] $start
289
     */
290
    private static function arrayStartsWith(array $array, array $start): bool
291
    {
292
        $prefixLength = count($start);
293
294
        for ($index = 0; $index < $prefixLength; ++$index) {
295
            if ($array[$index] !== $start[$index]) {
296
                return false;
297
            }
298
        }
299
300
        return true;
301
    }
302
303
    private static function doesNameBelongToNamespace(
304
        Name $originalName,
305
        Name $resolvedName,
306
        ?Name $namespace
307
    ): bool
308
    {
309
        if (
310
            $namespace === null
311
            || !($resolvedName instanceof FullyQualified)
312
            // In case the original name is a FQ, we do not skip the prefixing
313
            // and keep it as FQ
314
            || $originalName instanceof FullyQualified
315
        ) {
316
            return false;
317
        }
318
319
        $originalNameFQParts = [
320
            ...$namespace->parts,
321
            ...$originalName->parts,
322
        ];
323
324
        return $originalNameFQParts === $resolvedName->parts;
325
    }
326
327
    private static function doesNameBelongToUseStatement(
328
        Name $originalName,
329
        Name $resolvedName,
330
        Node $parentNode,
331
        ?Name $useStatementName,
332
        Whitelist $whitelist
333
    ): bool
334
    {
335
        if (
336
            null === $useStatementName
337
            || !($resolvedName instanceof FullyQualified)
338
            // In case the original name is a FQ, we do not skip the prefixing
339
            // and keep it as FQ
340
            || $originalName instanceof FullyQualified
341
            // TODO: review Isolated Finder support
342
            || $resolvedName->parts === ['Isolated', 'Symfony', 'Component', 'Finder', 'Finder']
343
            || !self::arrayStartsWith($resolvedName->parts, $useStatementName->parts)
344
        ) {
345
            return false;
346
        }
347
348
        [$useStmtAlias, $useStmtType] = self::getUseStmtAliasAndType($useStatementName);
0 ignored issues
show
Bug Best Practice introduced by
The method Humbug\PhpScoper\PhpPars...etUseStmtAliasAndType() is not static, but was called statically. ( Ignorable by Annotation )

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

348
        /** @scrutinizer ignore-call */ 
349
        [$useStmtAlias, $useStmtType] = self::getUseStmtAliasAndType($useStatementName);
Loading history...
349
350
        if ($parentNode instanceof ConstFetch) {
351
            // If a constant is whitelisted, it can be that letting a non FQ breaks
352
            // things. For example the whitelisted namespaced constant could be
353
            // used via a partial import (in which case it is a regular import not
354
            // a constant one) which may not be prefixed.
355
            if ($whitelist->isGlobalWhitelistedConstant($resolvedName->toString())
356
                || $whitelist->isSymbolWhitelisted($resolvedName->toString(), true)
357
            ) {
358
                return Use_::TYPE_CONSTANT === $useStmtType;
359
            }
360
361
            return null !== $useStatementName;
362
        }
363
364
        if (null === $useStmtAlias) {
365
            return true;
366
        }
367
368
        // Classes and namespaces usages are case-insensitive
369
        $caseSensitiveUseStmt = !in_array(
370
            $useStmtType,
371
            [Use_::TYPE_UNKNOWN, Use_::TYPE_NORMAL],
372
            true,
373
        );
374
375
        return $caseSensitiveUseStmt
376
            ? $originalName->getFirst() === $useStmtAlias->toString()
377
            : strtolower($originalName->getFirst()) === $useStmtAlias->toLowerString();
378
    }
379
380
    /**
381
     * @return array{Identifier|string|null, Use_::TYPE_*}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{Identifier|string|null, Use_::TYPE_*} at position 2 could not be parsed: Expected ':' at position 2, but found 'Identifier'.
Loading history...
382
     */
383
    private function getUseStmtAliasAndType(Name $name): array
384
    {
385
        $use = ParentNodeAppender::getParent($name);
386
387
        if (!($use instanceof UseUse)) {
388
            throw new UnexpectedValueException(
389
                sprintf(
390
                    'Unexpected use statement name parent "%s"',
391
                    get_class($use),
392
                ),
393
            );
394
        }
395
396
        $useParent = ParentNodeAppender::getParent($use);
397
398
        if (!($useParent instanceof Use_)) {
399
            throw new UnexpectedValueException(
400
                sprintf(
401
                    'Unexpected UseUse parent "%s"',
402
                    get_class($useParent),
403
                ),
404
            );
405
        }
406
407
        return [
408
            $use->alias,
409
            $useParent->type,
410
        ];
411
    }
412
}
413