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

StringScalarPrefixer::prefixArrayItemString()   C

Complexity

Conditions 12
Paths 14

Size

Total Lines 48
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 12.1081

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 25
c 1
b 0
f 0
nc 14
nop 3
dl 0
loc 48
ccs 20
cts 22
cp 0.9091
crap 12.1081
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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