Passed
Branch master (c976c6)
by Théo
03:51
created

StringScalarPrefixer::isConstantNode()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6.2163

Importance

Changes 0
Metric Value
cc 6
eloc 10
nc 4
nop 1
dl 0
loc 23
ccs 9
cts 11
cp 0.8182
crap 6.2163
rs 9.2222
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\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\Name;
27
use PhpParser\Node\Name\FullyQualified;
28
use PhpParser\Node\Param;
29
use PhpParser\Node\Scalar\String_;
30
use PhpParser\Node\Stmt\PropertyProperty;
31
use PhpParser\Node\Stmt\Return_;
32
use PhpParser\NodeVisitorAbstract;
33
use function array_key_exists;
34
use function array_shift;
35
use function array_values;
36
use function implode;
37
use function in_array;
38
use function is_string;
39
use function preg_match;
40
use function strpos;
41
42
/**
43
 * Prefixes the string scalar values when appropriate.
44
 *
45
 * ```
46
 * $x = 'Foo\Bar';
47
 * ```
48
 *
49
 * =>
50
 *
51
 * ```
52
 * $x = 'Humbug\Foo\Bar';
53
 * ```
54
 *
55
 * @private
56
 */
57
final class StringScalarPrefixer extends NodeVisitorAbstract
58
{
59
    private const SPECIAL_FUNCTION_NAMES = [
60
        'class_alias',
61
        'class_exists',
62
        'define',
63
        'defined',
64
        'function_exists',
65
        'interface_exists',
66
        'is_a',
67
        'is_subclass_of',
68
        'trait_exists',
69
    ];
70
71
    private $prefix;
72
    private $whitelist;
73
    private $reflector;
74
75 549
    public function __construct(string $prefix, Whitelist $whitelist, Reflector $reflector)
76
    {
77 549
        $this->prefix = $prefix;
78 549
        $this->whitelist = $whitelist;
79 549
        $this->reflector = $reflector;
80
    }
81
82
    /**
83
     * @inheritdoc
84
     */
85 548
    public function enterNode(Node $node): Node
86
    {
87 548
        return $node instanceof String_
88 102
            ? $this->prefixStringScalar($node)
89 548
            : $node
90
        ;
91
    }
92
93 102
    private function prefixStringScalar(String_ $string): String_
94
    {
95 102
        if (false === (ParentNodeAppender::hasParent($string) && is_string($string->value))
96 102
            || 1 !== preg_match('/^((\\\\)?[\p{L}_\d]+)$|((\\\\)?(?:[\p{L}_\d]+\\\\+)+[\p{L}_\d]+)$/u', $string->value)
97
        ) {
98 34
            return $string;
99
        }
100
101 82
        if ($this->whitelist->belongsToWhitelistedNamespace($string->value)) {
102 10
            return $string;
103
        }
104
105
        // From this point either the symbol belongs to the global namespace or the symbol belongs to the symbol
106
        // namespace is whitelisted
107
108 78
        $parentNode = ParentNodeAppender::getParent($string);
109
110
        // The string scalar either has a class form or a simple string which can either be a symbol from the global
111
        // namespace or a completely unrelated string.
112
113 78
        if ($parentNode instanceof Arg) {
114 40
            return $this->prefixStringArg($string, $parentNode);
115
        }
116
117 58
        if ($parentNode instanceof ArrayItem) {
118 10
            return $this->prefixArrayItemString($string, $parentNode);
119
        }
120
121
        if (false === (
122 50
                $parentNode instanceof Assign
123 45
                || $parentNode instanceof Param
124 42
                || $parentNode instanceof Const_
125 22
                || $parentNode instanceof PropertyProperty
126 50
                || $parentNode instanceof Return_
127
            )
128
        ) {
129 17
            return $string;
130
        }
131
132
        // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
133 33
        return $this->belongsToTheGlobalNamespace($string)
134 27
            ? $string
135 33
            : $this->createPrefixedString($string)
136
        ;
137
    }
138
139 40
    private function prefixStringArg(String_ $string, Arg $parentNode): String_
140
    {
141 40
        $functionNode = ParentNodeAppender::getParent($parentNode);
142
143 40
        if (false === ($functionNode instanceof FuncCall)) {
144
            // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
145 11
            return $this->belongsToTheGlobalNamespace($string)
146 7
                ? $string
147 11
                : $this->createPrefixedString($string)
148
            ;
149
        }
150
        /** @var FuncCall $functionNode */
151
152
        // In the case of a function call, we allow to prefix strings which could be classes belonging to the global
153
        // namespace in some cases
154 30
        $functionName = $functionNode->name instanceof Name ? (string) $functionNode->name : null;
155
156 30
        if (false === in_array($functionName, self::SPECIAL_FUNCTION_NAMES, true)) {
157 5
            return $this->belongsToTheGlobalNamespace($string)
158 4
                ? $string
159 5
                : $this->createPrefixedString($string)
160
            ;
161
        }
162
163 28
        if ('function_exists' === $functionName) {
164 8
            return $this->reflector->isFunctionInternal($string->value)
165 3
                ? $string
166 8
                : $this->createPrefixedString($string)
167
            ;
168
        }
169
170 23
        $isConstantNode = $this->isConstantNode($string);
171
172 23
        if (false === $isConstantNode) {
173 10
            if ('define' === $functionName
174 7
                && $this->belongsToTheGlobalNamespace($string)
175
            ) {
176 6
                return $string;
177
            }
178
179 4
            return $this->reflector->isClassInternal($string->value)
180 3
                ? $string
181 4
                : $this->createPrefixedString($string)
182
            ;
183
        }
184
185
        return
186
            (
187 19
                $this->whitelist->isSymbolWhitelisted($string->value, true)
188 13
                || $this->whitelist->isGlobalWhitelistedConstant($string->value)
189 9
                || $this->reflector->isConstantInternal($string->value)
190
            )
191 15
            ? $string
192 19
            : $this->createPrefixedString($string)
193
        ;
194
    }
195
196 10
    private function prefixArrayItemString(String_ $string, ArrayItem $parentNode): String_
197
    {
198
        // ArrayItem can lead to two results: either the string is used for `spl_autoload_register()`, e.g.
199
        // `spl_autoload_register(['Swift', 'autoload'])` in which case the string `'Swift'` is guaranteed to be class
200
        // name, or something else in which case a string like `'Swift'` can be anything and cannot be prefixed.
201
202 10
        $arrayItemNode = $parentNode;
203
204 10
        $parentNode = ParentNodeAppender::getParent($parentNode);
205
206
        /** @var Array_ $arrayNode */
207 10
        $arrayNode = $parentNode;
208 10
        $parentNode = ParentNodeAppender::getParent($parentNode);
209
210 10
        if (false === ($parentNode instanceof Arg)
211 9
            || null === $functionNode = ParentNodeAppender::findParent($parentNode)
1 ignored issue
show
Unused Code introduced by
The assignment to $functionNode is dead and can be removed.
Loading history...
212
        ) {
213
            // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
214 3
            return $this->belongsToTheGlobalNamespace($string)
215 2
                ? $string
216 3
                : $this->createPrefixedString($string)
217
            ;
218
        }
219
220 9
        $functionNode = ParentNodeAppender::getParent($parentNode);
221
222 9
        if (false === ($functionNode instanceof FuncCall)) {
223
            // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
224 3
            return $this->belongsToTheGlobalNamespace($string)
225 1
                ? $string
226 3
                : $this->createPrefixedString($string)
227
            ;
228
        }
229
230
        /** @var FuncCall $functionNode */
231 6
        if (false === ($functionNode->name instanceof Name)) {
232 3
            return $string;
233
        }
234
235 5
        $functionName = (string) $functionNode->name;
236
237 5
        return ('spl_autoload_register' === $functionName
238 5
                && array_key_exists(0, $arrayNode->items)
239 5
                && $arrayItemNode === $arrayNode->items[0]
240 3
                && false === $this->reflector->isClassInternal($string->value)
241
            )
242 3
            ? $this->createPrefixedString($string)
243 5
            : $string
244
        ;
245
    }
246
247 23
    private function isConstantNode(String_ $node): bool
248
    {
249 23
        $parent = ParentNodeAppender::getParent($node);
250
251 23
        if (false === ($parent instanceof Arg)) {
252
            return false;
253
        }
254
255
        /** @var Arg $parent */
256 23
        $argParent = ParentNodeAppender::getParent($parent);
257
258 23
        if (false === ($argParent instanceof FuncCall)) {
259
            return false;
260
        }
261
262
        /* @var FuncCall $argParent */
263 23
        if (false === ($argParent->name instanceof Name)
264 23
            || ('define' !== (string) $argParent->name && 'defined' !== (string) $argParent->name)
265
        ) {
266 3
            return false;
267
        }
268
269 20
        return $parent === $argParent->args[0];
270
    }
271
272 33
    private function createPrefixedString(String_ $previous): String_
273
    {
274 33
        $previousValueParts = array_values(
275 33
            array_filter(
276 33
                explode('\\', $previous->value)
277
            )
278
        );
279
280 33
        if ($this->prefix === $previousValueParts[0]) {
281 20
            array_shift($previousValueParts);
282
        }
283
284 33
        $previousValue = implode('\\', $previousValueParts);
285
286 33
        $string = new String_(
287 33
            (string) FullyQualified::concat($this->prefix, $previousValue),
288 33
            $previous->getAttributes()
289
        );
290
291 33
        $string->setAttribute(ParentNodeAppender::PARENT_ATTRIBUTE, $string);
292
293 33
        return $string;
294
    }
295
296 46
    private function belongsToTheGlobalNamespace(String_ $string): bool
297
    {
298 46
        return '' === $string->value || 0 === (int) strpos($string->value, '\\', 1);
299
    }
300
}
301