Passed
Pull Request — master (#578)
by Théo
02:47
created

StringScalarPrefixer::prefixStringScalar()   C

Complexity

Conditions 13
Paths 7

Size

Total Lines 45
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 13

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 13
eloc 21
c 3
b 0
f 0
nc 7
nop 1
dl 0
loc 45
ccs 21
cts 21
cp 1
crap 13
rs 6.6166

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