Completed
Push — master ( b9e92e...40b894 )
by Théo
37:17 queued 24:34
created

StringScalarPrefixer::createPrefixedString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 23
ccs 12
cts 12
cp 1
crap 2
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
        '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 101
            ? $this->prefixStringScalar($node)
88 547
            : $node
89
        ;
90
    }
91
92 101
    private function prefixStringScalar(String_ $string): String_
93
    {
94 101
        if (false === (ParentNodeAppender::hasParent($string) && is_string($string->value))
95 101
            || 1 !== preg_match('/^((\\\\)?[\p{L}_\d]+)$|((\\\\)?(?:[\p{L}_\d]+\\\\+)+[\p{L}_\d]+)$/u', $string->value)
96
        ) {
97 34
            return $string;
98
        }
99
100 81
        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 77
        $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 77
        if ($parentNode instanceof Arg) {
113 40
            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 40
    private function prefixStringArg(String_ $string, Arg $parentNode): String_
138
    {
139 40
        $functionNode = ParentNodeAppender::getParent($parentNode);
140
141 40
        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 30
        $functionName = $functionNode->name instanceof Name ? (string) $functionNode->name : null;
153
154 30
        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 28
        if ('function_exists' === $functionName) {
162 8
            return $this->reflector->isFunctionInternal($string->value)
163 3
                ? $string
164 8
                : $this->createPrefixedString($string)
165
            ;
166
        }
167
168 23
        $isConstantNode = $this->isConstantNode($string);
169
170 23
        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
        return
184
            (
185 19
                $this->whitelist->isSymbolWhitelisted($string->value, true)
186 19
                || $this->whitelist->isGlobalWhitelistedConstant($string->value)
187 19
                || $this->reflector->isConstantInternal($string->value)
188
            )
189 15
            ? $string
190 19
            : $this->createPrefixedString($string)
191
        ;
192
    }
193
194 10
    private function prefixArrayItemString(String_ $string, ArrayItem $parentNode): String_
195
    {
196
        // ArrayItem can lead to two results: either the string is used for `spl_autoload_register()`, e.g.
197
        // `spl_autoload_register(['Swift', 'autoload'])` in which case the string `'Swift'` is guaranteed to be class
198
        // name, or something else in which case a string like `'Swift'` can be anything and cannot be prefixed.
199
200 10
        $arrayItemNode = $parentNode;
201
202 10
        $parentNode = ParentNodeAppender::getParent($parentNode);
203
204
        /** @var Array_ $arrayNode */
205 10
        $arrayNode = $parentNode;
206 10
        $parentNode = ParentNodeAppender::getParent($parentNode);
207
208 10
        if (false === ($parentNode instanceof Arg)
209 10
            || null === $functionNode = ParentNodeAppender::findParent($parentNode)
210
        ) {
211
            // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
212 3
            return $this->belongsToTheGlobalNamespace($string)
213 2
                ? $string
214 3
                : $this->createPrefixedString($string)
215
            ;
216
        }
217
218 9
        $functionNode = ParentNodeAppender::getParent($parentNode);
219
220 9
        if (false === ($functionNode instanceof FuncCall)) {
221
            // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string
222 3
            return $this->belongsToTheGlobalNamespace($string)
223 1
                ? $string
224 3
                : $this->createPrefixedString($string)
225
            ;
226
        }
227
228
        /** @var FuncCall $functionNode */
229 6
        if (false === ($functionNode->name instanceof Name)) {
230 3
            return $string;
231
        }
232
233 5
        $functionName = (string) $functionNode->name;
234
235 5
        return ('spl_autoload_register' === $functionName
236 5
                && array_key_exists(0, $arrayNode->items)
237 5
                && $arrayItemNode === $arrayNode->items[0]
238 5
                && false === $this->reflector->isClassInternal($string->value)
239
            )
240 3
            ? $this->createPrefixedString($string)
241 5
            : $string
242
        ;
243
    }
244
245 23
    private function isConstantNode(String_ $node): bool
246
    {
247 23
        $parent = ParentNodeAppender::getParent($node);
248
249 23
        if (false === ($parent instanceof Arg)) {
250
            return false;
251
        }
252
253
        /** @var Arg $parent */
254 23
        $argParent = ParentNodeAppender::getParent($parent);
255
256 23
        if (false === ($argParent instanceof FuncCall)) {
257
            return false;
258
        }
259
260
        /* @var FuncCall $argParent */
261 23
        if (false === ($argParent->name instanceof Name)
262 23
            || ('define' !== (string) $argParent->name && 'defined' !== (string) $argParent->name)
263
        ) {
264 3
            return false;
265
        }
266
267 20
        return $parent === $argParent->args[0];
268
    }
269
270 32
    private function createPrefixedString(String_ $previous): String_
271
    {
272 32
        $previousValueParts = array_values(
273 32
            array_filter(
274 32
                explode('\\', $previous->value)
275
            )
276
        );
277
278 32
        if ($this->prefix === $previousValueParts[0]) {
279 19
            array_shift($previousValueParts);
280
        }
281
282 32
        $previousValue = implode('\\', $previousValueParts);
283
284 32
        $string = new String_(
285 32
            (string) FullyQualified::concat($this->prefix, $previousValue),
286 32
            $previous->getAttributes()
287
        );
288
289 32
        $string->setAttribute(ParentNodeAppender::PARENT_ATTRIBUTE, $string);
290
291 32
        return $string;
292
    }
293
294 45
    private function belongsToTheGlobalNamespace(String_ $string): bool
295
    {
296 45
        return '' === $string->value || 0 === (int) strpos($string->value, '\\', 1);
297
    }
298
}
299