From 983c23cefba138d3bd677da467dbfa9853a0f7aa Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 18 May 2025 10:43:37 +0200 Subject: [PATCH 1/9] PHPStan level 9 --- composer.json | 7 +- phpstan.neon.dist | 19 ++++++ src/Plugin_AutoUpdates_Command.php | 11 ++-- src/Plugin_Command.php | 102 ++++++++++++++++++++++------- src/Theme_AutoUpdates_Command.php | 12 ++-- src/Theme_Command.php | 29 ++++++-- src/Theme_Mod_Command.php | 3 + src/WP_CLI/CommandWithUpgrade.php | 87 +++++++++++++++++------- src/WP_CLI/Fetchers/Plugin.php | 4 +- src/WP_CLI/ParseThemeNameInput.php | 25 +++++-- 10 files changed, 227 insertions(+), 72 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/composer.json b/composer.json index 31d45d68..873100dc 100644 --- a/composer.json +++ b/composer.json @@ -25,14 +25,15 @@ "wp-cli/entity-command": "^1.3 || ^2", "wp-cli/language-command": "^2.0", "wp-cli/scaffold-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "^4.3.7" + "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 }, @@ -92,11 +93,13 @@ "lint": "run-linter-tests", "phpcs": "run-phpcs-tests", "phpcbf": "run-phpcbf-cleanup", + "phpstan": "run-phpstan-tests", "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 00000000..37be17de --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,19 @@ +parameters: + level: 9 + paths: + - src + 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/Plugin_AutoUpdates_Command.php b/src/Plugin_AutoUpdates_Command.php index 3a0b4377..c2303b43 100644 --- a/src/Plugin_AutoUpdates_Command.php +++ b/src/Plugin_AutoUpdates_Command.php @@ -72,10 +72,13 @@ public function __construct() { * $ wp plugin auto-updates enable hello * Plugin auto-updates for 'hello' enabled. * Success: Enabled 1 of 1 plugin auto-updates. + * + * @param array $args + * @param array $assoc_args */ public function enable( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); - $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $disabled_only = (bool) Utils\get_flag_value( $assoc_args, 'disabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { @@ -151,7 +154,7 @@ public function enable( $args, $assoc_args ) { * Success: Disabled 1 of 1 plugin auto-updates. */ public function disable( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); @@ -259,7 +262,7 @@ public function disable( $args, $assoc_args ) { * duplicate-post */ public function status( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 8bc741b9..6a3b2f4e 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -41,6 +41,8 @@ * Success: Installed 1 of 1 plugins. * * @package wp-cli + * + * @phpstan-type PluginInformation object{name: string, slug: non-empty-string, version: string, new_version: string, download_link: string, requires_php?: string, requires?: string, package: string}&\stdClass */ class Plugin_Command extends \WP_CLI\CommandWithUpgrade { @@ -209,6 +211,9 @@ public function search( $args, $assoc_args ) { } protected function status_single( $args ) { + /** + * @var object{name: string, file: string} $plugin + */ $plugin = $this->fetcher->get_check( $args[0] ); $file = $plugin->file; @@ -345,11 +350,18 @@ protected function get_all_items() { * Plugin 'bbpress' network activated. * Plugin 'buddypress' network activated. * Success: Activated 2 of 2 plugins. + * + * @param array $args + * @param array $assoc_args */ public function activate( $args, $assoc_args = array() ) { - $network_wide = Utils\get_flag_value( $assoc_args, 'network', false ); - $all = Utils\get_flag_value( $assoc_args, 'all', false ); - $all_exclude = Utils\get_flag_value( $assoc_args, 'exclude' ); + $network_wide = (bool) Utils\get_flag_value( $assoc_args, 'network', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $all_exclude = Utils\get_flag_value( $assoc_args, 'exclude', '' ); + + /** + * @var string $all_exclude + */ $args = $this->check_optional_args_and_all( $args, $all, 'activate', $all_exclude ); if ( ! $args ) { @@ -358,7 +370,11 @@ public function activate( $args, $assoc_args = array() ) { $successes = 0; $errors = 0; - $plugins = $this->fetcher->get_many( $args ); + + /** + * @var array $plugins + */ + $plugins = $this->fetcher->get_many( $args ); if ( count( $plugins ) < count( $args ) ) { $errors = count( $args ) - count( $plugins ); } @@ -387,7 +403,7 @@ public function activate( $args, $assoc_args = array() ) { if ( is_wp_error( $result ) ) { $message = $result->get_error_message(); - $message = preg_replace( '/]+>.*<\/a>/im', '', $message ); + $message = (string) preg_replace( '/]+>.*<\/a>/im', '', $message ); $message = wp_strip_all_tags( $message ); $message = str_replace( 'Error: ', '', $message ); WP_CLI::warning( "Failed to activate plugin. {$message}" ); @@ -438,9 +454,13 @@ public function activate( $args, $assoc_args = array() ) { * Success: Deactivated 2 of 2 plugins. */ public function deactivate( $args, $assoc_args = array() ) { - $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); - $disable_all = Utils\get_flag_value( $assoc_args, 'all' ); - $disable_all_exclude = Utils\get_flag_value( $assoc_args, 'exclude' ); + $network_wide = (bool) Utils\get_flag_value( $assoc_args, 'network' ); + $disable_all = (bool) Utils\get_flag_value( $assoc_args, 'all' ); + $disable_all_exclude = Utils\get_flag_value( $assoc_args, 'exclude', '' ); + + /** + * @var string $disable_all_exclude + */ $args = $this->check_optional_args_and_all( $args, $disable_all, 'deactivate', $disable_all_exclude ); if ( ! $args ) { @@ -574,6 +594,9 @@ public function path( $args, $assoc_args ) { $path = untrailingslashit( WP_PLUGIN_DIR ); if ( ! empty( $args ) ) { + /** + * @var object{name: string, file: string} $plugin + */ $plugin = $this->fetcher->get_check( $args[0] ); $path .= '/' . $plugin->file; @@ -591,6 +614,9 @@ protected function install_from_repo( $slug, $assoc_args ) { list($wp_core_version) = explode( '-', $wp_version ); $wp_core_version = implode( '.', array_slice( explode( '.', $wp_core_version ), 0, 2 ) ); + /** + * @var \WP_Error|PluginInformation $api + */ $api = plugins_api( 'plugin_information', array( 'slug' => $slug ) ); if ( is_wp_error( $api ) ) { @@ -726,7 +752,7 @@ protected function install_from_repo( $slug, $assoc_args ) { * @alias upgrade */ public function update( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { @@ -755,6 +781,9 @@ protected function get_item_list() { $auto_updates = []; } + /** + * @var string[] $recently_active + */ $recently_active = is_network_admin() ? get_site_option( 'recently_activated' ) : get_option( 'recently_activated' ); if ( false === $recently_active ) { @@ -875,7 +904,7 @@ protected function get_item_list() { if ( isset( $plugin_update_info->requires ) && version_compare( $wp_version, $requires, '>=' ) ) { $reason = "This update requires WordPress version $plugin_update_info->requires, but the version installed is $wp_version."; - } elseif ( ! isset( $update_info['package'] ) ) { + } elseif ( ! isset( $plugin_update_info->package ) ) { $reason = 'Update file not provided. Contact author for more details'; } else { $reason = 'Update not available'; @@ -904,7 +933,7 @@ protected function get_item_list() { * * @param string $plugin_name The plugin slug. * - * @return string The status of the plugin, includes the last update date. + * @return array{status: string, last_updated: string|false, status?: string, last_updated?: string} The status of the plugin, includes the last update date. */ protected function get_wporg_data( $plugin_name ) { $data = [ @@ -947,10 +976,12 @@ protected function get_wporg_data( $plugin_name ) { $r_body = wp_remote_retrieve_body( $request ); if ( strpos( $r_body, 'pubDate' ) !== false ) { // Very raw check, not validating the format or anything else. - $xml = simplexml_load_string( $r_body ); - $xml_pub_date = $xml->xpath( '//pubDate' ); - if ( $xml_pub_date ) { - $data['last_updated'] = wp_date( 'Y-m-d', (string) strtotime( $xml_pub_date[0] ) ); + $xml = simplexml_load_string( $r_body ); + if ( false !== $xml ) { + $xml_pub_date = $xml->xpath( '//pubDate' ); + if ( $xml_pub_date ) { + $data['last_updated'] = wp_date( 'Y-m-d', strtotime( $xml_pub_date[0] ) ?: null ); + } } } @@ -1115,6 +1146,9 @@ public function get( $args, $assoc_args ) { 'status', ); + /** + * @var object{name: string, file: string} $plugin + */ $plugin = $this->fetcher->get_check( $args[0] ); $file = $plugin->file; @@ -1174,10 +1208,13 @@ public function get( $args, $assoc_args ) { * Success: Uninstalled 2 of 2 plugins. */ public function uninstall( $args, $assoc_args = array() ) { - - $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $all_exclude = Utils\get_flag_value( $assoc_args, 'exclude', false ); + /** + * @var string $all_exclude + */ + // Check if plugin names or --all is passed. $args = $this->check_optional_args_and_all( $args, $all, 'uninstall', $all_exclude ); if ( ! $args ) { @@ -1222,6 +1259,9 @@ public function uninstall( $args, $assoc_args = array() ) { if ( '.' !== $plugin_slug && ! empty( $plugin_translations[ $plugin_slug ] ) ) { $translations = $plugin_translations[ $plugin_slug ]; + /** + * @var \WP_Filesystem_Base $wp_filesystem + */ global $wp_filesystem; require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); @@ -1233,7 +1273,11 @@ public function uninstall( $args, $assoc_args = array() ) { $json_translation_files = glob( WP_LANG_DIR . '/plugins/' . $plugin_slug . '-' . $translation . '-*.json' ); if ( $json_translation_files ) { - array_map( array( $wp_filesystem, 'delete' ), $json_translation_files ); + /** + * @var callable $callback + */ + $callback = array( $wp_filesystem, 'delete' ); + array_map( $callback, $json_translation_files ); } } } @@ -1257,6 +1301,10 @@ public function uninstall( $args, $assoc_args = array() ) { // Remove deleted plugins from the plugin updates list. $current = get_site_transient( $this->upgrade_transient ); if ( $current ) { + /** + * @var object{response: array, checked: array}&\stdClass $current + */ + // Don't remove the plugins that weren't deleted. $deleted = array_diff( $deleted_plugin_files, $delete_errors ); @@ -1323,7 +1371,7 @@ public function is_installed( $args, $assoc_args = array() ) { * @subcommand is-active */ public function is_active( $args, $assoc_args = array() ) { - $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); + $network_wide = (bool) Utils\get_flag_value( $assoc_args, 'network' ); $plugin = $this->fetcher->get( $args[0] ); @@ -1367,9 +1415,13 @@ public function is_active( $args, $assoc_args = array() ) { * Success: Deleted 2 of 2 plugins. */ public function delete( $args, $assoc_args = array() ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $all_exclude = Utils\get_flag_value( $assoc_args, 'exclude', false ); + /** + * @var string $all_exclude + */ + // Check if plugin names or --all is passed. $args = $this->check_optional_args_and_all( $args, $all, 'delete', $all_exclude ); if ( ! $args ) { @@ -1505,7 +1557,11 @@ public function delete( $args, $assoc_args = array() ) { * @subcommand list */ public function list_( $_, $assoc_args ) { + /** + * @var string $fields + */ $fields = Utils\get_flag_value( $assoc_args, 'fields' ); + if ( ! empty( $fields ) ) { $fields = explode( ',', $fields ); $this->check_wporg['status'] = in_array( 'wporg_status', $fields, true ); @@ -1578,7 +1634,7 @@ private static function get_template_path( $template ) { /** * Gets the details of a plugin. * - * @param object + * @param string $file Plugin file name. * @return array */ private function get_details( $file ) { @@ -1591,8 +1647,8 @@ private function get_details( $file ) { /** * Performs deletion of plugin files * - * @param $plugin - Plugin fetcher object (name, file) - * @return bool - If plugin was deleted + * @param $plugin Plugin fetcher object (name, file) + * @return bool Whether plugin was deleted */ private function delete_plugin( $plugin ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound diff --git a/src/Theme_AutoUpdates_Command.php b/src/Theme_AutoUpdates_Command.php index f92a67b7..b886fb7e 100644 --- a/src/Theme_AutoUpdates_Command.php +++ b/src/Theme_AutoUpdates_Command.php @@ -74,7 +74,7 @@ public function __construct() { * Success: Enabled 1 of 1 theme auto-updates. */ public function enable( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); @@ -151,8 +151,8 @@ public function enable( $args, $assoc_args ) { * Success: Disabled 1 of 1 theme auto-updates. */ public function disable( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); - $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $enabled_only = (bool) Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { @@ -259,9 +259,9 @@ public function disable( $args, $assoc_args ) { * twentyseventeen */ public function status( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); - $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); - $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $enabled_only = (bool) Utils\get_flag_value( $assoc_args, 'enabled-only', false ); + $disabled_only = (bool) Utils\get_flag_value( $assoc_args, 'disabled-only', false ); if ( $enabled_only && $disabled_only ) { WP_CLI::error( diff --git a/src/Theme_Command.php b/src/Theme_Command.php index 50806cca..16999759 100644 --- a/src/Theme_Command.php +++ b/src/Theme_Command.php @@ -41,6 +41,8 @@ * Author: the WordPress team * * @package wp-cli + * + * @phpstan-type ThemeInformation object{name: string, slug: non-empty-string, version: string, new_version: string, download_link: string, requires_php?: string, requires?: string}&\stdClass */ class Theme_Command extends CommandWithUpgrade { @@ -211,8 +213,11 @@ protected function get_all_items() { * * $ wp theme activate twentysixteen * Success: Switched to 'Twenty Sixteen' theme. + * + * @param array $args + * @param array $assoc_args */ - public function activate( $args = array() ) { + public function activate( $args, $assoc_args = [] ) { $theme = $this->fetcher->get_check( $args[0] ); $errors = $theme->errors(); @@ -232,7 +237,7 @@ public function activate( $args = array() ) { WP_CLI::error( "The '{$theme->get_stylesheet()}' theme cannot be activated without its parent, '{$theme->get_template()}'." ); } - switch_theme( $theme->get_template(), $theme->get_stylesheet() ); + switch_theme( $theme->get_stylesheet() ); if ( $this->is_active_theme( $theme ) ) { WP_CLI::success( "Switched to '$name' theme." ); @@ -279,6 +284,9 @@ public function enable( $args, $assoc_args ) { WP_CLI::error( 'This is not a multisite installation.' ); } + /** + * @var \WP_Theme $theme + */ $theme = $this->fetcher->get_check( $args[0] ); $name = $theme->get( 'Name' ); @@ -290,6 +298,11 @@ public function enable( $args, $assoc_args ) { if ( empty( $allowed_themes ) ) { $allowed_themes = array(); } + + /** + * @var array $allowed_themes + */ + $allowed_themes[ $theme->get_stylesheet() ] = true; call_user_func( "update{$_site}_option", 'allowedthemes', $allowed_themes ); @@ -344,6 +357,11 @@ public function disable( $args, $assoc_args ) { # Add the current theme to the allowed themes option or site option $allowed_themes = call_user_func( "get{$_site}_option", 'allowedthemes' ); + + /** + * @var array $allowed_themes + */ + if ( ! empty( $allowed_themes[ $theme->get_stylesheet() ] ) ) { unset( $allowed_themes[ $theme->get_stylesheet() ] ); } @@ -400,6 +418,9 @@ protected function install_from_repo( $slug, $assoc_args ) { list($wp_core_version) = explode( '-', $wp_version ); $wp_core_version = implode( '.', array_slice( explode( '.', $wp_core_version ), 0, 2 ) ); + /** + * @var \WP_Error|ThemeInformation $api + */ $api = themes_api( 'theme_information', array( 'slug' => $slug ) ); if ( is_wp_error( $api ) ) { @@ -692,7 +713,7 @@ public function get( $args, $assoc_args ) { * @alias upgrade */ public function update( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { @@ -797,7 +818,7 @@ public function is_active( $args, $assoc_args = array() ) { */ public function delete( $args, $assoc_args ) { - $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all, 'delete' ); if ( ! $args ) { diff --git a/src/Theme_Mod_Command.php b/src/Theme_Mod_Command.php index 9aea4c87..ad439e76 100644 --- a/src/Theme_Mod_Command.php +++ b/src/Theme_Mod_Command.php @@ -232,6 +232,9 @@ public function remove( $args = array(), $assoc_args = array() ) { * # Set theme mod * $ wp theme mod set background_color 000000 * Success: Theme mod background_color set to 000000. + * + * @param string[] $args + * @param array $assoc_args */ public function set( $args = array(), $assoc_args = array() ) { list( $mod, $value ) = $args; diff --git a/src/WP_CLI/CommandWithUpgrade.php b/src/WP_CLI/CommandWithUpgrade.php index 5c03ae84..3950d5a1 100755 --- a/src/WP_CLI/CommandWithUpgrade.php +++ b/src/WP_CLI/CommandWithUpgrade.php @@ -11,6 +11,10 @@ use WP_CLI\Utils; use WP_Error; +/** + * @phpstan-import-type ThemeInformation from \Theme_Command + * @phpstan-import-type PluginInformation from \Plugin_Command + */ abstract class CommandWithUpgrade extends \WP_CLI_Command { protected $fetcher; @@ -66,8 +70,8 @@ abstract protected function get_upgrader_class( $force ); abstract protected function get_item_list(); /** - * @param array List of update candidates - * @param array List of item names + * @param array $items List of update candidates + * @param array $args List of item names * @return array List of update candidates */ abstract protected function filter_item_list( $items, $args ); @@ -80,6 +84,8 @@ abstract protected function status_single( $args ); abstract protected function install_from_repo( $slug, $assoc_args ); + abstract public function activate( $args, $assoc_args = [] ); + public function status( $args ) { // Force WordPress to check for updates. call_user_func( $this->upgrade_refresh ); @@ -197,13 +203,16 @@ public function install( $args, $assoc_args ) { $filter = false; // If a GitHub URL, do some guessing as to the correct plugin/theme directory. - if ( $is_remote && 'github.com' === $this->parse_url_host_component( $slug, PHP_URL_HOST ) + if ( $is_remote && 'github.com' === \WP_CLI\Utils\parse_url( $slug, PHP_URL_HOST ) // Don't attempt to rename ZIPs uploaded to the releases page or coming from a raw source. && ! preg_match( '#github\.com/[^/]+/[^/]+/(?:releases/download|raw)/#', $slug ) ) { $filter = function ( $source ) use ( $slug ) { - - $slug_dir = Utils\basename( $this->parse_url_host_component( $slug, PHP_URL_PATH ), '.zip' ); + /** + * @var string $path + */ + $path = \WP_CLI\Utils\parse_url( $slug, PHP_URL_PATH ); + $slug_dir = Utils\basename( $path, '.zip' ); // Don't use the zip name if archive attached to release, as name likely to contain version tag/branch. if ( preg_match( '#github\.com/[^/]+/([^/]+)/archive/#', $slug, $matches ) ) { @@ -215,7 +224,7 @@ public function install( $args, $assoc_args ) { if ( $source_dir === $slug_dir ) { return $source; } - $new_path = substr_replace( $source, $slug_dir, strrpos( $source, $source_dir ), strlen( $source_dir ) ); + $new_path = substr_replace( $source, $slug_dir, (int) strrpos( $source, $source_dir ), strlen( $source_dir ) ); if ( $GLOBALS['wp_filesystem']->move( $source, $new_path ) ) { WP_CLI::log( sprintf( "Renamed Github-based project from '%s' to '%s'.", $source_dir, $slug_dir ) ); @@ -294,8 +303,10 @@ public function install( $args, $assoc_args ) { /** * Prepare an API response for downloading a particular version of an item. * - * @param object $response wordpress.org API response - * @param string $version The desired version of the package + * @param object $response Wordpress.org API response. + * @param string $version The desired version of the package. + * + * @phpstan-param PluginInformation|ThemeInformation $response */ protected static function alter_api_response( $response, $version ) { if ( $response->version === $version ) { @@ -397,6 +408,9 @@ function ( $item ) { $items_to_update = self::get_minor_or_patch_updates( $items_to_update, $type, $insecure, true, $this->item_type ); } + /** + * @var string|null $exclude + */ $exclude = Utils\get_flag_value( $assoc_args, 'exclude' ); if ( isset( $exclude ) ) { $exclude_items = explode( ',', trim( $assoc_args['exclude'], ',' ) ); @@ -475,6 +489,9 @@ function ( $item ) { foreach ( $items_to_update as $name => $item_data ) { if ( isset( $transient->response[ $name ] ) ) { if ( is_object( $transient->response[ $name ] ) ) { + /** + * @var object{response: array} $transient + */ $transient->response[ $name ]->new_version = $item_data['update_version']; $transient->response[ $name ]->package = $item_data['update_package']; } else { @@ -490,6 +507,10 @@ function ( $item ) { remove_filter( 'site_transient_' . $this->upgrade_transient, $transient_filter, 999 ); } + /** + * @var array $items_to_update + */ + // Let the user know the results. $num_to_update = count( $items_to_update ); $num_updated = count( @@ -624,6 +645,9 @@ function ( $value ) { * @return bool */ protected function has_update( $slug ) { + /** + * @var object{checked: array, response: array, no_update: array} $update_list + */ $update_list = get_site_transient( $this->upgrade_transient ); return isset( $update_list->response[ $slug ] ); @@ -632,10 +656,15 @@ protected function has_update( $slug ) { /** * Get the available update info * - * @return mixed + * @return object{checked: array, response: array, no_update: array} $update_list */ protected function get_update_info() { - return get_site_transient( $this->upgrade_transient ); + /** + * @var object{checked: array, response: array, no_update: array} $update_list + */ + $update_list = get_site_transient( $this->upgrade_transient ); + + return $update_list; } private $map = [ @@ -688,8 +717,12 @@ private function get_minor_or_patch_updates( $items, $type, $insecure, $require_ $wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] ); foreach ( $items as $i => $item ) { try { - $data = call_user_func( - [ $wp_org_api, "get_{$item_type}_info" ], + /** + * @var callable $callback + */ + $callback = [ $wp_org_api, "get_{$item_type}_info" ]; + $data = call_user_func( + $callback, $item['name'], // The default. 'en_US', @@ -780,9 +813,15 @@ protected function _search( $args, $assoc_args ) { if ( 'plugin' === $this->item_type ) { $api = plugins_api( 'query_plugins', $api_args ); } else { + // fields[screenshot_count] could be an int, not a bool. + // @phpstan-ignore argument.type $api = themes_api( 'query_themes', $api_args ); } + /** + * @var \WP_Error|object{info: object{page: int, pages: int, results: int}} $api + */ + if ( is_wp_error( $api ) ) { WP_CLI::error( $api->get_error_message() . __( ' Try again' ) ); } @@ -803,7 +842,10 @@ protected function _search( $args, $assoc_args ) { } if ( 'table' === $format ) { - $count = Utils\get_flag_value( $api->info, 'results', 'unknown' ); + /** + * @var string $count + */ + $count = Utils\get_flag_value( (array) $api->info, 'results', 'unknown' ); WP_CLI::success( sprintf( 'Showing %s of %s %s.', count( $items ), $count, $plural ) ); } @@ -832,18 +874,6 @@ public static function error_handler( $errno, $errstr, $errfile, $errline, $errc return true; } - /** - * Retrieves PHP_URL_HOST component from URL. - * - * @param int $component The component to retrieve. - * - * @return string - */ - private function parse_url_host_component( $url, $component ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url. - return function_exists( 'wp_parse_url' ) ? wp_parse_url( $url, $component ) : parse_url( $url, $component ); - } - /** * Add versioned GitHub URLs to cache allowlist. * @@ -895,6 +925,9 @@ protected function get_the_latest_github_version( $repo_slug ) { } if ( 404 === wp_remote_retrieve_response_code( $response ) ) { + /** + * @var object{status: string, message: string} $decoded_body + */ return new \WP_Error( $decoded_body->status, $decoded_body->message @@ -905,6 +938,10 @@ protected function get_the_latest_github_version( $repo_slug ) { return new \WP_Error( 500, 'Empty response received from GitHub.com API' ); } + /** + * @var array $decoded_body + */ + if ( ! isset( $decoded_body[0] ) ) { return new \WP_Error( '400', 'The given Github repository does not have any releases' ); } diff --git a/src/WP_CLI/Fetchers/Plugin.php b/src/WP_CLI/Fetchers/Plugin.php index e7528e9e..7a0e03b9 100644 --- a/src/WP_CLI/Fetchers/Plugin.php +++ b/src/WP_CLI/Fetchers/Plugin.php @@ -15,8 +15,8 @@ class Plugin extends Base { /** * Get a plugin object by name * - * @param string $name - * @return object|false + * @param string $name Plugin name. + * @return object{name: string, file: string}|false */ public function get( $name ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. diff --git a/src/WP_CLI/ParseThemeNameInput.php b/src/WP_CLI/ParseThemeNameInput.php index 6fb529d4..e765dadc 100644 --- a/src/WP_CLI/ParseThemeNameInput.php +++ b/src/WP_CLI/ParseThemeNameInput.php @@ -54,11 +54,17 @@ private function get_all_themes() { $theme_version_info = array(); if ( is_multisite() ) { + /** + * @var array} $site_enabled + */ $site_enabled = get_option( 'allowedthemes' ); if ( empty( $site_enabled ) ) { $site_enabled = array(); } + /** + * @var array} $network_enabled + */ $network_enabled = get_site_option( 'allowedthemes' ); if ( empty( $network_enabled ) ) { $network_enabled = array(); @@ -169,7 +175,9 @@ private function get_all_themes() { * @return bool|string */ protected function is_theme_version_valid( $slug, $version ) { - // Get Theme Info. + /** + * @var \WP_Error|object{name: string, slug: string, version: string, download_link: string} $theme_info + */ $theme_info = themes_api( 'theme_information', array( 'slug' => $slug ) ); // Return empty string for themes not on WP.org. @@ -184,7 +192,7 @@ protected function is_theme_version_valid( $slug, $version ) { /** * Get the status for a given theme. * - * @param WP_Theme $theme Theme to get the status for. + * @param \WP_Theme $theme Theme to get the status for. * * @return string Status of the theme. */ @@ -203,7 +211,7 @@ protected function get_status( $theme ) { /** * Check whether a given theme is the active theme. * - * @param WP_Theme $theme Theme to check. + * @param \WP_Theme $theme Theme to check. * * @return bool Whether the provided theme is the active theme. */ @@ -214,7 +222,7 @@ protected function is_active_theme( $theme ) { /** * Check whether a given theme is the active theme parent. * - * @param WP_Theme $theme Theme to check. + * @param \WP_Theme $theme Theme to check. * * @return bool Whether the provided theme is the active theme. */ @@ -225,9 +233,14 @@ protected function is_active_parent_theme( $theme ) { /** * Get the available update info. * - * @return mixed Available update info. + * @return object{checked: array, response: array, no_update: array} Available update info. */ protected function get_update_info() { - return get_site_transient( 'update_themes' ); + /** + * @var object{checked: array, response: array, no_update: array} $result + */ + $result = get_site_transient( 'update_themes' ); + + return $result; } } From 1975695dd1feaadf19c1b670c1316a35ce834980 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 19 May 2025 10:33:01 +0200 Subject: [PATCH 2/9] Scan main file too --- phpstan.neon.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 37be17de..8027f36e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,6 +2,7 @@ parameters: level: 9 paths: - src + - extension-command.php scanDirectories: - vendor/wp-cli/wp-cli/php - vendor/wp-cli/wp-cli-tests From f7d3b4c19063fd2906c5a18932b2f400f6af1786 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Jun 2025 11:15:11 +0200 Subject: [PATCH 3/9] Apply suggestions from code review Co-authored-by: Alain Schlesser --- src/Plugin_Command.php | 2 +- src/WP_CLI/CommandWithUpgrade.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 6a3b2f4e..8643c30f 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1276,7 +1276,7 @@ public function uninstall( $args, $assoc_args = array() ) { /** * @var callable $callback */ - $callback = array( $wp_filesystem, 'delete' ); + $callback = [ $wp_filesystem, 'delete' ]; array_map( $callback, $json_translation_files ); } } diff --git a/src/WP_CLI/CommandWithUpgrade.php b/src/WP_CLI/CommandWithUpgrade.php index 3950d5a1..06ebce19 100755 --- a/src/WP_CLI/CommandWithUpgrade.php +++ b/src/WP_CLI/CommandWithUpgrade.php @@ -203,7 +203,7 @@ public function install( $args, $assoc_args ) { $filter = false; // If a GitHub URL, do some guessing as to the correct plugin/theme directory. - if ( $is_remote && 'github.com' === \WP_CLI\Utils\parse_url( $slug, PHP_URL_HOST ) + if ( $is_remote && 'github.com' === Utils\parse_url( $slug, PHP_URL_HOST ) // Don't attempt to rename ZIPs uploaded to the releases page or coming from a raw source. && ! preg_match( '#github\.com/[^/]+/[^/]+/(?:releases/download|raw)/#', $slug ) ) { @@ -211,7 +211,7 @@ public function install( $args, $assoc_args ) { /** * @var string $path */ - $path = \WP_CLI\Utils\parse_url( $slug, PHP_URL_PATH ); + $path = Utils\parse_url( $slug, PHP_URL_PATH ); $slug_dir = Utils\basename( $path, '.zip' ); // Don't use the zip name if archive attached to release, as name likely to contain version tag/branch. From 1c11924d3ffe3cb2d2e33563d6d994ee8a1b6ec1 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Jun 2025 11:22:13 +0200 Subject: [PATCH 4/9] More consistent signaturs & type hints --- src/Plugin_AutoUpdates_Command.php | 4 ++-- src/Theme_Mod_Command.php | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Plugin_AutoUpdates_Command.php b/src/Plugin_AutoUpdates_Command.php index c2303b43..7ee17cdd 100644 --- a/src/Plugin_AutoUpdates_Command.php +++ b/src/Plugin_AutoUpdates_Command.php @@ -73,8 +73,8 @@ public function __construct() { * Plugin auto-updates for 'hello' enabled. * Success: Enabled 1 of 1 plugin auto-updates. * - * @param array $args - * @param array $assoc_args + * @param string[] $args Positional arguments. + * @param array{all?: bool, 'disabled-only'?: bool} $assoc_args Associative arguments. */ public function enable( $args, $assoc_args ) { $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); diff --git a/src/Theme_Mod_Command.php b/src/Theme_Mod_Command.php index ad439e76..b9340354 100644 --- a/src/Theme_Mod_Command.php +++ b/src/Theme_Mod_Command.php @@ -74,8 +74,11 @@ class Theme_Mod_Command extends WP_CLI_Command { * | background_color | dd3333 | * | header_textcolor | | * +------------------+--------+ + * + * @param string[] $args Positional arguments. + * @param array{field?: string, all?: bool, format: string} $assoc_args Associative arguments. */ - public function get( $args = array(), $assoc_args = array() ) { + public function get( $args, $assoc_args ) { if ( ! \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) { WP_CLI::error( 'You must specify at least one mod or use --all.' ); @@ -162,10 +165,13 @@ public function get( $args = array(), $assoc_args = array() ) { * +------------------+---------+ * * @subcommand list + * + * @param string[] $args Positional arguments. Unused. + * @param array{field?: string, format: string} $assoc_args Associative arguments. */ - public function list_( $args = array(), $assoc_args = array() ) { + public function list_( $args, $assoc_args ) { - $assoc_args['all'] = 1; + $assoc_args['all'] = true; $this->get( $args, $assoc_args ); } @@ -194,8 +200,11 @@ public function list_( $args = array(), $assoc_args = array() ) { * # Remove multiple theme mods. * $ wp theme mod remove background_color header_textcolor * Success: 2 mods removed. + * + * @param string[] $args Positional arguments. + * @param array{all?: bool} $assoc_args Associative arguments. */ - public function remove( $args = array(), $assoc_args = array() ) { + public function remove( $args, $assoc_args, ) { if ( ! \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) { WP_CLI::error( 'You must specify at least one mod or use --all.' ); @@ -233,10 +242,10 @@ public function remove( $args = array(), $assoc_args = array() ) { * $ wp theme mod set background_color 000000 * Success: Theme mod background_color set to 000000. * - * @param string[] $args - * @param array $assoc_args + * @param array{0: string, 1: string} $args Positional arguments. + * @param array $assoc_args Associative arguments. Unused. */ - public function set( $args = array(), $assoc_args = array() ) { + public function set( $args, $assoc_args ) { list( $mod, $value ) = $args; set_theme_mod( $mod, $value ); From 153657023fcbe823b933fb0637b8f2655f0e6e43 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Jun 2025 11:22:18 +0200 Subject: [PATCH 5/9] use latest and greatest --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 873100dc..bb90468a 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "wp-cli/entity-command": "^1.3 || ^2", "wp-cli/language-command": "^2.0", "wp-cli/scaffold-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "dev-main" + "wp-cli/wp-cli-tests": "dev-add/phpstan-enhancements" }, "config": { "process-timeout": 7200, From b0fafd74aaf628d6ee5f35b9cc836646574f0ce9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Jun 2025 12:38:14 +0200 Subject: [PATCH 6/9] More fixes --- phpstan.neon.dist | 4 --- src/Plugin_AutoUpdates_Command.php | 8 +++--- src/Plugin_Command.php | 46 ++++++++++++++++-------------- src/Theme_AutoUpdates_Command.php | 15 ++++++---- src/Theme_Command.php | 16 +++++++---- src/Theme_Mod_Command.php | 2 +- src/WP_CLI/CommandWithUpgrade.php | 33 +++++++++++++++------ src/WP_CLI/Fetchers/Plugin.php | 2 ++ src/WP_CLI/Fetchers/Theme.php | 2 ++ src/WP_CLI/ParseThemeNameInput.php | 9 ++++-- 10 files changed, 83 insertions(+), 54 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 8027f36e..11b95961 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,10 +9,6 @@ parameters: 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 diff --git a/src/Plugin_AutoUpdates_Command.php b/src/Plugin_AutoUpdates_Command.php index 7ee17cdd..229f8442 100644 --- a/src/Plugin_AutoUpdates_Command.php +++ b/src/Plugin_AutoUpdates_Command.php @@ -77,8 +77,8 @@ public function __construct() { * @param array{all?: bool, 'disabled-only'?: bool} $assoc_args Associative arguments. */ public function enable( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); - $disabled_only = (bool) Utils\get_flag_value( $assoc_args, 'disabled-only', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { @@ -154,7 +154,7 @@ public function enable( $args, $assoc_args ) { * Success: Disabled 1 of 1 plugin auto-updates. */ public function disable( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); @@ -262,7 +262,7 @@ public function disable( $args, $assoc_args ) { * duplicate-post */ public function status( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 8643c30f..e76224bd 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1,5 +1,6 @@ */ -class Plugin_Command extends \WP_CLI\CommandWithUpgrade { - +class Plugin_Command extends CommandWithUpgrade { use ParsePluginNameInput; protected $item_type = 'plugin'; @@ -72,6 +73,8 @@ class Plugin_Command extends \WP_CLI\CommandWithUpgrade { * Plugin fetcher instance. * * @var \WP_CLI\Fetchers\Plugin + * + * @phpstan-ignore property.phpDocType (To be fixed with in https://github.com/wp-cli/wp-cli/pull/6096) */ protected $fetcher; @@ -354,9 +357,9 @@ protected function get_all_items() { * @param array $args * @param array $assoc_args */ - public function activate( $args, $assoc_args = array() ) { - $network_wide = (bool) Utils\get_flag_value( $assoc_args, 'network', false ); - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + public function activate( $args, $assoc_args = [] ) { + $network_wide = Utils\get_flag_value( $assoc_args, 'network', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $all_exclude = Utils\get_flag_value( $assoc_args, 'exclude', '' ); /** @@ -453,9 +456,9 @@ public function activate( $args, $assoc_args = array() ) { * Plugin 'ninja-forms' deactivated. * Success: Deactivated 2 of 2 plugins. */ - public function deactivate( $args, $assoc_args = array() ) { - $network_wide = (bool) Utils\get_flag_value( $assoc_args, 'network' ); - $disable_all = (bool) Utils\get_flag_value( $assoc_args, 'all' ); + public function deactivate( $args, $assoc_args = [] ) { + $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); + $disable_all = Utils\get_flag_value( $assoc_args, 'all' ); $disable_all_exclude = Utils\get_flag_value( $assoc_args, 'exclude', '' ); /** @@ -550,7 +553,7 @@ public function deactivate( $args, $assoc_args = array() ) { * Plugin 'akismet' activated. * Success: Toggled 1 of 1 plugins. */ - public function toggle( $args, $assoc_args = array() ) { + public function toggle( $args, $assoc_args ) { $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); $successes = 0; @@ -752,7 +755,7 @@ protected function install_from_repo( $slug, $assoc_args ) { * @alias upgrade */ public function update( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { @@ -792,10 +795,11 @@ protected function get_item_list() { foreach ( $this->get_all_plugins() as $file => $details ) { $all_update_info = $this->get_update_info(); - $update_info = ( isset( $all_update_info->response[ $file ] ) && null !== $all_update_info->response[ $file ] ) ? (array) $all_update_info->response[ $file ] : null; - $name = Utils\get_plugin_name( $file ); - $wporg_info = $this->get_wporg_data( $name ); - $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); + // @phpstan-ignore notIdentical.alwaysTrue + $update_info = ( isset( $all_update_info->response[ $file ] ) && null !== $all_update_info->response[ $file ] ) ? (array) $all_update_info->response[ $file ] : null; + $name = Utils\get_plugin_name( $file ); + $wporg_info = $this->get_wporg_data( $name ); + $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); if ( ! isset( $duplicate_names[ $name ] ) ) { $duplicate_names[ $name ] = array(); @@ -1207,8 +1211,8 @@ public function get( $args, $assoc_args ) { * Uninstalled and deleted 'tinymce-templates' plugin. * Success: Uninstalled 2 of 2 plugins. */ - public function uninstall( $args, $assoc_args = array() ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + public function uninstall( $args, $assoc_args = [] ) { + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $all_exclude = Utils\get_flag_value( $assoc_args, 'exclude', false ); /** @@ -1340,7 +1344,7 @@ public function uninstall( $args, $assoc_args = array() ) { * * @subcommand is-installed */ - public function is_installed( $args, $assoc_args = array() ) { + public function is_installed( $args, $assoc_args ) { if ( $this->fetcher->get( $args[0] ) ) { WP_CLI::halt( 0 ); } else { @@ -1370,8 +1374,8 @@ public function is_installed( $args, $assoc_args = array() ) { * * @subcommand is-active */ - public function is_active( $args, $assoc_args = array() ) { - $network_wide = (bool) Utils\get_flag_value( $assoc_args, 'network' ); + public function is_active( $args, $assoc_args ) { + $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); $plugin = $this->fetcher->get( $args[0] ); @@ -1414,8 +1418,8 @@ public function is_active( $args, $assoc_args = array() ) { * Deleted 'tinymce-templates' plugin. * Success: Deleted 2 of 2 plugins. */ - public function delete( $args, $assoc_args = array() ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + public function delete( $args, $assoc_args ) { + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $all_exclude = Utils\get_flag_value( $assoc_args, 'exclude', false ); /** diff --git a/src/Theme_AutoUpdates_Command.php b/src/Theme_AutoUpdates_Command.php index b886fb7e..fafcc815 100644 --- a/src/Theme_AutoUpdates_Command.php +++ b/src/Theme_AutoUpdates_Command.php @@ -28,6 +28,9 @@ */ class Theme_AutoUpdates_Command { + /** + * @use ParseThemeNameInput<\WP_Theme> + */ use ParseThemeNameInput; /** @@ -74,7 +77,7 @@ public function __construct() { * Success: Enabled 1 of 1 theme auto-updates. */ public function enable( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); @@ -151,8 +154,8 @@ public function enable( $args, $assoc_args ) { * Success: Disabled 1 of 1 theme auto-updates. */ public function disable( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); - $enabled_only = (bool) Utils\get_flag_value( $assoc_args, 'enabled-only', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { @@ -259,9 +262,9 @@ public function disable( $args, $assoc_args ) { * twentyseventeen */ public function status( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); - $enabled_only = (bool) Utils\get_flag_value( $assoc_args, 'enabled-only', false ); - $disabled_only = (bool) Utils\get_flag_value( $assoc_args, 'disabled-only', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); + $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); if ( $enabled_only && $disabled_only ) { WP_CLI::error( diff --git a/src/Theme_Command.php b/src/Theme_Command.php index 16999759..5ae8459f 100644 --- a/src/Theme_Command.php +++ b/src/Theme_Command.php @@ -43,9 +43,13 @@ * @package wp-cli * * @phpstan-type ThemeInformation object{name: string, slug: non-empty-string, version: string, new_version: string, download_link: string, requires_php?: string, requires?: string}&\stdClass + * @extends CommandWithUpgrade<\WP_Theme> */ class Theme_Command extends CommandWithUpgrade { + /** + * @use ParseThemeNameInput<\WP_Theme> + */ use ParseThemeNameInput; protected $item_type = 'theme'; @@ -214,8 +218,8 @@ protected function get_all_items() { * $ wp theme activate twentysixteen * Success: Switched to 'Twenty Sixteen' theme. * - * @param array $args - * @param array $assoc_args + * @param string[] $args Positional arguments. + * @param array $assoc_args Associative arguments. Unused. */ public function activate( $args, $assoc_args = [] ) { $theme = $this->fetcher->get_check( $args[0] ); @@ -713,7 +717,7 @@ public function get( $args, $assoc_args ) { * @alias upgrade */ public function update( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { @@ -753,7 +757,7 @@ public function update( $args, $assoc_args ) { * * @subcommand is-installed */ - public function is_installed( $args, $assoc_args = array() ) { + public function is_installed( $args, $assoc_args ) { $theme = wp_get_theme( $args[0] ); if ( $theme->exists() ) { @@ -782,7 +786,7 @@ public function is_installed( $args, $assoc_args = array() ) { * * @subcommand is-active */ - public function is_active( $args, $assoc_args = array() ) { + public function is_active( $args, $assoc_args ) { $theme = wp_get_theme( $args[0] ); if ( ! $theme->exists() ) { @@ -818,7 +822,7 @@ public function is_active( $args, $assoc_args = array() ) { */ public function delete( $args, $assoc_args ) { - $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all, 'delete' ); if ( ! $args ) { diff --git a/src/Theme_Mod_Command.php b/src/Theme_Mod_Command.php index b9340354..f5525332 100644 --- a/src/Theme_Mod_Command.php +++ b/src/Theme_Mod_Command.php @@ -204,7 +204,7 @@ public function list_( $args, $assoc_args ) { * @param string[] $args Positional arguments. * @param array{all?: bool} $assoc_args Associative arguments. */ - public function remove( $args, $assoc_args, ) { + public function remove( $args, $assoc_args ) { if ( ! \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) { WP_CLI::error( 'You must specify at least one mod or use --all.' ); diff --git a/src/WP_CLI/CommandWithUpgrade.php b/src/WP_CLI/CommandWithUpgrade.php index 06ebce19..c945c47e 100755 --- a/src/WP_CLI/CommandWithUpgrade.php +++ b/src/WP_CLI/CommandWithUpgrade.php @@ -14,6 +14,8 @@ /** * @phpstan-import-type ThemeInformation from \Theme_Command * @phpstan-import-type PluginInformation from \Plugin_Command + * + * @template T */ abstract class CommandWithUpgrade extends \WP_CLI_Command { @@ -78,12 +80,25 @@ abstract protected function filter_item_list( $items, $args ); abstract protected function get_all_items(); + /** + * Get the status for a given extension. + * + * @param T $file Extension to get the status for. + * + * @return string Status of the extension. + */ abstract protected function get_status( $file ); abstract protected function status_single( $args ); abstract protected function install_from_repo( $slug, $assoc_args ); + /** + * Activates an extension. + * + * @param string[] $args Positional arguments. + * @param array $assoc_args Associative arguments. + */ abstract public function activate( $args, $assoc_args = [] ); public function status( $args ) { @@ -357,8 +372,8 @@ protected static function alter_api_response( $response, $version ) { } protected function get_upgrader( $assoc_args ) { - $force = (bool) Utils\get_flag_value( $assoc_args, 'force', false ); - $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); + $force = Utils\get_flag_value( $assoc_args, 'force', false ); + $insecure = Utils\get_flag_value( $assoc_args, 'insecure', false ); $upgrader_class = $this->get_upgrader_class( $force ); return Utils\get_upgrader( $upgrader_class, $insecure ); } @@ -395,15 +410,15 @@ function ( $item ) { } ); - $minor = (bool) Utils\get_flag_value( $assoc_args, 'minor', false ); - $patch = (bool) Utils\get_flag_value( $assoc_args, 'patch', false ); + $minor = Utils\get_flag_value( $assoc_args, 'minor', false ); + $patch = Utils\get_flag_value( $assoc_args, 'patch', false ); if ( in_array( $this->item_type, [ 'plugin', 'theme' ], true ) && ( $minor || $patch ) ) { $type = $minor ? 'minor' : 'patch'; - $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); + $insecure = Utils\get_flag_value( $assoc_args, 'insecure', false ); $items_to_update = self::get_minor_or_patch_updates( $items_to_update, $type, $insecure, true, $this->item_type ); } @@ -566,14 +581,14 @@ static function ( $result ) { protected function _list( $_, $assoc_args ) { // Force WordPress to check for updates if `--skip-update-check` is not passed. - if ( false === (bool) Utils\get_flag_value( $assoc_args, 'skip-update-check', false ) ) { + if ( false === Utils\get_flag_value( $assoc_args, 'skip-update-check', false ) ) { delete_site_transient( $this->upgrade_transient ); call_user_func( $this->upgrade_refresh ); } $all_items = $this->get_all_items(); - if ( false !== (bool) Utils\get_flag_value( $assoc_args, 'recently-active', false ) ) { + if ( false !== Utils\get_flag_value( $assoc_args, 'recently-active', false ) ) { $all_items = array_filter( $all_items, function ( $value ) { @@ -656,11 +671,11 @@ protected function has_update( $slug ) { /** * Get the available update info * - * @return object{checked: array, response: array, no_update: array} $update_list + * @return object{checked: array, response: array>, no_update: array} $update_list */ protected function get_update_info() { /** - * @var object{checked: array, response: array, no_update: array} $update_list + * @var object{checked: array, response: array>, no_update: array} $update_list */ $update_list = get_site_transient( $this->upgrade_transient ); diff --git a/src/WP_CLI/Fetchers/Plugin.php b/src/WP_CLI/Fetchers/Plugin.php index 7a0e03b9..221919b1 100644 --- a/src/WP_CLI/Fetchers/Plugin.php +++ b/src/WP_CLI/Fetchers/Plugin.php @@ -17,6 +17,8 @@ class Plugin extends Base { * * @param string $name Plugin name. * @return object{name: string, file: string}|false + * + * @phpstan-ignore method.childParameterType (To be fixed with in https://github.com/wp-cli/wp-cli/pull/6096) */ public function get( $name ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. diff --git a/src/WP_CLI/Fetchers/Theme.php b/src/WP_CLI/Fetchers/Theme.php index 126d333f..9e1b5b00 100644 --- a/src/WP_CLI/Fetchers/Theme.php +++ b/src/WP_CLI/Fetchers/Theme.php @@ -19,6 +19,8 @@ class Theme extends Base { * * @param string $name * @return object|false + * + * @phpstan-ignore method.childParameterType (To be fixed with in https://github.com/wp-cli/wp-cli/pull/6096) */ public function get( $name ) { // Workaround to equalize folder naming conventions across Win/Mac/Linux. diff --git a/src/WP_CLI/ParseThemeNameInput.php b/src/WP_CLI/ParseThemeNameInput.php index e765dadc..6b6433c3 100644 --- a/src/WP_CLI/ParseThemeNameInput.php +++ b/src/WP_CLI/ParseThemeNameInput.php @@ -5,6 +5,9 @@ use WP_CLI; use Theme_AutoUpdates_Command; +/** + * @template T of \WP_Theme + */ trait ParseThemeNameInput { /** @@ -192,7 +195,7 @@ protected function is_theme_version_valid( $slug, $version ) { /** * Get the status for a given theme. * - * @param \WP_Theme $theme Theme to get the status for. + * @param T $theme Theme to get the status for. * * @return string Status of the theme. */ @@ -233,11 +236,11 @@ protected function is_active_parent_theme( $theme ) { /** * Get the available update info. * - * @return object{checked: array, response: array, no_update: array} Available update info. + * @return object{checked: array, response: array>, no_update: array} Available update info. */ protected function get_update_info() { /** - * @var object{checked: array, response: array, no_update: array} $result + * @var object{checked: array, response: array>, no_update: array} $result */ $result = get_site_transient( 'update_themes' ); From 0ef0b14ea6f2e0e34b0def08663070a222403b06 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 3 Jul 2025 14:25:38 +0200 Subject: [PATCH 7/9] Use wp-cli-tests v5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bb90468a..e45fab00 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "wp-cli/entity-command": "^1.3 || ^2", "wp-cli/language-command": "^2.0", "wp-cli/scaffold-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "dev-add/phpstan-enhancements" + "wp-cli/wp-cli-tests": "^5" }, "config": { "process-timeout": 7200, From e0386c5f0fda54d7cdacb03c6fa6e2191f1a08f6 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 3 Jul 2025 14:25:55 +0200 Subject: [PATCH 8/9] Update phpstan config --- phpstan.neon.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 11b95961..abaa502d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,7 +5,6 @@ parameters: - extension-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 374a34d3e29d12dfffa51eb20e88c9a326af4c78 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 10 Jul 2025 17:46:24 +0200 Subject: [PATCH 9/9] Update with latest framework version --- composer.json | 2 +- src/Plugin_Command.php | 9 --------- src/WP_CLI/CommandWithUpgrade.php | 3 +++ src/WP_CLI/Fetchers/Plugin.php | 8 +++++--- src/WP_CLI/Fetchers/Theme.php | 10 +++++----- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index e45fab00..4827a65c 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ], "require": { "composer/semver": "^1.4 || ^2 || ^3", - "wp-cli/wp-cli": "^2.12" + "wp-cli/wp-cli": "^2.13" }, "require-dev": { "wp-cli/cache-command": "^2.0", diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index e76224bd..3a395b75 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -69,15 +69,6 @@ class Plugin_Command extends CommandWithUpgrade { 'auto_update', ); - /** - * Plugin fetcher instance. - * - * @var \WP_CLI\Fetchers\Plugin - * - * @phpstan-ignore property.phpDocType (To be fixed with in https://github.com/wp-cli/wp-cli/pull/6096) - */ - protected $fetcher; - public function __construct() { require_once ABSPATH . 'wp-admin/includes/plugin.php'; require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; diff --git a/src/WP_CLI/CommandWithUpgrade.php b/src/WP_CLI/CommandWithUpgrade.php index c945c47e..798d8ca9 100755 --- a/src/WP_CLI/CommandWithUpgrade.php +++ b/src/WP_CLI/CommandWithUpgrade.php @@ -67,6 +67,9 @@ function () { $this->fetcher = new Fetchers\Plugin(); } + /** + * @return class-string<\WP_Upgrader> + */ abstract protected function get_upgrader_class( $force ); abstract protected function get_item_list(); diff --git a/src/WP_CLI/Fetchers/Plugin.php b/src/WP_CLI/Fetchers/Plugin.php index 221919b1..bffe012b 100644 --- a/src/WP_CLI/Fetchers/Plugin.php +++ b/src/WP_CLI/Fetchers/Plugin.php @@ -4,6 +4,8 @@ /** * Fetch a WordPress plugin based on one of its attributes. + * + * @extends Base */ class Plugin extends Base { @@ -15,12 +17,12 @@ class Plugin extends Base { /** * Get a plugin object by name * - * @param string $name Plugin name. + * @param string|int $name Plugin name. * @return object{name: string, file: string}|false - * - * @phpstan-ignore method.childParameterType (To be fixed with in https://github.com/wp-cli/wp-cli/pull/6096) */ public function get( $name ) { + $name = (string) $name; + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. foreach ( apply_filters( 'all_plugins', get_plugins() ) as $file => $_ ) { if ( "$name.php" === $file || diff --git a/src/WP_CLI/Fetchers/Theme.php b/src/WP_CLI/Fetchers/Theme.php index 9e1b5b00..e2e99ef2 100644 --- a/src/WP_CLI/Fetchers/Theme.php +++ b/src/WP_CLI/Fetchers/Theme.php @@ -6,6 +6,8 @@ /** * Fetch a WordPress theme based on one of its attributes. + * + * @extends Base<\WP_Theme> */ class Theme extends Base { @@ -17,10 +19,8 @@ class Theme extends Base { /** * Get a theme object by name * - * @param string $name - * @return object|false - * - * @phpstan-ignore method.childParameterType (To be fixed with in https://github.com/wp-cli/wp-cli/pull/6096) + * @param string|int $name + * @return \WP_Theme|false */ public function get( $name ) { // Workaround to equalize folder naming conventions across Win/Mac/Linux. @@ -28,7 +28,7 @@ public function get( $name ) { $existing_themes = wp_get_themes( array( 'errors' => null ) ); $existing_stylesheets = array_keys( $existing_themes ); if ( ! in_array( $name, $existing_stylesheets, true ) ) { - $inexact_match = $this->find_inexact_match( $name, $existing_themes ); + $inexact_match = $this->find_inexact_match( (string) $name, $existing_themes ); if ( false !== $inexact_match ) { $this->msg .= sprintf( " Did you mean '%s'?", $inexact_match ); }