Passed
Pull Request — master (#591)
by Théo
02:01
created

lowerCaseConstantName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 11
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 InvalidArgumentException;
10
use function array_key_exists;
11
use function array_keys;
12
use function array_map;
13
use function array_pop;
14
use function array_values;
15
use function explode;
16
use function gettype;
17
use function implode;
18
use function is_array;
19
use function is_bool;
20
use function is_string;
21
use function ltrim;
22
use function Safe\preg_match as native_preg_match;
23
use function Safe\sprintf;
24
use function Safe\substr;
25
use function str_replace;
26
use function strpos;
27
use function strtolower;
28
use function trim;
29
30
final class ConfigurationSymbolsConfigurationFactory
31
{
32
    private RegexChecker $regexChecker;
33
34
    public function __construct(RegexChecker $regexChecker)
35
    {
36
        $this->regexChecker = $regexChecker;
37
    }
38
39
    public function createSymbolsConfiguration(array $config): SymbolsConfiguration
40
    {
41
        [
42
            $excludedNamespaceRegexes,
43
            $excludedNamespaceNames,
44
        ] = $this->retrieveExcludedNamespaces($config);
45
46
        $legacyExposedElements = self::retrieveLegacyExposedElements($config);
47
48
        [
49
            $legacyExposedSymbols,
50
            $legacyExposedSymbolsPatterns,
51
            $legacyExposedConstants,
52
            $excludedNamespaceNames,
53
        ] = self::parseLegacyExposedElements($legacyExposedElements, $excludedNamespaceNames);
54
55
        $exposeGlobalConstants = self::retrieveExposeGlobalSymbol(
56
            $config,
57
            ConfigurationKeys::EXPOSE_GLOBAL_CONSTANTS_KEYWORD,
58
        );
59
        $exposeGlobalClasses = self::retrieveExposeGlobalSymbol(
60
            $config,
61
            ConfigurationKeys::EXPOSE_GLOBAL_CLASSES_KEYWORD,
62
        );
63
        $exposeGlobalFunctions = self::retrieveExposeGlobalSymbol(
64
            $config,
65
            ConfigurationKeys::EXPOSE_GLOBAL_FUNCTIONS_KEYWORD,
66
        );
67
68
        return SymbolsConfiguration::create(
69
            $exposeGlobalConstants,
70
            $exposeGlobalClasses,
71
            $exposeGlobalFunctions,
72
            NamespaceRegistry::create(
73
                $excludedNamespaceRegexes,
74
                $excludedNamespaceNames,
75
            ),
76
            null,
77
            $legacyExposedSymbols,
78
            $legacyExposedSymbolsPatterns,
79
            $legacyExposedSymbols,
80
            $legacyExposedSymbolsPatterns,
81
            $legacyExposedConstants,
82
            $legacyExposedSymbolsPatterns,
83
        );
84
    }
85
86
    /**
87
     * @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...
88
     */
89
    private function retrieveExcludedNamespaces(array $config): array
90
    {
91
        $key = ConfigurationKeys::EXCLUDE_NAMESPACES_KEYWORD;
92
93
        if (!array_key_exists($key, $config)) {
94
            return [[], []];
95
        }
96
97
        $regexesAndNamespaceNames = $config[$key];
98
99
        if (!is_array($regexesAndNamespaceNames)) {
100
            throw new InvalidArgumentException(
101
                sprintf(
102
                    'Expected "%s" to be an array of strings, got "%s" instead.',
103
                    $key,
104
                    gettype($regexesAndNamespaceNames),
105
                ),
106
            );
107
        }
108
109
        // Store the strings in the keys for avoiding a unique check later on
110
        $regexes = [];
111
        $namespaceNames = [];
112
113
        foreach ($regexesAndNamespaceNames as $index => $regexOrNamespaceName) {
114
            if (!is_string($regexOrNamespaceName)) {
115
                throw new InvalidArgumentException(
116
                    sprintf(
117
                        'Expected "%s" to be an array of strings, got "%s" for the element with the index "%s".',
118
                        $key,
119
                        gettype($regexOrNamespaceName),
120
                        $index,
121
                    ),
122
                );
123
            }
124
125
            if (!$this->regexChecker->isRegexLike($regexOrNamespaceName)) {
126
                $namespaceNames[$regexOrNamespaceName] = null;
127
128
                continue;
129
            }
130
131
            $excludeNamespaceRegex = $regexOrNamespaceName;
132
133
            $errorMessage = $this->regexChecker->validateRegex($excludeNamespaceRegex);
134
135
            if (null !== $errorMessage) {
136
                throw new InvalidArgumentException(
137
                    sprintf(
138
                        'Expected "%s" to be an array of valid regexes. The element "%s" with the index "%s" is not: %s.',
139
                        $key,
140
                        $excludeNamespaceRegex,
141
                        $index,
142
                        $errorMessage,
143
                    ),
144
                );
145
            }
146
147
            // Ensure namespace comparisons are always case-insensitive
148
            // TODO: double check that we are not adding it twice or that adding it twice does not break anything
149
            $excludeNamespaceRegex .= 'i';
150
            $regexes[$excludeNamespaceRegex] = null;
151
        }
152
153
        return [
154
            array_keys($regexes),
155
            array_keys($namespaceNames),
156
        ];
157
    }
158
159
    /**
160
     * return list<string>
161
     */
162
    private static function retrieveLegacyExposedElements(array $config): array
163
    {
164
        $key = ConfigurationKeys::WHITELIST_KEYWORD;
165
166
        if (!array_key_exists($key, $config)) {
167
            return [];
168
        }
169
170
        $whitelist = $config[$key];
171
172
        if (!is_array($whitelist)) {
173
            throw new InvalidArgumentException(
174
                sprintf(
175
                    'Expected "%s" to be an array of strings, found "%s" instead.',
176
                    $key,
177
                    gettype($whitelist),
178
                ),
179
            );
180
        }
181
182
        foreach ($whitelist as $index => $className) {
183
            if (is_string($className)) {
184
                continue;
185
            }
186
187
            throw new InvalidArgumentException(
188
                sprintf(
189
                    'Expected whitelist to be an array of string, the "%d" element is not.',
190
                    $index,
191
                ),
192
            );
193
        }
194
195
        return array_values($whitelist);
196
    }
197
198
    /**
199
     * @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...
200
     * @param list<string> $excludedNamespaceNames
201
     */
202
    private static function parseLegacyExposedElements(array $elements, array $excludedNamespaceNames): array
203
    {
204
        $exposedSymbols = [];
205
        $exposedConstants = [];
206
        $exposedSymbolsPatterns = [];
207
        $excludedNamespaceNames = array_map('strtolower', $excludedNamespaceNames);
208
209
        foreach ($elements as $element) {
210
            $element = ltrim(trim($element), '\\');
211
212
            self::assertValidElement($element);
213
214
            if ('\*' === substr($element, -2)) {
215
                $excludedNamespaceNames[] = strtolower(substr($element, 0, -2));
216
            } elseif ('*' === $element) {
217
                $excludedNamespaceNames[] = '';
218
            } elseif (false !== strpos($element, '*')) {
219
                $exposedSymbolsPatterns[] = self::createExposePattern($element);
220
            } else {
221
                $exposedSymbols[] = strtolower($element);
222
                $exposedConstants[] = self::lowerCaseConstantName($element);
223
            }
224
        }
225
226
        return [
227
            $exposedSymbols,
228
            $exposedSymbolsPatterns,
229
            $exposedConstants,
230
            $excludedNamespaceNames,
231
        ];
232
    }
233
234
    private static function assertValidElement(string $element): void
235
    {
236
        if ('' !== $element) {
237
            return;
238
        }
239
240
        throw new InvalidArgumentException(
241
            sprintf(
242
                'Invalid whitelist element "%s": cannot accept an empty string',
243
                $element,
244
            ),
245
        );
246
    }
247
248
    private static function createExposePattern(string $element): string
249
    {
250
        self::assertValidPattern($element);
251
252
        return sprintf(
253
            '/^%s$/u',
254
            str_replace(
255
                '\\',
256
                '\\\\',
257
                str_replace(
258
                    '*',
259
                    '.*',
260
                    $element,
261
                ),
262
            ),
263
        );
264
    }
265
266
    private static function assertValidPattern(string $element): void
267
    {
268
        if (1 !== native_preg_match('/^(([\p{L}_]+\\\\)+)?[\p{L}_]*\*$/u', $element)) {
269
            throw new InvalidArgumentException(
270
                sprintf(
271
                    'Invalid whitelist pattern "%s".',
272
                    $element,
273
                ),
274
            );
275
        }
276
    }
277
278
    /**
279
     * Transforms the constant FQ name "Acme\Foo\X" to "acme\foo\X" since the namespace remains case insensitive for
280
     * constants regardless of whether or not constants actually are case insensitive.
281
     */
282
    private static function lowerCaseConstantName(string $name): string
283
    {
284
        $parts = explode('\\', $name);
285
286
        $lastPart = array_pop($parts);
287
288
        $parts = array_map('strtolower', $parts);
289
290
        $parts[] = $lastPart;
291
292
        return implode('\\', $parts);
293
    }
294
295
    private static function retrieveExposeGlobalSymbol(array $config, string $key): bool
296
    {
297
        if (!array_key_exists($key, $config)) {
298
            return false;
299
        }
300
301
        $value = $config[$key];
302
303
        if (!is_bool($value)) {
304
            throw new InvalidArgumentException(
305
                sprintf(
306
                    'Expected %s to be a boolean, found "%s" instead.',
307
                    $key,
308
                    gettype($value),
309
                ),
310
            );
311
        }
312
313
        return $value;
314
    }
315
}
316