Passed
Push — master ( 8154e5...402dc3 )
by Théo
02:32
created

StringScalarPrefixer   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Test Coverage

Coverage 98.1%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 137
dl 0
loc 307
ccs 103
cts 105
cp 0.981
rs 4.08
c 5
b 0
f 0
wmc 59

12 Methods

Rating   Name   Duplication   Size   Complexity  
A createPrefixedStringIfDoesNotBelongToGlobalNamespace() 0 6 2
A prefixStaticCallStringArg() 0 19 5
A prefixNewStringArg() 0 13 3
A enterNode() 0 5 2
A isConstantNode() 0 21 5
B prefixFunctionStringArg() 0 37 11
A prefixStringArg() 0 19 4
A __construct() 0 6 1
B prefixArrayItemString() 0 51 10
C prefixStringScalar() 0 46 13
A createPrefixedString() 0 27 2
A belongsToTheGlobalNamespace() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like StringScalarPrefixer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StringScalarPrefixer, and based on these observations, apply Extract Interface, too.

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\Symbol\EnrichedReflector;
18
use PhpParser\Node;
19
use PhpParser\Node\Arg;
20
use PhpParser\Node\Const_;
21
use PhpParser\Node\Expr\Array_;
22
use PhpParser\Node\Expr\ArrayItem;
23
use PhpParser\Node\Expr\Assign;
24
use PhpParser\Node\Expr\FuncCall;
25
use PhpParser\Node\Expr\New_;
26
use PhpParser\Node\Expr\StaticCall;
27
use PhpParser\Node\Identifier;
28
use PhpParser\Node\Name;
29
use PhpParser\Node\Name\FullyQualified;
30
use PhpParser\Node\Param;
31
use PhpParser\Node\Scalar\String_;
32
use PhpParser\Node\Stmt\PropertyProperty;
33
use PhpParser\Node\Stmt\Return_;
34
use PhpParser\NodeVisitorAbstract;
35
use function array_filter;
36
use function array_key_exists;
37
use function array_shift;
38
use function array_values;
39
use function explode;
40
use function implode;
41
use function in_array;
42
use function is_string;
43
use function ltrim;
44
use function preg_match as native_preg_match;
45
use function strpos;
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
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
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
    {
260
        // ArrayItem can lead to two results: either the string is used for
261
        // `spl_autoload_register()`, e.g. `spl_autoload_register(['Swift', 'autoload'])`
262
        // in which case the string `'Swift'` is guaranteed to be class name, or
263 23
        // something else in which case a string like `'Swift'` can be anything
264 23
        // and cannot be prefixed.
265
        $arrayItemNode = $parentNode;
266 3
267
        $parentNode = ParentNodeAppender::getParent($parentNode);
268
269 20
        if (!($parentNode instanceof Array_)) {
270
            return $string;
271
        }
272 33
273
        $arrayNode = $parentNode;
274 33
        $parentNode = ParentNodeAppender::getParent($parentNode);
275 33
276 33
        if (!($parentNode instanceof Arg)
277
            || !ParentNodeAppender::hasParent($parentNode)
278
        ) {
279
            // If belongs to the global namespace then we cannot differentiate
280 33
            // the value from a symbol and a regular string
281 20
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
282
        }
283
284 33
        $functionNode = ParentNodeAppender::getParent($parentNode);
285
286 33
        if (!($functionNode instanceof FuncCall)) {
287 33
            // If belongs to the global namespace then we cannot differentiate
288 33
            // the value from a symbol and a regular string
289
            return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string);
290
        }
291 33
292
        if (!($functionNode->name instanceof Name)) {
293 33
            return $string;
294
        }
295
296 46
        $functionName = (string) $functionNode->name;
297
298 46
        return ('spl_autoload_register' === $functionName
299
                && array_key_exists(0, $arrayNode->items)
300
                && $arrayItemNode === $arrayNode->items[0]
301
                && !$this->enrichedReflector->isClassExcluded($normalizedValue)
302
            )
303
            ? $this->createPrefixedString($string)
304
            : $string;
305
    }
306
307
    private static function isConstantNode(String_ $node): bool
308
    {
309
        $parent = ParentNodeAppender::getParent($node);
310
311
        if (!($parent instanceof Arg)) {
312
            return false;
313
        }
314
315
        $argParent = ParentNodeAppender::getParent($parent);
316
317
        if (!($argParent instanceof FuncCall)) {
318
            return false;
319
        }
320
321
        if (!($argParent->name instanceof Name)
322
            || !in_array((string) $argParent->name, ['define', 'defined'], true)
323
        ) {
324
            return false;
325
        }
326
327
        return $parent === $argParent->args[0];
328
    }
329
330
    private function createPrefixedStringIfDoesNotBelongToGlobalNamespace(String_ $string): String_
331
    {
332
        // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
333
        return $this->belongsToTheGlobalNamespace($string)
334
            ? $string
335
            : $this->createPrefixedString($string);
336
    }
337
338
    private function belongsToTheGlobalNamespace(String_ $string): bool
339
    {
340
        return $this->enrichedReflector->belongsToGlobalNamespace($string->value);
341
    }
342
343
    private function createPrefixedString(String_ $previous): String_
344
    {
345
        $previousValueParts = array_values(
346
            array_filter(
347
                explode('\\', $previous->value),
348
            ),
349
        );
350
351
        $previousValueAlreadyPrefixed = $this->prefix === $previousValueParts[0];
352
353
        if ($previousValueAlreadyPrefixed) {
354
            // Remove the prefix and proceed as usual: this ensures that even
355
            // if the value was correct-ish it is cleaned up (e.g. of leading
356
            // backslashes)
357
            array_shift($previousValueParts);
358
        }
359
360
        $previousValue = implode('\\', $previousValueParts);
361
362
        $string = new String_(
363
            (string) FullyQualified::concat($this->prefix, $previousValue),
364
            $previous->getAttributes(),
365
        );
366
367
        ParentNodeAppender::setParent($string, $string);
368
369
        return $string;
370
    }
371
}
372