Passed
Push — master ( 7d5ecd...bf567e )
by Théo
06:25 queued 02:42
created

belongsToTheGlobalNamespace()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 2
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 4
ccs 0
cts 0
cp 0
crap 6
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\Reflector;
18
use Humbug\PhpScoper\Whitelist;
19
use PhpParser\Node;
20
use PhpParser\Node\Arg;
21
use PhpParser\Node\Const_;
22
use PhpParser\Node\Expr\Array_;
23
use PhpParser\Node\Expr\ArrayItem;
24
use PhpParser\Node\Expr\Assign;
25
use PhpParser\Node\Expr\FuncCall;
26
use PhpParser\Node\Expr\New_;
27
use PhpParser\Node\Expr\StaticCall;
28
use PhpParser\Node\Identifier;
29
use PhpParser\Node\Name;
30
use PhpParser\Node\Name\FullyQualified;
31
use PhpParser\Node\Param;
32
use PhpParser\Node\Scalar\String_;
33
use PhpParser\Node\Stmt\PropertyProperty;
34
use PhpParser\Node\Stmt\Return_;
35
use PhpParser\NodeVisitorAbstract;
36
use function array_filter;
37
use function array_key_exists;
38
use function array_shift;
39
use function array_values;
40
use function explode;
41
use function implode;
42
use function in_array;
43
use function is_string;
44
use function ltrim;
45
use function preg_match as native_preg_match;
46
use function strpos;
47
use function strtolower;
48
49
/**
50
 * Prefixes the string scalar values when appropriate.
51
 *
52
 * ```
53
 * $x = 'Foo\Bar';
54
 * ```
55
 *
56
 * =>
57
 *
58
 * ```
59
 * $x = 'Humbug\Foo\Bar';
60
 * ```
61
 *
62
 * @private
63
 */
