Passed
Push — master ( 1c5a50...565b8d )
by Théo
10:21
created

NameStmtPrefixer::array_starts_with()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 9
ccs 0
cts 0
cp 0
crap 12
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\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\NodeVisitorAbstract;
46
use function array_merge;
47
use function count;
48
use function in_array;
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
    public const PHP_SPECIAL_KEYWORDS = [
68 549
        'self',
69
        'static',
70
        'parent',
71
    ];
72
73
    private string $prefix;
74 549
    private Whitelist $whitelist;
75 549
    private NamespaceStmtCollection $namespaceStatements;
76 549
    private UseStmtCollection $useStatements;
77 549
    private FullyQualifiedNameResolver $nameResolver;
78
    private Reflector $reflector;
79
80
    public function __construct(
81
        string $prefix,
82
        Whitelist $whitelist,
83 548
        NamespaceStmtCollection $namespaceStatements,
84
        UseStmtCollection $useStatements,
85 548
        FullyQualifiedNameResolver $nameResolver,
86 544
        Reflector $reflector
87 548
    ) {
88
        $this->prefix = $prefix;
89
        $this->whitelist = $whitelist;
90
        $this->namespaceStatements = $namespaceStatements;
91 544
        $this->useStatements = $useStatements;
92
        $this->nameResolver = $nameResolver;
93 544
        $this->reflector = $reflector;
94
    }
95 544
96 4
    public function enterNode(Node $node): Node
97
    {
98
        if (!($node instanceof Name)) {
99
            return $node;
100 4
        }
101
102
        $parent = self::findParent($node);
103
104 544
        return null !== $parent
105 542
            ? $this->prefixName($node, $parent)
106 541
            : $node;
107 540
    }
108 540
109 538
    private static function findParent(Node $name): ?Node
110 538
    {
111 538
        $parent = ParentNodeAppender::findParent($name);
112 538
113 538
        if (null === $parent) {
114 538
            return $parent;
115 538
        }
116 544
117
        if (!($parent instanceof NullableType)) {
118
            return $parent;
119 537
        }
120
121
        return self::findParent($parent);
122
    }
123
124 405
    private function prefixName(Name $resolvedName, Node $parentNode): Node
125 349
    {
126 298
        if (false === (
127 236
            $parentNode instanceof Alias
128 185
            || $parentNode instanceof ArrowFunction
129 133
            || $parentNode instanceof Catch_
130 131
            || $parentNode instanceof ConstFetch
131 120
            || $parentNode instanceof Class_
132
            || $parentNode instanceof ClassConstFetch
133 320
            || $parentNode instanceof ClassMethod
134
            || $parentNode instanceof FuncCall
135 9
            || $parentNode instanceof Function_
136
            || $parentNode instanceof Instanceof_
137
            || $parentNode instanceof Interface_
138 403
            || $parentNode instanceof New_
139 4
            || $parentNode instanceof Param
140
            || $parentNode instanceof Precedence
141
            || $parentNode instanceof Property
142 403
            || $parentNode instanceof StaticCall
143
            || $parentNode instanceof StaticPropertyFetch
144 403
            || $parentNode instanceof TraitUse
145
        )
146 403
        ) {
147 403
            return $resolvedName;
148
        }
149 23
150
        if (
151
            (
152
                $parentNode instanceof Catch_
153 392
                || $parentNode instanceof ClassConstFetch
154 257
                || $parentNode instanceof New_
155
                || $parentNode instanceof FuncCall
156 61
                || $parentNode instanceof Instanceof_
157
                || $parentNode instanceof Param
158
                || $parentNode instanceof Property
159 355
                || $parentNode instanceof StaticCall
160 85
                || $parentNode instanceof StaticPropertyFetch
161 21
            )
162
            && in_array((string) $resolvedName, self::PHP_SPECIAL_KEYWORDS, true)
163
        ) {
164 67
            return $resolvedName;
165 19
        }
166
167
        $originalName = OriginalNameResolver::getOriginalName($resolvedName);
168
169
        if ($parentNode instanceof ConstFetch && 'null' === (string) $originalName) {
170 48
            return $originalName;
171 8
        }
172
173
        // Do not prefix if there is a matching use statement.
174 46
        $useStatement = $this->useStatements->findStatementForNode(
175
            $this->namespaceStatements->findNamespaceForNode($resolvedName),
176 11
            $resolvedName,
177
        );
178
179
        if (
180
            self::doesNameBelongToUseStatement(
181
                $originalName,
182
                $resolvedName,
183
                $parentNode,
184 320
                $useStatement,
185 76
                $this->whitelist,
186 37
            )
187
        ) {
188
            return $originalName;
189 54
        }
190 9
191
        if ($resolvedName instanceof FullyQualified
192
            && (
193
                $this->prefix === $resolvedName->getFirst() // Skip if is already prefixed
194 297
                || $this->whitelist->belongsToWhitelistedNamespace((string) $resolvedName)  // Skip if the namespace node is whitelisted
195 3
            )
196
        ) {
197
            return $resolvedName;
198 297
        }
199 297
200 297
        // Do not prefix if the Name is inside of the current namespace
201 297
        $currentNamespace = $this->namespaceStatements->getCurrentNamespaceName();
202
203
        if (
204
            self::doesNameBelongToNamespace(
205
                $originalName,
206
                $resolvedName,
207
                $currentNamespace,
208
            )
209
            || (
210
                // In the global scope
211
                $currentNamespace === null
212
                && $originalName->parts === $resolvedName->parts
213
                && !($originalName instanceof FullyQualified)
214
                && !($parentNode instanceof ConstFetch)
215
                && $resolvedName instanceof FullyQualified
216
                && !$this->whitelist->isSymbolWhitelisted($resolvedName->toString())
217
                && !$this->reflector->isFunctionInternal($resolvedName->toString())
218
                && !$this->reflector->isClassInternal($resolvedName->toString())
219
            )
220
        ) {
221
            return $originalName;
222
        }
223
224
        // Check if the class can be prefixed
225
        if (!($parentNode instanceof ConstFetch || $parentNode instanceof FuncCall)
226
            && $resolvedName instanceof FullyQualified
227
            && $this->reflector->isClassInternal($resolvedName->toString())
228
        ) {
229
            return $resolvedName;
230
        }
231
232
        if ($parentNode instanceof ConstFetch) {
233
            if ($this->whitelist->isSymbolWhitelisted($resolvedName->toString(), true)) {
234
                return $resolvedName;
235
            }
236
237
            if ($this->reflector->isConstantInternal($resolvedName->toString())) {
238
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
239
            }
240
241
            // Constants have an autoloading fallback so we cannot prefix them when the name is ambiguous
242
            // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
243
            if (false === ($resolvedName instanceof FullyQualified)) {
244
                return $resolvedName;
245
            }
246
247
            if ($this->whitelist->isGlobalWhitelistedConstant((string) $resolvedName)) {
248
                // Unlike classes & functions, whitelisted are not prefixed with aliases registered in scoper-autoload.php
249
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
250
            }
251
252
            // Continue
253
        }
254
255
        // Functions have a fallback auto-loading so we cannot prefix them when the name is ambiguous
256
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
257
        if ($parentNode instanceof FuncCall) {
258
            if ($this->reflector->isFunctionInternal($originalName->toString())) {
259
                return new FullyQualified(
260
                    $originalName->toString(),
261
                    $originalName->getAttributes(),
262
                );
263
            }
264
265
            if (!($resolvedName instanceof FullyQualified)) {
266
                return $resolvedName;
267
            }
268
        }
269
270
        if ($parentNode instanceof ClassMethod && $resolvedName->isSpecialClassName()) {
271
            return $resolvedName;
272
        }
273
274
        return FullyQualifiedFactory::concat(
275
            $this->prefix,
276
            $resolvedName->toString(),
277
            $resolvedName->getAttributes()
278
        );
279
    }
