Passed
Pull Request — master (#603)
by Théo
02:20
created

SymbolsConfigurationFactory::assertValidPattern()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Humbug\PhpScoper\Configuration;
6
7
use Humbug\PhpScoper\RegexChecker;
8
use Humbug\PhpScoper\Symbol\NamespaceRegistry;
9
use Humbug\PhpScoper\Symbol\SymbolRegistry;
10
use InvalidArgumentException;
11
use function array_key_exists;
12
use function array_keys;
13
use function array_map;
14
use function array_merge;
15
use function array_pop;
16
use function array_values;
17
use function explode;
18
use function get_debug_type;
19
use function gettype;
20
use function implode;
21
use function is_array;
22
use function is_bool;
23
use function is_string;
24
use function ltrim;
25
use function Safe\preg_match as native_preg_match;
26
use function Safe\sprintf;
27
use function Safe\substr;
28
use function str_replace;
29
use function strpos;
30
use function strtolower;
31
use function trim;
32
33
final class SymbolsConfigurationFactory
34
{
35
    private RegexChecker $regexChecker;
36
37
    public function __construct(RegexChecker $regexChecker)
38
    {
39
        $this->regexChecker = $regexChecker;
40
    }
41
42
    public function createSymbolsConfiguration(array $config): SymbolsConfiguration
43
    {
44
        [
45
            $excludedNamespaceNames,
46
            $excludedNamespaceRegexes,
47
        ] = $this->retrieveElements(
48
            $config,
49
            ConfigurationKeys::EXCLUDE_NAMESPACES_KEYWORD,
50
        );
51
52
        $legacyExposedElements = self::retrieveLegacyExposedElements($config);
53
54
        [
55
            $legacyExposedSymbols,
56
            $legacyExposedSymbolsPatterns,
57
            $legacyExposedConstants,
58
            $excludedNamespaceNames,
59
        ] = self::parseLegacyExposedElements($legacyExposedElements, $excludedNamespaceNames);
0 ignored issues
show
Deprecated Code introduced by
The function Humbug\PhpScoper\Configu...LegacyExposedElements() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

59
        ] = /** @scrutinizer ignore-deprecated */ self::parseLegacyExposedElements($legacyExposedElements, $excludedNamespaceNames);
Loading history...
60
61
        $exposeGlobalConstants = self::retrieveExposeGlobalSymbol(
62
            $config,
63
            ConfigurationKeys::EXPOSE_GLOBAL_CONSTANTS_KEYWORD,
64
        );
65
        $exposeGlobalClasses = self::retrieveExposeGlobalSymbol(
66
            $config,
67
            ConfigurationKeys::EXPOSE_GLOBAL_CLASSES_KEYWORD,
68
        );
69
        $exposeGlobalFunctions = self::retrieveExposeGlobalSymbol(
70
            $config,
71
            ConfigurationKeys::EXPOSE_GLOBAL_FUNCTIONS_KEYWORD,
72
        );
73
74
        [$exposedClassNames, $exposedClassRegexes] = $this->retrieveElements(
75
            $config,
76
            ConfigurationKeys::EXPOSE_CLASSES_SYMBOLS_KEYWORD,
77
        );
78
79
        [$exposedFunctionNames, $exposedFunctionRegexes] = $this->retrieveElements(
80
            $config,
81
            ConfigurationKeys::EXPOSE_FUNCTIONS_SYMBOLS_KEYWORD,
82
        );
83
84
        [$exposedConstantNames, $exposedConstantRegexes] = $this->retrieveElements(
85
            $config,
86
            ConfigurationKeys::EXPOSE_CONSTANTS_SYMBOLS_KEYWORD,
87
        );
88
89
        return SymbolsConfiguration::create(
90
            $exposeGlobalConstants,
91
            $exposeGlobalClasses,
92
            $exposeGlobalFunctions,
93
            NamespaceRegistry::create(
94
                $excludedNamespaceNames,
95
                $excludedNamespaceRegexes,
96
            ),
97
            null,
98
            SymbolRegistry::create(
99
                array_merge(
100
                    $exposedClassNames,
101
                    $legacyExposedSymbols,
102
                ),
103
                array_merge(
104
                    $exposedClassRegexes,
105
                    $legacyExposedSymbolsPatterns,
106
                ),
107
            ),
108
            SymbolRegistry::create(
109
                array_merge(
110
                    $exposedFunctionNames,
111
                    $legacyExposedSymbols,
112
                ),
113
                array_merge(
114
                    $exposedFunctionRegexes,
115
                    $legacyExposedSymbolsPatterns,
116
                ),
117
            ),
118
            SymbolRegistry::createForConstants(
119
                array_merge(
120
                    $exposedConstantNames,
121
                    $legacyExposedConstants,
122
                ),
123
                array_merge(
124
                    $exposedConstantRegexes,
125
                    $legacyExposedSymbolsPatterns,
126
                ),
127
            ),
128
            ...self::retrieveAllExcludedSymbols($config),
0 ignored issues
show
Bug introduced by
self::retrieveAllExcludedSymbols($config) is expanded, but the parameter $excludedClassNames of Humbug\PhpScoper\Configu...Configuration::create() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

128
            /** @scrutinizer ignore-type */ ...self::retrieveAllExcludedSymbols($config),
Loading history...
129
        );
130
    }
131
132
    private static function retrieveExposeGlobalSymbol(array $config, string $key): bool
133
    {
134
        if (!array_key_exists($key, $config)) {
135
            return false;
136
        }
137
138
        $value = $config[$key];
139
140
        if (!is_bool($value)) {
141
            throw new InvalidArgumentException(
142
                sprintf(
143
                    'Expected %s to be a boolean, found "%s" instead.',
144
                    $key,
145
                    gettype($value),
146
                ),
147
            );
148
        }
149
150
        return $value;
151
    }
152
153
    /**
154
     * return list<string>
155
     */
156
    private static function retrieveLegacyExposedElements(array $config): array
157
    {
158
        $key = ConfigurationKeys::WHITELIST_KEYWORD;
159
160
        if (!array_key_exists($key, $config)) {
161
            return [];
162
        }
163
164
        $whitelist = $config[$key];
165
166
        if (!is_array($whitelist)) {
167
            throw new InvalidArgumentException(
168
                sprintf(
169
                    'Expected "%s" to be an array of strings, found "%s" instead.',
170
                    $key,
171
                    gettype($whitelist),
172
                ),
173
            );
174
        }
175
176
        foreach ($whitelist as $index => $className) {
177
            if (is_string($className)) {
178
                continue;
179
            }
180
181
            throw new InvalidArgumentException(
182
                sprintf(
183
                    'Expected whitelist to be an array of string, the "%d" element is not.',
184
                    $index,
185
                ),
186
            );
187
        }
188
189
        return array_values($whitelist);
190
    }
191
192
    /**
193
     * return array{string[], string[]}
194
     */
195
    private function retrieveElements(array $config, string $key): array
196
    {
197
        if (!array_key_exists($key, $config)) {
198
            return [[], []];
199
        }
200
201
        $symbolNamesAndRegexes = $config[$key];
202
203
        self::assertIsArrayOfStrings($config[$key], $key);
204
205
        // Store the strings in the keys for avoiding a unique check later on
206
        $names = [];
207
        $regexes = [];
208
209
        foreach ($symbolNamesAndRegexes as $index => $nameOrRegex) {
210
            if (!$this->regexChecker->isRegexLike($nameOrRegex)) {
211
                $names[$nameOrRegex] = null;
212
213
                continue;
214
            }
215
216
            $regex = $nameOrRegex;
217
218
            $this->assertValidRegex($regex, $key, (string) $index);
219
220
            $errorMessage = $this->regexChecker->validateRegex($regex);
221
222
            if (null !== $errorMessage) {
223
                throw new InvalidArgumentException(
224
                    sprintf(
225
                        'Expected "%s" to be an array of valid regexes. The element "%s" with the index "%s" is not: %s.',
226
                        $key,
227
                        $regex,
228
                        $index,
229
                        $errorMessage,
230
                    ),
231
                );
232
            }
233
234
            // Ensure namespace comparisons are always case-insensitive
235
            // TODO: double check that we are not adding it twice or that adding it twice does not break anything
236
            $regex .= 'i';
237
            $regexes[$regex] = null;
238
        }
239
240
        return [
241
            array_keys($names),
242
            array_keys($regexes),
243
        ];
244
    }
245
246
    /**
247
     * @return array{string[], string[], string[]}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string[], string[], string[]} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
248
     */
249
    private static function retrieveAllExcludedSymbols(array $config): array
250
    {
251
        return [
252
            self::retrieveExcludedSymbols($config, ConfigurationKeys::CLASSES_INTERNAL_SYMBOLS_KEYWORD),
253
            self::retrieveExcludedSymbols($config, ConfigurationKeys::FUNCTIONS_INTERNAL_SYMBOLS_KEYWORD),
254
            self::retrieveExcludedSymbols($config, ConfigurationKeys::CONSTANTS_INTERNAL_SYMBOLS_KEYWORD),
255
        ];
256
    }
257
258
    /**
259
     * @deprecated
260
     *
261
     * @param list<string> $elements
0 ignored issues
show
Bug introduced by
The type Humbug\PhpScoper\Configuration\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...
262
     * @param list<string> $excludedNamespaceNames
263
     */
264
    private static function parseLegacyExposedElements(array $elements, array $excludedNamespaceNames): array
265
    {
266
        $exposedSymbols = [];
267
        $exposedConstants = [];
268
        $exposedSymbolsPatterns = [];
269
        $excludedNamespaceNames = array_map('strtolower', $excludedNamespaceNames);
270
271
        foreach ($elements as $element) {
272
            $element = ltrim(trim($element), '\\');
273
274
            self::assertValidElement($element);
0 ignored issues
show
Deprecated Code introduced by
The function Humbug\PhpScoper\Configu...y::assertValidElement() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

274
            /** @scrutinizer ignore-deprecated */ self::assertValidElement($element);
Loading history...
275
276
            if ('\*' === substr($element, -2)) {
277
                $excludedNamespaceNames[] = strtolower(substr($element, 0, -2));
278
            } elseif ('*' === $element) {
279
                $excludedNamespaceNames[] = '';
280
            } elseif (false !== strpos($element, '*')) {
281
                $exposedSymbolsPatterns[] = self::createExposePattern($element);
0 ignored issues
show
Deprecated Code introduced by
The function Humbug\PhpScoper\Configu...::createExposePattern() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

281
                $exposedSymbolsPatterns[] = /** @scrutinizer ignore-deprecated */ self::createExposePattern($element);
Loading history...
282
            } else {
283
                $exposedSymbols[] = strtolower($element);
284
                $exposedConstants[] = self::lowerCaseConstantName($element);
0 ignored issues
show
Deprecated Code introduced by
The function Humbug\PhpScoper\Configu...lowerCaseConstantName() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

284
                $exposedConstants[] = /** @scrutinizer ignore-deprecated */ self::lowerCaseConstantName($element);
Loading history...
285
            }
286
        }
287
288
        return [
289
            $exposedSymbols,
290
            $exposedSymbolsPatterns,
291
            $exposedConstants,
292
            $excludedNamespaceNames,
293
        ];
294
    }
295
296
    /**
297
     * @psalm-assert string[] $value
298
     *
299
     * @param mixed $value
300
     */
301
    private static function assertIsArrayOfStrings($value, string $key): void
302
    {
303
        if (!is_array($value)) {
304
            throw new InvalidArgumentException(
305
                sprintf(
306
                    'Expected "%s" to be an array of strings, found "%s" instead.',
307
                    $key,
308
                    get_debug_type($value),
309
                ),
310
            );
311
        }
312
313
        foreach ($value as $index => $element) {
314
            if (is_string($element)) {
315
                continue;
316
            }
317
318
            throw new InvalidArgumentException(
319
                sprintf(
320
                    'Expected "%s" to be an array of strings, found "%s" for the element with the index "%s".',
321
                    $key,
322
                    get_debug_type($element),
323
                    $index,
324
                ),
325
            );
326
        }
327
    }
328
329
    private function assertValidRegex(string $regex, string $key, string $index): void
330
    {
331
        $errorMessage = $this->regexChecker->validateRegex($regex);
332
333
        if (null !== $errorMessage) {
334
            throw new InvalidArgumentException(
335
                sprintf(
336
                    'Expected "%s" to be an array of valid regexes. The element "%s" with the index "%s" is not: %s.',
337
                    $key,
338
                    $regex,
339
                    $index,
340
                    $errorMessage,
341
                ),
342
            );
343
        }
344
    }
345
346
    /**
347
     * @deprecated
348
     */
349
    private static function assertValidElement(string $element): void
350
    {
351
        if ('' !== $element) {
352
            return;
353
        }
354
355
        throw new InvalidArgumentException(
356
            sprintf(
357
                'Invalid whitelist element "%s": cannot accept an empty string',
358
                $element,
359
            ),
360
        );
361
    }
362
363
    /**
364
     * @deprecated
365
     */
366
    private static function createExposePattern(string $element): string
367
    {
368
        self::assertValidPattern($element);
0 ignored issues
show
Deprecated Code introduced by
The function Humbug\PhpScoper\Configu...y::assertValidPattern() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

368
        /** @scrutinizer ignore-deprecated */ self::assertValidPattern($element);
Loading history...
369
370
        return sprintf(
371
            '/^%s$/u',
372
            str_replace(
373
                '\\',
374
                '\\\\',
375
                str_replace(
376
                    '*',
377
                    '.*',
378
                    $element,
379
                ),
380
            ),
381
        );
382
    }
383
384
    /**
385
     * @deprecated
386
     */
387
    private static function assertValidPattern(string $element): void
388
    {
389
        if (1 !== native_preg_match('/^(([\p{L}_]+\\\\)+)?[\p{L}_]*\*$/u', $element)) {
390
            throw new InvalidArgumentException(
391
                sprintf(
392
                    'Invalid whitelist pattern "%s".',
393
                    $element,
394
                ),
395
            );
396
        }
397
    }
398
399
    /**
400
     * @deprecated
401
     */
402
    private static function lowerCaseConstantName(string $name): string
403
    {
404
        $parts = explode('\\', $name);
405
406
        $lastPart = array_pop($parts);
407
408
        $parts = array_map('strtolower', $parts);
409
410
        $parts[] = $lastPart;
411
412
        return implode('\\', $parts);
413
    }
414
415
    /**
416
     * @return string[]
417
     */
418
    private static function retrieveExcludedSymbols(array $config, string $key): array
419
    {
420
        if (!array_key_exists($key, $config)) {
421
            return [];
422
        }
423
424
        $symbols = $config[$key];
425
426
        self::assertIsArrayOfStrings($symbols, $key);
427
428
        return $symbols;
429
    }
430
}
431