Passed
Pull Request — master (#584)
by Théo
02:04
created

Whitelist::getExposedSymbols()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 3
cts 3
cp 1
crap 1
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;
16
17
use Countable;
18
use Humbug\PhpScoper\Symbol\NamespaceRegistry;
19
use Humbug\PhpScoper\Symbol\SymbolsRegistry;
20
use InvalidArgumentException;
21
use PhpParser\Node\Name\FullyQualified;
22
use function array_key_exists;
23
use function array_keys;
24
use function array_map;
25
use function array_pop;
26
use function array_unique;
27
use function array_values;
28
use function count;
29
use function explode;
30
use function implode;
31
use function ltrim;
32
use function preg_match as native_preg_match;
33
use function Safe\array_flip;
34
use function Safe\sprintf;
35
use function Safe\substr;
36
use function str_replace;
37
use function strpos;
38
use function strtolower;
39
use function trim;
40
41
final class Whitelist implements Countable
42
{
43
    /**
44
     * @var list<string>
0 ignored issues
show
Bug introduced by
The type Humbug\PhpScoper\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
45
     */
46
    private array $originalElements;
47
48
    /**
49
     * @var array<string, mixed>
50
     */
51
    private array $exposedSymbols;
52 15
53
    /**
54
     * @var array<string, mixed>
55
     */
56
    private array $exposedConstants;
57
58 15
    /**
59 15
     * @var list<string>
60 15
     */
61 15
    private array $exposedSymbolsPatterns;
62 15
63
    private bool $exposeGlobalConstants;
64 15
    private bool $exposeGlobalClasses;
65 12
    private bool $exposeGlobalFunctions;
66 2
67
    private array $whitelistedFunctions = [];
68
    private array $whitelistedClasses = [];
69 12
70
    private NamespaceRegistry $excludedNamespaces;
71
72
    /**
73
     * @param string[] $excludedNamespaceRegexes
74
     * @param string[] $excludedNamespaceNames
75
     */
76
    public static function create(
77
        bool $exposeGlobalConstants = false,
78 12
        bool $exposeGlobalClasses = false,
79
        bool $exposeGlobalFunctions = false,
80 12
        array $excludedNamespaceRegexes = [],
81 2
        array $excludedNamespaceNames = [],
82 11
        string ...$exposedElements
83 3
    ): self {
84 9
        $exposedSymbols = [];
85
        $exposedConstants = [];
86
        $exposedSymbolsPatterns = [];
87
        $originalElements = [];
88
        $excludedNamespaceNames = array_map('strtolower', $excludedNamespaceNames);
89
90
        foreach ($exposedElements as $element) {
91
            $element = ltrim(trim($element), '\\');
92
93
            self::assertValidElement($element);
94
95
            $originalElements[] = $element;
96
97
            if ('\*' === substr($element, -2)) {
98
                $excludedNamespaceNames[] = strtolower(substr($element, 0, -2));
99
            } elseif ('*' === $element) {
100 9
                $excludedNamespaceNames[] = '';
101 9
            } elseif (false !== strpos($element, '*')) {
102
                $exposedSymbolsPatterns[] = self::createExposePattern($element);
103
            } else {
104
                $exposedSymbols[] = strtolower($element);
105 15
                $exposedConstants[] = self::lowerCaseConstantName($element);
106 15
            }
107 15
        }
108 15
109 15
        return new self(
110 15
            $exposeGlobalConstants,
111 15
            $exposeGlobalClasses,
112 15
            $exposeGlobalFunctions,
113 15
            array_unique($originalElements),
114
            array_flip($exposedSymbols),
115
            array_flip($exposedConstants),
116
            array_unique($exposedSymbolsPatterns),
117
            array_unique($excludedNamespaceNames),
118
            array_unique($excludedNamespaceRegexes),
119
        );
120
    }
121
122
    /**
123
     * @param list<string>       $originalElements
124
     * @param array<string, int> $exposedSymbols
125
     * @param array<string, int> $exposedConstants
126
     * @param list<string>       $exposedSymbolsPatterns
127
     * @param list<string>       $excludedNamespaceNames
128
     * @param list<string>       $excludedNamespaceRegexes
129
     */
130
    public function __construct(
131
        bool $exposeGlobalConstants,
132
        bool $exposeGlobalClasses,
133
        bool $exposeGlobalFunctions,
134 15
        array $originalElements,
135
        array $exposedSymbols,
136
        array $exposedConstants,
137
        array $exposedSymbolsPatterns,
138
        array $excludedNamespaceNames,
139
        array $excludedNamespaceRegexes
140
    ) {
141
        $this->exposeGlobalConstants = $exposeGlobalConstants;
142
        $this->exposeGlobalClasses = $exposeGlobalClasses;
143
        $this->exposeGlobalFunctions = $exposeGlobalFunctions;
144 15
        $this->originalElements = $originalElements;
0 ignored issues
show
Documentation Bug introduced by
It seems like $originalElements of type array is incompatible with the declared type Humbug\PhpScoper\list of property $originalElements.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
145 15
        $this->exposedSymbols = $exposedSymbols;
146 15
        $this->exposedConstants = $exposedConstants;
147 15
        $this->exposedSymbolsPatterns = $exposedSymbolsPatterns;
0 ignored issues
show
Documentation Bug introduced by
It seems like $exposedSymbolsPatterns of type array is incompatible with the declared type Humbug\PhpScoper\list of property $exposedSymbolsPatterns.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
148 15
        $this->excludedNamespaces = NamespaceRegistry::create(
149 15
            $excludedNamespaceRegexes,
150 15
            $excludedNamespaceNames,
151 15
        );
152
    }
153
154 521
    public function belongsToExcludedNamespace(string $name): bool
155
    {
156 521
        return $this->excludedNamespaces->belongsToRegisteredNamespace($name);
157
    }
158 521
159 41
    public function isExcludedNamespace(string $name): bool
160 37
    {
161
        return $this->excludedNamespaces->isRegisteredNamespace($name);
162
    }
163
164 492
    /**
165
     * @internal
166
     */
167 561
    public function getExcludedNamespaces(): NamespaceRegistry
168
    {
169 561
        return $this->excludedNamespaces;
170
    }
171 561
172
    /**
173
     * @internal
174
     */
175 561
    public function getExposedSymbols(): array
176 41
    {
177 20
        return array_keys($this->exposedSymbols);
178
    }
179
180 21
    /**
181 9
     * @internal
182
     */
183
    public function getExposedConstants(): array
184 17
    {
185 17
        return array_keys($this->exposedConstants);
186
    }
187 17
188 17
    /**
189 2
     * @internal
190
     */
191
    public function getExposedSymbolsPatterns(): array
192
    {
193 15
        return $this->exposedSymbolsPatterns;
194
    }
195
196 529
    /**
197
     * @internal
198
     */
199
    public function exposeGlobalFunctions(): bool
200
    {
201
        return $this->exposeGlobalFunctions;
202 551
    }
203
204 551
    public function isExposedFunctionFromGlobalNamespace(string $functionName): bool
205
    {
206
        return $this->exposeGlobalFunctions && !strpos($functionName, '\\');
207 109
    }
208
209 109
    public function recordWhitelistedFunction(FullyQualified $original, FullyQualified $alias): void
210
    {
211
        $this->whitelistedFunctions[(string) $original] = [(string) $original, (string) $alias];
212 24
    }
213
214 24
    public function getRecordedWhitelistedFunctions(): array
215
    {
216
        return array_values($this->whitelistedFunctions);
217 555
    }
218
219 555
    /**
220
     * @internal
221
     */
222
    public function exposeGlobalConstants(): bool
223
    {
224
        return $this->exposeGlobalConstants;
225 551
    }
226
227 551
    public function isExposedConstantFromGlobalNamespace(string $constantName): bool
228
    {
229
        return $this->exposeGlobalConstants && !strpos($constantName, '\\');
230 81
    }
231
232 81
    /**
233
     * @internal
234
     */
235
    public function exposeGlobalClasses(): bool
236
    {
237
        return $this->exposeGlobalClasses;
238 6
    }
239
240 6
    public function isExposedClassFromGlobalNamespace(string $className): bool
241
    {
242
        return $this->exposeGlobalClasses && !strpos($className, '\\');
243 324
    }
244
245 324
    public function recordWhitelistedClass(FullyQualified $original, FullyQualified $alias): void
246
    {
247
        $this->whitelistedClasses[(string) $original] = [(string) $original, (string) $alias];
248 82
    }
249
250 82
    public function getRecordedWhitelistedClasses(): array
251
    {
252
        return array_values($this->whitelistedClasses);
253 555
    }
254
255 555
    /**
256
     * Tells if a given symbol is exposed. Note however that it does not account for when:
257
     *
258
     * - The symbol belongs to the global namespace and the symbols of the global namespace of this type are exposed
259
     * - Belongs to an excluded namespace
260
     *
261
     * @param bool $constant Unlike other symbols, constants _can_ be case insensitive but 99% are not so we leave out
262
     *                       the case where they are not case sensitive.
263
     */
264
    public function isSymbolExposed(string $name, bool $constant = false): bool
265
    {
266
        if (!$constant && array_key_exists(strtolower($name), $this->exposedSymbols)) {
267 442
            return true;
268
        }
269 442
270 94
        if ($constant && array_key_exists(self::lowerCaseConstantName($name), $this->exposedConstants)) {
271
            return true;
272
        }
273 400
274 24
        foreach ($this->exposedSymbolsPatterns as $pattern) {
275
            $pattern = !$constant ? $pattern.'i' : $pattern;
276
277 385
            if (1 === native_preg_match($pattern, $name)) {
278 18
                return true;
279
            }
280 18
        }
281 11
282
        return false;
283
    }
284
285 378
    /**
286
     * @return string[]
287
     */
288
    public function toArray(): array
289
    {
290
        return $this->originalElements;
291
    }
292
293
    public function count(): int
294
    {
295
        return count($this->whitelistedFunctions) + count($this->whitelistedClasses);
296
    }
297
298
    public function registerFromRegistry(SymbolsRegistry $registry): void
299
    {
300
        foreach ($registry->getRecordedClasses() as [$original, $alias]) {
301
            $this->recordWhitelistedClass(
302
                new FullyQualified($original),
303 554
                new FullyQualified($alias),
304
            );
305 554
        }
306
307
        foreach ($registry->getRecordedFunctions() as [$original, $alias]) {
308
            $this->recordWhitelistedFunction(
309
                new FullyQualified($original),
310
                new FullyQualified($alias),
311
            );
312
        }
313
    }
314
315
    private static function assertValidElement(string $element): void
316
    {
317
        if ('' !== $element) {
318
            return;
319
        }
320 124
321
        throw new InvalidArgumentException(
322 124
            sprintf(
323
                'Invalid whitelist element "%s": cannot accept an empty string',
324 124
                $element,
325
            ),
326 124
        );
327
    }
328 124
329
    private static function createExposePattern(string $element): string
330 124
    {
331
        self::assertValidPattern($element);
332
333 521
        return sprintf(
334
            '/^%s$/u',
335 521
            str_replace(
336
                '\\',
337 521
                '\\\\',
338 23
                str_replace(
339
                    '*',
340
                    '.*',
341 521
                    $element,
342
                ),
343 521
            ),
344
        );
345 521
    }
346
347
    private static function assertValidPattern(string $element): void
348
    {
349
        if (1 !== native_preg_match('/^(([\p{L}_]+\\\\)+)?[\p{L}_]*\*$/u', $element)) {
350
            throw new InvalidArgumentException(sprintf('Invalid whitelist pattern "%s".', $element));
351
        }
352
    }
353
354
    /**
355
     * Transforms the constant FQ name "Acme\Foo\X" to "acme\foo\X" since the namespace remains case insensitive for
356
     * constants regardless of whether or not constants actually are case insensitive.
357
     */
358
    private static function lowerCaseConstantName(string $name): string
359
    {
360
        $parts = explode('\\', $name);
361
362
        $lastPart = array_pop($parts);
363
364
        $parts = array_map('strtolower', $parts);
365
366
        $parts[] = $lastPart;
367
368
        return implode('\\', $parts);
369
    }
370
}
371