Passed
Pull Request — master (#580)
by Théo
09:22
created

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