From 4fc34afd0a5a64740357476d2ccb1bc6091a57a2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 21 May 2025 10:25:16 +0200 Subject: [PATCH 1/5] PHPStan level 9 --- composer.json | 7 +++++-- phpstan.neon.dist | 18 ++++++++++++++++ src/Cache_Command.php | 25 +++++++++++++++++----- src/Transient_Command.php | 44 +++++++++++++++++++++++++++------------ 4 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/composer.json b/composer.json index 3da063d2..0dc69c08 100644 --- a/composer.json +++ b/composer.json @@ -16,14 +16,15 @@ }, "require-dev": { "wp-cli/entity-command": "^1.3 || ^2", - "wp-cli/wp-cli-tests": "^4" + "wp-cli/wp-cli-tests": "dev-main" }, "config": { "process-timeout": 7200, "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, - "johnpbloch/wordpress-core-installer": true + "johnpbloch/wordpress-core-installer": true, + "phpstan/extension-installer": true }, "lock": false }, @@ -72,12 +73,14 @@ "behat-rerun": "rerun-behat-tests", "lint": "run-linter-tests", "phpcs": "run-phpcs-tests", + "phpstan": "run-phpstan-tests", "phpcbf": "run-phpcbf-cleanup", "phpunit": "run-php-unit-tests", "prepare-tests": "install-package-tests", "test": [ "@lint", "@phpcs", + "@phpstan", "@phpunit", "@behat" ] diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..19e60349 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,18 @@ +parameters: + level: 9 + paths: + - src + - cache-command.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + - vendor/wp-cli/wp-cli-tests + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + treatPhpDocTypesAsCertain: false + dynamicConstantNames: + - WP_DEBUG + - WP_DEBUG_LOG + - WP_DEBUG_DISPLAY + ignoreErrors: + - identifier: missingType.parameter + - identifier: missingType.return diff --git a/src/Cache_Command.php b/src/Cache_Command.php index b56eda0f..7be61bce 100644 --- a/src/Cache_Command.php +++ b/src/Cache_Command.php @@ -158,7 +158,8 @@ public function delete( $args, $assoc_args ) { * Success: The cache was flushed. */ public function flush( $args, $assoc_args ) { - + // TODO: Needs fixing in wp-cli/wp-cli + // @phpstan-ignore offsetAccess.nonOffsetAccessible if ( WP_CLI::has_config( 'url' ) && ! empty( WP_CLI::get_config()['url'] ) && is_multisite() ) { WP_CLI::warning( 'Flushing the cache may affect all sites in a multisite installation, depending on the implementation of the object cache.' ); } @@ -439,7 +440,11 @@ public function flush_group( $args, $assoc_args ) { */ public function pluck( $args, $assoc_args ) { list( $key ) = $args; - $group = Utils\get_flag_value( $assoc_args, 'group' ); + + /** + * @var string $group + */ + $group = Utils\get_flag_value( $assoc_args, 'group' ); $value = wp_cache_get( $key, $group ); @@ -512,11 +517,21 @@ function ( $key ) { * - plaintext * - json * --- + * + * @param string[] $args */ public function patch( $args, $assoc_args ) { list( $action, $key ) = $args; - $group = Utils\get_flag_value( $assoc_args, 'group' ); - $expiration = Utils\get_flag_value( $assoc_args, 'expiration' ); + + /** + * @var string $group + */ + $group = Utils\get_flag_value( $assoc_args, 'group' ); + + /** + * @var string|null $expiration + */ + $expiration = Utils\get_flag_value( $assoc_args, 'expiration' ); $key_path = array_map( function ( $key ) { @@ -569,7 +584,7 @@ function ( $key ) { if ( $patched_value === $old_value ) { WP_CLI::success( "Value passed for cache key '$key' is unchanged." ); } else { - $success = wp_cache_set( $key, $patched_value, $group, $expiration ); + $success = wp_cache_set( $key, $patched_value, $group, (int) $expiration ); if ( $success ) { WP_CLI::success( "Updated cache key '$key'." ); } else { diff --git a/src/Transient_Command.php b/src/Transient_Command.php index 9166935a..74f392f7 100644 --- a/src/Transient_Command.php +++ b/src/Transient_Command.php @@ -116,14 +116,16 @@ public function get( $args, $assoc_args ) { * * $ wp transient set sample_key "test data" 3600 * Success: Transient added. + * + * @param string[] $args */ public function set( $args, $assoc_args ) { list( $key, $value ) = $args; - $expiration = Utils\get_flag_value( $args, 2, 0 ); + $expiration = $args[2] ?? 0; $func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'set_site_transient' : 'set_transient'; - if ( $func( $key, $value, $expiration ) ) { + if ( $func( $key, $value, (int) $expiration ) ) { WP_CLI::success( 'Transient added.' ); } else { WP_CLI::error( 'Transient could not be set.' ); @@ -180,9 +182,9 @@ public function set( $args, $assoc_args ) { public function delete( $args, $assoc_args ) { $key = ( ! empty( $args ) ) ? $args[0] : null; - $all = Utils\get_flag_value( $assoc_args, 'all' ); - $expired = Utils\get_flag_value( $assoc_args, 'expired' ); - $network = Utils\get_flag_value( $assoc_args, 'network' ); + $all = (bool) Utils\get_flag_value( $assoc_args, 'all' ); + $expired = (bool) Utils\get_flag_value( $assoc_args, 'expired' ); + $network = (bool) Utils\get_flag_value( $assoc_args, 'network' ); if ( true === $all ) { $this->delete_all( $network ); @@ -301,9 +303,9 @@ public function list_( $args, $assoc_args ) { WP_CLI::warning( 'Transients are stored in an external object cache, and this command only shows those stored in the database.' ); } - $network = Utils\get_flag_value( $assoc_args, 'network', false ); - $unserialize = Utils\get_flag_value( $assoc_args, 'unserialize', false ); - $human_readable = Utils\get_flag_value( $assoc_args, 'human-readable', false ); + $network = (bool) Utils\get_flag_value( $assoc_args, 'network', false ); + $unserialize = (bool) Utils\get_flag_value( $assoc_args, 'unserialize', false ); + $human_readable = (bool) Utils\get_flag_value( $assoc_args, 'human-readable', false ); $fields = array( 'name', 'value', 'expiration' ); if ( isset( $assoc_args['fields'] ) ) { @@ -505,7 +507,12 @@ function ( $key ) { */ public function patch( $args, $assoc_args ) { list( $action, $key ) = $args; - $expiration = (int) Utils\get_flag_value( $assoc_args, 'expiration', 0 ); + + /** + * @var string $expiration + */ + $expiration = Utils\get_flag_value( $assoc_args, 'expiration', 0 ); + $expiration = (int) $expiration; $read_func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'get_site_transient' : 'get_transient'; $write_func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'set_site_transient' : 'set_transient'; @@ -582,20 +589,31 @@ function ( $key ) { private function get_transient_expiration( $name, $is_site_transient = false, $human_readable = false ) { if ( $is_site_transient ) { if ( is_multisite() ) { - $expiration = (int) get_site_option( '_site_transient_timeout_' . $name ); + /** + * @var string $expiration + */ + $expiration = get_site_option( '_site_transient_timeout_' . $name ); } else { - $expiration = (int) get_option( '_site_transient_timeout_' . $name ); + /** + * @var string $expiration + */ + $expiration = get_option( '_site_transient_timeout_' . $name ); } } else { - $expiration = (int) get_option( '_transient_timeout_' . $name ); + /** + * @var string $expiration + */ + $expiration = get_option( '_transient_timeout_' . $name ); } + $expiration = (int) $expiration; + if ( 0 === $expiration ) { return $human_readable ? 'never expires' : 'false'; } if ( ! $human_readable ) { - return $expiration; + return (string) $expiration; } $now = time(); From bca0905db130f6f78e86a24e7db8653b6b003b61 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 22 May 2025 22:42:14 +0200 Subject: [PATCH 2/5] Improve param types --- composer.json | 2 +- phpstan.neon.dist | 8 ++++++- src/Cache_Command.php | 49 +++++++++++++++++++++++++++++++-------- src/Transient_Command.php | 36 ++++++++++++++++++---------- 4 files changed, 71 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index 0dc69c08..ae65dcff 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ }, "require-dev": { "wp-cli/entity-command": "^1.3 || ^2", - "wp-cli/wp-cli-tests": "dev-main" + "wp-cli/wp-cli-tests": "dev-add/phpstan-enhancements" }, "config": { "process-timeout": 7200, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 19e60349..5f68c8b7 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -13,6 +13,12 @@ parameters: - WP_DEBUG - WP_DEBUG_LOG - WP_DEBUG_DISPLAY + strictRules: + uselessCast: true + closureUsesThis: true + overwriteVariablesWithLoop: true + matchingInheritedMethodNames: true + numericOperandsInArithmeticOperators: true + switchConditionsMatchingType: true ignoreErrors: - - identifier: missingType.parameter - identifier: missingType.return diff --git a/src/Cache_Command.php b/src/Cache_Command.php index 7be61bce..62435d04 100644 --- a/src/Cache_Command.php +++ b/src/Cache_Command.php @@ -58,11 +58,14 @@ class Cache_Command extends WP_CLI_Command { * # Add cache. * $ wp cache add my_key my_group my_value 300 * Success: Added object 'my_key' in group 'my_value'. + * + * @param array{string, string, string, string} $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function add( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; - if ( ! wp_cache_add( $key, $value, $group, $expiration ) ) { + if ( ! wp_cache_add( $key, $value, $group, (int) $expiration ) ) { WP_CLI::error( "Could not add object '$key' in group '$group'. Does it already exist?" ); } @@ -96,10 +99,13 @@ public function add( $args, $assoc_args ) { * # Decrease cache value. * $ wp cache decr my_key 2 my_group * 48 + * + * @param array{string, string, string} $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function decr( $args, $assoc_args ) { list( $key, $offset, $group ) = $args; - $value = wp_cache_decr( $key, $offset, $group ); + $value = wp_cache_decr( $key, (int) $offset, $group ); if ( false === $value ) { WP_CLI::error( 'The value was not decremented.' ); @@ -129,6 +135,9 @@ public function decr( $args, $assoc_args ) { * # Delete cache. * $ wp cache delete my_key my_group * Success: Object deleted. + * + * @param array{string, string} $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function delete( $args, $assoc_args ) { list( $key, $group ) = $args; @@ -157,7 +166,7 @@ public function delete( $args, $assoc_args ) { * $ wp cache flush * Success: The cache was flushed. */ - public function flush( $args, $assoc_args ) { + public function flush() { // TODO: Needs fixing in wp-cli/wp-cli // @phpstan-ignore offsetAccess.nonOffsetAccessible if ( WP_CLI::has_config( 'url' ) && ! empty( WP_CLI::get_config()['url'] ) && is_multisite() ) { @@ -193,6 +202,9 @@ public function flush( $args, $assoc_args ) { * # Get cache. * $ wp cache get my_key my_group * my_value + * + * @param array{string, string} $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function get( $args, $assoc_args ) { list( $key, $group ) = $args; @@ -232,10 +244,13 @@ public function get( $args, $assoc_args ) { * # Increase cache value. * $ wp cache incr my_key 2 my_group * 50 + * + * @param array{string, string, string} $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function incr( $args, $assoc_args ) { list( $key, $offset, $group ) = $args; - $value = wp_cache_incr( $key, $offset, $group ); + $value = wp_cache_incr( $key, (int) $offset, $group ); if ( false === $value ) { WP_CLI::error( 'The value was not incremented.' ); @@ -274,10 +289,13 @@ public function incr( $args, $assoc_args ) { * # Replace cache. * $ wp cache replace my_key new_value my_group * Success: Replaced object 'my_key' in group 'my_group'. + * + * @param array{string, string, string, string} $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function replace( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; - $result = wp_cache_replace( $key, $value, $group, $expiration ); + $result = wp_cache_replace( $key, $value, $group, (int) $expiration ); if ( false === $result ) { WP_CLI::error( "Could not replace object '$key' in group '$group'. Does it not exist?" ); @@ -316,10 +334,13 @@ public function replace( $args, $assoc_args ) { * # Set cache. * $ wp cache set my_key my_value my_group 300 * Success: Set object 'my_key' in group 'my_group'. + * + * @param array{string, string, string, string} $args Positional arguments. + * @param array $assoc_args Associative arguments. */ public function set( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; - $result = wp_cache_set( $key, $value, $group, $expiration ); + $result = wp_cache_set( $key, $value, $group, (int) $expiration ); if ( false === $result ) { WP_CLI::error( "Could not add object '$key' in group '$group'." ); @@ -342,7 +363,7 @@ public function set( $args, $assoc_args ) { * $ wp cache type * Default */ - public function type( $args, $assoc_args ) { + public function type() { $message = WP_CLI\Utils\wp_get_cache_type(); WP_CLI::line( $message ); } @@ -366,8 +387,10 @@ public function type( $args, $assoc_args ) { * if ! wp cache supports non_existing; then * echo 'non_existing is not supported' * fi + * + * @param array{string} $args Positional arguments. */ - public function supports( $args, $assoc_args ) { + public function supports( $args ) { list ( $feature ) = $args; if ( ! function_exists( 'wp_cache_supports' ) ) { @@ -397,8 +420,10 @@ public function supports( $args, $assoc_args ) { * Success: Cache group 'my_group' was flushed. * * @subcommand flush-group + * + * @param array{string} $args Positional arguments. */ - public function flush_group( $args, $assoc_args ) { + public function flush_group( $args ) { list( $group ) = $args; if ( ! function_exists( 'wp_cache_supports' ) || ! wp_cache_supports( 'flush_group' ) ) { @@ -437,6 +462,9 @@ public function flush_group( $args, $assoc_args ) { * - json * - yaml * --- + * + * @param array{string, string} $args Positional arguments. + * @param array{group: string, format: string} $assoc_args Associative arguments. */ public function pluck( $args, $assoc_args ) { list( $key ) = $args; @@ -518,7 +546,8 @@ function ( $key ) { * - json * --- * - * @param string[] $args + * @param string[] $args Positional arguments. + * @param array{group: string, expiration: string, format: string} $assoc_args Associative arguments. */ public function patch( $args, $assoc_args ) { list( $action, $key ) = $args; diff --git a/src/Transient_Command.php b/src/Transient_Command.php index 74f392f7..d787620c 100644 --- a/src/Transient_Command.php +++ b/src/Transient_Command.php @@ -73,6 +73,9 @@ class Transient_Command extends WP_CLI_Command { * * $ wp transient get random_key * Warning: Transient with key "random_key" is not set. + * + * @param array{string} $args Positional arguments. + * @param array{format: string} $assoc_args Associative arguments. */ public function get( $args, $assoc_args ) { list( $key ) = $args; @@ -117,7 +120,8 @@ public function get( $args, $assoc_args ) { * $ wp transient set sample_key "test data" 3600 * Success: Transient added. * - * @param string[] $args + * @param array{0: string, 1: string, 2?: string} $args Positional arguments. + * @param array{network?: bool} $assoc_args Associative arguments. */ public function set( $args, $assoc_args ) { list( $key, $value ) = $args; @@ -178,13 +182,16 @@ public function set( $args, $assoc_args ) { * * # Delete all transients in a multisite. * $ wp transient delete --all --network && wp site list --field=url | xargs -n1 -I % wp --url=% transient delete --all + * + * @param array{string} $args Positional arguments. + * @param array{network?: bool, all?: bool, expired?: bool} $assoc_args Associative arguments. */ public function delete( $args, $assoc_args ) { $key = ( ! empty( $args ) ) ? $args[0] : null; - $all = (bool) Utils\get_flag_value( $assoc_args, 'all' ); - $expired = (bool) Utils\get_flag_value( $assoc_args, 'expired' ); - $network = (bool) Utils\get_flag_value( $assoc_args, 'network' ); + $all = Utils\get_flag_value( $assoc_args, 'all' ); + $expired = Utils\get_flag_value( $assoc_args, 'expired' ); + $network = Utils\get_flag_value( $assoc_args, 'network' ); if ( true === $all ) { $this->delete_all( $network ); @@ -295,6 +302,9 @@ public function type() { * +------+-------+---------------+ * * @subcommand list + * + * @param string[] $args Positional arguments. Unused. + * @param array{search?: string, exclude?: string, network?: bool, unserialize?: bool, 'human-readable'?: bool, fields?: string, format?: string} $assoc_args Associative arguments. */ public function list_( $args, $assoc_args ) { global $wpdb; @@ -303,9 +313,9 @@ public function list_( $args, $assoc_args ) { WP_CLI::warning( 'Transients are stored in an external object cache, and this command only shows those stored in the database.' ); } - $network = (bool) Utils\get_flag_value( $assoc_args, 'network', false ); - $unserialize = (bool) Utils\get_flag_value( $assoc_args, 'unserialize', false ); - $human_readable = (bool) Utils\get_flag_value( $assoc_args, 'human-readable', false ); + $network = Utils\get_flag_value( $assoc_args, 'network', false ); + $unserialize = Utils\get_flag_value( $assoc_args, 'unserialize', false ); + $human_readable = Utils\get_flag_value( $assoc_args, 'human-readable', false ); $fields = array( 'name', 'value', 'expiration' ); if ( isset( $assoc_args['fields'] ) ) { @@ -432,6 +442,9 @@ public function list_( $args, $assoc_args ) { * : Get the value of a network|site transient. On single site, this is * a specially-named cache key. On multisite, this is a global cache * (instead of local to the site). + * + * @param string[] $args Positional arguments. + * @param array{format: string} $assoc_args Associative arguments. */ public function pluck( $args, $assoc_args ) { list( $key ) = $args; @@ -504,15 +517,14 @@ function ( $key ) { * : Get the value of a network|site transient. On single site, this is * a specially-named cache key. On multisite, this is a global cache * (instead of local to the site). + * + * @param string[] $args Positional arguments. + * @param array{format: string} $assoc_args Associative arguments. */ public function patch( $args, $assoc_args ) { list( $action, $key ) = $args; - /** - * @var string $expiration - */ - $expiration = Utils\get_flag_value( $assoc_args, 'expiration', 0 ); - $expiration = (int) $expiration; + $expiration = (int) Utils\get_flag_value( $assoc_args, 'expiration', 0 ); $read_func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'get_site_transient' : 'get_transient'; $write_func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'set_site_transient' : 'set_transient'; From 0c51fcbd329bd60f5d8054e58677a525979bde1f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 12 Jun 2025 10:55:41 +0200 Subject: [PATCH 3/5] Cleanup --- phpstan.neon.dist | 4 ---- src/Cache_Command.php | 9 --------- 2 files changed, 13 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5f68c8b7..60e92eef 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 strictRules: uselessCast: true closureUsesThis: true diff --git a/src/Cache_Command.php b/src/Cache_Command.php index 62435d04..8fd4e295 100644 --- a/src/Cache_Command.php +++ b/src/Cache_Command.php @@ -469,9 +469,6 @@ public function flush_group( $args ) { public function pluck( $args, $assoc_args ) { list( $key ) = $args; - /** - * @var string $group - */ $group = Utils\get_flag_value( $assoc_args, 'group' ); $value = wp_cache_get( $key, $group ); @@ -552,14 +549,8 @@ function ( $key ) { public function patch( $args, $assoc_args ) { list( $action, $key ) = $args; - /** - * @var string $group - */ $group = Utils\get_flag_value( $assoc_args, 'group' ); - /** - * @var string|null $expiration - */ $expiration = Utils\get_flag_value( $assoc_args, 'expiration' ); $key_path = array_map( From acbed4d473c2df45288db46dbf150ed617324b46 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 3 Jul 2025 14:23:44 +0200 Subject: [PATCH 4/5] Use wp-cli-tests v5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ae65dcff..de56882c 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ }, "require-dev": { "wp-cli/entity-command": "^1.3 || ^2", - "wp-cli/wp-cli-tests": "dev-add/phpstan-enhancements" + "wp-cli/wp-cli-tests": "^5" }, "config": { "process-timeout": 7200, From a2acbe8c52189ca4278726d99840eb2a0ed8ff83 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 3 Jul 2025 14:23:52 +0200 Subject: [PATCH 5/5] Update phpstan config --- phpstan.neon.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 60e92eef..18a58311 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,7 +5,6 @@ parameters: - cache-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