diff --git a/includes/CLI/Plugin_Check_Command.php b/includes/CLI/Plugin_Check_Command.php index 282c422f3..4e1e28d32 100644 --- a/includes/CLI/Plugin_Check_Command.php +++ b/includes/CLI/Plugin_Check_Command.php @@ -62,6 +62,10 @@ public function __construct( Plugin_Context $plugin_context ) { * [--checks=] * : Only runs checks provided as an argument in comma-separated values, e.g. i18n_usage, late_escaping. Otherwise runs all checks. * + * [--exclude-checks=] + * : Exclude checks provided as an argument in comma-separated values, e.g. i18n_usage, late_escaping. + * Applies after evaluating `--checks`. + * * [--format=] * : Format to display the results. Options are table, csv, and json. The default will be a table. * --- diff --git a/includes/Checker/AJAX_Runner.php b/includes/Checker/AJAX_Runner.php index 381c46e1d..467f2dc29 100644 --- a/includes/Checker/AJAX_Runner.php +++ b/includes/Checker/AJAX_Runner.php @@ -82,6 +82,20 @@ protected function get_check_slugs_param() { return $checks; } + /** + * Returns an array of Check slugs to exclude based on the request. + * + * @since n.e.x.t + * + * @return array An array of Check slugs to exclude. + */ + protected function get_check_exclude_slugs_param() { + $checks = filter_input( INPUT_POST, 'exclude-checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $checks = is_null( $checks ) ? array() : $checks; + + return $checks; + } + /** * Returns the include experimental parameter based on the request. * diff --git a/includes/Checker/Abstract_Check_Runner.php b/includes/Checker/Abstract_Check_Runner.php index 59138e1bd..7aa5df10e 100644 --- a/includes/Checker/Abstract_Check_Runner.php +++ b/includes/Checker/Abstract_Check_Runner.php @@ -36,6 +36,14 @@ abstract class Abstract_Check_Runner implements Check_Runner { */ protected $check_slugs; + /** + * The check slugs to exclude. + * + * @since n.e.x.t + * @var array + */ + protected $check_exclude_slugs; + /** * The plugin parameter. * @@ -110,6 +118,15 @@ abstract protected function get_plugin_param(); */ abstract protected function get_check_slugs_param(); + /** + * Returns an array of Check slugs to exclude based on the request. + * + * @since n.e.x.t + * + * @return array An array of Check slugs. + */ + abstract protected function get_check_exclude_slugs_param(); + /** * Returns the include experimental parameter based on the request. * @@ -161,6 +178,28 @@ final public function set_check_slugs( array $check_slugs ) { $this->check_slugs = $check_slugs; } + /** + * Sets the check slugs to be excluded. + * + * @since n.e.x.t + * + * @param array $check_slugs An array of check slugs to be excluded. + * + * @throws Exception Thrown if the checks do not match those in the original request. + */ + final public function set_check_exclude_slugs( array $check_slugs ) { + if ( $this->initialized_early ) { + // Compare the check slugs to see if there was an error. + if ( $check_slugs !== $this->get_check_exclude_slugs_param() ) { + throw new Exception( + __( 'Invalid checks: The checks to exclude do not match the original request.', 'plugin-check' ) + ); + } + } + + $this->check_exclude_slugs = $check_slugs; + } + /** * Sets the plugin slug or basename to be checked. * @@ -368,18 +407,21 @@ final public function get_checks_to_run() { $check_flags = $check_flags | Check_Repository::INCLUDE_EXPERIMENTAL; } - $checks = $this->check_repository->get_checks( $check_flags ) - ->require( $check_slugs ) // Ensures all of the given slugs are valid. + $excluded_checks = $this->get_check_exclude_slugs(); + + $collection = $this->check_repository->get_checks( $check_flags ) ->include( $check_slugs ) // Ensures only the checks with the given slugs are included. - ->to_map(); + ->exclude( $excluded_checks ); // Exclude provided checks from list. // Filters the checks by specific categories. $categories = $this->get_categories(); if ( $categories ) { - $checks = Check_Categories::filter_checks_by_categories( $checks, $categories ); + $collection = Check_Categories::filter_checks_by_categories( $collection, $categories ); } - return $checks; + return $collection + ->require( $check_slugs ) // Ensures all of the given slugs are valid. + ->to_map(); } /** @@ -416,6 +458,21 @@ private function get_check_slugs() { return $this->get_check_slugs_param(); } + /** + * Returns the check slugs to exclude. + * + * @since n.e.x.t + * + * @return array An array of check slugs to exclude. + */ + private function get_check_exclude_slugs() { + if ( null !== $this->check_exclude_slugs ) { + return $this->check_exclude_slugs; + } + + return $this->get_check_exclude_slugs_param(); + } + /** * Returns the plugin basename. * diff --git a/includes/Checker/CLI_Runner.php b/includes/Checker/CLI_Runner.php index 03e0ecd09..050322ac0 100644 --- a/includes/Checker/CLI_Runner.php +++ b/includes/Checker/CLI_Runner.php @@ -89,6 +89,26 @@ protected function get_check_slugs_param() { return $checks; } + /** + * Returns an array of Check slugs to exclude based on the request. + * + * @since n.e.x.t + * + * @return array An array of Check slugs to run. + */ + protected function get_check_exclude_slugs_param() { + $checks = array(); + + foreach ( $_SERVER['argv'] as $value ) { + if ( false !== strpos( $value, '--exclude-checks=' ) ) { + $checks = wp_parse_list( str_replace( '--exclude-checks=', '', $value ) ); + break; + } + } + + return $checks; + } + /** * Returns the include experimental parameter based on the request. * diff --git a/includes/Checker/Check_Categories.php b/includes/Checker/Check_Categories.php index 2b694ed49..cf4d17101 100644 --- a/includes/Checker/Check_Categories.php +++ b/includes/Checker/Check_Categories.php @@ -57,13 +57,12 @@ static function ( $key ) { * * @since n.e.x.t * - * @param array $checks An array of Check instances. - * @param array $categories An array of categories to filter by. - * @return array Filtered $checks list. + * @param Check_Collection $collection Check collection. + * @param array $categories An array of categories to filter by. + * @return Check_Collection Filtered check collection. */ - public static function filter_checks_by_categories( array $checks, array $categories ) { - return array_filter( - $checks, + public static function filter_checks_by_categories( Check_Collection $collection, array $categories ): Check_Collection { + return $collection->filter( static function ( $check ) use ( $categories ) { // Return true if at least one of the check categories is among the filter categories. return (bool) array_intersect( $check->get_categories(), $categories ); diff --git a/includes/Checker/Check_Collection.php b/includes/Checker/Check_Collection.php index c5698cbe3..0296b84c3 100644 --- a/includes/Checker/Check_Collection.php +++ b/includes/Checker/Check_Collection.php @@ -42,8 +42,10 @@ public function to_map(): array; * * @since n.e.x.t * - * @param callable $filter_fn Filter function that accepts a single check object and should return a boolean for - * whether to include the check in the new collection. + * @phpstan-param callable(Check,string): bool $filter_fn + * + * @param callable $filter_fn Filter function that accepts a Check object and a Check slug and + * should return a boolean for whether to include the check in the new collection. * @return Check_Collection New check collection, effectively a subset of this one. */ public function filter( callable $filter_fn ): Check_Collection; @@ -60,6 +62,18 @@ public function filter( callable $filter_fn ): Check_Collection; */ public function include( array $check_slugs ): Check_Collection; + /** + * Returns a new check collection excluding the provided checks. + * + * If the given list is empty, the same collection will be returned without any change. + * + * @since n.e.x.t + * + * @param array $check_slugs List of slugs to exclude. If empty, the same collection is returned. + * @return Check_Collection New check collection, effectively a subset of this one. + */ + public function exclude( array $check_slugs ): Check_Collection; + /** * Throws an exception if any of the given check slugs are not present, or returns the same collection otherwise. * diff --git a/includes/Checker/Default_Check_Collection.php b/includes/Checker/Default_Check_Collection.php index de81e85b9..50fd23dde 100644 --- a/includes/Checker/Default_Check_Collection.php +++ b/includes/Checker/Default_Check_Collection.php @@ -73,15 +73,18 @@ public function to_map(): array { * * @since n.e.x.t * - * @param callable $filter_fn Filter function that accepts a single check object and should return a boolean for - * whether to include the check in the new collection. + * @phpstan-param callable(Check,string): bool $filter_fn + * + * @param callable $filter_fn Filter function that accepts a Check object and a Check slug and + * should return a boolean for whether to include the check in the new collection. * @return Check_Collection New check collection, effectively a subset of this one. */ public function filter( callable $filter_fn ): Check_Collection { return new self( array_filter( $this->checks, - $filter_fn + $filter_fn, + ARRAY_FILTER_USE_BOTH ) ); } @@ -116,6 +119,29 @@ public function include( array $check_slugs ): Check_Collection { return new self( $checks ); } + /** + * Returns a new check collection excluding the provided checks. + * + * If the given list is empty, the same collection will be returned without any change. + * + * @since n.e.x.t + * + * @param array $check_slugs List of slugs to exclude. If empty, the same collection is returned. + * @return Check_Collection New check collection, effectively a subset of this one. + */ + public function exclude( array $check_slugs ): Check_Collection { + // Return unmodified collection if no check slugs to exclude are given. + if ( ! $check_slugs ) { + return $this; + } + + return $this->filter( + static function ( Check $check, $slug ) use( $check_slugs ) { + return ! in_array( $slug, $check_slugs, true ); + } + ); + } + /** * Throws an exception if any of the given check slugs are not present, or returns the same collection otherwise. * diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php index aa1670c14..3199d41fa 100644 --- a/includes/Checker/Default_Check_Repository.php +++ b/includes/Checker/Default_Check_Repository.php @@ -103,7 +103,7 @@ public function get_checks( $flags = self::TYPE_ALL ) { // Remove experimental checks before returning. return ( new Default_Check_Collection( $checks ) )->filter( - static function ( $check ) { + static function ( Check $check ) { return $check->get_stability() !== Check::STABILITY_EXPERIMENTAL; } ); diff --git a/tests/phpunit/Checker/Check_Categories_Tests.php b/tests/phpunit/Checker/Check_Categories_Tests.php index 7399a80e6..f4d96f316 100644 --- a/tests/phpunit/Checker/Check_Categories_Tests.php +++ b/tests/phpunit/Checker/Check_Categories_Tests.php @@ -44,13 +44,12 @@ public function test_filter_checks_by_categories( array $categories, array $all_ $this->repository->register_check( $check[0], $check[1] ); } - $checks = $this->repository->get_checks() - ->to_map(); + $checks = $this->repository->get_checks(); $check_categories = new Check_Categories(); $filtered_checks = $check_categories->filter_checks_by_categories( $checks, $categories ); - $this->assertEquals( $expected_filtered_checks, $filtered_checks ); + $this->assertEquals( $expected_filtered_checks, $filtered_checks->to_map() ); } public function data_checks_by_categories() { diff --git a/tests/phpunit/Checker/Default_Check_Collection_Tests.php b/tests/phpunit/Checker/Default_Check_Collection_Tests.php index 152e27370..df64b793f 100644 --- a/tests/phpunit/Checker/Default_Check_Collection_Tests.php +++ b/tests/phpunit/Checker/Default_Check_Collection_Tests.php @@ -90,4 +90,25 @@ public function test_require_with_invalid() { $this->collection->require( array( 'static_check', 'invalid_check' ) ); } + + public function test_exclude() { + $this->assertSame( + array( $this->checks['runtime_check'] ), + $this->collection->exclude( array( 'static_check' ) )->to_array() + ); + } + + public function test_exclude_with_empty() { + $this->assertSame( + array_values( $this->checks ), + $this->collection->exclude( array() )->to_array() + ); + } + + public function test_exclude_with_invalid() { + $this->assertSame( + array( $this->checks['static_check'], $this->checks['runtime_check'] ), + $this->collection->exclude( array( 'invalid_check' ) )->to_array() + ); + } }