diff --git a/features/check-file-type.feature b/features/check-file-type.feature index 009801b..7dd4dda 100644 --- a/features/check-file-type.feature +++ b/features/check-file-type.feature @@ -1,5 +1,6 @@ Feature: Check the type of file + @skip-object-cache @skip-windows Scenario: Check that object-cache.php isn't a symlink Given a WP install And a config.yml file: @@ -32,7 +33,7 @@ Feature: Check the type of file """ And the return code should be 1 - + @skip-object-cache @skip-windows Scenario: Check that object-cache.php is a symlink Given a WP install And a config.yml file: diff --git a/features/check-network-site-option-value.feature b/features/check-network-site-option-value.feature index d47d907..001c508 100644 --- a/features/check-network-site-option-value.feature +++ b/features/check-network-site-option-value.feature @@ -26,7 +26,7 @@ Feature: Check the value of a network option option: registration value: all """ - And I run `wp eval 'update_site_option( "registration", "none" );'` + And I run `wp eval "update_site_option( 'registration', 'none' );"` When I try `wp doctor check network-registration --config=config.yml` Then STDOUT should be a table containing rows: @@ -38,7 +38,7 @@ Feature: Check the value of a network option """ And the return code should be 1 - When I run `wp eval 'update_site_option( "registration", "all" );'` + When I run `wp eval "update_site_option( 'registration', 'all' );"` Then STDOUT should be empty When I run `wp doctor check network-registration --config=config.yml` @@ -56,7 +56,7 @@ Feature: Check the value of a network option option: registration value_is_not: none """ - And I run `wp eval 'update_site_option( "registration", "none" );'` + And I run `wp eval "update_site_option( 'registration', 'none' );"` When I try `wp doctor check network-registration --config=config.yml` Then STDOUT should be a table containing rows: @@ -68,7 +68,7 @@ Feature: Check the value of a network option """ And the return code should be 1 - When I run `wp eval 'update_site_option( "registration", "all" );'` + When I run `wp eval "update_site_option( 'registration', 'all' );"` Then STDOUT should be empty When I run `wp doctor check network-registration --config=config.yml` @@ -86,7 +86,7 @@ Feature: Check the value of a network option option: registration value: none """ - And I run `wp eval 'update_site_option( "registration", array( "users" => "all" ) );'` + And I run `wp eval "update_site_option( 'registration', array( 'users' => 'all' ) );"` When I try `wp doctor check network-registration --config=config.yml` Then STDOUT should contain: diff --git a/features/check.feature b/features/check.feature index ba4eb00..51b1a14 100644 --- a/features/check.feature +++ b/features/check.feature @@ -80,9 +80,9 @@ Feature: Basic check usage Then STDOUT should be: """ name,status - cache-flush,warning option-blog-public,error php-in-upload,success + cache-flush,warning """ And the return code should be 1 @@ -104,8 +104,8 @@ Feature: Basic check usage Then STDOUT should be: """ name,status - cache-flush,warning option-blog-public,error + cache-flush,warning """ And the return code should be 1 diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..b753594 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,14 @@ +parameters: + level: 9 + paths: + - src + - doctor-command.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - tests/phpstan/scan-files.php + treatPhpDocTypesAsCertain: false + ignoreErrors: + - '#Call to deprecated method setAccessible\(\) of class ReflectionProperty\.#' + diff --git a/src/Check.php b/src/Check.php index c0fc4e2..c86b31b 100644 --- a/src/Check.php +++ b/src/Check.php @@ -31,6 +31,8 @@ abstract class Check { /** * Initialize the check. + * + * @param array $options */ public function __construct( $options = array() ) { @@ -48,6 +50,8 @@ public function __construct( $options = array() ) { /** * Get when the check is expected to run. + * + * @return string */ public function get_when() { return $this->_when; @@ -57,6 +61,7 @@ public function get_when() { * Set when the check is expected to run. * * @param string $when + * @return void */ public function set_when( $when ) { $this->_when = $when; @@ -66,6 +71,7 @@ public function set_when( $when ) { * Set the status of the check. * * @param string $status + * @return void */ protected function set_status( $status ) { $this->_status = $status; @@ -75,6 +81,7 @@ protected function set_status( $status ) { * Set the message of the check. * * @param string $message + * @return void */ protected function set_message( $message ) { $this->_message = $message; @@ -85,13 +92,15 @@ protected function set_message( $message ) { * * Because each check checks for something different, this method must be * subclassed. Method is expected to set $status_code and $status_message. + * + * @return void */ abstract public function run(); /** * Get results of the check. * - * @return array + * @return array */ public function get_results() { return array( diff --git a/src/Check/Autoload_Options_Size.php b/src/Check/Autoload_Options_Size.php index db2437d..8752410 100644 --- a/src/Check/Autoload_Options_Size.php +++ b/src/Check/Autoload_Options_Size.php @@ -17,6 +17,9 @@ class Autoload_Options_Size extends Check { */ protected $threshold_kb = 900; + /** + * @return void + */ public function run() { ob_start(); WP_CLI::run_command( @@ -40,9 +43,14 @@ public function run() { } } + /** + * @param int|float $size Size in bytes. + * @param int $precision Precision. + * @return string + */ private static function format_bytes( $size, $precision = 2 ) { $base = log( $size, 1024 ); $suffixes = array( '', 'kb', 'mb', 'g', 't' ); - return round( pow( 1024, $base - floor( $base ) ), $precision ) . $suffixes[ floor( $base ) ]; + return round( pow( 1024, $base - floor( $base ) ), $precision ) . $suffixes[ (int) floor( $base ) ]; } } diff --git a/src/Check/Cache_Flush.php b/src/Check/Cache_Flush.php index 3f82d9f..f51e651 100644 --- a/src/Check/Cache_Flush.php +++ b/src/Check/Cache_Flush.php @@ -11,6 +11,9 @@ */ class Cache_Flush extends File_Contents { + /** + * @return void + */ public function run() { // Path to wp-content directory. @@ -22,6 +25,9 @@ public function run() { $this->regex = 'wp_cache_flush\(\)'; foreach ( $iterator as $file ) { + if ( ! $file instanceof \SplFileInfo ) { + continue; + } $this->check_file( $file ); } @@ -34,7 +40,9 @@ public function run() { // Show relative paths in output. $relative_paths = array_map( function ( $file ) use ( $wp_content_dir ) { - return str_replace( $wp_content_dir . '/', '', $file ); + $normalized_file = \WP_CLI\Path::normalize( (string) $file ); + $normalized_dir = \WP_CLI\Path::normalize( $wp_content_dir ); + return str_replace( rtrim( $normalized_dir, '/' ) . '/', '', $normalized_file ); }, $this->_matches ); diff --git a/src/Check/Constant_Definition.php b/src/Check/Constant_Definition.php index 765a6b8..c33cfae 100644 --- a/src/Check/Constant_Definition.php +++ b/src/Check/Constant_Definition.php @@ -26,9 +26,9 @@ class Constant_Definition extends Check { /** * Whether or not the constant is expected to be a falsy value. * - * @var bool + * @var bool|null */ - protected $falsy; + protected $falsy = null; /** * Expected value of the constant. @@ -39,6 +39,8 @@ class Constant_Definition extends Check { /** * Initialize the constant check + * + * @param array $options */ public function __construct( $options = array() ) { parent::__construct( $options ); @@ -47,6 +49,9 @@ public function __construct( $options = array() ) { } } + /** + * @return void + */ public function run() { if ( isset( $this->falsy ) ) { @@ -99,7 +104,7 @@ public function run() { return; } - if ( $this->defined && ! isset( $this->value ) ) { + if ( ! isset( $this->value ) ) { $this->set_status( 'success' ); $this->set_message( "Constant '{$this->constant}' is defined." ); return; @@ -118,12 +123,21 @@ public function run() { } } + /** + * @param mixed $value + * @return string + */ private static function human_value( $value ) { if ( true === $value ) { - $value = 'true'; + return 'true'; } elseif ( false === $value ) { - $value = 'false'; + return 'false'; + } elseif ( is_null( $value ) ) { + return 'null'; + } + if ( is_scalar( $value ) ) { + return (string) $value; } - return $value; + return gettype( $value ); } } diff --git a/src/Check/Core_Update.php b/src/Check/Core_Update.php index 55d4c4d..1f2b43e 100644 --- a/src/Check/Core_Update.php +++ b/src/Check/Core_Update.php @@ -13,11 +13,17 @@ class Core_Update extends Check { public function run() { ob_start(); WP_CLI::run_command( array( 'core', 'check-update' ), array( 'format' => 'json' ) ); - $ret = ob_get_clean(); - $updates = ! empty( $ret ) ? json_decode( $ret, true ) : array(); + $ret = ob_get_clean(); + $updates = ! empty( $ret ) ? json_decode( $ret, true ) : array(); + if ( ! is_array( $updates ) ) { + $updates = array(); + } $has_minor = false; $has_major = false; foreach ( $updates as $update ) { + if ( ! is_array( $update ) || ! isset( $update['update_type'] ) ) { + continue; + } switch ( $update['update_type'] ) { case 'minor': $has_minor = true; diff --git a/src/Check/Core_Verify_Checksums.php b/src/Check/Core_Verify_Checksums.php index 3b7abdf..300dc50 100644 --- a/src/Check/Core_Verify_Checksums.php +++ b/src/Check/Core_Verify_Checksums.php @@ -10,11 +10,17 @@ */ class Core_Verify_Checksums extends Check { + /** + * @param array $options + */ public function __construct( $options = array() ) { parent::__construct( $options ); $this->set_when( 'before_wp_load' ); } + /** + * @return void + */ public function run() { $return_code = WP_CLI::runcommand( 'core verify-checksums', diff --git a/src/Check/Cron.php b/src/Check/Cron.php index 3b4e1de..77a9da0 100644 --- a/src/Check/Cron.php +++ b/src/Check/Cron.php @@ -7,8 +7,14 @@ abstract class Cron extends Check { + /** + * @var array>|null + */ protected static $crons; + /** + * @return array> + */ protected static function get_crons() { if ( isset( self::$crons ) ) { @@ -23,8 +29,13 @@ protected static function get_crons() { 'fields' => 'hook,args', ) ); - $ret = ob_get_clean(); - self::$crons = ! empty( $ret ) ? json_decode( $ret, true ) : array(); + $ret = ob_get_clean(); + $decoded = ! empty( $ret ) ? json_decode( $ret, true ) : array(); + if ( ! is_array( $decoded ) ) { + $decoded = array(); + } + /** @var array> $decoded */ + self::$crons = $decoded; return self::$crons; } } diff --git a/src/Check/Cron_Count.php b/src/Check/Cron_Count.php index 31cdde9..f9494c6 100644 --- a/src/Check/Cron_Count.php +++ b/src/Check/Cron_Count.php @@ -14,6 +14,9 @@ class Cron_Count extends Cron { */ protected $threshold_count = 50; + /** + * @return void + */ public function run() { $crons = self::get_crons(); if ( count( $crons ) >= $this->threshold_count ) { diff --git a/src/Check/Cron_Duplicates.php b/src/Check/Cron_Duplicates.php index b21eef5..ad4e5fa 100644 --- a/src/Check/Cron_Duplicates.php +++ b/src/Check/Cron_Duplicates.php @@ -14,11 +14,17 @@ class Cron_Duplicates extends Cron { */ protected $threshold_count = 10; + /** + * @return void + */ public function run() { $crons = self::get_crons(); $job_counts = array(); $excess_duplicates = false; foreach ( $crons as $job ) { + if ( ! isset( $job['hook'] ) ) { + continue; + } $key_data = array( $job['hook'], isset( $job['args'] ) ? $job['args'] : array() ); if ( function_exists( 'wp_json_encode' ) ) { $key = wp_json_encode( $key_data ); diff --git a/src/Check/File.php b/src/Check/File.php index fc1b496..2d9b95d 100644 --- a/src/Check/File.php +++ b/src/Check/File.php @@ -12,7 +12,7 @@ abstract class File extends Check { /** * File checks are run as their own group. */ - protected $_when = false; + protected $_when = 'manual'; // Run manually via group /** * File extension to check. @@ -42,14 +42,14 @@ abstract class File extends Check { /** * Any files matching the check. * - * @var array + * @var array */ protected $_matches = array(); /** * Get the options for this check * - * @return string + * @return array */ public function get_options() { return array( @@ -58,4 +58,12 @@ public function get_options() { 'path' => $this->path, ); } + + /** + * Check a specific file. + * + * @param \SplFileInfo $file File to check. + * @return void + */ + abstract public function check_file( \SplFileInfo $file ); } diff --git a/src/Check/File_Contents.php b/src/Check/File_Contents.php index 4927c08..c4431cb 100644 --- a/src/Check/File_Contents.php +++ b/src/Check/File_Contents.php @@ -12,9 +12,9 @@ class File_Contents extends File { /** * Regex pattern to check against each file’s contents. * - * @var string + * @var string|null */ - protected $regex; + protected $regex = null; /** * Assert existence or absence of the regex pattern. @@ -56,12 +56,19 @@ public function run() { } } + /** + * @param SplFileInfo $file + * @return void + */ public function check_file( SplFileInfo $file ) { if ( $file->isDir() || ! isset( $this->regex ) ) { return; } $contents = file_get_contents( $file->getPathname() ); + if ( false === $contents ) { + return; + } if ( preg_match( '#' . $this->regex . '#i', $contents ) ) { $this->_matches[] = $file; diff --git a/src/Check/File_Type.php b/src/Check/File_Type.php index 1a161f1..445c3d5 100644 --- a/src/Check/File_Type.php +++ b/src/Check/File_Type.php @@ -2,8 +2,6 @@ namespace WP_CLI\Doctor\Check; -use SplFileInfo; - /** * Checks files on the filesystem to be of a certain type. */ @@ -12,9 +10,9 @@ class File_Type extends File { /** * Assert the file type is or isn't a symlink. * - * @var bool + * @var bool|null */ - protected $symlink; + protected $symlink = null; public function run() { @@ -32,7 +30,11 @@ public function run() { } } - public function check_file( SplFileInfo $file ) { + /** + * @param \SplFileInfo $file + * @return void + */ + public function check_file( \SplFileInfo $file ) { if ( isset( $this->symlink ) ) { if ( 'link' === $file->getType() && false === $this->symlink ) { $this->_matches[] = $file; diff --git a/src/Check/Language_Update.php b/src/Check/Language_Update.php index ec12882..c0f19bc 100644 --- a/src/Check/Language_Update.php +++ b/src/Check/Language_Update.php @@ -22,6 +22,10 @@ public function run() { ) ); + if ( ! is_array( $languages ) ) { + $languages = []; + } + // Returns the count of each value that the key 'update' is mapped to. $counts = array_count_values( array_column( $languages, 'update' ) ); diff --git a/src/Check/Network_Required_Plugins.php b/src/Check/Network_Required_Plugins.php index 970bb90..a510bda 100644 --- a/src/Check/Network_Required_Plugins.php +++ b/src/Check/Network_Required_Plugins.php @@ -13,10 +13,13 @@ class Network_Required_Plugins extends Check { /** * List of required plugin slugs. * - * @var array + * @var array */ protected $plugins = array(); + /** + * @param array $options + */ public function __construct( $options = array() ) { parent::__construct( $options ); @@ -32,6 +35,9 @@ public function __construct( $options = array() ) { } } + /** + * @return void + */ public function run() { if ( ! is_multisite() ) { $this->set_status( 'success' ); @@ -80,10 +86,8 @@ public function run() { } /** - * Resolve a plugin slug or file to an installed plugin file path. - * - * @param string $plugin_slug Requested plugin slug/file. - * @param array $installed_plugins Installed plugins keyed by plugin file. + * @param string $plugin_slug Requested plugin slug/file. + * @param array $installed_plugins Installed plugins keyed by plugin file. * @return string|null */ private function get_plugin_file( $plugin_slug, $installed_plugins ) { diff --git a/src/Check/Network_Site_Count.php b/src/Check/Network_Site_Count.php index f2359ef..721c498 100644 --- a/src/Check/Network_Site_Count.php +++ b/src/Check/Network_Site_Count.php @@ -24,6 +24,9 @@ class Network_Site_Count extends Check { */ protected $maximum = 500; + /** + * @param array $options + */ public function __construct( $options = array() ) { parent::__construct( $options ); @@ -34,6 +37,9 @@ public function __construct( $options = array() ) { } } + /** + * @return void + */ public function run() { if ( ! is_multisite() ) { $this->set_status( 'success' ); diff --git a/src/Check/Network_Site_Option_Value.php b/src/Check/Network_Site_Option_Value.php index 3b750b7..dfe3d00 100644 --- a/src/Check/Network_Site_Option_Value.php +++ b/src/Check/Network_Site_Option_Value.php @@ -30,6 +30,9 @@ class Network_Site_Option_Value extends Check { */ protected $value_is_not; + /** + * @return void + */ public function run() { if ( ! is_multisite() ) { $this->set_status( 'success' ); @@ -95,6 +98,9 @@ private function format_value_for_message( $value ) { if ( false === $value ) { return 'false'; } - return (string) $value; + if ( is_scalar( $value ) ) { + return (string) $value; + } + return gettype( $value ); } } diff --git a/src/Check/Option_Value.php b/src/Check/Option_Value.php index 5122fe5..1735cdc 100644 --- a/src/Check/Option_Value.php +++ b/src/Check/Option_Value.php @@ -30,6 +30,9 @@ class Option_Value extends Check { */ protected $value_is_not; + /** + * @return void + */ public function run() { if ( isset( $this->value ) && isset( $this->value_is_not ) ) { @@ -38,24 +41,29 @@ public function run() { return; } + if ( ! isset( $this->value ) && ! isset( $this->value_is_not ) ) { + $this->set_status( 'error' ); + $this->set_message( 'You must use either "value" or "value_is_not".' ); + return; + } + $actual_value = get_option( $this->option ); if ( isset( $this->value ) ) { if ( $actual_value == $this->value ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Keep existing behavior. $status = 'success'; - $message = "Option '{$this->option}' is '{$this->value}' as expected."; + $message = "Option '{$this->option}' is '" . $this->format_value_for_message( $this->value ) . "' as expected."; } else { $status = 'error'; - $message = "Option '{$this->option}' is '{$actual_value}' but expected to be '{$this->value}'."; - } - } elseif ( isset( $this->value_is_not ) ) { - if ( $actual_value == $this->value_is_not ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Keep existing behavior. - $status = 'error'; - $message = "Option '{$this->option}' is '{$actual_value}' and expected not to be."; - } else { - $status = 'success'; - $message = "Option '{$this->option}' is not '{$this->value_is_not}' as expected."; + $message = "Option '{$this->option}' is '" . $this->format_value_for_message( $actual_value ) . "' but expected to be '" . $this->format_value_for_message( $this->value ) . "'."; } + } elseif ( $actual_value == $this->value_is_not ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Keep existing behavior. + $status = 'error'; + $message = "Option '{$this->option}' is '" . $this->format_value_for_message( $actual_value ) . "' and expected not to be."; + + } else { + $status = 'success'; + $message = "Option '{$this->option}' is not '" . $this->format_value_for_message( $this->value_is_not ) . "' as expected."; } $this->set_status( $status ); @@ -78,4 +86,34 @@ public function run() { break; } } + + /** + * Format arbitrary option values for stable string output. + * + * @param mixed $value Value to render. + * @return string + */ + private function format_value_for_message( $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + $encoded = wp_json_encode( $value ); + if ( false !== $encoded ) { + return $encoded; + } + return 'unrepresentable value'; + } + + if ( null === $value ) { + return 'null'; + } + if ( true === $value ) { + return 'true'; + } + if ( false === $value ) { + return 'false'; + } + if ( is_scalar( $value ) ) { + return (string) $value; + } + return gettype( $value ); + } } diff --git a/src/Check/PHP_In_Upload.php b/src/Check/PHP_In_Upload.php index ae257d5..549e7f9 100644 --- a/src/Check/PHP_In_Upload.php +++ b/src/Check/PHP_In_Upload.php @@ -14,10 +14,13 @@ class PHP_In_Upload extends Check { /** * Array containing list of files found in the uploads folder * - * @var array + * @var array<\SplFileInfo> */ protected $php_files_array = array(); + /** + * @return void + */ public function run() { // Path to the uploads folder. @@ -26,6 +29,9 @@ public function run() { $iterator = new RecursiveIteratorIterator( $directory, RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $iterator as $file ) { + if ( ! $file instanceof \SplFileInfo ) { + continue; + } if ( 'php' === $file->getExtension() ) { $this->php_files_array[] = $file; } diff --git a/src/Check/Plugin.php b/src/Check/Plugin.php index b759be9..ff876b3 100644 --- a/src/Check/Plugin.php +++ b/src/Check/Plugin.php @@ -7,8 +7,14 @@ abstract class Plugin extends Check { + /** + * @var array>|null + */ protected static $plugins; + /** + * @return array> + */ protected static function get_plugins() { if ( isset( self::$plugins ) ) { @@ -17,8 +23,13 @@ protected static function get_plugins() { ob_start(); WP_CLI::run_command( array( 'plugin', 'list' ), array( 'format' => 'json' ) ); - $ret = ob_get_clean(); - self::$plugins = ! empty( $ret ) ? json_decode( $ret, true ) : array(); + $ret = ob_get_clean(); + $decoded = ! empty( $ret ) ? json_decode( $ret, true ) : array(); + if ( ! is_array( $decoded ) ) { + $decoded = array(); + } + /** @var array> $decoded */ + self::$plugins = $decoded; return self::$plugins; } } diff --git a/src/Check/Plugin_Active_Count.php b/src/Check/Plugin_Active_Count.php index ea0e7c5..85b7a53 100644 --- a/src/Check/Plugin_Active_Count.php +++ b/src/Check/Plugin_Active_Count.php @@ -14,9 +14,15 @@ class Plugin_Active_Count extends Plugin { */ protected $threshold_count = 80; + /** + * @return void + */ public function run() { $active = 0; foreach ( self::get_plugins() as $plugin ) { + if ( ! isset( $plugin['status'] ) ) { + continue; + } if ( 'active' === $plugin['status'] || 'active-network' === $plugin['status'] ) { ++$active; } diff --git a/src/Check/Plugin_Deactivated.php b/src/Check/Plugin_Deactivated.php index b63c95d..63bf568 100644 --- a/src/Check/Plugin_Deactivated.php +++ b/src/Check/Plugin_Deactivated.php @@ -14,12 +14,18 @@ class Plugin_Deactivated extends Plugin { */ protected $threshold_percentage = 40; + /** + * @return void + */ public function run() { $plugins = self::get_plugins(); $active = 0; $inactive = 0; foreach ( self::get_plugins() as $plugin ) { + if ( ! isset( $plugin['status'] ) ) { + continue; + } if ( 'active' === $plugin['status'] || 'active-network' === $plugin['status'] ) { ++$active; } elseif ( 'inactive' === $plugin['status'] ) { diff --git a/src/Check/Plugin_Status.php b/src/Check/Plugin_Status.php index 20e5fe0..f287982 100644 --- a/src/Check/Plugin_Status.php +++ b/src/Check/Plugin_Status.php @@ -27,6 +27,9 @@ class Plugin_Status extends Plugin { */ protected $status; + /** + * @param array $options + */ public function __construct( $options = array() ) { $valid_statuses = array( 'uninstalled', 'installed', 'active' ); if ( ! in_array( $options['status'], $valid_statuses, true ) ) { @@ -35,13 +38,19 @@ public function __construct( $options = array() ) { parent::__construct( $options ); } + /** + * @return void + */ public function run() { $plugins = self::get_plugins(); $current_status = 'uninstalled'; foreach ( self::get_plugins() as $plugin ) { + if ( ! isset( $plugin['name'] ) || ! isset( $plugin['status'] ) ) { + continue; + } if ( $plugin['name'] === $this->name ) { - $current_status = $plugin['status']; + $current_status = is_string( $plugin['status'] ) ? $plugin['status'] : 'uninstalled'; break; } } diff --git a/src/Check/Plugin_Update.php b/src/Check/Plugin_Update.php index c37a277..7d9bf0d 100644 --- a/src/Check/Plugin_Update.php +++ b/src/Check/Plugin_Update.php @@ -7,10 +7,16 @@ */ class Plugin_Update extends Plugin { + /** + * @return void + */ public function run() { $plugins = self::get_plugins(); $update_count = 0; foreach ( $plugins as $plugin ) { + if ( ! isset( $plugin['update'] ) ) { + continue; + } if ( 'available' === $plugin['update'] ) { ++$update_count; } diff --git a/src/Check/Theme_Update.php b/src/Check/Theme_Update.php index dd3a679..ddc6654 100644 --- a/src/Check/Theme_Update.php +++ b/src/Check/Theme_Update.php @@ -10,13 +10,22 @@ */ class Theme_Update extends Check { + /** + * @return void + */ public function run() { ob_start(); WP_CLI::run_command( array( 'theme', 'list' ), array( 'format' => 'json' ) ); - $ret = ob_get_clean(); - $themes = ! empty( $ret ) ? json_decode( $ret, true ) : array(); + $ret = ob_get_clean(); + $themes = ! empty( $ret ) ? json_decode( $ret, true ) : array(); + if ( ! is_array( $themes ) ) { + $themes = array(); + } $update_count = 0; foreach ( $themes as $theme ) { + if ( ! is_array( $theme ) || ! isset( $theme['update'] ) ) { + continue; + } if ( 'available' === $theme['update'] ) { ++$update_count; } diff --git a/src/Checks.php b/src/Checks.php index d13f9b7..07f970a 100644 --- a/src/Checks.php +++ b/src/Checks.php @@ -4,15 +4,29 @@ use Mustangostang\Spyc; use WP_CLI; -use WP_CLI\Utils; +use WP_CLI\Path; + class Checks { + /** + * @var Checks|null + */ private static $instance; - private $checks = array(); + /** + * @var array + */ + private $checks = array(); + + /** + * @var array> + */ private $skipped_checks = array(); + /** + * @return Checks + */ public static function get_instance() { if ( ! isset( self::$instance ) ) { self::$instance = new Checks(); @@ -24,13 +38,19 @@ public static function get_instance() { * Register checks from a config file * * @param string $file + * @return void */ public static function register_config( $file ) { if ( ! is_file( $file ) ) { WP_CLI::error( 'Invalid configuration file.' ); } - $check_data = Spyc::YAMLLoad( file_get_contents( $file ) ); + $contents = file_get_contents( $file ); + if ( false === $contents ) { + WP_CLI::error( 'Could not read configuration file.' ); + } + + $check_data = Spyc::YAMLLoad( $contents ); if ( ! empty( $check_data['_']['inherit'] ) ) { $inherited = $check_data['_']['inherit']; @@ -77,8 +97,9 @@ public static function register_config( $file ) { /** * Register a check with the Doctor * - * @param string $name Name for the check. - * @param string $class Check class name. + * @param string $name Name for the check. + * @param object|string $check Check class name or instance. + * @return void */ public static function add_check( $name, $check ) { @@ -96,21 +117,25 @@ public static function add_check( $name, $check ) { $class = get_class( $check ); WP_CLI::error( "Class '{$class}' for check '{$name}' needs to extend Check base class. Verify check registration." ); } - self::$instance->checks[ $name ] = $check; + self::get_instance()->checks[ $name ] = $check; } /** - * Get checks registred with the Doctor. + * Get checks registered with the Doctor. * - * @param array $args Filter checks based on some attribute. + * @param array $args Filter checks based on some attribute. + * @return array */ public static function get_checks( $args = array() ) { if ( ! empty( $args['name'] ) ) { $checks = array(); $names = is_array( $args['name'] ) ? $args['name'] : array( $args['name'] ); foreach ( $names as $name ) { - if ( isset( self::$instance->checks[ $name ] ) ) { - $checks[ $name ] = self::$instance->checks[ $name ]; + if ( ! is_string( $name ) ) { + continue; + } + if ( isset( self::get_instance()->checks[ $name ] ) ) { + $checks[ $name ] = self::get_instance()->checks[ $name ]; } } return $checks; @@ -123,9 +148,10 @@ public static function get_checks( $args = array() ) { * * @param string $path Path to file. * @param string $base Base path to prepend. + * @return string */ private static function absolutize( $path, $base ) { - if ( ! empty( $path ) && ! Utils\is_path_absolute( $path ) ) { + if ( ! empty( $path ) && ! Path::is_absolute( $path ) ) { $path = $base . DIRECTORY_SEPARATOR . $path; } return $path; diff --git a/src/Command.php b/src/Command.php index 2f6d6db..3dd5753 100644 --- a/src/Command.php +++ b/src/Command.php @@ -108,10 +108,13 @@ class Command { * +--------------------+--------+--------------------------------------------+ * * @when before_wp_load + * + * @param array $args Names of one or more checks to run. + * @param array $assoc_args Command options. + * @return void */ public function check( $args, $assoc_args ) { - - $config = Utils\get_flag_value( $assoc_args, 'config', self::get_default_config() ); + $config = (string) Utils\get_flag_value( $assoc_args, 'config', self::get_default_config() ); Checks::register_config( $config ); $all = Utils\get_flag_value( $assoc_args, 'all' ); @@ -135,7 +138,7 @@ public function check( $args, $assoc_args ) { } foreach ( $checks as $name => $check ) { $when = $check->get_when(); - if ( $when ) { + if ( $when && 'manual' !== $when ) { WP_CLI::add_hook( $when, static function () use ( $name, $check, &$completed, &$progress ) { @@ -156,56 +159,6 @@ static function () use ( $name, $check, &$completed, &$progress ) { } } } - if ( ! empty( $file_checks ) ) { - WP_CLI::add_hook( - 'after_wp_config_load', - static function () use ( $file_checks, &$completed, &$progress ) { - WP_CLI::debug( 'Scanning filesystem for file checks...', 'doctor' ); - try { - $directory = new RecursiveDirectoryIterator( ABSPATH, RecursiveDirectoryIterator::SKIP_DOTS ); - $iterator = new RecursiveIteratorIterator( $directory, RecursiveIteratorIterator::CHILD_FIRST ); - $wp_content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content'; - $item_count = 0; - foreach ( $iterator as $file ) { - ++$item_count; - if ( 0 === $item_count % self::DEBUG_FILE_SCAN_INTERVAL ) { - WP_CLI::debug( " Visited {$item_count} items...", 'doctor' ); - } - foreach ( $file_checks as $name => $check ) { - $options = $check->get_options(); - if ( ! empty( $options['only_wp_content'] ) - && 0 !== stripos( $file->getPath(), $wp_content_dir ) ) { - continue; - } - if ( ! empty( $options['path'] ) - && 0 !== stripos( $file->getPathname(), ABSPATH . $options['path'] ) ) { - continue; - } - $extension = explode( '|', $options['extension'] ); - if ( ! in_array( $file->getExtension(), $extension, true ) ) { - continue; - } - $check->check_file( $file ); - } - } - WP_CLI::debug( " Total items visited: {$item_count}", 'doctor' ); - } catch ( Exception $e ) { - WP_CLI::warning( $e->getMessage() ); - } - foreach ( $file_checks as $name => $check ) { - WP_CLI::debug( "Running check: {$name}", 'doctor' ); - $check->run(); - $completed[ $name ] = $check; - if ( $progress ) { - $progress->tick(); - } - $results = $check->get_results(); - WP_CLI::debug( " Status: {$results['status']}", 'doctor' ); - } - } - ); - } - if ( ! isset( WP_CLI::get_runner()->config['url'] ) ) { WP_CLI::add_wp_hook( 'muplugins_loaded', @@ -221,6 +174,52 @@ static function () { WP_CLI::warning( $e->getMessage() ); } + // Run file checks manually after WordPress is loaded. + if ( ! empty( $file_checks ) ) { + WP_CLI::debug( 'Scanning filesystem for file checks...', 'doctor' ); + try { + $directory = new RecursiveDirectoryIterator( ABSPATH, RecursiveDirectoryIterator::SKIP_DOTS ); + $iterator = new RecursiveIteratorIterator( $directory, RecursiveIteratorIterator::CHILD_FIRST ); + $wp_content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content'; + foreach ( $iterator as $file ) { + if ( ! $file instanceof \SplFileInfo ) { + continue; + } + foreach ( $file_checks as $name => $check ) { + $options = $check->get_options(); + if ( ! empty( $options['only_wp_content'] ) + && 0 !== stripos( \WP_CLI\Path::normalize( $file->getPath() ), \WP_CLI\Path::normalize( $wp_content_dir ) ) ) { + continue; + } + if ( ! empty( $options['path'] ) + && 0 !== stripos( \WP_CLI\Path::normalize( $file->getPathname() ), \WP_CLI\Path::normalize( ABSPATH . $options['path'] ) ) ) { + continue; + } + + $ext_option = isset( $options['extension'] ) && is_string( $options['extension'] ) ? $options['extension'] : ''; + $extension = explode( '|', $ext_option ); + if ( ! in_array( $file->getExtension(), $extension, true ) ) { + continue; + } + /** @var Check\File $check */ + $check->check_file( $file ); + } + } + } catch ( Exception $e ) { + WP_CLI::warning( $e->getMessage() ); + } + foreach ( $file_checks as $name => $check ) { + WP_CLI::debug( "Running check: {$name}", 'doctor' ); + $check->run(); + $completed[ $name ] = $check; + if ( $progress ) { + $progress->tick(); + } + $results = $check->get_results(); + WP_CLI::debug( " Status: {$results['status']}", 'doctor' ); + } + } + $results = array(); foreach ( $completed as $name => $check ) { $results[] = array_merge( $check->get_results(), array( 'name' => $name ) ); @@ -332,6 +331,10 @@ function ( $check ) { * * @when before_wp_load * @subcommand list + * + * @param array $args Command arguments. + * @param array $assoc_args Command options. + * @return void */ public function list_( $args, $assoc_args ) { @@ -342,13 +345,14 @@ public function list_( $args, $assoc_args ) { $assoc_args ); - $config = Utils\get_flag_value( $assoc_args, 'config', self::get_default_config() ); + $config = (string) Utils\get_flag_value( $assoc_args, 'config', self::get_default_config() ); Checks::register_config( $config ); $items = array(); foreach ( Checks::get_checks() as $check_name => $class ) { $reflection = new \ReflectionClass( $class ); - $description = self::remove_decorations( $reflection->getDocComment() ); + $doc_comment = $reflection->getDocComment(); + $description = $doc_comment ? self::remove_decorations( $doc_comment ) : ''; $tokens = array(); foreach ( $reflection->getProperties() as $prop ) { $prop_name = $prop->getName(); @@ -362,7 +366,11 @@ public function list_( $args, $assoc_args ) { if ( is_array( $value ) ) { $value = json_encode( $value ); } - $tokens[ '%' . $prop_name . '%' ] = $value; + if ( is_scalar( $value ) ) { + $tokens[ '%' . $prop_name . '%' ] = (string) $value; + } else { + $tokens[ '%' . $prop_name . '%' ] = gettype( $value ); + } } if ( ! empty( $tokens ) ) { $description = str_replace( array_keys( $tokens ), array_values( $tokens ), $description ); @@ -373,11 +381,13 @@ public function list_( $args, $assoc_args ) { 'description' => $description, ); } - Utils\format_items( $assoc_args['format'], $items, explode( ',', $assoc_args['fields'] ) ); + Utils\format_items( (string) $assoc_args['format'], $items, explode( ',', (string) $assoc_args['fields'] ) ); } /** * Runs through the entirety of the WP bootstrap process + * + * @return void */ private function load_wordpress_with_template() { global $wp_query; @@ -437,14 +447,22 @@ static function ( $template ) { * @return string */ private static function remove_decorations( $comment ) { - $comment = preg_replace( '|^/\*\*[\r\n]+|', '', $comment ); - $comment = preg_replace( '|\n[\t ]*\*/$|', '', $comment ); - $comment = preg_replace( '|^[\t ]*\* ?|m', '', $comment ); + $replaced = preg_replace( '|^/\*\*[\r\n]+|', '', $comment ); + $comment = is_string( $replaced ) ? $replaced : ''; + + $replaced = preg_replace( '|\n[\t ]*\*/$|', '', $comment ); + $comment = is_string( $replaced ) ? $replaced : ''; + + $replaced = preg_replace( '|^[\t ]*\* ?|m', '', $comment ); + $comment = is_string( $replaced ) ? $replaced : ''; + return $comment; } /** * Get the path to the default config file + * + * @return string */ private static function get_default_config() { return dirname( __DIR__ ) . '/doctor.yml'; diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php new file mode 100644 index 0000000..81f3d91 --- /dev/null +++ b/tests/phpstan/scan-files.php @@ -0,0 +1,6 @@ +