Completed
Push — master ( 0647fd...b9e92e )
by Théo
27:07 queued 08:01
created

StringScalarPrefixer::enterNode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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