Skip to content
3 changes: 2 additions & 1 deletion features/check-file-type.feature
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Feature: Check the type of file

@skip-object-cache
Scenario: Check that object-cache.php isn't a symlink
Given a WP install
And a config.yml file:
Expand Down Expand Up @@ -32,7 +33,7 @@ Feature: Check the type of file
"""
And the return code should be 1


@skip-object-cache
Scenario: Check that object-cache.php is a symlink
Given a WP install
And a config.yml file:
Expand Down
4 changes: 2 additions & 2 deletions features/check.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
14 changes: 14 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -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\.#'

11 changes: 10 additions & 1 deletion src/Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ abstract class Check {

/**
* Initialize the check.
*
* @param array<string, mixed> $options
*/
public function __construct( $options = array() ) {

Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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<string, string>
*/
public function get_results() {
return array(
Expand Down
10 changes: 9 additions & 1 deletion src/Check/Autoload_Options_Size.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 ) ];
}
}
10 changes: 9 additions & 1 deletion src/Check/Cache_Flush.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
*/
class Cache_Flush extends File_Contents {

/**
* @return void
*/
public function run() {

// Path to wp-content directory.
Expand All @@ -21,7 +24,12 @@ public function run() {
// Regex to match.
$this->regex = 'wp_cache_flush\(\)';

foreach ( $iterator as $file ) {
$files = iterator_to_array( $iterator );

foreach ( $files as $file ) {
if ( ! $file instanceof \SplFileInfo ) {
continue;
}
$this->check_file( $file );
Comment thread
swissspidy marked this conversation as resolved.
Outdated
}

Expand Down
26 changes: 20 additions & 6 deletions src/Check/Constant_Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -39,6 +39,8 @@ class Constant_Definition extends Check {

/**
* Initialize the constant check
*
* @param array<string, mixed> $options
*/
public function __construct( $options = array() ) {
parent::__construct( $options );
Expand All @@ -47,6 +49,9 @@ public function __construct( $options = array() ) {
}
}

/**
Comment thread
swissspidy marked this conversation as resolved.
Outdated
* @return void
*/
public function run() {

if ( isset( $this->falsy ) ) {
Expand Down Expand Up @@ -99,7 +104,7 @@ public function run() {
return;
}

if ( $this->defined && ! isset( $this->value ) ) {
if ( ! isset( $this->value ) ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Removing $this->defined from this condition changes the behavior of the check. Previously, this block only executed if the configuration explicitly requested a definition check (e.g., defined: true in YAML). Now, it will execute for any constant check where a value is not specified, potentially leading to incorrect success reports if the constant is not actually defined. If PHPStan is flagging $this->defined as an undefined property, it should be explicitly declared in the class rather than removed from the logic.

if ( $this->defined && ! isset( $this->value ) ) {

$this->set_status( 'success' );
$this->set_message( "Constant '{$this->constant}' is defined." );
return;
Expand All @@ -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 );
}
}
10 changes: 8 additions & 2 deletions src/Check/Core_Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/Check/Core_Verify_Checksums.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@
*/
class Core_Verify_Checksums extends Check {

/**
* @param array<string, mixed> $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',
Expand Down
15 changes: 13 additions & 2 deletions src/Check/Cron.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@

abstract class Cron extends Check {

/**
* @var array<array<string, mixed>>|null
*/
protected static $crons;

/**
* @return array<array<string, mixed>>
*/
protected static function get_crons() {

if ( isset( self::$crons ) ) {
Expand All @@ -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<array<string, mixed>> $decoded */
self::$crons = $decoded;
return self::$crons;
}
}
3 changes: 3 additions & 0 deletions src/Check/Cron_Count.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand Down
6 changes: 6 additions & 0 deletions src/Check/Cron_Duplicates.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
14 changes: 11 additions & 3 deletions src/Check/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Changing $_when from false to 'manual' breaks the discovery logic for file-based checks. In src/Command.php, the code identifies File checks by checking if get_when() returns a falsy value (see lines 142 and 156-161). By setting this to a string, these checks will be incorrectly treated as regular hooked checks and will not be added to the $file_checks array, causing the filesystem scan to skip them entirely. If this change was made to satisfy PHPStan's type requirements, consider updating the base class type hint or using a @phpstan-ignore instead.

protected $_when = false;


/**
* File extension to check.
Expand Down Expand Up @@ -42,14 +42,14 @@ abstract class File extends Check {
/**
* Any files matching the check.
*
* @var array
* @var array<string|\SplFileInfo>
*/
protected $_matches = array();

/**
* Get the options for this check
*
* @return string
* @return array<string, bool|string>
*/
public function get_options() {
return array(
Expand All @@ -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 );
}
11 changes: 9 additions & 2 deletions src/Check/File_Contents.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading