Test Setup Failed
Push — master ( 58c170...8423ed )
by Théo
02:31
created

StringScalarPrefixer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 3
dl 0
loc 6
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 1
crap 1
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\Expr\MethodCall;
27
use PhpParser\Node\Expr\StaticCall;
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\NodeVisitorAbstract;
34
use function array_key_exists;
35
use function count;
36
use function Humbug\PhpScoper\is_stringable;
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.
44
 *
45
 * ```
46
 * $x = 'Foo\Bar';
47
 * ```
48
 *
49
 * =>
50
 *
51
 * ```
52
 * $x = 'Humbug\Foo\Bar';
53
 * ```
54
 *
55 422
 * @private
56
 */
57 422
final class StringScalarPrefixer extends NodeVisitorAbstract
58 422
{
59
    private const SPECIAL_FUNCTION_NAMES = [
60
        'is_a',
61
        'is_subclass_of',
62
        'interface_exists',
63
        'class_exists',
64 421
        'trait_exists',
65
        'function_exists',
66 421
        'class_alias',
67 12
    ];
68 421
69
    private $prefix;
70
    private $whitelist;
71
    private $reflector;
72 421
73
    public function __construct(string $prefix, Whitelist $whitelist, Reflector $reflector)
74 421
    {
75 421
        $this->prefix = $prefix;
76
        $this->whitelist = $whitelist;
77 421
        $this->reflector = $reflector;
78
    }
79
80 14
    /**
81
     * @inheritdoc
82 14
     */
83 14
    public function enterNode(Node $node): Node
84
    {
85 4
        $isSpecialFunction = false;
86
87 4
        return ($this->shouldPrefixScalar($node, $isSpecialFunction))
88 2
            ? $this->prefixStringScalar($node, $isSpecialFunction)
0 ignored issues
show
Compatibility introduced by
$node of type object<PhpParser\Node> is not a sub-type of object<PhpParser\Node\Scalar\String_>. It seems like you assume a concrete implementation of the interface PhpParser\Node to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
89
            : $node
90
        ;
91 2
    }
92
93
    private function shouldPrefixScalar(Node $node, bool &$isSpecialFunction): bool
94 12
    {
95 12
        if (false === ($node instanceof String_ && AppendParentNode::hasParent($node) && is_string($node->value))
96 12
            || 1 !== preg_match('/^((\\\\)?[\p{L}_]+)|((\\\\)?(?:[\p{L}_]+\\\\+)+[\p{L}_]+)$/u', $node->value)
97 12
        ) {
98 12
            return false;
99
        }
100
101
        /** @var String_ $node */
102 12
        $parentNode = AppendParentNode::getParent($node);
103
104 12
        // The string scalar either has a class form or a simple string which can either be a symbol from the global
105 12
        // namespace or a completely unrelated string.
106 12
107
        if ($parentNode instanceof Arg
108
            && null !== $functionNode = AppendParentNode::findParent($parentNode)
109
        ) {
110 12
            $functionNode = AppendParentNode::getParent($parentNode);
111 10
112
            if ($functionNode instanceof FuncCall) {
113
                $functionName = is_stringable($functionNode->name) ? (string) $functionNode->name : null;
114 12
115 12
                if (false === strpos((string) $node->value, '\\')
116
                    && null !== $functionName
117
                    && in_array($functionName, self::SPECIAL_FUNCTION_NAMES, true)
118
                ) {
119 12
                    $isSpecialFunction = true;
120
121
                    return
122 12
                        (
123
                            'function_exists' === $functionName
124
                            && false === $this->reflector->isFunctionInternal($node->value)
125
                        )
126
                        || (
127
                            'function_exists' !== $functionName
128
                            && false === $this->reflector->isClassInternal($node->value)
129
                            && false === $this->whitelist->isClassWhitelisted($node->value)
130
                        )
131
                    ;
132
                }
133
134
                return $functionNode->name instanceof Name && false === $functionNode->hasAttribute('whitelist_class_alias');
135
            }
136
137
            return $functionNode instanceof MethodCall || $functionNode instanceof StaticCall;
138
        }
139
140
        if (false === ($parentNode instanceof ArrayItem)) {
141
            return $parentNode instanceof Assign
142
                || $parentNode instanceof Param
143
                || $parentNode instanceof Const_
144
                || $parentNode instanceof PropertyProperty
145
            ;
146
        }
147
148
        // ArrayItem can lead to two results: either the string is used for `spl_autoload_register()`, e.g.
149
        // `spl_autoload_register(['Swift', 'autoload'])` in which case the string `'Swift'` is guaranteed to be class
150
        // name, or something else in which case a string like `'Swift'` can be anything and cannot be prefixed.
151
152
        if (substr_count($node->value, '\\') + 1 > 1) {
153
            return true;
154
        }
155
156
        $arrayItemNode = $parentNode;
157
158
        if (false === AppendParentNode::hasParent($parentNode)) {
159
            return false;
160
        }
161
162
        $parentNode = AppendParentNode::getParent($parentNode);
163
164
        if (false === ($parentNode instanceof Array_) || false === AppendParentNode::hasParent($parentNode)) {
165
            return false;
166
        }
167
168
        /** @var Array_ $arrayNode */
169
        $arrayNode = $parentNode;
170
        $parentNode = AppendParentNode::getParent($parentNode);
171
172
        if (false === ($parentNode instanceof Arg)
173
            || null === $functionNode = AppendParentNode::findParent($parentNode)
174
        ) {
175
            return false;
176
        }
177
178
        $functionNode = AppendParentNode::getParent($parentNode);
179
180
        if (false === ($functionNode instanceof FuncCall)) {
181
            return false;
182
        }
183
184
        /** @var FuncCall $functionNode */
185
        if (is_stringable($functionNode->name)) {
186
            $functionName = (string) $functionNode->name;
187
        } else {
188
            return false;
189
        }
190
191
        if ('spl_autoload_register' === $functionName
192
            && array_key_exists(0, $arrayNode->items)
193
            && $arrayItemNode === $arrayNode->items[0]
194
        ) {
195
            $isSpecialFunction = true;
196
197
            return
198
                false === $this->whitelist->isClassWhitelisted($node->value)
199
                && false === $this->reflector->isClassInternal($node->value)
200
            ;
201
        }
202
203
        return false;
204
    }
205
206
    private function prefixStringScalar(String_ $string, bool $isSpecialFunction): Node
207
    {
208
        $stringName = new Name(
209
            preg_replace('/^\\\\(.+)$/', '$1', $string->value),
210
            $string->getAttributes()
211
        );
212
213
        $isConstantNode = $this->isConstantNode($string);
214
215
        // Skip if is already prefixed
216
        if ($this->prefix === $stringName->getFirst()) {
217
            $newStringName = $stringName;
218
        } elseif ($isSpecialFunction) {
219
            $newStringName = FullyQualified::concat($this->prefix, $stringName->toString(), $stringName->getAttributes());
220
        // Check if the class can be prefixed: class not from the global namespace or which the namespace is not
221
        // whitelisted
222
        } elseif (
223
            1 === count($stringName->parts)
224
            || $this->reflector->isClassInternal($stringName->toString())
225
            || (false === $isConstantNode && $this->whitelist->isClassWhitelisted((string) $stringName))
226
            || ($isConstantNode && $this->whitelist->isConstantWhitelisted((string) $stringName))
227
            || $this->whitelist->isNamespaceWhitelisted((string) $stringName)
228
        ) {
229
            $newStringName = $stringName;
230
        } else {
231
            $newStringName = FullyQualified::concat($this->prefix, $stringName->toString(), $stringName->getAttributes());
232
        }
233
234
        return new String_($newStringName->toString(), $string->getAttributes());
235
    }
236
237
    private function isConstantNode(String_ $node): bool
238
    {
239
        $parent = AppendParentNode::getParent($node);
240
241
        if (false === ($parent instanceof Arg)) {
242
            return false;
243
        }
244
245
        /** @var Arg $parent */
246
        $argParent = AppendParentNode::getParent($parent);
247
248
        if (false === ($argParent instanceof FuncCall)) {
249
            return false;
250
        }
251
252
        /* @var FuncCall $argParent */
253
        return 'define' === (string) $argParent->name;
254
    }
255
}
256