Passed
Pull Request — master (#691)
by Théo
02:43
created

src/Configuration/SymbolsConfigurationFactory.php (3 issues)

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\Configuration;
16
17
use Humbug\PhpScoper\Symbol\NamespaceRegistry;
18
use Humbug\PhpScoper\Symbol\SymbolRegistry;
19
use InvalidArgumentException;
20
use function array_key_exists;
21
use function array_keys;
22
use function array_map;
23
use function array_merge;
24
use function array_pop;
25
use function array_values;
26
use function explode;
27
use function get_debug_type;
28
use function gettype;
29
use function implode;
30
use function is_array;
31
use function is_bool;
32
use function is_string;
33
use function ltrim;
34
use function Safe\preg_match as native_preg_match;
35
use function Safe\sprintf;
36
use function Safe\substr;
37
use function str_replace;
38
use function strpos;
39
use function strtolower;
40
use function trim;
41
42
final class SymbolsConfigurationFactory
43
{
44
    private RegexChecker $regexChecker;
45
46
    public function __construct(RegexChecker $regexChecker)
47
    {
48
        $this->regexChecker = $regexChecker;
49
    }
50
51
    public function createSymbolsConfiguration(array $config): SymbolsConfiguration
52
    {
53
        [
54
            $excludedNamespaceNames,
55
            $excludedNamespaceRegexes,
56
        ] = $this->retrieveElements(
57
            $config,
58
            ConfigurationKeys::EXCLUDE_NAMESPACES_KEYWORD,
59
        );
60
61
        [
62
            $exposedNamespaceNames,
63
            $exposedNamespaceRegexes,
64
        ] = $this->retrieveElements(
65
            $config,
66
            ConfigurationKeys::EXPOSE_NAMESPACES_KEYWORD,
67
        );
68
69
        $legacyExposedElements = self::retrieveLegacyExposedElements($config);
70
71
        [
72
            $legacyExposedSymbols,
73
            $legacyExposedSymbolsPatterns,
74
            $legacyExposedConstants,
75
            $excludedNamespaceNames,
76
        ] = self::parseLegacyExposedElements($legacyExposedElements, $excludedNamespaceNames);
77
78
        $exposeGlobalConstants = self::retrieveExposeGlobalSymbol(
79
            $config,
80
            ConfigurationKeys::EXPOSE_GLOBAL_CONSTANTS_KEYWORD,
81
        );
82
        $exposeGlobalClasses = self::retrieveExposeGlobalSymbol(
83
            $config,
84
            ConfigurationKeys::EXPOSE_GLOBAL_CLASSES_KEYWORD,
85
        );
86
        $exposeGlobalFunctions = self::retrieveExposeGlobalSymbol(
87
            $config,
88
            ConfigurationKeys::EXPOSE_GLOBAL_FUNCTIONS_KEYWORD,
89
        );
90
91
        [$exposedClassNames, $exposedClassRegexes] = $this->retrieveElements(
92
            $config,
93
            ConfigurationKeys::EXPOSE_CLASSES_SYMBOLS_KEYWORD,
94
        );
95
96
        [$exposedFunctionNames, $exposedFunctionRegexes] = $this->retrieveElements(
97
            $config,
98
            ConfigurationKeys::EXPOSE_FUNCTIONS_SYMBOLS_KEYWORD,
99
        );
100
101
        [$exposedConstantNames, $exposedConstantRegexes] = $this->retrieveElements(
102
            $config,
103
            ConfigurationKeys::EXPOSE_CONSTANTS_SYMBOLS_KEYWORD,
104
        );
105
106
        $excludedClasses = SymbolRegistry::create(
107
            ...$this->retrieveElements(
108
                $config,
109
                ConfigurationKeys::CLASSES_INTERNAL_SYMBOLS_KEYWORD,
110
            ),
111
        );
112
113
        $excludedFunctions = SymbolRegistry::create(
114
            ...$this->retrieveElements(
115
                $config,
116
                ConfigurationKeys::FUNCTIONS_INTERNAL_SYMBOLS_KEYWORD,
117
            ),
118
        );
119
120
        $excludedConstants = SymbolRegistry::createForConstants(
121
            ...$this->retrieveElements(
122
                $config,
123
                ConfigurationKeys::CONSTANTS_INTERNAL_SYMBOLS_KEYWORD,
124
            ),
125
        );
126
127
        return SymbolsConfiguration::create(
128
            $exposeGlobalConstants,
129
            $exposeGlobalClasses,
130
            $exposeGlobalFunctions,
131
            NamespaceRegistry::create(
132
                $excludedNamespaceNames,
133
                $excludedNamespaceRegexes,
134
            ),
135
            NamespaceRegistry::create(
136
                $exposedNamespaceNames,
137
                $exposedNamespaceRegexes,
138
            ),
139
            SymbolRegistry::create(
140
                array_merge(
141
                    $exposedClassNames,
142
                    $legacyExposedSymbols,
143
                ),
144
                array_merge(
145
                    $exposedClassRegexes,
146
                    $legacyExposedSymbolsPatterns,
147
                ),
148
            ),
149
            SymbolRegistry::create(
150
                array_merge(
151
                    $exposedFunctionNames,
152
                    $legacyExposedSymbols,
153
                ),
154
                array_merge(
155
                    $exposedFunctionRegexes,
156
                    $legacyExposedSymbolsPatterns,
157
                ),
158
            ),
159
            SymbolRegistry::createForConstants(
160
                array_merge(
161
                    $exposedConstantNames,
162
                    $legacyExposedConstants,
163
                ),
164
                array_merge(
165
                    $exposedConstantRegexes,
166
                    $legacyExposedSymbolsPatterns,
167
                ),
168
            ),
169
            $excludedClasses,
170
            $excludedFunctions,
171
            $excludedConstants,
172
        );
173
    }
174
175
    private static function retrieveExposeGlobalSymbol(array $config, string $key): bool
176
    {
177
        if (!array_key_exists($key, $config)) {
178
            return false;
179
        }
180
181
        $value = $config[$key];
182
183
        if (!is_bool($value)) {
184
            throw new InvalidArgumentException(
185
                sprintf(
186
                    'Expected %s to be a boolean, found "%s" instead.',
187
                    $key,
188
                    gettype($value),
189
                ),
190
            );
191
        }
192
193
        return $value;
194
    }
195
196
    /**
197
     * @return list<string>
198
     */
199
    private static function retrieveLegacyExposedElements(array $config): array
200
    {
201
        $key = ConfigurationKeys::WHITELIST_KEYWORD;
202
203
        if (!array_key_exists($key, $config)) {
204
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type Humbug\PhpScoper\Configuration\list.
Loading history...
205
        }
206
207
        $whitelist = $config[$key];
208
209
        if (!is_array($whitelist)) {
210
            throw new InvalidArgumentException(
211
                sprintf(
212
                    'Expected "%s" to be an array of strings, found "%s" instead.',
213
                    $key,
214
                    gettype($whitelist),
215
                ),
216
            );
217
        }
218
219
        foreach ($whitelist as $index => $className) {
220
            if (is_string($className)) {
221
                continue;
222
            }
223
224
            throw new InvalidArgumentException(
225
                sprintf(
226
                    'Expected whitelist to be an array of string, the "%d" element is not.',
227
                    $index,
228
                ),
229
            );
230
        }
231
232
        return array_values($whitelist);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_values($whitelist) returns the type array which is incompatible with the documented return type Humbug\PhpScoper\Configuration\list.
Loading history...
233
    }
234
235
    /**
236
     * @return array{string[], string[]}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string[], string[]} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
237
     */
238
    private function retrieveElements(array $config, string $key): array
239
    {
240
        if (!array_key_exists($key, $config)) {
241
            return [[], []];
242
        }
243
244
        $symbolNamesAndRegexes = $config[$key];
245
246
        self::assertIsArrayOfStrings($config[$key], $key);
247
248
        // Store the strings in the keys for avoiding a unique check later on
249
        $names = [];
250
        $regexes = [];
251
252
        foreach ($symbolNamesAndRegexes as $index => $nameOrRegex) {
253
            if (!$this->regexChecker->isRegexLike($nameOrRegex)) {
254
                $names[$nameOrRegex] = null;
255
256
                continue;
257
            }
258
259
            $regex = $nameOrRegex;
260
261
            $this->assertValidRegex($regex, $key, (string) $index);
262
263
            $errorMessage = $this->regexChecker->validateRegex($regex);
264
265
            if (null !== $errorMessage) {
266
                throw new InvalidArgumentException(
267
                    sprintf(
268
                        'Expected "%s" to be an array of valid regexes. The element "%s" with the index "%s" is not: %s.',
269
                        $key,
270
                        $regex,
271
                        $index,
272
                        $errorMessage,
273
                    ),
274
                );
275
            }
276
277
            // Ensure namespace comparisons are always case-insensitive
278
            // TODO: double check that we are not adding it twice or that adding it twice does not break anything
279
            $regex .= 'i';
280
            $regexes[$regex] = null;
281
        }
282
283
        return [
284
            array_keys($names),
285
            array_keys($regexes),
286
        ];
287
    }
288
289
    /**
290
     * @deprecated
291
     *
292
     * @param list<string> $elements
293
     * @param list<string> $excludedNamespaceNames
294
     */
295
    private static function parseLegacyExposedElements(array $elements, array $excludedNamespaceNames): array
296
    {
297
        $exposedSymbols = [];
298
        $exposedConstants = [];
299
        $exposedSymbolsPatterns = [];
300
        $excludedNamespaceNames = array_map('strtolower', $excludedNamespaceNames);
301
302
        foreach ($elements as $element) {
303
            $element = ltrim(trim($element), '\\');
304
305
            self::assertValidElement($element);
306
307
            if ('\*' === substr($element, -2)) {
308
                $excludedNamespaceNames[] = strtolower(substr($element, 0, -2));
309
            } elseif ('*' === $element) {
310
                $excludedNamespaceNames[] = '';
311
            } elseif (false !== strpos($element, '*')) {
312
                $exposedSymbolsPatterns[] = self::createExposePattern($element);
313
            } else {
314
                $exposedSymbols[] = strtolower($element);
315
                $exposedConstants[] = self::lowerCaseConstantName($element);
316
            }
317
        }
318
319
        return [
320
            $exposedSymbols,
321
            $exposedSymbolsPatterns,
322
            $exposedConstants,
323
            $excludedNamespaceNames,
324
        ];
325
    }
326
327
    /**
328
     * @psalm-assert string[] $value
329
     *
330
     * @param mixed $value
331
     */
332
    private static function assertIsArrayOfStrings($value, string $key): void
333
    {
334
        if (!is_array($value)) {
335
            throw new InvalidArgumentException(
336
                sprintf(
337
                    'Expected "%s" to be an array of strings, found "%s" instead.',
338
                    $key,
339
                    get_debug_type($value),
340
                ),
341
            );
342
        }
343
344
        foreach ($value as $index => $element) {
345
            if (is_string($element)) {
346
                continue;
347
            }
348
349
            throw new InvalidArgumentException(
350
                sprintf(
351
                    'Expected "%s" to be an array of strings, found "%s" for the element with the index "%s".',
352
                    $key,
353
                    get_debug_type($element),
354
                    $index,
355
                ),
356
            );
357
        }
358
    }
359
360
    private function assertValidRegex(string $regex, string $key, string $index): void
361
    {
362
        $errorMessage = $this->regexChecker->validateRegex($regex);
363
364
        if (null !== $errorMessage) {
365
            throw new InvalidArgumentException(
366
                sprintf(
367
                    'Expected "%s" to be an array of valid regexes. The element "%s" with the index "%s" is not: %s.',
368
                    $key,
369
                    $regex,
370
                    $index,
371
                    $errorMessage,
372
                ),
373
            );
374
        }
375
    }
376
377
    /**
378
     * @deprecated
379
     */
380
    private static function assertValidElement(string $element): void
381
    {
382
        if ('' !== $element) {
383
            return;
384
        }
385
386
        throw new InvalidArgumentException(
387
            sprintf(
388
                'Invalid whitelist element "%s": cannot accept an empty string',
389
                $element,
390
            ),
391
        );
392
    }
393
394
    /**
395
     * @deprecated
396
     */
397
    private static function createExposePattern(string $element): string
398
    {
399
        self::assertValidPattern($element);
400
401
        return sprintf(
402
            '/^%s$/u',
403
            str_replace(
404
                '\\',
405
                '\\\\',
406
                str_replace(
407
                    '*',
408
                    '.*',
409
                    $element,
410
                ),
411
            ),
412
        );
413
    }
414
415
    /**
416
     * @deprecated
417
     */
418
    private static function assertValidPattern(string $element): void
419
    {
420
        if (1 !== native_preg_match('/^(([\p{L}_]+\\\\)+)?[\p{L}_]*\*$/u', $element)) {
421
            throw new InvalidArgumentException(
422
                sprintf(
423
                    'Invalid whitelist pattern "%s".',
424
                    $element,
425
                ),
426
            );
427
        }
428
    }
429
430
    /**
431
     * @deprecated
432
     */
433
    private static function lowerCaseConstantName(string $name): string
434
    {
435
        $parts = explode('\\', $name);
436
437
        $lastPart = array_pop($parts);
438
439
        $parts = array_map('strtolower', $parts);
440
441
        $parts[] = $lastPart;
442
443
        return implode('\\', $parts);
444
    }
445
}
446