Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ temp/

.wp-env.override.json
.wp-env.tests.override.json
wp-tests-config.php
15 changes: 15 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ By default, `.git`, `vendor`, `vendor_prefixed`, `vendor-prefixed` and `node_mod
[--exclude-files=<files>]
: Additional files to exclude from checks.

[--include-files=<files>]
: Specific files to include in checks (comma-separated). Mutually exclusive with --exclude-files.
When specified, only the listed files will be checked.

[--include-directories=<directories>]
: Specific directories to include in checks (comma-separated, recursive). Mutually exclusive with --exclude-directories.
When specified, only files within the listed directories will be checked.

[--use-config]
: Load `.plugin-check.json` and `.distignore` from the plugin root. Off by default to keep the scanner behavior predictable.

[--severity=<severity>]
: Severity level.

Expand Down Expand Up @@ -90,6 +101,10 @@ wp plugin check akismet --checks=late_escaping
wp plugin check akismet --format=json
wp plugin check akismet --format=ctrf
wp plugin check akismet --mode=update
wp plugin check akismet --include-files=akismet.php,class.akismet.php
wp plugin check akismet --include-directories=includes,views
wp plugin check akismet --exclude-directories=tests,vendor
wp plugin check akismet --use-config
```

# wp plugin list-checks
Expand Down
102 changes: 97 additions & 5 deletions includes/CLI/Plugin_Check_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ public function __construct( Plugin_Context $plugin_context ) {
* [--exclude-files=<files>]
* : Additional files to exclude from checks.
*
* [--include-files=<files>]
* : Specific files to include in checks (comma-separated). Mutually exclusive with --exclude-files.
* When specified, only the listed files will be checked.
*
* [--include-directories=<directories>]
* : Specific directories to include in checks (comma-separated, recursive). Mutually exclusive with --exclude-directories.
* When specified, only files within the listed directories will be checked.
*
* [--use-config]
* : Load .plugin-check.json and .distignore from the plugin root. Off by default.
* Must be set explicitly so the scanner behavior stays predictable (e.g. for WordPress.org automated checks).
*
* [--severity=<severity>]
* : Severity level.
*
Expand Down Expand Up @@ -157,6 +169,10 @@ public function __construct( Plugin_Context $plugin_context ) {
* wp plugin check akismet --mode=update
* wp plugin check akismet --ai
* wp plugin check akismet --ai --ai-model=openai::gpt-4o
* wp plugin check akismet --include-files=akismet.php,class.akismet.php
* wp plugin check akismet --include-directories=includes,views
* wp plugin check akismet --exclude-directories=tests,vendor
* wp plugin check akismet --use-config
*
* @subcommand check
*
Expand All @@ -172,9 +188,21 @@ public function __construct( Plugin_Context $plugin_context ) {
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function check( $args, $assoc_args ) {
// Get options based on the CLI arguments.
$options = $this->get_options(
$assoc_args,
$plugin = isset( $args[0] ) ? $args[0] : '';
$plugin_path = '';

if ( ! empty( $plugin ) ) {
if ( is_dir( $plugin ) ) {
$plugin_path = $plugin;
} elseif ( is_file( $plugin ) ) {
$plugin_path = dirname( $plugin );
} elseif ( ! filter_var( $plugin, FILTER_VALIDATE_URL ) ) {
// Assume slug for installed plugin.
$plugin_path = WP_PLUGIN_DIR . '/' . $plugin;
}
}

$defaults = array_merge(
array(
'checks' => '',
'format' => 'table',
Expand All @@ -191,11 +219,22 @@ public function check( $args, $assoc_args ) {
'mode' => 'new',
'ai' => false,
'ai-model' => '',
)
'use-config' => false,
),
array()
);

// Get options based on the CLI arguments.
$options = $this->get_options( $assoc_args, $defaults );

// Load config files only when explicitly opted in.
$config = self::maybe_load_plugin_config( $plugin_path, ! empty( $options['use-config'] ) );

// Re-merge config into defaults so flags parsed later see the values.
$options = wp_parse_args( $config, $options );

// Create the plugin and checks array from CLI arguments.
$plugin = isset( $args[0] ) ? $args[0] : '';
// $plugin is already set above.
$checks = wp_parse_list( $options['checks'] );

// Ignore codes.
Expand All @@ -205,6 +244,14 @@ public function check( $args, $assoc_args ) {
$categories = isset( $options['categories'] ) ? wp_parse_list( $options['categories'] ) : array();

$excluded_directories = isset( $options['exclude-directories'] ) ? wp_parse_list( $options['exclude-directories'] ) : array();
$included_directories = isset( $options['include-directories'] ) ? wp_parse_list( $options['include-directories'] ) : array();

// Validate mutual exclusivity for directories.
if ( ! empty( $excluded_directories ) && ! empty( $included_directories ) ) {
WP_CLI::error(
__( 'The --include-directories and --exclude-directories options are mutually exclusive. Please use only one.', 'plugin-check' )
);
}

add_filter(
'wp_plugin_check_ignore_directories',
Expand All @@ -213,7 +260,22 @@ static function ( $dirs ) use ( $excluded_directories ) {
}
);

add_filter(
'wp_plugin_check_include_directories',
static function ( $dirs ) use ( $included_directories ) {
return array_unique( array_merge( $dirs, $included_directories ) );
}
);

$excluded_files = isset( $options['exclude-files'] ) ? wp_parse_list( $options['exclude-files'] ) : array();
$included_files = isset( $options['include-files'] ) ? wp_parse_list( $options['include-files'] ) : array();

// Validate mutual exclusivity for files.
if ( ! empty( $excluded_files ) && ! empty( $included_files ) ) {
WP_CLI::error(
__( 'The --include-files and --exclude-files options are mutually exclusive. Please use only one.', 'plugin-check' )
);
}

add_filter(
'wp_plugin_check_ignore_files',
Expand All @@ -222,6 +284,13 @@ static function ( $dirs ) use ( $excluded_files ) {
}
);

add_filter(
'wp_plugin_check_include_files',
static function ( $dirs ) use ( $included_files ) {
return array_unique( array_merge( $dirs, $included_files ) );
}
);

// Get the CLI Runner.
$runner = Plugin_Request_Utility::get_runner();

Expand Down Expand Up @@ -423,6 +492,29 @@ static function ( $dirs ) use ( $excluded_files ) {
}
}

/**
* Loads .plugin-check.json and .distignore when the --use-config flag is set.
*
* Gated explicitly so the scanner does not silently read author-controlled
* config files. Returns the parsed config so it can be merged into defaults.
*
* @since 2.1.0
*
* @param string $plugin_path Plugin root path.
* @param bool $use_config Whether the --use-config flag is set.
* @return array Parsed config from .plugin-check.json, or empty array.
*/
public static function maybe_load_plugin_config( $plugin_path, $use_config ) {
if ( empty( $use_config ) || empty( $plugin_path ) || ! is_dir( $plugin_path ) ) {
return array();
}

$config = Plugin_Request_Utility::get_plugin_configuration( $plugin_path );
Plugin_Request_Utility::load_filters_from_config( $plugin_path );

return is_array( $config ) ? $config : array();
}

/**
* Lists the available checks for plugins.
*
Expand Down
42 changes: 41 additions & 1 deletion includes/Checker/Checks/Abstract_File_Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ private static function file_get_contents( $file ) {
*
* @param Check_Context $plugin Context for the plugin to check.
* @return array List of absolute file paths.
*
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private static function get_files( Check_Context $plugin ) {
$location = wp_normalize_path( $plugin->location() );
Expand All @@ -284,7 +287,10 @@ private static function get_files( Check_Context $plugin ) {

$directories_to_ignore = Plugin_Request_Utility::get_directories_to_ignore();

$files_to_ignore = Plugin_Request_Utility::get_files_to_ignore();
$files_to_ignore = Plugin_Request_Utility::get_files_to_ignore();
$directories_to_include = Plugin_Request_Utility::get_directories_to_include();
$files_to_include = Plugin_Request_Utility::get_files_to_include();
$ignore_patterns = Plugin_Request_Utility::get_files_to_ignore_patterns();

foreach ( $iterator as $file ) {
if ( ! $file->isFile() ) {
Expand All @@ -296,6 +302,30 @@ private static function get_files( Check_Context $plugin ) {
// Flag to check if the file should be included or not.
$include_file = true;

if ( ! empty( $directories_to_include ) || ! empty( $files_to_include ) ) {
$include_file = false;

foreach ( $directories_to_include as $directory ) {
if ( false !== strpos( $file_path, '/' . $directory . '/' ) ) {
$include_file = true;
break;
}
}

if ( ! $include_file ) {
foreach ( $files_to_include as $inc_file ) {
if ( str_ends_with( $file_path, '/' . $inc_file ) ) {
$include_file = true;
break;
}
}
}

if ( ! $include_file ) {
continue;
}
}

foreach ( $directories_to_ignore as $directory ) {
// Check if the current file belongs to the directory you want to ignore.
if ( false !== strpos( $file_path, '/' . $directory . '/' ) ) {
Expand All @@ -311,6 +341,16 @@ private static function get_files( Check_Context $plugin ) {
}
}

if ( $include_file && ! empty( $ignore_patterns ) ) {
$relative_path = substr( $file_path, strlen( $location ) + 1 );
foreach ( $ignore_patterns as $pattern ) {
if ( preg_match( $pattern, $relative_path ) ) {
$include_file = false;
break;
}
}
}

if ( $include_file ) {
self::$file_list_cache[ $location ][] = $file_path;
}
Expand Down
Loading
Loading