280
281
    /**
282
     * @param string[] $array
283
     * @param string[] $start
284
     */
285
    private static function arrayStartsWith(array $array, array $start): bool
286
    {
287
        $prefixLength = count($start);
288
289
        for ($index = 0; $index < $prefixLength; ++$index) {
290
            if ($array[$index] !== $start[$index]) {
291
                return false;
292
            }
293
        }
294
295
        return true;
296
    }
297
298
    private static function doesNameBelongToNamespace(
299
        Name $originalName,
300
        Name $resolvedName,
301
        ?Name $namespace
302
    ): bool
303
    {
304
        if (
305
            $namespace === null
306
            || !($resolvedName instanceof FullyQualified)
307
            // In case the original name is a FQ, we do not skip the prefixing
308
            // and keep it as FQ
309
            || $originalName instanceof FullyQualified
310
        ) {
311
            return false;
312
        }
313
314
        $originalNameFQParts = [
315
            ...$namespace->parts,
316
            ...$originalName->parts,
317
        ];
318
319
        return $originalNameFQParts === $resolvedName->parts;
320
    }
321
322
    private static function doesNameBelongToUseStatement(
323
        Name $originalName,
324
        Name $resolvedName,
325
        Node $parentNode,
326
        ?Name $useStatement,
327
        Whitelist $whitelist
328
    ): bool
329
    {
330
        if (
331
            null === $useStatement
332
            || !($resolvedName instanceof FullyQualified)
333
            // In case the original name is a FQ, we do not skip the prefixing
334
            // and keep it as FQ
335
            || $originalName instanceof FullyQualified
336
            // TODO: review Isolated Finder support
337
            || $resolvedName->parts === ['Isolated', 'Symfony', 'Component', 'Finder', 'Finder']
338
            || !self::arrayStartsWith($resolvedName->parts, $useStatement->parts)
339
        ) {
340
            return false;
341
        }
342
343
        if ($parentNode instanceof ConstFetch) {
344
            // If a constant is whitelisted, it can be that letting a non FQ breaks
345
            // things. For example the whitelisted namespaced constant could be
346
            // used via a partial import (in which case it is a regular import not
347
            // a constant one) which may not be prefixed.
348
            // For this reason in this scenario we will always transform the
349
            // constant in a FQ one.
350
            // Note that this could be adjusted based on the type of the use
351
            // statement but that requires further changes as at this point we
352
            // only have the use statement name.
353
            if ($whitelist->isGlobalWhitelistedConstant($resolvedName->toString())
354
                || $whitelist->isSymbolWhitelisted($resolvedName->toString(), true)
355
            ) {
356
                return false;
357
            }
358
359
            return null !== $useStatement;
360
        }
361
362
        $useStatementParent = ParentNodeAppender::findParent($useStatement);
363
364
        if (null !== $useStatementParent) {
365
            // TODO: see the type of the parent
366
            return null === $useStatementParent->alias
0 ignored issues
show
Bug introduced by
Accessing alias on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
367
                || !$whitelist->isSymbolWhitelisted($useStatement->toString());
368
        }
369
370
        return true;
371
    }
372
}
373