diff --git a/composer.json b/composer.json index e1fd5854..4827a65c 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,14 @@ ], "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", "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": "^5.0.1" + "wp-cli/wp-cli-tests": "^5" }, "config": { "process-timeout": 7200, diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..abaa502d --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,15 @@ +parameters: + level: 9 + paths: + - src + - extension-command.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + treatPhpDocTypesAsCertain: false + 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..229f8442 100644 --- a/src/Plugin_AutoUpdates_Command.php +++ b/src/Plugin_AutoUpdates_Command.php @@ -72,6 +72,9 @@ public function __construct() { * $ wp plugin auto-updates enable hello * Plugin auto-updates for 'hello' enabled. * Success: Enabled 1 of 1 plugin auto-updates. + * + * @param string[] $args Positional arguments. + * @param array{all?: bool, 'disabled-only'?: bool} $assoc_args Associative arguments. */ public function enable( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 6366262b..ef5b061f 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'; @@ -66,13 +69,6 @@ class Plugin_Command extends \WP_CLI\CommandWithUpgrade { 'auto_update', ); - /** - * Plugin fetcher instance. - * - * @var \WP_CLI\Fetchers\Plugin - */ - protected $fetcher; - public function __construct() { require_once ABSPATH . 'wp-admin/includes/plugin.php'; require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; @@ -209,6 +205,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 +344,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() ) { + 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' ); + $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 +364,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 +397,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}" ); @@ -437,10 +447,14 @@ public function activate( $args, $assoc_args = array() ) { * Plugin 'ninja-forms' deactivated. * Success: Deactivated 2 of 2 plugins. */ - public function deactivate( $args, $assoc_args = array() ) { + 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' ); + $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 ) { @@ -530,7 +544,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; @@ -574,6 +588,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 +608,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 ) ) { @@ -755,6 +775,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 ) { @@ -763,10 +786,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(); @@ -875,7 +899,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 +928,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 +971,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 +1141,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; @@ -1173,11 +1202,14 @@ 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() ) { - + 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 ); + /** + * @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 +1254,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 +1268,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 = [ $wp_filesystem, 'delete' ]; + array_map( $callback, $json_translation_files ); } } } @@ -1257,6 +1296,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 ); @@ -1292,7 +1335,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 { @@ -1322,7 +1365,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 ) { $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); $plugin = $this->fetcher->get( $args[0] ); @@ -1366,10 +1409,14 @@ 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() ) { + 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 ); + /** + * @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 +1552,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 +1629,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 +1642,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..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; /** diff --git a/src/Theme_Command.php b/src/Theme_Command.php index 50806cca..5ae8459f 100644 --- a/src/Theme_Command.php +++ b/src/Theme_Command.php @@ -41,9 +41,15 @@ * 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 + * @extends CommandWithUpgrade<\WP_Theme> */ class Theme_Command extends CommandWithUpgrade { + /** + * @use ParseThemeNameInput<\WP_Theme> + */ use ParseThemeNameInput; protected $item_type = 'theme'; @@ -211,8 +217,11 @@ protected function get_all_items() { * * $ wp theme activate twentysixteen * Success: Switched to 'Twenty Sixteen' theme. + * + * @param string[] $args Positional arguments. + * @param array $assoc_args Associative arguments. Unused. */ - public function activate( $args = array() ) { + public function activate( $args, $assoc_args = [] ) { $theme = $this->fetcher->get_check( $args[0] ); $errors = $theme->errors(); @@ -232,7 +241,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 +288,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 +302,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 +361,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 +422,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 ) ) { @@ -732,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() ) { @@ -761,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() ) { diff --git a/src/Theme_Mod_Command.php b/src/Theme_Mod_Command.php index 9aea4c87..f5525332 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.' ); @@ -232,8 +241,11 @@ 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 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 ); diff --git a/src/WP_CLI/CommandWithUpgrade.php b/src/WP_CLI/CommandWithUpgrade.php index 5c03ae84..798d8ca9 100755 --- a/src/WP_CLI/CommandWithUpgrade.php +++ b/src/WP_CLI/CommandWithUpgrade.php @@ -11,6 +11,12 @@ use WP_CLI\Utils; use WP_Error; +/** + * @phpstan-import-type ThemeInformation from \Theme_Command + * @phpstan-import-type PluginInformation from \Plugin_Command + * + * @template T + */ abstract class CommandWithUpgrade extends \WP_CLI_Command { protected $fetcher; @@ -61,25 +67,43 @@ function () { $this->fetcher = new Fetchers\Plugin(); } + /** + * @return class-string<\WP_Upgrader> + */ 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 ); 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 ) { // Force WordPress to check for updates. call_user_func( $this->upgrade_refresh ); @@ -197,13 +221,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' === 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 = 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 +242,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 +321,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 ) { @@ -346,8 +375,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 ); } @@ -384,19 +413,22 @@ 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 ); } + /** + * @var string|null $exclude + */ $exclude = Utils\get_flag_value( $assoc_args, 'exclude' ); if ( isset( $exclude ) ) { $exclude_items = explode( ',', trim( $assoc_args['exclude'], ',' ) ); @@ -475,6 +507,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 +525,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( @@ -545,14 +584,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 ) { @@ -624,6 +663,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 +674,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 +735,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 +831,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 +860,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 +892,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 +943,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 +956,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..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,10 +17,12 @@ class Plugin extends Base { /** * Get a plugin object by name * - * @param string $name - * @return object|false + * @param string|int $name Plugin name. + * @return object{name: string, file: string}|false */ 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 126d333f..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,8 +19,8 @@ class Theme extends Base { /** * Get a theme object by name * - * @param string $name - * @return object|false + * @param string|int $name + * @return \WP_Theme|false */ public function get( $name ) { // Workaround to equalize folder naming conventions across Win/Mac/Linux. @@ -26,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 ); } diff --git a/src/WP_CLI/ParseThemeNameInput.php b/src/WP_CLI/ParseThemeNameInput.php index 6fb529d4..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 { /** @@ -54,11 +57,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 +178,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 +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. */ @@ -203,7 +214,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 +225,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 +236,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; } }