From ea4c07de1cf10b87696617e7ba646492334b84c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Wed, 1 Nov 2023 20:24:50 +0100 Subject: [PATCH 1/3] fix: Various fixes --- docs/configuration.md | 38 +++- scoper.inc.php | 83 +++++---- src/Configuration/ConfigurationFactory.php | 6 +- src/Console/Command/InspectCommand.php | 9 +- src/Symbol/Reflector.php | 170 ++++++++++++++++++ .../Reflector/PhpStormStubsReflectorTest.php | 3 + 6 files changed, 260 insertions(+), 49 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 4c75560aa..d8eba4fd2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -207,9 +207,43 @@ return [ This enriches the list of Symbols PHP-Scoper's Reflector considers as "internal", i.e. PHP engine or extension symbols. Such symbols will be left completely -untouched. +untouched.* -This feature should be use very carefully as it can easily break the Composer +*: There is _one_ exception, which is declarations of functions. If you have the function +`trigger_deprecation` excluded, then any usage of it in the code will be left alone: + +```php +use function trigger_deprecation; // Will not be turned into Prefix\trigger_deprecation +``` + +However, PHP-Scoper may come across its declaration: + +```php +// global namespace! + +if (!function_exists('trigger_deprecation')) { + function trigger_deprecation() {} +} +``` + +Then it will be scoped into: + +```php +namespace Prefix; + +if (!function_exists('Prefix\trigger_deprecation')) { + function trigger_deprecation() {} +} +``` + +Indeed the namespace _needs_ to be added in order to not break autoloading, in which +case wrapping the function declaration into a non-namespace could work, but is tricky +(so not implemented so far, PoC for supporting it are welcomed) hence was not attempted. + +So if left alone, this will break any piece of code that relied on `\trigger_deprecation`, +which is why PHP-Scoper will still add an alias for it, as if it was an exposed function. + +**WARNING**: This exclusion feature should be use very carefully as it can easily break the Composer auto-loading. Indeed, if you have the following package: ```json diff --git a/scoper.inc.php b/scoper.inc.php index e99996fd1..3c2d38c72 100644 --- a/scoper.inc.php +++ b/scoper.inc.php @@ -13,33 +13,35 @@ */ $jetBrainStubs = (static function (): array { + $packageDir = __DIR__.'/vendor/jetbrains/phpstorm-stubs'; $files = []; + $ignoredDirectories = [ + $packageDir.'/tests', + $packageDir.'/meta', + ]; - foreach (new DirectoryIterator(__DIR__.'/vendor/jetbrains/phpstorm-stubs') as $directoryInfo) { - if ($directoryInfo->isDot()) { - continue; - } - - if (false === $directoryInfo->isDir()) { - continue; - } - - if (in_array($directoryInfo->getBasename(), ['tests', 'meta'], true)) { - continue; - } - - foreach (new DirectoryIterator($directoryInfo->getPathName()) as $fileInfo) { - if ($fileInfo->isDot()) { + $collectFiles = static function (RecursiveIteratorIterator $iterator) use (&$collectFiles, &$files, $ignoredDirectories): void { + foreach ($iterator as $fileInfo) { + /** @var SplFileInfo $fileInfo */ + if (str_starts_with($fileInfo->getFilename(), '.') + || $fileInfo->isDir() + || !$fileInfo->isReadable() + || 'php' !== $fileInfo->getExtension() + ) { continue; } - if (1 !== preg_match('/\.php$/', $fileInfo->getBasename())) { - continue; + foreach ($ignoredDirectories as $ignoredDirectory) { + if (str_starts_with($fileInfo->getPathname(), $ignoredDirectory)) { + continue 2; + } } $files[] = $fileInfo->getPathName(); } - } + }; + + $collectFiles(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($packageDir))); return $files; })(); @@ -50,37 +52,32 @@ 'exclude-classes' => [ 'Isolated\Symfony\Component\Finder\Finder', ], + 'exclude-functions' => [ + // symfony/deprecation-contracts + 'trigger_deprecation', + + // nikic/php-parser + // https://github.com/nikic/PHP-Parser/issues/957 + 'assertArgs', + 'ensureDirExists', + 'execCmd', + 'formatErrorMessage', + 'magicSplit', + 'parseArgs', + 'preprocessGrammar', + 'regex', + 'removeTrailingWhitespace', + 'resolveMacros', + 'resolveNodes', + 'resolveStackAccess', + 'showHelp', + ], 'exclude-constants' => [ // Symfony global constants '/^SYMFONY\_[\p{L}_]+$/', ], 'exclude-files' => $jetBrainStubs, 'patchers' => [ - // - // PHPStorm stub map: leave it unchanged - // - static function (string $filePath, string $prefix, string $contents): string { - if ('vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php' !== $filePath) { - return $contents; - } - - return str_replace( - [ - $prefix.'\\\\', - $prefix.'\\', - 'namespace JetBrains\PHPStormStub;', - ], - [ - '', - '', - sprintf( - 'namespace %s\JetBrains\PHPStormStub;', - $prefix, - ), - ], - $contents, - ); - }, // // Reflector: leave the registered internal symbols unchanged // diff --git a/src/Configuration/ConfigurationFactory.php b/src/Configuration/ConfigurationFactory.php index 10dbdf812..983a8c8ee 100644 --- a/src/Configuration/ConfigurationFactory.php +++ b/src/Configuration/ConfigurationFactory.php @@ -56,8 +56,8 @@ final class ConfigurationFactory public const DEFAULT_FILE_NAME = 'scoper.inc.php'; public function __construct( - private readonly Filesystem $fileSystem, - private readonly SymbolsConfigurationFactory $configurationWhitelistFactory, + private readonly Filesystem $fileSystem, + private readonly SymbolsConfigurationFactory $symbolsConfigurationFactory, ) { } @@ -91,7 +91,7 @@ public function create(?string $path = null, array $paths = []): Configuration array_unshift($patchers, new SymfonyParentTraitPatcher()); array_unshift($patchers, new ComposerPatcher()); - $symbolsConfiguration = $this->configurationWhitelistFactory->createSymbolsConfiguration($config); + $symbolsConfiguration = $this->symbolsConfigurationFactory->createSymbolsConfiguration($config); $finders = self::retrieveFinders($config); $filesFromPaths = self::retrieveFilesFromPaths($paths); diff --git a/src/Console/Command/InspectCommand.php b/src/Console/Command/InspectCommand.php index 562a358d8..12f30c7cd 100644 --- a/src/Console/Command/InspectCommand.php +++ b/src/Console/Command/InspectCommand.php @@ -34,6 +34,7 @@ use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; use function array_key_exists; +use function file_exists; use function Safe\getcwd; use function sprintf; use const DIRECTORY_SEPARATOR; @@ -157,7 +158,13 @@ private function getConfigFilePath(IO $io, string $cwd): ?string { $configFilePath = (string) $io->getOption(self::CONFIG_FILE_OPT)->asNullableString(); - return '' === $configFilePath ? null : $this->canonicalizePath($configFilePath, $cwd); + if ('' !== $configFilePath) { + return $this->canonicalizePath($configFilePath, $cwd); + } + + $defaultConfigFilePath = $this->canonicalizePath(ConfigurationFactory::DEFAULT_FILE_NAME, $cwd); + + return file_exists($defaultConfigFilePath) ? $defaultConfigFilePath : null; } /** diff --git a/src/Symbol/Reflector.php b/src/Symbol/Reflector.php index 5bc2fc73b..182e76452 100644 --- a/src/Symbol/Reflector.php +++ b/src/Symbol/Reflector.php @@ -39,6 +39,168 @@ final class Reflector // https://youtrack.jetbrains.com/issue/WI-29503 'bson_encode', 'bson_decode', + + // https://youtrack.jetbrains.com/issue/WI-74920 + 'swoole_async_dns_lookup', + 'swoole_async_readfile', + 'swoole_async_write', + 'swoole_async_writefile', + + // https://youtrack.jetbrains.com/issue/WI-74921 + 'ssh2_send_eof', + + // https://youtrack.jetbrains.com/issue/WI-74922 + 'ssdeep_fuzzy_compare', + 'ssdeep_fuzzy_hash', + 'ssdeep_fuzzy_hash_filename', + + // https://youtrack.jetbrains.com/issue/WI-74918 + 'ps_add_bookmark', + 'ps_add_launchlink', + 'ps_add_locallink', + 'ps_add_note', + 'ps_add_pdflink', + 'ps_add_weblink', + 'ps_arc', + 'ps_arcn', + 'ps_begin_page', + 'ps_begin_pattern', + 'ps_begin_template', + 'ps_circle', + 'ps_clip', + 'ps_close_image', + 'ps_close', + 'ps_closepath_stroke', + 'ps_closepath', + 'ps_continue_text', + 'ps_curveto', + 'ps_delete', + 'ps_end_page', + 'ps_end_pattern', + 'ps_end_template', + 'ps_fill_stroke', + 'ps_fill', + 'ps_findfont', + 'ps_get_buffer', + 'ps_get_parameter', + 'ps_get_value', + 'ps_hyphenate', + 'ps_include_file', + 'ps_lineto', + 'ps_makespotcolor', + 'ps_moveto', + 'ps_new', + 'ps_open_file', + 'ps_open_image_file', + 'ps_open_image', + 'ps_open_memory_image', + 'ps_place_image', + 'ps_rect', + 'ps_restore', + 'ps_rotate', + 'ps_save', + 'ps_scale', + 'ps_set_border_color', + 'ps_set_border_dash', + 'ps_set_border_style', + 'ps_set_info', + 'ps_set_parameter', + 'ps_set_text_pos', + 'ps_set_value', + 'ps_setcolor', + 'ps_setdash', + 'ps_setflat', + 'ps_setfont', + 'ps_setgray', + 'ps_setlinecap', + 'ps_setlinejoin', + 'ps_setlinewidth', + 'ps_setmiterlimit', + 'ps_setoverprintmode', + 'ps_setpolydash', + 'ps_shading_pattern', + 'ps_shading', + 'ps_shfill', + 'ps_show_boxed', + 'ps_show_xy2', + 'ps_show_xy', + 'ps_show2', + 'ps_show', + 'ps_string_geometry', + 'ps_stringwidth', + 'ps_stroke', + 'ps_symbol_name', + 'ps_symbol_width', + 'ps_symbol', + 'ps_translate', + + // https://youtrack.jetbrains.com/issue/WI-74919 + 'yaz_addinfo', + 'yaz_ccl_conf', + 'yaz_ccl_parse', + 'yaz_close', + 'yaz_connect', + 'yaz_database', + 'yaz_element', + 'yaz_errno', + 'yaz_error', + 'yaz_es_result', + 'yaz_es', + 'yaz_get_option', + 'yaz_hits', + 'yaz_itemorder', + 'yaz_present', + 'yaz_range', + 'yaz_record', + 'yaz_scan_result', + 'yaz_scan', + 'yaz_schema', + 'yaz_search', + 'yaz_set_option', + 'yaz_sort', + 'yaz_syntax', + 'yaz_wait', + + // https://youtrack.jetbrains.com/issue/WI-74923 + 'setproctitle', + 'setthreadtitle', + + // https://youtrack.jetbrains.com/issue/WI-74924 + 'rpmaddtag', + + // https://youtrack.jetbrains.com/issue/WI-74925 + 'oci_set_prefetch_lob', + + // https://youtrack.jetbrains.com/issue/WI-74926 + 'normalizer_is_normalized', + 'normalizer_normalize', + + // https://youtrack.jetbrains.com/issue/WI-74927 + 'mysql_drop_db', + 'mysql_create_db', + + // https://youtrack.jetbrains.com/issue/WI-74928 + 'event_base_reinit', + 'event_priority_set', + + // https://youtrack.jetbrains.com/issue/WI-74929 + 'db2_pclose', + + // https://youtrack.jetbrains.com/issue/WI-74930 + 'cubrid_current_oid', + + // https://youtrack.jetbrains.com/issue/WI-74931 + 'ctype_alnum', + 'ctype_alpha', + 'ctype_cntrl', + 'ctype_digit', + 'ctype_graph', + 'ctype_lower', + 'ctype_print', + 'ctype_punct', + 'ctype_space', + 'ctype_upper', + 'ctype_xdigit', ]; /** @@ -83,6 +245,14 @@ final class Reflector // https://youtrack.jetbrains.com/issue/WI-29503 'MONGODB_VERSION', 'MONGODB_STABILITY', + + // https://youtrack.jetbrains.com/issue/WI-74918/Missing-PostScript-extension-symbols + 'ps_LINECAP_BUTT', + 'ps_LINECAP_ROUND', + 'ps_LINECAP_SQUARED', + 'ps_LINEJOIN_MITER', + 'ps_LINEJOIN_ROUND', + 'ps_LINEJOIN_BEVEL', ]; public static function createWithPhpStormStubs(): self diff --git a/tests/Symbol/Reflector/PhpStormStubsReflectorTest.php b/tests/Symbol/Reflector/PhpStormStubsReflectorTest.php index 63b350172..2aea6b906 100644 --- a/tests/Symbol/Reflector/PhpStormStubsReflectorTest.php +++ b/tests/Symbol/Reflector/PhpStormStubsReflectorTest.php @@ -16,6 +16,8 @@ use Humbug\PhpScoper\Symbol\Reflector; use PHPUnit\Framework\TestCase; +use function event_add; +use function event_base_reinit; /** * @covers \Humbug\PhpScoper\Symbol\Reflector @@ -142,6 +144,7 @@ public static function provideFunctions(): iterable yield 'PHPDBG internal function' => [ 'phpdbg_break_file', true, + event_add() ]; } From c054e51e2462dab7ad8ba1dbaedc01c6b3be2a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Wed, 1 Nov 2023 23:20:34 +0100 Subject: [PATCH 2/3] udpate doc --- README.md | 1 - docs/configuration.md | 3 ++- docs/further-reading.md | 58 ----------------------------------------- 3 files changed, 2 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index b06bab2ab..c49cba7b8 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ potentially very difficult to debug due to dissimilar or unsupported package ver - [Step 2: Run PHP-Scoper](#step-2-run-php-scoper) - [Recommendations](#recommendations) - [Further Reading](docs/further-reading.md#further-reading) - - [Polyfills](docs/further-reading.md#polyfills) - [How to deal with unknown third-party symbols](docs/further-reading.md#how-to-deal-with-unknown-third-party-symbols) - [Autoload aliases](docs/further-reading.md#autoload-aliases) - [Class aliases](docs/further-reading.md#class-aliases) diff --git a/docs/configuration.md b/docs/configuration.md index d8eba4fd2..3e181bff3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -236,12 +236,13 @@ if (!function_exists('Prefix\trigger_deprecation')) { } ``` -Indeed the namespace _needs_ to be added in order to not break autoloading, in which +Indeed, the namespace _needs_ to be added in order to not break autoloading, in which case wrapping the function declaration into a non-namespace could work, but is tricky (so not implemented so far, PoC for supporting it are welcomed) hence was not attempted. So if left alone, this will break any piece of code that relied on `\trigger_deprecation`, which is why PHP-Scoper will still add an alias for it, as if it was an exposed function. +Another benefit of this, is that it allows to scope any polyfill without any issues. **WARNING**: This exclusion feature should be use very carefully as it can easily break the Composer auto-loading. Indeed, if you have the following package: diff --git a/docs/further-reading.md b/docs/further-reading.md index e7e90c318..33ab9a6be 100644 --- a/docs/further-reading.md +++ b/docs/further-reading.md @@ -1,69 +1,11 @@ ## Further Reading -- [Polyfills](#polyfills) - [How to deal with unknown third-party symbols](#how-to-deal-with-unknown-third-party-symbols) - [Autoload aliases](#autoload-aliases) - [Class aliases](#class-aliases) - [Function aliases](#function-aliases) -### Polyfills - -**Note: should be obsolete as of 0.18.0.** - -At the moment there is no way to automatically handles polyfills. This is mainly -due to the nature of polyfills: the code is sometimes a bit... special and there -is also not only one way on how to approach it. - -If all of what you have is Symfony polyfills however, the following should get -you covered: - -```php - $fileInfo->getPathname(), - iterator_to_array( - Finder::create() - ->files() - ->in(__DIR__ . '/vendor/symfony/polyfill-*') - ->name('bootstrap*.php'), - false, - ), -); - -$polyfillsStubs = array_map( - static fn (SplFileInfo $fileInfo) => $fileInfo->getPathname(), - iterator_to_array( - Finder::create() - ->files() - ->in(__DIR__ . '/vendor/symfony/polyfill-*/Resources/stubs') - ->name('*.php'), - false, - ), -); - -return [ - // ... - - 'exclude-namespaces' => [ - 'Symfony\Polyfill' - ], - 'exclude-constants' => [ - // Symfony global constants - '/^SYMFONY\_[\p{L}_]+$/', - ], - 'exclude-files' => [ - ...$polyfillsBootstraps, - ...$polyfillsStubs, - ], -]; - -``` - - ### How to deal with unknown third-party symbols If you consider the following code: From 722f4a320570ccf04879525fd5dbba90d66faab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Wed, 1 Nov 2023 23:21:06 +0100 Subject: [PATCH 3/3] cleanup --- scoper.inc.php | 83 ++++++++++--------- src/Console/Command/InspectCommand.php | 9 +- .../Reflector/PhpStormStubsReflectorTest.php | 3 - 3 files changed, 44 insertions(+), 51 deletions(-) diff --git a/scoper.inc.php b/scoper.inc.php index 3c2d38c72..e99996fd1 100644 --- a/scoper.inc.php +++ b/scoper.inc.php @@ -13,35 +13,33 @@ */ $jetBrainStubs = (static function (): array { - $packageDir = __DIR__.'/vendor/jetbrains/phpstorm-stubs'; $files = []; - $ignoredDirectories = [ - $packageDir.'/tests', - $packageDir.'/meta', - ]; - $collectFiles = static function (RecursiveIteratorIterator $iterator) use (&$collectFiles, &$files, $ignoredDirectories): void { - foreach ($iterator as $fileInfo) { - /** @var SplFileInfo $fileInfo */ - if (str_starts_with($fileInfo->getFilename(), '.') - || $fileInfo->isDir() - || !$fileInfo->isReadable() - || 'php' !== $fileInfo->getExtension() - ) { + foreach (new DirectoryIterator(__DIR__.'/vendor/jetbrains/phpstorm-stubs') as $directoryInfo) { + if ($directoryInfo->isDot()) { + continue; + } + + if (false === $directoryInfo->isDir()) { + continue; + } + + if (in_array($directoryInfo->getBasename(), ['tests', 'meta'], true)) { + continue; + } + + foreach (new DirectoryIterator($directoryInfo->getPathName()) as $fileInfo) { + if ($fileInfo->isDot()) { continue; } - foreach ($ignoredDirectories as $ignoredDirectory) { - if (str_starts_with($fileInfo->getPathname(), $ignoredDirectory)) { - continue 2; - } + if (1 !== preg_match('/\.php$/', $fileInfo->getBasename())) { + continue; } $files[] = $fileInfo->getPathName(); } - }; - - $collectFiles(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($packageDir))); + } return $files; })(); @@ -52,32 +50,37 @@ 'exclude-classes' => [ 'Isolated\Symfony\Component\Finder\Finder', ], - 'exclude-functions' => [ - // symfony/deprecation-contracts - 'trigger_deprecation', - - // nikic/php-parser - // https://github.com/nikic/PHP-Parser/issues/957 - 'assertArgs', - 'ensureDirExists', - 'execCmd', - 'formatErrorMessage', - 'magicSplit', - 'parseArgs', - 'preprocessGrammar', - 'regex', - 'removeTrailingWhitespace', - 'resolveMacros', - 'resolveNodes', - 'resolveStackAccess', - 'showHelp', - ], 'exclude-constants' => [ // Symfony global constants '/^SYMFONY\_[\p{L}_]+$/', ], 'exclude-files' => $jetBrainStubs, 'patchers' => [ + // + // PHPStorm stub map: leave it unchanged + // + static function (string $filePath, string $prefix, string $contents): string { + if ('vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php' !== $filePath) { + return $contents; + } + + return str_replace( + [ + $prefix.'\\\\', + $prefix.'\\', + 'namespace JetBrains\PHPStormStub;', + ], + [ + '', + '', + sprintf( + 'namespace %s\JetBrains\PHPStormStub;', + $prefix, + ), + ], + $contents, + ); + }, // // Reflector: leave the registered internal symbols unchanged // diff --git a/src/Console/Command/InspectCommand.php b/src/Console/Command/InspectCommand.php index 221bb2a2b..5a2d578d4 100644 --- a/src/Console/Command/InspectCommand.php +++ b/src/Console/Command/InspectCommand.php @@ -34,7 +34,6 @@ use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; use function array_key_exists; -use function file_exists; use function Safe\getcwd; use function sprintf; use const DIRECTORY_SEPARATOR; @@ -158,13 +157,7 @@ private function getConfigFilePath(IO $io, string $cwd): ?string { $configFilePath = (string) $io->getOption(self::CONFIG_FILE_OPT)->asNullableString(); - if ('' !== $configFilePath) { - return $this->canonicalizePath($configFilePath, $cwd); - } - - $defaultConfigFilePath = $this->canonicalizePath(ConfigurationFactory::DEFAULT_FILE_NAME, $cwd); - - return file_exists($defaultConfigFilePath) ? $defaultConfigFilePath : null; + return '' === $configFilePath ? null : $this->canonicalizePath($configFilePath, $cwd); } /** diff --git a/tests/Symbol/Reflector/PhpStormStubsReflectorTest.php b/tests/Symbol/Reflector/PhpStormStubsReflectorTest.php index 2aea6b906..63b350172 100644 --- a/tests/Symbol/Reflector/PhpStormStubsReflectorTest.php +++ b/tests/Symbol/Reflector/PhpStormStubsReflectorTest.php @@ -16,8 +16,6 @@ use Humbug\PhpScoper\Symbol\Reflector; use PHPUnit\Framework\TestCase; -use function event_add; -use function event_base_reinit; /** * @covers \Humbug\PhpScoper\Symbol\Reflector @@ -144,7 +142,6 @@ public static function provideFunctions(): iterable yield 'PHPDBG internal function' => [ 'phpdbg_break_file', true, - event_add() ]; }