diff --git a/bin/check-package-autoload.php b/bin/check-package-autoload.php index a1a263da2..c4b97a519 100644 --- a/bin/check-package-autoload.php +++ b/bin/check-package-autoload.php @@ -40,7 +40,7 @@ exit( 2 ); } -$declared = collect_declared_symbols( $pkg_dir ); +$declared = collect_declared_symbols( $pkg_dir, load_classmap_excludes( $pkg_dir ) ); if ( empty( $declared ) ) { echo "NOTE: {$pkg} declares no class/interface/trait symbols (file-only package)\n"; exit( 0 ); @@ -66,20 +66,46 @@ exit( 1 ); /** - * Walks every PHP file under $dir (skipping Tests, vendor, fixtures) and - * extracts the fully-qualified name of each top-level class, interface, and - * trait. Uses PHP's tokenizer so we never execute the package code. + * Walks every PHP file under $dir and extracts the fully-qualified name of + * each top-level class, interface, and trait. Uses PHP's tokenizer so we + * never execute the package code. + * + * Skips standard non-autoloaded directories (Tests, fixtures, vendor) and + * any path the package itself excluded from its classmap via the + * exclude-from-classmap directive in composer.json. We honour that + * directive because anything excluded there is, by definition, not part of + * the autoload surface — checking that those classes are reachable would + * be a false positive. */ -function collect_declared_symbols( $dir ) { - $out = array(); - $iterator = new RecursiveIteratorIterator( +function collect_declared_symbols( $dir, array $excluded_paths = array() ) { + $out = array(); + $dir_norm = rtrim( str_replace( DIRECTORY_SEPARATOR, '/', $dir ), '/' ); + $skipped_dir = array( 'Tests', 'tests', 'fixtures', 'vendor' ); + $iterator = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), - function ( $current ) { + function ( $current ) use ( $dir_norm, $excluded_paths, $skipped_dir ) { $name = $current->getFilename(); - if ( $current->isDir() && in_array( $name, array( 'Tests', 'tests', 'fixtures', 'vendor' ), true ) ) { + if ( $current->isDir() && in_array( $name, $skipped_dir, true ) ) { return false; } + if ( $excluded_paths ) { + // Build a leading-slash, forward-slash path relative to + // the package root so substring matching against entries + // like "/Tests/" or "/vendor-patched/foo/bar/" behaves + // the way composer's classmap exclusion does. + $path = str_replace( DIRECTORY_SEPARATOR, '/', $current->getPathname() ); + if ( 0 === strpos( $path, $dir_norm . '/' ) ) { + $path = substr( $path, strlen( $dir_norm ) ); + } + $rel = '/' . ltrim( $path, '/' ); + $haystack = $current->isDir() ? rtrim( $rel, '/' ) . '/' : $rel; + foreach ( $excluded_paths as $excluded ) { + if ( false !== strpos( $haystack, $excluded ) ) { + return false; + } + } + } return true; } ) @@ -95,6 +121,39 @@ function ( $current ) { return $out; } +/** + * Reads the package's composer.json and returns its + * autoload.exclude-from-classmap entries, normalised to paths that start + * with a leading "/". Returns an empty array if the file is missing or + * unreadable. + */ +function load_classmap_excludes( $pkg_dir ) { + $composer_json = $pkg_dir . '/composer.json'; + if ( ! is_file( $composer_json ) ) { + return array(); + } + $decoded = json_decode( file_get_contents( $composer_json ), true ); + if ( ! is_array( $decoded ) ) { + return array(); + } + $raw = $decoded['autoload']['exclude-from-classmap'] ?? array(); + if ( ! is_array( $raw ) ) { + return array(); + } + $out = array(); + foreach ( $raw as $entry ) { + if ( ! is_string( $entry ) || '' === $entry ) { + continue; + } + $entry = str_replace( '\\', '/', $entry ); + if ( '/' !== $entry[0] ) { + $entry = '/' . $entry; + } + $out[] = $entry; + } + return $out; +} + function extract_symbols( $source ) { $tokens = token_get_all( $source ); $namespace = ''; diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index 05abf0fa7..8fd9ad8cf 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -35,7 +35,23 @@ * ✅ @TODO: Prevent remote resources from using local bundle paths */ -require __DIR__ . '/../../../vendor/autoload.php'; +if ( ! class_exists( 'Composer\\Autoload\\ClassLoader', false ) ) { + $_blueprint_autoload_candidates = array( + // Installed as a dependency: vendor/wp-php-toolkit/blueprints/bin/. + __DIR__ . '/../../../autoload.php', + // Standalone clone of the wp-php-toolkit/blueprints repo. + __DIR__ . '/../vendor/autoload.php', + // Inside the php-toolkit monorepo. + __DIR__ . '/../../../vendor/autoload.php', + ); + foreach ( $_blueprint_autoload_candidates as $_blueprint_autoload ) { + if ( file_exists( $_blueprint_autoload ) ) { + require $_blueprint_autoload; + break; + } + } + unset( $_blueprint_autoload_candidates, $_blueprint_autoload ); +} use WordPress\CLI\CLI; use WordPress\Blueprints\DataReference\AbsoluteLocalPath; diff --git a/components/Blueprints/composer.json b/components/Blueprints/composer.json index a5e312373..1c3f759b4 100644 --- a/components/Blueprints/composer.json +++ b/components/Blueprints/composer.json @@ -36,7 +36,11 @@ "./" ], "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/bin/", + "/Steps/scripts/", + "/vendor-patched/symfony/event-dispatcher/DependencyInjection/", + "/vendor-patched/symfony/event-dispatcher/ContainerAwareEventDispatcher.php" ] } } diff --git a/components/CORSProxy/composer.json b/components/CORSProxy/composer.json index 40dc34810..85d1e159f 100644 --- a/components/CORSProxy/composer.json +++ b/components/CORSProxy/composer.json @@ -30,10 +30,14 @@ "phpunit/phpunit": "^9.5" }, "autoload": { - "psr-4": { - "WordPress\\CORSProxy\\": "" - }, + "classmap": [ + "./" + ], + "files": [ + "cors-proxy-functions.php" + ], "exclude-from-classmap": [ + "/tests/", "/Tests/" ] } diff --git a/components/CORSProxy/cors-proxy-functions.php b/components/CORSProxy/cors-proxy-functions.php index 5e84cbe03..f8899ae48 100644 --- a/components/CORSProxy/cors-proxy-functions.php +++ b/components/CORSProxy/cors-proxy-functions.php @@ -391,7 +391,7 @@ function rewrite_relative_redirect( } if ( ! parse_url( $redirect_location, PHP_URL_SCHEME ) ) { - $target_scheme = parse_url( $request_url, PHP_URL_SCHEME ) ? PHP_URL_SCHEME ) : 'https'; + $target_scheme = parse_url( $request_url, PHP_URL_SCHEME ) ?: 'https'; $redirect_location = "$target_scheme://$redirect_location"; } diff --git a/components/DataLiberation/composer.json b/components/DataLiberation/composer.json index 4184c8e3d..6e72e8350 100644 --- a/components/DataLiberation/composer.json +++ b/components/DataLiberation/composer.json @@ -27,6 +27,7 @@ "php": ">=7.2", "wp-php-toolkit/bytestream": "^0.8", "wp-php-toolkit/filesystem": "^0.8", + "wp-php-toolkit/html": "^0.8", "wp-php-toolkit/http-client": "^0.8", "wp-php-toolkit/xml": "^0.8" }, @@ -39,7 +40,8 @@ "URL/functions.php" ], "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/bin/" ] } } diff --git a/components/Markdown/composer.json b/components/Markdown/composer.json index dc3d4e41f..30e2e5297 100644 --- a/components/Markdown/composer.json +++ b/components/Markdown/composer.json @@ -34,7 +34,12 @@ "vendor-patched" ], "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/bin/", + "/vendor-patched/symfony/yaml/Command/", + "/vendor-patched/webuni/front-matter/src/Haml/", + "/vendor-patched/webuni/front-matter/src/Pug/", + "/vendor-patched/webuni/front-matter/src/Twig/" ] } } diff --git a/components/Merge/composer.json b/components/Merge/composer.json index 4f58f4bc4..6ee4554e1 100644 --- a/components/Merge/composer.json +++ b/components/Merge/composer.json @@ -19,10 +19,9 @@ "docs": "https://wordpress.github.io/php-toolkit/reference/merge.html" }, "require": { - "php": ">=7.4", - "yetanotherape/diff-match-patch": "^1.0", - "wordpress/data-liberation": "dev-trunk", - "ext-mbstring": "*" + "php": ">=7.2", + "ext-mbstring": "*", + "wp-php-toolkit/data-liberation": "^0.8" }, "require-dev": { "phpunit/phpunit": "^9.5", diff --git a/components/Polyfill/class-wp-error.php b/components/Polyfill/class-wp-error.php new file mode 100644 index 000000000..bf1f206da --- /dev/null +++ b/components/Polyfill/class-wp-error.php @@ -0,0 +1,28 @@ +code = $code; + $this->message = $message; + $this->data = $data; + } + } +} diff --git a/components/Polyfill/class-wp-exception.php b/components/Polyfill/class-wp-exception.php new file mode 100644 index 000000000..6909c3bb9 --- /dev/null +++ b/components/Polyfill/class-wp-exception.php @@ -0,0 +1,16 @@ +code = $code; - $this->message = $message; - $this->data = $data; - } - } -} +// `WP_Error` and `WP_Exception` are deliberately NOT declared in this +// file: declaring WordPress-core class names at composer-bootstrap time +// fatals as soon as a downstream consumer loads WordPress, because WP +// core re-declares the same names without a guard. Those polyfills live +// in class-wp-error.php / class-wp-exception.php and are picked up by +// the autoloader's classmap on demand. if ( ! function_exists( '_doing_it_wrong' ) ) { $GLOBALS['_doing_it_wrong_messages'] = array(); @@ -29,11 +19,6 @@ function _doing_it_wrong( $method, $message, $version ) { } } -if ( ! class_exists( 'WP_Exception' ) ) { - class WP_Exception extends Exception { - } -} - if ( ! function_exists( 'wp_trigger_error' ) ) { function wp_trigger_error( $function_name, $message, $error_level = E_USER_NOTICE ) { if ( ! empty( $function_name ) ) { diff --git a/composer-ci-matrix-tests.json b/composer-ci-matrix-tests.json index de4747f71..4feee07f6 100644 --- a/composer-ci-matrix-tests.json +++ b/composer-ci-matrix-tests.json @@ -31,6 +31,7 @@ "components/Blueprints/", "components/Blueprints/vendor-patched/", "components/CLI/", + "components/CORSProxy/", "components/DataLiberation/", "components/DataLiberation/vendor-patched/", "components/Filesystem/", @@ -43,10 +44,12 @@ "components/Merge/", "components/Merge/vendor-patched", "components/ByteStream/", + "components/Polyfill/", "components/XML/", "components/Zip/" ], "files": [ + "components/CORSProxy/cors-proxy-functions.php", "components/DataLiberation/URL/functions.php", "components/Encoding/utf8.php", "components/Encoding/compat-utf8.php", @@ -55,12 +58,12 @@ "components/Zip/functions.php", "components/Polyfill/wordpress.php", "components/Polyfill/mbstring.php", + "components/Polyfill/php-functions.php", "components/Git/functions.php" ], "psr-4": { "Rowbot\\": "components/DataLiberation/vendor-patched/", - "Brick\\": "components/DataLiberation/vendor-patched/", - "WordPress\\CORSProxy\\": "components/CORSProxy/" + "Brick\\": "components/DataLiberation/vendor-patched/" } }, "scripts": { diff --git a/composer.json b/composer.json index a2808b657..b6dcb074f 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "components/Blueprints/", "components/Blueprints/vendor-patched/", "components/CLI/", + "components/CORSProxy/", "components/DataLiberation/", "components/DataLiberation/vendor-patched/", "components/Filesystem/", @@ -60,11 +61,13 @@ "components/Merge/", "components/Merge/vendor-patched", "components/ByteStream/", + "components/Polyfill/", "components/ToolkitCodingStandards/", "components/XML/", "components/Zip/" ], "files": [ + "components/CORSProxy/cors-proxy-functions.php", "components/DataLiberation/URL/functions.php", "components/Encoding/utf8.php", "components/Encoding/compat-utf8.php", @@ -78,8 +81,7 @@ ], "psr-4": { "Rowbot\\": "components/DataLiberation/vendor-patched/", - "Brick\\": "components/DataLiberation/vendor-patched/", - "WordPress\\CORSProxy\\": "components/CORSProxy/" + "Brick\\": "components/DataLiberation/vendor-patched/" } }, "scripts": {