Passed
Pull Request — master (#457)
by Gena
03:04 queued 44s
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 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 3
b 0
f 0
nc 3
nop 2
dl 0
loc 9
ccs 0
cts 0
cp 0
crap 12
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\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\NodeVisitorAbstract;
43
use function count;
44
use function in_array;
45
46
/**
47
 * Prefixes names when appropriate.
48
 *
49
 * ```
50
 * new Foo\Bar();
51
 * ```.
52
 *
53
 * =>
54
 *
55
 * ```
56
 * new \Humbug\Foo\Bar();
57
 * ```
58
 *
59
 * @private
60
 */
61
final class NameStmtPrefixer extends NodeVisitorAbstract
62
{
63
    public const PHP_FUNCTION_KEYWORDS = [
64
        'self',
65
        'static',
66
        'parent',
67
    ];
68 549
69
    private $prefix;
70
    private $whitelist;
71
    private $namespaceStatements;
72
    private $useStatements;
73
    private $nameResolver;
74 549
    private $reflector;
75 549
76 549
    public function __construct(
77 549
        string $prefix,
78
        Whitelist $whitelist,
79
        NamespaceStmtCollection $namespaceStatements,
80
        UseStmtCollection $useStatements,
81
        FullyQualifiedNameResolver $nameResolver,
82
        Reflector $reflector
83 548
    ) {
84
        $this->prefix = $prefix;
85 548
        $this->whitelist = $whitelist;
86 544
        $this->namespaceStatements = $namespaceStatements;
87 548
        $this->useStatements = $useStatements;
88
        $this->nameResolver = $nameResolver;
89
        $this->reflector = $reflector;
90
    }
91 544
92
    /**
93 544
     * @inheritdoc
94
     */
95 544
    public function enterNode(Node $node): Node
96 4
    {
97
        return ($node instanceof Name && ParentNodeAppender::hasParent($node))
98
            ? $this->prefixName($node)
99
            : $node
100 4
        ;
101
    }
102
103
    private function prefixName(Name $name): Node
104 544
    {
105 542
        $parentNode = ParentNodeAppender::getParent($name);
106 541
107 540
        if ($parentNode instanceof NullableType) {
108 540
            if (false === ParentNodeAppender::hasParent($parentNode)) {
109 538
                return $name;
110 538
            }
111 538
112 538
            $parentNode = ParentNodeAppender::getParent($parentNode);
113 538
        }
114 538
115 538
        if (false === (
116 544
            $parentNode instanceof ArrowFunction
117
                || $parentNode instanceof Catch_
118
                || $parentNode instanceof ConstFetch
119 537
                || $parentNode instanceof Class_
120
                || $parentNode instanceof ClassConstFetch
121
                || $parentNode instanceof ClassMethod
122
                || $parentNode instanceof FuncCall
123
                || $parentNode instanceof Function_
124 405
                || $parentNode instanceof Instanceof_
125 349
                || $parentNode instanceof Interface_
126 298
                || $parentNode instanceof New_
127 236
                || $parentNode instanceof Param
128 185
                || $parentNode instanceof Property
129 133
                || $parentNode instanceof StaticCall
130 131
                || $parentNode instanceof StaticPropertyFetch
131 120
        )
132
        ) {
133 320
            return $name;
134
        }
135 9
136
        if (
137
            (
138 403
                $parentNode instanceof Catch_
139 4
                || $parentNode instanceof ClassConstFetch
140
                || $parentNode instanceof New_
141
                || $parentNode instanceof FuncCall
142 403
                || $parentNode instanceof Instanceof_
143
                || $parentNode instanceof Param
144 403
                || $parentNode instanceof StaticCall
145
                || $parentNode instanceof StaticPropertyFetch
146 403
            )
147 403
            && in_array((string) $name, self::PHP_FUNCTION_KEYWORDS, true)
148
        ) {
149 23
            return $name;
150
        }
151
152
        if ($parentNode instanceof ConstFetch && 'null' === (string) $name) {
153 392
            return $name;
154 257
        }
155
156 61
        $resolvedName = $this->nameResolver->resolveName($name)->getName();
157
158
        // Do not prefix if there is a matching use statement.
159 355
        $useStatement = $this->useStatements->findStatementForNode($this->namespaceStatements->findNamespaceForNode($name), $name);
160 85
161 21
        if (
162
            $useStatement !== null
163
            && !($name instanceof FullyQualified)
164 67
            && self::array_starts_with($resolvedName->parts, $useStatement->parts)
165 19
            && !(
166
                $parentNode instanceof ConstFetch
167
                && (
168
                    $this->whitelist->isGlobalWhitelistedConstant($resolvedName->toString())
169
                    || $this->whitelist->isSymbolWhitelisted($resolvedName->toString(), true)
170 48
                )
171 8
            )
172
            && !(
173
                $useStatement->getAttribute('parent')
174 46
                && $useStatement->getAttribute('parent')->alias !== null
175
                && $this->whitelist->isSymbolWhitelisted($useStatement->toString())
176 11
            )
177
            && $resolvedName->parts !== ['Isolated', 'Symfony', 'Component', 'Finder', 'Finder']
178
        ) {
179
            return $name;
180
        }
181
182
        if ($this->prefix === $resolvedName->getFirst() // Skip if is already prefixed
183
            || $this->whitelist->belongsToWhitelistedNamespace((string) $resolvedName)  // Skip if the namespace node is whitelisted
184 320
        ) {
185 76
            return $resolvedName;
186 37
        }
187
188
        // Check if the class can be prefixed
189 54
        if (false === ($parentNode instanceof ConstFetch || $parentNode instanceof FuncCall)
190 9
            && $this->reflector->isClassInternal($resolvedName->toString())
191
        ) {
192
            return $resolvedName;
193
        }
194 297
195 3
        if ($parentNode instanceof ConstFetch) {
196
            if ($this->whitelist->isSymbolWhitelisted($resolvedName->toString(), true)) {
197
                return $resolvedName;
198 297
            }
199 297
200 297
            if ($this->reflector->isConstantInternal($resolvedName->toString())) {
201 297
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
202
            }
203
204
            // Constants have an autoloading fallback so we cannot prefix them when the name is ambiguous
205
            // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
206
            if (false === ($resolvedName instanceof FullyQualified)) {
207
                return $resolvedName;
208
            }
209
210
            if ($this->whitelist->isGlobalWhitelistedConstant((string) $resolvedName)) {
211
                // Unlike classes & functions, whitelisted are not prefixed with aliases registered in scoper-autoload.php
212
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
213
            }
214
215
            // Continue
216
        }
217
218
        // Functions have a fallback autoloading so we cannot prefix them when the name is ambiguous
219
        // See https://wiki.php.net/rfc/fallback-to-root-scope-deprecation
220
        if ($parentNode instanceof FuncCall) {
221
            if ($this->reflector->isFunctionInternal($resolvedName->toString())) {
222
                return new FullyQualified($resolvedName->toString(), $resolvedName->getAttributes());
223
            }
224
225
            if (false === ($resolvedName instanceof FullyQualified)) {
226
                return $resolvedName;
227
            }
228
        }
229
230
        if ($parentNode instanceof ClassMethod && $resolvedName->isSpecialClassName()) {
231
            return $name;
232
        }
233
234
        return FullyQualifiedFactory::concat(
235
            $this->prefix,
236
            $resolvedName->toString(),
237
            $resolvedName->getAttributes()
238
        );
239
    }
240
241
    private static function array_starts_with($arr, $prefix): bool
242
    {
243
        $prefixLength = count($prefix);
244
        for ($i = 0; $i < $prefixLength; ++$i) {
245
            if ($arr[$i] !== $prefix[$i]) {
246
                return false;
247
            }
248
        }
249
        return true;
250
    }
251
}
252