Passed
Push — master ( e69999...fd1dc4 )
by Théo
01:40
created

StringScalarPrefixer::isConstantNode()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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