Passed
Push — master ( f406c5...576062 )
by Théo
02:09
created

NameStmtPrefixer::isClassNamePrefixable()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 6
nop 2
dl 0
loc 11
ccs 0
cts 0
cp 0
crap 20
rs 10
c 0
b 0
f 0
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\Reflector;
21
use Humbug\PhpScoper\Whitelist;
22
use PhpParser\Node;
23
use PhpParser\Node\Expr\ArrowFunction;
24
use PhpParser\Node\Expr\ClassConstFetch;
25
use PhpParser\Node\Expr\ConstFetch;
26
use PhpParser\Node\Expr\FuncCall;
27
use PhpParser\Node\Expr\Instanceof_;
28
use PhpParser\Node\Expr\New_;
29
use PhpParser\Node\Expr\StaticCall;
30
use PhpParser\Node\Expr\StaticPropertyFetch;
31
use PhpParser\Node\Name;
32
use PhpParser\Node\Name\FullyQualified;
33
use PhpParser\Node\NullableType;
34
use PhpParser\Node\Param;
35
use PhpParser\Node\Stmt\Catch_;
36
use PhpParser\Node\Stmt\Class_;
37
use PhpParser\Node\Stmt\ClassMethod;
38
use PhpParser\Node\Stmt\Function_;
39
use PhpParser\Node\Stmt\Interface_;
40
use PhpParser\Node\Stmt\Property;
41
use PhpParser\Node\Stmt\TraitUse;
42
use PhpParser\Node\Stmt\TraitUseAdaptation\Alias;
43
use PhpParser\Node\Stmt\TraitUseAdaptation\Precedence;
44
use PhpParser\Node\Stmt\Use_;
45
use PhpParser\Node\Stmt\UseUse;
46
use PhpParser\NodeVisitorAbstract;
47
use UnexpectedValueException;
48
use function count;
49
use function get_class;
50
use function in_array;
51
use function Safe\sprintf;
52
use function strtolower;
53
54
/**
55
 * Prefixes names when appropriate.
56
 *
57
 * ```
58
 * new Foo\Bar();
59
 * ```.
60
 *
61
 * =>
62
 *
63
 * ```
64
 * new \Humbug\Foo\Bar();
65
 * ```
66
 *
67
 * @private
68 549
 */
