Completed
Push — master ( 550011...0647fd )
by Théo
16:20 queued 07:57
created

StringScalarPrefixer::createPrefixedString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2.0023

Importance

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