Passed
Pull Request — master (#578)
by Théo
02:15
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 EnrichedReflector $enrichedReflector;
96 102
97
    public function __construct(
98 34
        string $prefix,
99
        EnrichedReflector $enrichedReflector
100
    ) {
101 82
        $this->prefix = $prefix;
102 10
        $this->enrichedReflector = $enrichedReflector;
103
    }
104
105
    public function enterNode(Node $node): Node
106
    {
107
        return $node instanceof String_
108 78
            ? $this->prefixStringScalar($node)
109
            : $node;
110
    }
111
112
    private function prefixStringScalar(String_ $string): String_
113 78
    {
114 40
        if (!(ParentNodeAppender::hasParent($string) && is_string($string->value))
115
            || 1 !== native_preg_match(self::CLASS_LIKE_PATTERN, $string->value)
116
        ) {
117 58
            return $string;
118 10
        }
119
120
        $normalizedValue = ltrim($string->value, '\\');
121
122 50
        if ($this->enrichedReflector->belongsToExcludedNamespace($string->value)) {
123 45
            return $string;
124 42
        }
125 22
126 50
        // From this point either the symbol belongs to the global namespace or the symbol belongs to the symbol
127
        // namespace is whitelisted
128
129 17
        $parentNode = ParentNodeAppender::getParent($string);
130
131
        // The string scalar either has a class form or a simple string which can either be a symbol from the global
132
        // namespace or a completely unrelated string.
133 33
134 27
        if ($parentNode instanceof Arg) {
135 33
            return $this->prefixStringArg($string, $parentNode, $normalizedValue);
136
        }
137
138
        if ($parentNode instanceof ArrayItem) {
139 40
            return $this->prefixArrayItemString($string, $parentNode, $normalizedValue);
140
        }
141 40
142
        if (!(
143 40
            $parentNode instanceof Assign
144
                || $parentNode instanceof Param
145 11
                || $parentNode instanceof Const_
146 7
                || $parentNode instanceof PropertyProperty
147 11
                || $parentNode instanceof Return_
148
        )) {
149
            return $string;
150
        }
151
152
        // If belongs to the global namespace then we cannot differentiate the
153
        // value from a symbol and a regular string hence we leave it alone
154 30
        return self::belongsToTheGlobalNamespace($string)
155
            ? $string
156 30
            : $this->createPrefixedString($string);
157 5
    }
158 4
159 5
    private function prefixStringArg(String_ $string, Arg $parentNode, string $normalizedValue): String_
160
    {
161
        $callerNode = ParentNodeAppender::getParent($parentNode);
162
163 28
        if ($callerNode instanceof New_) {
164 8
            return $this->prefixNewStringArg($string, $callerNode);
165 3
        }
166 8
167
        if ($callerNode instanceof FuncCall) {
168
            return $this->prefixFunctionStringArg($string, $callerNode, $normalizedValue);
169
        }
170 23
171
        if ($callerNode instanceof StaticCall) {
172 23
            return $this->prefixStaticCallStringArg($string, $callerNode);
173 10
        }
174 7
175
        // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular
176 6
        // string
177
        return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
178
    }
179 4
180 3
    private function prefixNewStringArg(String_ $string, New_ $newNode): String_
181 4
    {
182
        $class = $newNode->class;
183
184
        if (!($class instanceof Name)) {
185
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
186
        }
187 19
188 13
        if (in_array(strtolower($class->toString()), self::DATETIME_CLASSES, true)) {
189 9
            return $string;
190
        }
191 15
192 19
        return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
193
    }
194
195
    private function prefixFunctionStringArg(String_ $string, FuncCall $functionNode, string $normalizedValue): String_
196 10
    {
197
        // In the case of a function call, we allow prefixing strings which
198
        // could be classes belonging to the global namespace in some cases
199
        $functionName = $functionNode->name instanceof Name ? (string) $functionNode->name : null;
200
201
        if (in_array($functionName, self::IGNORED_FUNCTIONS, true)) {
202 10
            return $string;
203
        }
204 10
205
        if (!in_array($functionName, self::SPECIAL_FUNCTION_NAMES, true)) {
206
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
207 10
        }
208 10
209
        if ('function_exists' === $functionName) {
210 10
            return $this->enrichedReflector->isFunctionExcluded($normalizedValue)
211 9
                ? $string
212
                : $this->createPrefixedString($string);
213
        }
214 3
215 2
        $isConstantNode = self::isConstantNode($string);
216 3
217
        if (!$isConstantNode) {
218
            if ('define' === $functionName
219
                && self::belongsToTheGlobalNamespace($string)
220 9
            ) {
221
                return $string;
222 9
            }
223
224 3
            return $this->enrichedReflector->isClassExcluded($normalizedValue)
225 1
                ? $string
226 3
                : $this->createPrefixedString($string);
227
        }
228
229
        return $this->enrichedReflector->isExposedConstant($normalizedValue)
230
            ? $string
231 6
            : $this->createPrefixedString($string);
232 3
    }
233
234
    private function prefixStaticCallStringArg(String_ $string, StaticCall $callNode): String_
235 5
    {
236
        $class = $callNode->class;
237 5
238 5
        if (!($class instanceof Name)) {
239 5
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
240 3
        }
241
242 3
        if (!in_array(strtolower($class->toString()), self::DATETIME_CLASSES, true)) {
243 5
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
244
        }
245
246
        if ($callNode->name instanceof Identifier
247 23
            && 'createFromFormat' === $callNode->name->toString()
248
        ) {
249 23
            return $string;
250
        }
251 23
252
        return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
253
    }
254
255
    private function prefixArrayItemString(
256 23
        String_ $string,
257
        ArrayItem $parentNode,
258 23
        string $normalizedValue
259
    ): String_
260
    {
261
        // ArrayItem can lead to two results: either the string is used for
262
        // `spl_autoload_register()`, e.g. `spl_autoload_register(['Swift', 'autoload'])`
263 23
        // in which case the string `'Swift'` is guaranteed to be class name, or
264 23
        // something else in which case a string like `'Swift'` can be anything
265
        // and cannot be prefixed.
266 3
        $arrayItemNode = $parentNode;
267
268
        $parentNode = ParentNodeAppender::getParent($parentNode);
269 20
270
        if (!($parentNode instanceof Array_)) {
271
            return $string;
272 33
        }
273
274 33
        $arrayNode = $parentNode;
275 33
        $parentNode = ParentNodeAppender::getParent($parentNode);
276 33
277
        if (!($parentNode instanceof Arg)
278
            || !ParentNodeAppender::hasParent($parentNode)
279
        ) {
280 33
            // If belongs to the global namespace then we cannot differentiate
281 20
            // the value from a symbol and a regular string
282
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
283
        }
284 33
285
        $functionNode = ParentNodeAppender::getParent($parentNode);
286 33
287 33
        if (!($functionNode instanceof FuncCall)) {
288 33
            // If belongs to the global namespace then we cannot differentiate
289
            // the value from a symbol and a regular string
290
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
291 33
        }
292
293 33
        if (!($functionNode->name instanceof Name)) {
294
            return $string;
295
        }
296 46
297
        $functionName = (string) $functionNode->name;
298 46
299
        return ('spl_autoload_register' === $functionName
300
                && array_key_exists(0, $arrayNode->items)
301
                && $arrayItemNode === $arrayNode->items[0]
302
                && !$this->enrichedReflector->isClassExcluded($normalizedValue)
303
            )
304
            ? $this->createPrefixedString($string)
305
            : $string;
306
    }
307
308
    private static function isConstantNode(String_ $node): bool
309
    {
310
        $parent = ParentNodeAppender::getParent($node);
311
312
        if (!($parent instanceof Arg)) {
313
            return false;
314
        }
315
316
        $argParent = ParentNodeAppender::getParent($parent);
317
318
        if (!($argParent instanceof FuncCall)) {
319
            return false;
320
        }
321
322
        if (!($argParent->name instanceof Name)
323
            || !in_array((string) $argParent->name, ['define', 'defined'], true)
324
        ) {
325
            return false;
326
        }
327
328
        return $parent === $argParent->args[0];
329
    }
330
331
    private function createPrefixedStringIfDoesNotBelongToGlobalNamespace(String_ $string): String_
332
    {
333
        // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
334
        return self::belongsToTheGlobalNamespace($string)
335
            ? $string
336
            : $this->createPrefixedString($string);
337
    }
338
339
    private static function belongsToTheGlobalNamespace(String_ $string): bool
340
    {
341
        return '' === $string->value
342
            || 0 === (int) strpos($string->value, '\\', 1);
343
    }
344
345
    private function createPrefixedString(String_ $previous): String_
346
    {
347
        $previousValueParts = array_values(
348
            array_filter(
349
                explode('\\', $previous->value),
350
            ),
351
        );
352
353
        $previousValueAlreadyPrefixed = $this->prefix === $previousValueParts[0];
354
355
        if ($previousValueAlreadyPrefixed) {
356
            // Remove the prefix and proceed as usual: this ensures that even
357
            // if the value was correct-ish it is cleaned up (e.g. of leading
358
            // backslashes)
359
            array_shift($previousValueParts);
360
        }
361
362
        $previousValue = implode('\\', $previousValueParts);
363
364
        $string = new String_(
365
            (string) FullyQualified::concat($this->prefix, $previousValue),
366
            $previous->getAttributes(),
367
        );
368
369
        ParentNodeAppender::setParent($string, $string);
370
371
        return $string;
372
    }
373
}
374