64
final class StringScalarPrefixer extends NodeVisitorAbstract
65
{
66
    private const SPECIAL_FUNCTION_NAMES = [
67
        'class_alias',
68
        'class_exists',
69
        'define',
70
        'defined',
71
        'function_exists',
72
        'interface_exists',
73
        'is_a',
74
        'is_subclass_of',
75 549
        'trait_exists',
76
    ];
77 549
78 549
    private const DATETIME_CLASSES = [
79 549
        'datetime',
80
        'datetimeimmutable',
81
    ];
82
83
    private string $prefix;
84
    private Whitelist $whitelist;
85 548
    private Reflector $reflector;
86
87 548
    public function __construct(string $prefix, Whitelist $whitelist, Reflector $reflector)
88 102
    {
89 548
        $this->prefix = $prefix;
90
        $this->whitelist = $whitelist;
91
        $this->reflector = $reflector;
92
    }
93 102
94
    public function enterNode(Node $node): Node
95 102
    {
96 102
        return $node instanceof String_
97
            ? $this->prefixStringScalar($node)
98 34
            : $node;
99
    }
100
101 82
    private function prefixStringScalar(String_ $string): String_
102 10
    {
103
        if (!(ParentNodeAppender::hasParent($string) && is_string($string->value))
104
            || 1 !== native_preg_match('/^((\\\\)?[\p{L}_\d]+)$|((\\\\)?(?:[\p{L}_\d]+\\\\+)+[\p{L}_\d]+)$/u', $string->value)
105
        ) {
106
            return $string;
107
        }
108 78
109
        $normalizedValue = ltrim($string->value, '\\');
110
111
        if ($this->whitelist->belongsToWhitelistedNamespace($string->value)) {
112
            return $string;
113 78
        }
114 40
115
        // From this point either the symbol belongs to the global namespace or the symbol belongs to the symbol
116
        // namespace is whitelisted
117 58
118 10
        $parentNode = ParentNodeAppender::getParent($string);
119
120
        // The string scalar either has a class form or a simple string which can either be a symbol from the global
121
        // namespace or a completely unrelated string.
122 50
123 45
        if ($parentNode instanceof Arg) {
124 42
            return $this->prefixStringArg($string, $parentNode, $normalizedValue);
125 22
        }
126 50
127
        if ($parentNode instanceof ArrayItem) {
128
            return $this->prefixArrayItemString($string, $parentNode, $normalizedValue);
129 17
        }
130
131
        if (!(
132
            $parentNode instanceof Assign
133 33
                || $parentNode instanceof Param
134 27
                || $parentNode instanceof Const_
135 33
                || $parentNode instanceof PropertyProperty
136
                || $parentNode instanceof Return_
137
        )) {
138
            return $string;
139 40
        }
140
141 40
        // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
142
        return $this->belongsToTheGlobalNamespace($string)
143 40
            ? $string
144
            : $this->createPrefixedString($string);
145 11
    }
146 7
147 11
    private function prefixStringArg(String_ $string, Arg $parentNode, string $normalizedValue): String_
148
    {
149
        $callerNode = ParentNodeAppender::getParent($parentNode);
150
151
        if ($callerNode instanceof New_) {
152
            return $this->prefixNewStringArg($string, $callerNode);
153
        }
154 30
155
        if ($callerNode instanceof FuncCall) {
156 30
            return $this->prefixFunctionStringArg($string, $callerNode, $normalizedValue);
157 5
        }
158 4
159 5
        if ($callerNode instanceof StaticCall) {
160
            return $this->prefixStaticCallStringArg($string, $callerNode);
161
        }
162
163 28
        // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular
164 8
        // string
165 3
        return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
166 8
    }
167
168
    private function prefixNewStringArg(String_ $string, New_ $newNode): String_
169
    {
170 23
        $class = $newNode->class;
171
172 23
        if (!($class instanceof Name)) {
173 10
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
174 7
        }
175
176 6
        if (in_array(strtolower($class->toString()), self::DATETIME_CLASSES, true)) {
177
            return $string;
178
        }
179 4
180 3
        return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
181 4
    }
182
183
    private function prefixFunctionStringArg(String_ $string, FuncCall $functionNode, string $normalizedValue): String_
184
    {
185
        // In the case of a function call, we allow to prefix strings which could be classes belonging to the global
186
        // namespace in some cases
187 19
        $functionName = $functionNode->name instanceof Name ? (string) $functionNode->name : null;
188 13
189 9
        if (in_array($functionName, ['date_create', 'date', 'gmdate', 'date_create_from_format'], true)) {
190
            return $string;
191 15
        }
192 19
193
        if (!in_array($functionName, self::SPECIAL_FUNCTION_NAMES, true)) {
194
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
195
        }
196 10
197
        if ('function_exists' === $functionName) {
198
            return $this->reflector->isFunctionInternal($normalizedValue)
199
                ? $string
200
                : $this->createPrefixedString($string);
201
        }
202 10
203
        $isConstantNode = $this->isConstantNode($string);
204 10
205
        if (!$isConstantNode) {
206
            if ('define' === $functionName
207 10
                && $this->belongsToTheGlobalNamespace($string)
208 10
            ) {
209
                return $string;
210 10
            }
211 9
212
            return $this->reflector->isClassInternal($normalizedValue)
213
                ? $string
214 3
                : $this->createPrefixedString($string);
215 2
        }
216 3
217
        return
218
            (
219
                $this->whitelist->isSymbolWhitelisted($string->value, true)
220 9
                || $this->whitelist->isGlobalWhitelistedConstant($string->value)
221
                || $this->reflector->isConstantInternal($normalizedValue)
222 9
            )
223
            ? $string
224 3
            : $this->createPrefixedString($string);
225 1
    }
226 3
227
    private function prefixStaticCallStringArg(String_ $string, StaticCall $callNode): String_
228
    {
229
        $class = $callNode->class;
230
231 6
        if (!($class instanceof Name)) {
232 3
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
233
        }
234
235 5
        if (!in_array(strtolower($class->toString()), self::DATETIME_CLASSES, true)) {
236
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
237 5
        }
238 5
239 5
        if ($callNode->name instanceof Identifier
240 3
            && 'createFromFormat' === $callNode->name->toString()
241
        ) {
242 3
            return $string;
243 5
        }
244
245
        return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
246
    }
247 23
248
    private function prefixArrayItemString(String_ $string, ArrayItem $parentNode, string $normalizedValue): String_
249 23
    {
250
        // ArrayItem can lead to two results: either the string is used for `spl_autoload_register()`, e.g.
251 23
        // `spl_autoload_register(['Swift', 'autoload'])` in which case the string `'Swift'` is guaranteed to be class
252
        // name, or something else in which case a string like `'Swift'` can be anything and cannot be prefixed.
253
254
        $arrayItemNode = $parentNode;
255
256 23
        $parentNode = ParentNodeAppender::getParent($parentNode);
257
258 23
        if (!($parentNode instanceof Array_)) {
259
            return $string;
260
        }
261
262
        $arrayNode = $parentNode;
263 23
        $parentNode = ParentNodeAppender::getParent($parentNode);
264 23
265
        if (!($parentNode instanceof Arg)
266 3
            || !ParentNodeAppender::hasParent($parentNode)
267
        ) {
268
            // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
269 20
            return $this->belongsToTheGlobalNamespace($string)
270
                ? $string
271
                : $this->createPrefixedString($string);
272 33
        }
273
274 33
        $functionNode = ParentNodeAppender::getParent($parentNode);
275 33
276 33
        if (!($functionNode instanceof FuncCall)) {
277
            // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
278
            return $this->belongsToTheGlobalNamespace($string)
279
                ? $string
280 33
                : $this->createPrefixedString($string);
281 20
        }
282
283
        if (!($functionNode->name instanceof Name)) {
284 33
            return $string;
285
        }
286 33
287 33
        $functionName = (string) $functionNode->name;
288 33
289
        return ('spl_autoload_register' === $functionName
290
                && array_key_exists(0, $arrayNode->items)
291 33
                && $arrayItemNode === $arrayNode->items[0]
292
                && !$this->reflector->isClassInternal($normalizedValue)
293 33
            )
294
            ? $this->createPrefixedString($string)
295
            : $string;
296 46
    }
297
298 46
    private function isConstantNode(String_ $node): bool
299
    {
300
        $parent = ParentNodeAppender::getParent($node);
301
302
        if (!($parent instanceof Arg)) {
303
            return false;
304
        }
305
306
        $argParent = ParentNodeAppender::getParent($parent);
307
308
        if (!($argParent instanceof FuncCall)) {
309
            return false;
310
        }
311
312
        if (!($argParent->name instanceof Name)
313
            || !in_array((string) $argParent->name, ['define', 'defined'], true)
314
        ) {
315
            return false;
316
        }
317
318
        return $parent === $argParent->args[0];
319
    }
320
321
    private function createPrefixedStringIfDoesNotBelongToGlobalNamespace(String_ $string): String_
322
    {
323
        // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
324
        return $this->belongsToTheGlobalNamespace($string)
325
            ? $string
326
            : $this->createPrefixedString($string);
327
    }
328
329
    private function createPrefixedString(String_ $previous): String_
330
    {
331
        $previousValueParts = array_values(
332
            array_filter(
333
                explode('\\', $previous->value),
334
            ),
335
        );
336
337
        if ($this->prefix === $previousValueParts[0]) {
338
            array_shift($previousValueParts);
339
        }
340
341
        $previousValue = implode('\\', $previousValueParts);
342
343
        $string = new String_(
344
            (string) FullyQualified::concat($this->prefix, $previousValue),
345
            $previous->getAttributes(),
346
        );
347
348
        ParentNodeAppender::setParent($string, $string);
349
350
        return $string;
351
    }
352
353
    private function belongsToTheGlobalNamespace(String_ $string): bool
354
    {
355
        return '' === $string->value
356
            || 0 === (int) strpos($string->value, '\\', 1);
357
    }
358
}
359