StringScalarPrefixer::createPrefixedString()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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