Passed
Push — master ( 206034...319324 )
by Théo
02:08
created

NameStmtPrefixer::prefixFuncCallNode()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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