humbug /
php-scoper
| 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
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
|
|||
| 233 | } |
||
| 234 | |||
| 235 | /** |
||
| 236 | * @return array{string[], string[]} |
||
|
0 ignored issues
–
show
|
|||
| 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 |