From 1b411d60d29c0e9010c99e7cb5df1b8d645e2ac8 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 May 2025 11:17:59 +0200 Subject: [PATCH 1/4] PHPStan level 9 --- composer.json | 7 +++-- phpstan.neon.dist | 20 +++++++++++++ src/Theme_Language_Command.php | 17 ++++++++--- src/WP_CLI/CommandWithTranslation.php | 42 +++++++++++++++++++++++---- src/WP_CLI/LanguagePackUpgrader.php | 23 +++++++++++---- 5 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/composer.json b/composer.json index 779a3b3dc..ae791c738 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,15 @@ "wp-cli/db-command": "^1.3 || ^2", "wp-cli/entity-command": "^1.3 || ^2", "wp-cli/extension-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "^4" + "wp-cli/wp-cli-tests": "dev-main" }, "config": { "process-timeout": 7200, "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "johnpbloch/wordpress-core-installer": true + "johnpbloch/wordpress-core-installer": true, + "phpstan/extension-installer": true }, "lock": false }, @@ -73,12 +74,14 @@ "behat-rerun": "rerun-behat-tests", "lint": "run-linter-tests", "phpcs": "run-phpcs-tests", + "phpstan": "run-phpstan-tests", "phpcbf": "run-phpcbf-cleanup", "phpunit": "run-php-unit-tests", "prepare-tests": "install-package-tests", "test": [ "@lint", "@phpcs", + "@phpstan", "@phpunit", "@behat" ] diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..2689bcb94 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,20 @@ +parameters: + level: 9 + paths: + - src + - language-command.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + - vendor/wp-cli/wp-cli-tests + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + treatPhpDocTypesAsCertain: false + dynamicConstantNames: + - WP_DEBUG + - WP_DEBUG_LOG + - WP_DEBUG_DISPLAY + ignoreErrors: + - identifier: missingType.iterableValue + - identifier: missingType.property + - identifier: missingType.parameter + - identifier: missingType.return diff --git a/src/Theme_Language_Command.php b/src/Theme_Language_Command.php index f3e67b333..77aa30a73 100644 --- a/src/Theme_Language_Command.php +++ b/src/Theme_Language_Command.php @@ -315,7 +315,11 @@ private function install_one( $args, $assoc_args ) { */ private function install_many( $args, $assoc_args ) { $language_codes = (array) $args; - $themes = wp_get_themes(); + + /** + * @var \WP_Theme[] $themes + */ + $themes = wp_get_themes(); if ( empty( $assoc_args['format'] ) ) { $assoc_args['format'] = 'table'; @@ -343,6 +347,11 @@ private function install_many( $args, $assoc_args ) { $available = $this->get_installed_languages( $theme_name ); + /** + * @var string $display_name + */ + $display_name = $theme_details['Name']; + foreach ( $language_codes as $language_code ) { $result = [ 'name' => $theme_name, @@ -350,7 +359,7 @@ private function install_many( $args, $assoc_args ) { ]; if ( in_array( $language_code, $available, true ) ) { - \WP_CLI::log( "Language '{$language_code}' for '{$theme_details['Name']}' already installed." ); + \WP_CLI::log( "Language '{$language_code}' for '{$display_name}' already installed." ); $result['status'] = 'already installed'; ++$skips; } else { @@ -358,7 +367,7 @@ private function install_many( $args, $assoc_args ) { if ( is_wp_error( $response ) ) { \WP_CLI::warning( $response ); - \WP_CLI::log( "Language '{$language_code}' for '{$theme_details['Name']}' not installed." ); + \WP_CLI::log( "Language '{$language_code}' for '{$display_name}' not installed." ); if ( 'not_found' === $response->get_error_code() ) { $result['status'] = 'not available'; @@ -368,7 +377,7 @@ private function install_many( $args, $assoc_args ) { ++$errors; } } else { - \WP_CLI::log( "Language '{$language_code}' for '{$theme_details['Name']}' installed." ); + \WP_CLI::log( "Language '{$language_code}' for '{$display_name}' installed." ); $result['status'] = 'installed'; ++$successes; } diff --git a/src/WP_CLI/CommandWithTranslation.php b/src/WP_CLI/CommandWithTranslation.php index ca4b2b9ea..b3e211cf1 100644 --- a/src/WP_CLI/CommandWithTranslation.php +++ b/src/WP_CLI/CommandWithTranslation.php @@ -57,12 +57,22 @@ public function update( $args, $assoc_args ) { $name = 'WordPress'; // Core. if ( 'plugin' === $update->type ) { - $plugins = get_plugins( '/' . $update->slug ); + /** + * @var array $plugins + */ + $plugins = get_plugins( '/' . $update->slug ); + + /** + * @var array{Name: string}> $plugin_data + */ $plugin_data = array_shift( $plugins ); $name = $plugin_data['Name']; } elseif ( 'theme' === $update->type ) { $theme_data = wp_get_theme( $update->slug ); - $name = $theme_data['Name']; + /** + * @var string $name + */ + $name = $theme_data['Name']; } // Gets the translation data. @@ -97,7 +107,12 @@ public function update( $args, $assoc_args ) { foreach ( $available_updates as $update ) { WP_CLI::line( "Updating '{$update->Language}' translation for {$update->Name} {$update->Version}..." ); - $result = Utils\get_upgrader( $upgrader )->upgrade( $update ); + /** + * @var \WP_CLI\LanguagePackUpgrader $upgrader_instance + */ + $upgrader_instance = Utils\get_upgrader( $upgrader ); + + $result = $upgrader_instance->upgrade( $update ); $results[] = $result; } @@ -181,7 +196,11 @@ protected function get_translation_updates() { break; } - $updates = array(); + $updates = array(); + + /** + * @var object{translations: array} $transient + */ $transient = get_site_transient( $transient ); if ( empty( $transient->translations ) ) { @@ -218,7 +237,8 @@ protected function download_language_pack( $download, $slug = null ) { if ( ! $translation_to_load ) { return new \WP_Error( 'not_found', $slug ? "Language '{$download}' for '{$slug}' not available." : "Language '{$download}' not available." ); } - $translation = (object) $translation; + + $translation = (object) $translation_to_load; $translation->type = rtrim( $this->obj_type, 's' ); @@ -228,7 +248,15 @@ protected function download_language_pack( $download, $slug = null ) { } $upgrader = 'WP_CLI\\LanguagePackUpgrader'; - $result = Utils\get_upgrader( $upgrader )->upgrade( $translation, array( 'clear_update_cache' => false ) ); + + /** + * @var \WP_CLI\LanguagePackUpgrader $upgrader_instance + */ + $upgrader_instance = Utils\get_upgrader( $upgrader ); + + // Incorrect docblock in WordPress core. + // @phpstan-ignore argument.type + $result = $upgrader_instance->upgrade( $translation, array( 'clear_update_cache' => false ) ); if ( is_wp_error( $result ) ) { return $result; @@ -268,6 +296,8 @@ protected function get_all_languages( $slug = null ) { require ABSPATH . WPINC . '/version.php'; // Include an unmodified $wp_version $args = array( + // False positive, because it's defined in version.php + // @phpstan-ignore variable.undefined 'version' => $wp_version, ); diff --git a/src/WP_CLI/LanguagePackUpgrader.php b/src/WP_CLI/LanguagePackUpgrader.php index f208a00b6..332d58f35 100644 --- a/src/WP_CLI/LanguagePackUpgrader.php +++ b/src/WP_CLI/LanguagePackUpgrader.php @@ -10,6 +10,13 @@ * @package wp-cli */ class LanguagePackUpgrader extends \Language_Pack_Upgrader { + /** + * The upgrader skin being used. + * + * @var \Language_Pack_Upgrader_Skin $skin + */ + public $skin = null; + /** * Initialize the upgrade strings. * @@ -57,10 +64,10 @@ public function download_package( $package, $check_signatures = false, $hook_ext * @since 3.7.0 * @since 5.5.0 Added the `$hook_extra` parameter. * - * @param bool $reply Whether to bail without returning the package. Default is false. - * @param string $package The package file name. - * @param \WP_Upgrader $this The WP_Upgrader instance. - * @param array $hook_extra Extra arguments passed to hooked filters. + * @param false|string|\WP_Error $reply Whether to bail without returning the package. Default is false. + * @param string $package The package file name. + * @param \WP_Upgrader $upgrader The WP_Upgrader instance. + * @param array $hook_extra Extra arguments passed to hooked filters. * * @phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using WP native hook. */ @@ -90,8 +97,12 @@ public function download_package( $package, $check_signatures = false, $hook_ext $temp = \WP_CLI\Utils\get_temp_dir() . uniqid( 'wp_' ) . '.' . $ext; - $cache = WP_CLI::get_cache(); - $cache_key = "translation/{$type}-{$slug}-{$version}-{$language}-{$updated}.{$ext}"; + $cache = WP_CLI::get_cache(); + $cache_key = "translation/{$type}-{$slug}-{$version}-{$language}-{$updated}.{$ext}"; + + /** + * @var string|false $cache_file + */ $cache_file = $cache->has( $cache_key ); if ( $cache_file ) { From cac17b4e10a4f9f235e67c9ce21a6f977487cade Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 23 May 2025 09:00:19 +0200 Subject: [PATCH 2/4] Further updates --- composer.json | 2 +- phpstan.neon.dist | 11 +++++++---- src/Core_Language_Command.php | 21 ++++++++++++++++++--- src/Plugin_Language_Command.php | 25 +++++++++++++++++++++---- src/Theme_Language_Command.php | 21 +++++++++++++++++++-- src/WP_CLI/CommandWithTranslation.php | 10 ++++++++-- src/WP_CLI/LanguagePackUpgrader.php | 2 ++ 7 files changed, 76 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index ae791c738..277473967 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "wp-cli/db-command": "^1.3 || ^2", "wp-cli/entity-command": "^1.3 || ^2", "wp-cli/extension-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "dev-main" + "wp-cli/wp-cli-tests": "dev-add/phpstan-enhancements" }, "config": { "process-timeout": 7200, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2689bcb94..aeb2a18a3 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,10 +9,13 @@ parameters: scanFiles: - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php treatPhpDocTypesAsCertain: false - dynamicConstantNames: - - WP_DEBUG - - WP_DEBUG_LOG - - WP_DEBUG_DISPLAY + strictRules: + uselessCast: true + closureUsesThis: true + overwriteVariablesWithLoop: true + matchingInheritedMethodNames: true + numericOperandsInArithmeticOperators: true + switchConditionsMatchingType: true ignoreErrors: - identifier: missingType.iterableValue - identifier: missingType.property diff --git a/src/Core_Language_Command.php b/src/Core_Language_Command.php index 4b6fb77ae..c1b01af1e 100644 --- a/src/Core_Language_Command.php +++ b/src/Core_Language_Command.php @@ -93,6 +93,9 @@ class Core_Language_Command extends WP_CLI\CommandWithTranslation { * | az | Azerbaijani | uninstalled | * * @subcommand list + * + * @param string[] $args Positional arguments. Unused. + * @param array{field?: string, format: string, language?: string, english_name?: string, native_name?: string, status?: string, update?: string, updated?: string} $assoc_args Associative arguments. */ public function list_( $args, $assoc_args ) { $translations = $this->get_all_languages(); @@ -154,8 +157,10 @@ function ( $translation ) use ( $available, $current_locale, $updates ) { * 1 * * @subcommand is-installed + * + * @param array{string} $args Positional arguments. */ - public function is_installed( $args, $assoc_args = array() ) { + public function is_installed( $args ) { list( $language_code ) = $args; $available = $this->get_installed_languages(); if ( in_array( $language_code, $available, true ) ) { @@ -191,6 +196,9 @@ public function is_installed( $args, $assoc_args = array() ) { * Success: Installed 1 of 1 languages. * * @subcommand install + * + * @param string[] $args Positional arguments. + * @param array{activate?: bool} $assoc_args Associative arguments. */ public function install( $args, $assoc_args ) { $language_codes = (array) $args; @@ -253,8 +261,10 @@ public function install( $args, $assoc_args ) { * * @subcommand uninstall * @throws WP_CLI\ExitException + * + * @param string[] $args Positional arguments. */ - public function uninstall( $args, $assoc_args ) { + public function uninstall( $args ) { global $wp_filesystem; $dir = 'core' === $this->obj_type ? '' : "/$this->obj_type"; @@ -339,6 +349,9 @@ public function uninstall( $args, $assoc_args ) { * Success: Updated 1/1 translation. * * @subcommand update + * + * @param string[] $args Positional arguments. + * @param array{'dry-run'?: bool} $assoc_args Associative arguments. */ public function update( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found -- Overruling the documentation, so not useless ;-). parent::update( $args, $assoc_args ); @@ -362,8 +375,10 @@ public function update( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnaly * * @subcommand activate * @throws WP_CLI\ExitException + * + * @param array{string} $args Positional arguments. */ - public function activate( $args, $assoc_args ) { + public function activate( $args ) { \WP_CLI::warning( 'This command is deprecated. use wp site switch-language instead' ); list( $language_code ) = $args; diff --git a/src/Plugin_Language_Command.php b/src/Plugin_Language_Command.php index 24f2c6443..532e51100 100644 --- a/src/Plugin_Language_Command.php +++ b/src/Plugin_Language_Command.php @@ -103,6 +103,9 @@ class Plugin_Language_Command extends WP_CLI\CommandWithTranslation { * | az | Azerbaijani | uninstalled | * * @subcommand list + * + * @param string[] $args Positional arguments. + * @param array{all?: bool, field?: string, format: string, plugin?: string, language?: string, english_name?: string, native_name?: string, status?: string, update?: string, updated?: string} $assoc_args Associative arguments. */ public function list_( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); @@ -154,7 +157,7 @@ public function list_( $args, $assoc_args ) { // Support features like --status=active. foreach ( array_keys( $translation ) as $field ) { - if ( isset( $assoc_args[ $field ] ) && ! in_array( $translation[ $field ], array_map( 'trim', explode( ',', $assoc_args[ $field ] ) ), true ) ) { + if ( isset( $assoc_args[ $field ] ) && ! in_array( $translation[ $field ], array_map( 'trim', explode( ',', (string) $assoc_args[ $field ] ) ), true ) ) { continue 2; } } @@ -188,10 +191,12 @@ public function list_( $args, $assoc_args ) { * 1 * * @subcommand is-installed + * + * @param non-empty-array $args Positional arguments. */ - public function is_installed( $args, $assoc_args = array() ) { + public function is_installed( $args ) { $plugin = array_shift( $args ); - $language_codes = (array) $args; + $language_codes = $args; $available = $this->get_installed_languages( $plugin ); @@ -244,6 +249,9 @@ public function is_installed( $args, $assoc_args = array() ) { * Success: Installed 1 of 1 languages. * * @subcommand install + * + * @param string[] $args Positional arguments. + * @param array{all?: bool, format: string} $assoc_args Associative arguments. */ public function install( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); @@ -267,7 +275,7 @@ public function install( $args, $assoc_args ) { */ private function install_one( $args, $assoc_args ) { $plugin = array_shift( $args ); - $language_codes = (array) $args; + $language_codes = $args; $count = count( $language_codes ); $available = $this->get_installed_languages( $plugin ); @@ -419,6 +427,9 @@ private function install_many( $args, $assoc_args ) { * Success: Uninstalled 1 of 1 languages. * * @subcommand uninstall + * + * @param string[] $args Positional arguments. + * @param array{all?: bool, format: string} $assoc_args Associative arguments. */ public function uninstall( $args, $assoc_args ) { /** @var WP_Filesystem_Base $wp_filesystem */ @@ -471,6 +482,9 @@ public function uninstall( $args, $assoc_args ) { $errors = 0; $skips = 0; + /** + * @var string $plugin + */ foreach ( $plugins as $plugin ) { $available = $this->get_installed_languages( $plugin ); @@ -584,6 +598,9 @@ public function uninstall( $args, $assoc_args ) { * Success: Updated 1/1 translation. * * @subcommand update + * + * @param string[] $args Positional arguments. + * @param array{'dry-run'?: bool, all?: bool} $assoc_args Associative arguments. */ public function update( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); diff --git a/src/Theme_Language_Command.php b/src/Theme_Language_Command.php index 77aa30a73..a95896272 100644 --- a/src/Theme_Language_Command.php +++ b/src/Theme_Language_Command.php @@ -103,6 +103,9 @@ class Theme_Language_Command extends WP_CLI\CommandWithTranslation { * | az | Azerbaijani | uninstalled | * * @subcommand list + * + * @param string[] $args Positional arguments. + * @param array{all?: bool, field?: string, format: string, theme?: string, language?: string, english_name?: string, native_name?: string, status?: string, update?: string, updated?: string} $assoc_args Associative arguments. */ public function list_( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); @@ -194,8 +197,10 @@ function ( $file ) { * 1 * * @subcommand is-installed + * + * @param non-empty-array $args Positional arguments. */ - public function is_installed( $args, $assoc_args = array() ) { + public function is_installed( $args ) { $theme = array_shift( $args ); $language_codes = (array) $args; @@ -249,6 +254,9 @@ public function is_installed( $args, $assoc_args = array() ) { * Success: Installed 1 of 1 languages. * * @subcommand install + * + * @param string[] $args Positional arguments. + * @param array{all?: bool, format: string} $assoc_args Associative arguments. */ public function install( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); @@ -272,7 +280,7 @@ public function install( $args, $assoc_args ) { */ private function install_one( $args, $assoc_args ) { $theme = array_shift( $args ); - $language_codes = (array) $args; + $language_codes = $args; $count = count( $language_codes ); $available = $this->get_installed_languages( $theme ); @@ -432,6 +440,9 @@ private function install_many( $args, $assoc_args ) { * Success: Uninstalled 1 of 1 languages. * * @subcommand uninstall + * + * @param string[] $args Positional arguments. + * @param array{all?: bool, format: string} $assoc_args Associative arguments. */ public function uninstall( $args, $assoc_args ) { /** @var WP_Filesystem_Base $wp_filesystem */ @@ -490,6 +501,9 @@ public function uninstall( $args, $assoc_args ) { // As of WP 4.0, no API for deleting a language pack WP_Filesystem(); + /** + * @var string $theme + */ foreach ( $process_themes as $theme ) { $available_languages = $this->get_installed_languages( $theme ); @@ -603,6 +617,9 @@ public function uninstall( $args, $assoc_args ) { * Success: Updated 1/1 translation. * * @subcommand update + * + * @param string[] $args Positional arguments. + * @param array{'dry-run'?: bool, all?: bool} $assoc_args Associative arguments. */ public function update( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); diff --git a/src/WP_CLI/CommandWithTranslation.php b/src/WP_CLI/CommandWithTranslation.php index b3e211cf1..c273dc8e3 100644 --- a/src/WP_CLI/CommandWithTranslation.php +++ b/src/WP_CLI/CommandWithTranslation.php @@ -24,6 +24,9 @@ protected function sort_translations_callback( $a, $b ) { /** * Updates installed languages for the current object type. + * + * @param string[] $args Positional arguments. + * @param array{'dry-run'?: bool, all?: bool} $assoc_args Associative arguments. */ public function update( $args, $assoc_args ) { $updates = $this->get_translation_updates(); @@ -274,9 +277,12 @@ protected function download_language_pack( $download, $slug = null ) { * * @param string $slug Optional. Plugin or theme slug. Defaults to 'default' for core. * - * @return array + * @return string[] */ protected function get_installed_languages( $slug = 'default' ) { + /** + * @var array>> $available + */ $available = wp_get_installed_translations( $this->obj_type ); $available = ! empty( $available[ $slug ] ) ? array_keys( $available[ $slug ] ) : array(); $available[] = 'en_US'; @@ -289,7 +295,7 @@ protected function get_installed_languages( $slug = 'default' ) { * * @param string $slug Optional. Plugin or theme slug. Not used for core. * - * @return array + * @return array */ protected function get_all_languages( $slug = null ) { require_once ABSPATH . '/wp-admin/includes/translation-install.php'; diff --git a/src/WP_CLI/LanguagePackUpgrader.php b/src/WP_CLI/LanguagePackUpgrader.php index 332d58f35..0b7acb7b6 100644 --- a/src/WP_CLI/LanguagePackUpgrader.php +++ b/src/WP_CLI/LanguagePackUpgrader.php @@ -14,6 +14,8 @@ class LanguagePackUpgrader extends \Language_Pack_Upgrader { * The upgrader skin being used. * * @var \Language_Pack_Upgrader_Skin $skin + * + * @phpstan-ignore property.phpDocType */ public $skin = null; From 4c71e45896ed1d17802892954499472c1583ad2b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 12 Jun 2025 10:57:29 +0200 Subject: [PATCH 3/4] Cleanup --- phpstan.neon.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index aeb2a18a3..4f134e997 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,7 +5,6 @@ parameters: - language-command.php scanDirectories: - vendor/wp-cli/wp-cli/php - - vendor/wp-cli/wp-cli-tests scanFiles: - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php treatPhpDocTypesAsCertain: false From 91e0724d472f00351dff755ce451d3cc2d6decc8 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 3 Jul 2025 14:24:51 +0200 Subject: [PATCH 4/4] Use wp-cli-tests v5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 277473967..f90ab423d 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "wp-cli/db-command": "^1.3 || ^2", "wp-cli/entity-command": "^1.3 || ^2", "wp-cli/extension-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "dev-add/phpstan-enhancements" + "wp-cli/wp-cli-tests": "^5" }, "config": { "process-timeout": 7200,