69
final class NameStmtPrefixer extends NodeVisitorAbstract
70
{
71
    // TODO: remove
72
    public const SPECIAL_CLASS_NAMES = [
73
        'self',
74 549
        'static',
75 549
        'parent',
76 549
    ];
77 549
78
    private const SUPPORTED_PARENT_NODE_CLASS_NAMES = [
79
        Alias::class,
80
        ArrowFunction::class,
81
        Catch_::class,
82
        ConstFetch::class,
83 548
        Class_::class,
84
        ClassConstFetch::class,
85 548
        ClassMethod::class,
86 544
        FuncCall::class,
87 548
        Function_::class,
88
        Instanceof_::class,
89
        Interface_::class,
90
        New_::class,
91 544
        Param::class,
92
        Precedence::class,
93 544
        Property::class,
94
        StaticCall::class,
95 544
        StaticPropertyFetch::class,
96 4
        TraitUse::class,
97
    ];
98
99
    private string $prefix;
100 4
    private Whitelist $whitelist;
101
    private NamespaceStmtCollection $namespaceStatements;
102
    private UseStmtCollection $useStatements;
103
    private Reflector $reflector;
104 544
105 542
    public function __construct(
106 541
        string $prefix,
107 540
        Whitelist $whitelist,
108 540
        NamespaceStmtCollection $namespaceStatements,
109 538
        UseStmtCollection $useStatements,
110 538
        Reflector $reflector
111 538
    ) {
112 538
        $this->prefix = $prefix;
113 538
        $this->whitelist = $whitelist;
114 538
        $this->namespaceStatements = $namespaceStatements;
115 538
        $this->useStatements = $useStatements;
116 544
        $this->reflector = $reflector;
117
    }
118
119 537
    public function enterNode(Node $node): Node
120
    {
121
        if (!($node instanceof Name)) {
122
            return $node;
123
        }
124 405
125 349
        $parent = self::findParent($node);
126 298
127 236
        return null !== $parent
128 185
            ? $this->prefixName($node, $parent)
129 133
            : $node;
130 131
    }
131 120
132
    private static function findParent(Node $name): ?Node
133 320
    {
134
        $parent = ParentNodeAppender::findParent($name);
135 9
136
        if (null === $parent) {
137
            return null;
138 403
        }
139 4
140
        if (!($parent instanceof NullableType)) {
141
            return $parent;
142 403
        }
143
144 403
        return self::findParent($parent);
145
    }
146 403
147 403
    private function prefixName(Name $resolvedName, Node $parentNode): Node
148
    {
149 23
        if (
150
            $resolvedName->isSpecialClassName()
151
            || !self::isParentNodeSupported($parentNode)
152
        ) {
153 392
            return $resolvedName;
154 257
        }
155
156 61
        $originalName = OriginalNameResolver::getOriginalName($resolvedName);
157
158
        if ($parentNode instanceof ConstFetch
159 355
            && 'null' === $originalName->toLowerString()
160 85
        ) {
161 21
            return $originalName;
162
        }
163
164 67
        // Do not prefix if there is a matching use statement.
165 19
        $useStatement = $this->useStatements->findStatementForNode(
166
            $this->namespaceStatements->findNamespaceForNode($resolvedName),
167
            $resolvedName,
168
        );
169
170 48
        if (
171 8
            self::doesNameBelongToUseStatement(
172
                $originalName,
173
                $resolvedName,
174 46
                $parentNode,
175
                $useStatement,
176 11
                $this->whitelist,
177
            )
178
        ) {
179
            return $originalName;
180
        }
181
182
        if ($this->isNamePrefixable($resolvedName)) {
183
            return $resolvedName;
184 320
        }
185 76
186 37
        // Do not prefix if the Name is inside of the current namespace
187
        $currentNamespace = $this->namespaceStatements->getCurrentNamespaceName();
188
189 54
        if (
190 9
            self::doesNameBelongToNamespace(
191
                $originalName,
192
                $resolvedName,
193
                $currentNamespace,
194 297
            )
195 3
            || $this->doesNameBelongToGlobalNamespace(
196
                $originalName,
197
                $resolvedName,
198 297
                $parentNode,
199 297
                $currentNamespace,
200 297
            )
201 297
        ) {
202
            return $originalName;
203
        }
204
205
        if (!$this->isClassNamePrefixable($resolvedName, $parentNode)) {
206
            return $resolvedName;
207
        }
208
209
        if ($parentNode instanceof ConstFetch) {
210
            $prefixedName = $this->prefixConstFetchNode($resolvedName);
211
212
            if (null !== $prefixedName) {
213
                return $prefixedName;
214
            }
215
216
            // Continue
217
        }
218
219
        // Functions have a fallback auto-loading so we cannot prefix them when the name is ambiguous
220
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
221
        if ($parentNode instanceof FuncCall) {
222
            $prefixedName = $this->prefixFuncCallNode($originalName, $resolvedName);
223
224
            if (null !== $prefixedName) {
225
                return $prefixedName;
226
            }
227
228
            // Continue
229
        }
230
231
        if ($parentNode instanceof ClassMethod
232
            && $resolvedName->isSpecialClassName()
233
        ) {
234
            return $resolvedName;
235
        }
236
237
        return FullyQualifiedFactory::concat(
238
            $this->prefix,
239
            $resolvedName->toString(),
240
            $resolvedName->getAttributes(),
241
        );
242
    }
243
244
    private static function isParentNodeSupported(Node $parentNode): bool
245
    {
246
        foreach (self::SUPPORTED_PARENT_NODE_CLASS_NAMES as $supportedClassName) {
247
            if ($parentNode instanceof $supportedClassName) {
248
                return true;
249
            }
250
        }
251
252
        return false;
253
    }
254
255
    /**
256
     * @param string[] $array
257
     * @param string[] $start
258
     */
259
    private static function arrayStartsWith(array $array, array $start): bool
260
    {
261
        $prefixLength = count($start);
262
263
        for ($index = 0; $index < $prefixLength; ++$index) {
264
            if ($array[$index] !== $start[$index]) {
265
                return false;
266
            }
267
        }
268
269
        return true;
270
    }
271
272
    private function isNamePrefixable(Name $resolvedName): bool
273
    {
274
        if (!($resolvedName instanceof FullyQualified)) {
275
            return false;
276
        }
277
278
        return (
279
            // Is already prefixed
280
            $this->prefix === $resolvedName->getFirst()
281
            // The namespace node is whitelisted
282
            || $this->whitelist->belongsToWhitelistedNamespace((string) $resolvedName)
283
        );
284
    }
285
286
    private static function doesNameBelongToNamespace(
287
        Name $originalName,
288
        Name $resolvedName,
289
        ?Name $namespaceName
290
    ): bool {
291
        if (
292
            $namespaceName === null
293
            || !($resolvedName instanceof FullyQualified)
294
            // In case the original name is a FQ, we do not skip the prefixing
295
            // and keep it as FQ
296
            || $originalName instanceof FullyQualified
297
        ) {
298
            return false;
299
        }
300
301
        $originalNameFQParts = [
302
            ...$namespaceName->parts,
303
            ...$originalName->parts,
304
        ];
305
306
        return $originalNameFQParts === $resolvedName->parts;
307
    }
308
309
    private function doesNameBelongToGlobalNamespace(
310
        Name $originalName,
311
        Name $resolvedName,
312
        Node $parentNode,
313
        ?Name $namespaceName
314
    ): bool {
315
        return (
316
            // In the global scope
317
            null === $namespaceName
318
            && !($originalName instanceof FullyQualified)
319
            && !($parentNode instanceof ConstFetch)
320
            && !$this->whitelist->isSymbolWhitelisted($resolvedName->toString())
321
            && !$this->reflector->isFunctionInternal($resolvedName->toString())
322
            && !$this->reflector->isClassInternal($resolvedName->toString())
323
        );
324
    }
325
326
    private static function doesNameBelongToUseStatement(
327
        Name $originalName,
328
        Name $resolvedName,
329
        Node $parentNode,
330
        ?Name $useStatementName,
331
        Whitelist $whitelist
332
    ): bool {
333
        if (
334
            null === $useStatementName
335
            || !($resolvedName instanceof FullyQualified)
336
            // In case the original name is a FQ, we do not skip the prefixing
337
            // and keep it as FQ
338
            || $originalName instanceof FullyQualified
339
            // TODO: review Isolated Finder support
340
            || $resolvedName->parts === ['Isolated', 'Symfony', 'Component', 'Finder', 'Finder']
341
            || !self::arrayStartsWith($resolvedName->parts, $useStatementName->parts)
342
        ) {
343
            return false;
344
        }
345
346
        [$useStmtAlias, $useStmtType] = self::getUseStmtAliasAndType($useStatementName);
347
348
        if ($parentNode instanceof ConstFetch) {
349
            // If a constant is whitelisted, it can be that letting a non FQ breaks
350
            // things. For example the whitelisted namespaced constant could be
351
            // used via a partial import (in which case it is a regular import not
352
            // a constant one) which may not be prefixed.
353
            if ($whitelist->isGlobalWhitelistedConstant($resolvedName->toString())
354
                || $whitelist->isSymbolWhitelisted($resolvedName->toString(), true)
355
            ) {
356
                return Use_::TYPE_CONSTANT === $useStmtType;
357
            }
358
359
            return null !== $useStatementName;
360
        }
361
362
        if (null === $useStmtAlias) {
363
            return true;
364
        }
365
366
        // Classes and namespaces usages are case-insensitive
367
        $caseSensitiveUseStmt = !in_array(
368
            $useStmtType,
369
            [Use_::TYPE_UNKNOWN, Use_::TYPE_NORMAL],
370
            true,
371
        );
372
373
        return $caseSensitiveUseStmt
374
            ? $originalName->getFirst() === $useStmtAlias
375
            : strtolower($originalName->getFirst()) === strtolower($useStmtAlias);
376
    }
377
378
    private function isClassNamePrefixable(
379
        Name $resolvedName,
380
        Node $parentNode
381
    ): bool
382
    {
383
        $isClassNode = !($parentNode instanceof ConstFetch || $parentNode instanceof FuncCall);
384
385
        return (
386
            !$isClassNode
387
            || !($resolvedName instanceof FullyQualified)
388
            || !$this->reflector->isClassInternal($resolvedName->toString())
389
        );
390
    }
391
392
    private function prefixConstFetchNode(Name $resolvedName): ?Name
393
    {
394
        $resolvedNameString = $resolvedName->toString();
395
396
        if ($this->whitelist->isSymbolWhitelisted($resolvedNameString, true)) {
397
            return $resolvedName;
398
        }
399
400
        if ($this->reflector->isConstantInternal($resolvedNameString)) {
401
            return new FullyQualified(
402
                $resolvedNameString,
403
                $resolvedName->getAttributes(),
404
            );
405
        }
406
407
        // Constants have an auto-loading fallback so we cannot prefix them when the name is ambiguous
408
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
409
        if (!($resolvedName instanceof FullyQualified)) {
410
            return $resolvedName;
411
        }
412
413
        if ($this->whitelist->isGlobalWhitelistedConstant($resolvedNameString)) {
414
            // Unlike classes & functions, whitelisted are not prefixed with aliases registered in scoper-autoload.php
415
            return new FullyQualified(
416
                $resolvedNameString,
417
                $resolvedName->getAttributes(),
418
            );
419
        }
420
421
        return null;
422
    }
423
424
    private function prefixFuncCallNode(Name $originalName, Name $resolvedName): ?Name
425
    {
426
        if ($this->reflector->isFunctionInternal($originalName->toString())) {
427
            return new FullyQualified(
428
                $originalName->toString(),
429
                $originalName->getAttributes(),
430
            );
431
        }
432
433
        if (!($resolvedName instanceof FullyQualified)) {
434
            return $resolvedName;
435
        }
436
437
        return null;
438
    }
439
440
    /**
441
     * @return array{string|null, Use_::TYPE_*}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string|null, Use_::TYPE_*} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
442
     */
443
    private static function getUseStmtAliasAndType(Name $name): array
444
    {
445
        $use = ParentNodeAppender::getParent($name);
446
447
        if (!($use instanceof UseUse)) {
448
            throw new UnexpectedValueException(
449
                sprintf(
450
                    'Unexpected use statement name parent "%s"',
451
                    get_class($use),
452
                ),
453
            );
454
        }
455
456
        $useParent = ParentNodeAppender::getParent($use);
457
458
        if (!($useParent instanceof Use_)) {
459
            throw new UnexpectedValueException(
460
                sprintf(
461
                    'Unexpected UseUse parent "%s"',
462
                    get_class($useParent),
463
                ),
464
            );
465
        }
466
467
        $alias = $use->alias;
468
469
        if (null !== $alias) {
470
            $alias = (string) $alias;
471
        }
472
473
        return [
474
            $alias,
475
            $useParent->type,
476
        ];
477
    }
478
}
479