From f082a71faefa1ac8a171f2cfb869c5e34b9cda0d Mon Sep 17 00:00:00 2001 From: Nilambar Sharma Date: Mon, 4 May 2026 11:55:15 +0545 Subject: [PATCH 01/15] Add new hooks for runtime setup and shutdown --- .../Checker/Runtime_Environment_Setup.php | 90 +++++++++++++++++++ .../Runtime_Environment_Setup_Tests.php | 82 +++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/includes/Checker/Runtime_Environment_Setup.php b/includes/Checker/Runtime_Environment_Setup.php index f01169364..5c9ec7301 100644 --- a/includes/Checker/Runtime_Environment_Setup.php +++ b/includes/Checker/Runtime_Environment_Setup.php @@ -24,10 +24,25 @@ final class Runtime_Environment_Setup { * * @global wpdb $wpdb WordPress database abstraction object. * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function set_up() { global $wpdb, $wp_filesystem; + /** + * Fires before the runtime environment is set up. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all setup steps. + * } + */ + do_action( 'wp_plugin_check_before_runtime_setup', array( 'early_exit' => false ) ); + require_once ABSPATH . '/wp-admin/includes/upgrade.php'; // Get the existing site URL. @@ -82,6 +97,18 @@ static function () use ( $permalink_structure ) { // Return early if the plugin check object cache already exists. if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + /** + * Fires after the runtime environment is set up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all setup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => true ) ); return; } @@ -92,6 +119,19 @@ static function () use ( $permalink_structure ) { $wp_filesystem->copy( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' ); } } + + /** + * Fires after the runtime environment is set up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all setup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => false ) ); } /** @@ -105,6 +145,19 @@ static function () use ( $permalink_structure ) { public function clean_up() { global $wpdb, $wp_filesystem; + /** + * Fires before the runtime environment is cleaned up. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_before_runtime_cleanup', array( 'early_exit' => false ) ); + require_once ABSPATH . '/wp-admin/includes/upgrade.php'; $prefix_cleanup = $this->amend_db_base_prefix(); @@ -121,12 +174,36 @@ public function clean_up() { // Return early if the plugin check object cache does not exist. if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) || ! WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + /** + * Fires after the runtime environment is cleaned up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => true ) ); return; } // Remove the object-cache.php file. if ( $wp_filesystem || WP_Filesystem() ) { if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { + /** + * Fires after the runtime environment is cleaned up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => true ) ); return; } @@ -138,6 +215,19 @@ public function clean_up() { $wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' ); } } + + /** + * Fires after the runtime environment is cleaned up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => false ) ); } /** diff --git a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php index 6f78930fb..ff943e155 100644 --- a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php +++ b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php @@ -92,6 +92,88 @@ public function test_can_set_up_with_failing_filesystem() { $this->assertFalse( $runtime_setup->can_set_up() ); } + public function test_before_runtime_setup_action_fires() { + $this->set_up_mock_filesystem(); + + $fired = false; + $context = null; + add_action( + 'wp_plugin_check_before_runtime_setup', + function ( $ctx ) use ( &$fired, &$context ) { + $fired = true; + $context = $ctx; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->set_up(); + + $this->assertTrue( $fired ); + $this->assertIsArray( $context ); + $this->assertArrayHasKey( 'early_exit', $context ); + $this->assertFalse( $context['early_exit'] ); + } + + public function test_after_runtime_setup_action_fires() { + $this->set_up_mock_filesystem(); + + $fired = false; + $context = null; + add_action( + 'wp_plugin_check_after_runtime_setup', + function ( $ctx ) use ( &$fired, &$context ) { + $fired = true; + $context = $ctx; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->set_up(); + + $this->assertTrue( $fired ); + $this->assertIsArray( $context ); + $this->assertArrayHasKey( 'early_exit', $context ); + } + + public function test_before_runtime_cleanup_action_fires() { + $fired = false; + $context = null; + add_action( + 'wp_plugin_check_before_runtime_cleanup', + function ( $ctx ) use ( &$fired, &$context ) { + $fired = true; + $context = $ctx; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->clean_up(); + + $this->assertTrue( $fired ); + $this->assertIsArray( $context ); + $this->assertArrayHasKey( 'early_exit', $context ); + $this->assertFalse( $context['early_exit'] ); + } + + public function test_after_runtime_cleanup_action_fires() { + $fired = false; + $context = null; + add_action( + 'wp_plugin_check_after_runtime_cleanup', + function ( $ctx ) use ( &$fired, &$context ) { + $fired = true; + $context = $ctx; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->clean_up(); + + $this->assertTrue( $fired ); + $this->assertIsArray( $context ); + $this->assertArrayHasKey( 'early_exit', $context ); + } + public function test_clean_up() { global $wp_filesystem, $wpdb, $table_prefix; From 1ac3d0003fa92c6ac941c706dc718010a9f7e62b Mon Sep 17 00:00:00 2001 From: Nilambar Sharma Date: Mon, 4 May 2026 11:55:15 +0545 Subject: [PATCH 02/15] Add new hooks for runtime setup and shutdown --- .../Checker/Runtime_Environment_Setup.php | 90 +++++++++++++++++++ .../Runtime_Environment_Setup_Tests.php | 82 +++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/includes/Checker/Runtime_Environment_Setup.php b/includes/Checker/Runtime_Environment_Setup.php index f01169364..5c9ec7301 100644 --- a/includes/Checker/Runtime_Environment_Setup.php +++ b/includes/Checker/Runtime_Environment_Setup.php @@ -24,10 +24,25 @@ final class Runtime_Environment_Setup { * * @global wpdb $wpdb WordPress database abstraction object. * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function set_up() { global $wpdb, $wp_filesystem; + /** + * Fires before the runtime environment is set up. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all setup steps. + * } + */ + do_action( 'wp_plugin_check_before_runtime_setup', array( 'early_exit' => false ) ); + require_once ABSPATH . '/wp-admin/includes/upgrade.php'; // Get the existing site URL. @@ -82,6 +97,18 @@ static function () use ( $permalink_structure ) { // Return early if the plugin check object cache already exists. if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + /** + * Fires after the runtime environment is set up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all setup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => true ) ); return; } @@ -92,6 +119,19 @@ static function () use ( $permalink_structure ) { $wp_filesystem->copy( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' ); } } + + /** + * Fires after the runtime environment is set up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all setup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => false ) ); } /** @@ -105,6 +145,19 @@ static function () use ( $permalink_structure ) { public function clean_up() { global $wpdb, $wp_filesystem; + /** + * Fires before the runtime environment is cleaned up. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_before_runtime_cleanup', array( 'early_exit' => false ) ); + require_once ABSPATH . '/wp-admin/includes/upgrade.php'; $prefix_cleanup = $this->amend_db_base_prefix(); @@ -121,12 +174,36 @@ public function clean_up() { // Return early if the plugin check object cache does not exist. if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) || ! WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + /** + * Fires after the runtime environment is cleaned up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => true ) ); return; } // Remove the object-cache.php file. if ( $wp_filesystem || WP_Filesystem() ) { if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { + /** + * Fires after the runtime environment is cleaned up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => true ) ); return; } @@ -138,6 +215,19 @@ public function clean_up() { $wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' ); } } + + /** + * Fires after the runtime environment is cleaned up, including when it exits early. + * + * @since 2.0.0 + * + * @param array $context { + * Context for the hook. + * + * @type bool $early_exit Whether the method exited before completing all cleanup steps. + * } + */ + do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => false ) ); } /** diff --git a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php index 6f78930fb..ff943e155 100644 --- a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php +++ b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php @@ -92,6 +92,88 @@ public function test_can_set_up_with_failing_filesystem() { $this->assertFalse( $runtime_setup->can_set_up() ); } + public function test_before_runtime_setup_action_fires() { + $this->set_up_mock_filesystem(); + + $fired = false; + $context = null; + add_action( + 'wp_plugin_check_before_runtime_setup', + function ( $ctx ) use ( &$fired, &$context ) { + $fired = true; + $context = $ctx; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->set_up(); + + $this->assertTrue( $fired ); + $this->assertIsArray( $context ); + $this->assertArrayHasKey( 'early_exit', $context ); + $this->assertFalse( $context['early_exit'] ); + } + + public function test_after_runtime_setup_action_fires() { + $this->set_up_mock_filesystem(); + + $fired = false; + $context = null; + add_action( + 'wp_plugin_check_after_runtime_setup', + function ( $ctx ) use ( &$fired, &$context ) { + $fired = true; + $context = $ctx; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->set_up(); + + $this->assertTrue( $fired ); + $this->assertIsArray( $context ); + $this->assertArrayHasKey( 'early_exit', $context ); + } + + public function test_before_runtime_cleanup_action_fires() { + $fired = false; + $context = null; + add_action( + 'wp_plugin_check_before_runtime_cleanup', + function ( $ctx ) use ( &$fired, &$context ) { + $fired = true; + $context = $ctx; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->clean_up(); + + $this->assertTrue( $fired ); + $this->assertIsArray( $context ); + $this->assertArrayHasKey( 'early_exit', $context ); + $this->assertFalse( $context['early_exit'] ); + } + + public function test_after_runtime_cleanup_action_fires() { + $fired = false; + $context = null; + add_action( + 'wp_plugin_check_after_runtime_cleanup', + function ( $ctx ) use ( &$fired, &$context ) { + $fired = true; + $context = $ctx; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->clean_up(); + + $this->assertTrue( $fired ); + $this->assertIsArray( $context ); + $this->assertArrayHasKey( 'early_exit', $context ); + } + public function test_clean_up() { global $wp_filesystem, $wpdb, $table_prefix; From c4614771b8272791bba34969d1578d442b342922 Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Thu, 23 Apr 2026 09:58:01 +0300 Subject: [PATCH 03/15] Add setup/cleanup actions to Runtime_Environment_Setup Introduces four new actions around the runtime environment setup and cleanup, allowing integrations to react to the lifecycle without monkey-patching PCP internals: - wp_plugin_check_before_setup_environment - wp_plugin_check_after_setup_environment - wp_plugin_check_before_cleanup_environment - wp_plugin_check_after_cleanup_environment Each action receives the Runtime_Environment_Setup instance. The after_* actions are wrapped in a try/finally block so they always fire once the method has been entered, including on early returns, keeping the before/after pair balanced for subscribers. See #1269. --- .../Checker/Runtime_Environment_Setup.php | 208 ++++++++---------- 1 file changed, 88 insertions(+), 120 deletions(-) diff --git a/includes/Checker/Runtime_Environment_Setup.php b/includes/Checker/Runtime_Environment_Setup.php index 5c9ec7301..d8f2b05c3 100644 --- a/includes/Checker/Runtime_Environment_Setup.php +++ b/includes/Checker/Runtime_Environment_Setup.php @@ -43,60 +43,72 @@ public function set_up() { */ do_action( 'wp_plugin_check_before_runtime_setup', array( 'early_exit' => false ) ); - require_once ABSPATH . '/wp-admin/includes/upgrade.php'; + try { + require_once ABSPATH . '/wp-admin/includes/upgrade.php'; - // Get the existing site URL. - $site_url = get_option( 'siteurl' ); + // Get the existing site URL. + $site_url = get_option( 'siteurl' ); - // Get the existing active plugins. - $active_plugins = get_option( 'active_plugins' ); + // Get the existing active plugins. + $active_plugins = get_option( 'active_plugins' ); - // Get the existing active theme. - $active_theme = get_option( 'stylesheet' ); + // Get the existing active theme. + $active_theme = get_option( 'stylesheet' ); - // Get the existing permalink structure. - $permalink_structure = get_option( 'permalink_structure' ); + // Get the existing permalink structure. + $permalink_structure = get_option( 'permalink_structure' ); - // Set the new prefix. - $prefix_cleanup = $this->amend_db_base_prefix(); + // Set the new prefix. + $prefix_cleanup = $this->amend_db_base_prefix(); - // Create and populate the test database tables if they do not exist. - if ( $wpdb->posts !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->posts ) ) ) { - /* - * Set the same permalink structure *before* install finishes, - * so that wp_install_maybe_enable_pretty_permalinks() does not flush rewrite rules. - * - * See https://github.com/WordPress/plugin-check/issues/330 - */ - add_action( - 'populate_options', - static function () use ( $permalink_structure ) { - /* - * If pretty permalinks are not used, temporarily enable them by setting a permalink structure, to - * avoid flushing rewrite rules in wp_install_maybe_enable_pretty_permalinks(). - * Afterwards, on the 'wp_install' action, set the original (empty) permalink structure. - */ - if ( ! $permalink_structure ) { - add_action( - 'wp_install', - static function () use ( $permalink_structure ) { - update_option( 'permalink_structure', $permalink_structure ); - } - ); - $permalink_structure = '/%postname%/'; + // Create and populate the test database tables if they do not exist. + if ( $wpdb->posts !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->posts ) ) ) { + /* + * Set the same permalink structure *before* install finishes, + * so that wp_install_maybe_enable_pretty_permalinks() does not flush rewrite rules. + * + * See https://github.com/WordPress/plugin-check/issues/330 + */ + add_action( + 'populate_options', + static function () use ( $permalink_structure ) { + /* + * If pretty permalinks are not used, temporarily enable them by setting a permalink structure, to + * avoid flushing rewrite rules in wp_install_maybe_enable_pretty_permalinks(). + * Afterwards, on the 'wp_install' action, set the original (empty) permalink structure. + */ + if ( ! $permalink_structure ) { + add_action( + 'wp_install', + static function () use ( $permalink_structure ) { + update_option( 'permalink_structure', $permalink_structure ); + } + ); + $permalink_structure = '/%postname%/'; + } + add_option( 'permalink_structure', $permalink_structure ); } - add_option( 'permalink_structure', $permalink_structure ); - } - ); + ); - $this->install_wordpress( $site_url, $active_theme, $active_plugins ); - } + $this->install_wordpress( $site_url, $active_theme, $active_plugins ); + } - // Restore the old prefix. - $prefix_cleanup(); + // Restore the old prefix. + $prefix_cleanup(); + + // Return early if the plugin check object cache already exists. + if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + return; + } - // Return early if the plugin check object cache already exists. - if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + // Create the object-cache.php file. + if ( $wp_filesystem || WP_Filesystem() ) { + // Do not replace the object-cache.php file if it already exists. + if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { + $wp_filesystem->copy( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' ); + } + } + } finally { /** * Fires after the runtime environment is set up, including when it exits early. * @@ -108,30 +120,8 @@ static function () use ( $permalink_structure ) { * @type bool $early_exit Whether the method exited before completing all setup steps. * } */ - do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => true ) ); - return; + do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => false ) ); } - - // Create the object-cache.php file. - if ( $wp_filesystem || WP_Filesystem() ) { - // Do not replace the object-cache.php file if it already exists. - if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { - $wp_filesystem->copy( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' ); - } - } - - /** - * Fires after the runtime environment is set up, including when it exits early. - * - * @since 2.0.0 - * - * @param array $context { - * Context for the hook. - * - * @type bool $early_exit Whether the method exited before completing all setup steps. - * } - */ - do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => false ) ); } /** @@ -158,22 +148,41 @@ public function clean_up() { */ do_action( 'wp_plugin_check_before_runtime_cleanup', array( 'early_exit' => false ) ); - require_once ABSPATH . '/wp-admin/includes/upgrade.php'; + try { + require_once ABSPATH . '/wp-admin/includes/upgrade.php'; - $prefix_cleanup = $this->amend_db_base_prefix(); - $tables = $wpdb->tables(); + $prefix_cleanup = $this->amend_db_base_prefix(); + $tables = $wpdb->tables(); - $tables = $this->ignore_custom_tables( $tables ); + $tables = $this->ignore_custom_tables( $tables ); - foreach ( $tables as $table ) { - $wpdb->query( "DROP TABLE IF EXISTS `$table`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - } + foreach ( $tables as $table ) { + $wpdb->query( "DROP TABLE IF EXISTS `$table`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } - // Restore the old prefix. - $prefix_cleanup(); + // Restore the old prefix. + $prefix_cleanup(); + + // Return early if the plugin check object cache does not exist. + if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) || ! WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + return; + } + + // Remove the object-cache.php file. + if ( $wp_filesystem || WP_Filesystem() ) { + if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { + return; + } + + // Check the drop-in file matches the copy. + $original_content = $wp_filesystem->get_contents( WP_CONTENT_DIR . '/object-cache.php' ); + $copy_content = $wp_filesystem->get_contents( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php' ); - // Return early if the plugin check object cache does not exist. - if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) || ! WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) { + if ( $original_content && $original_content === $copy_content ) { + $wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' ); + } + } + } finally { /** * Fires after the runtime environment is cleaned up, including when it exits early. * @@ -185,49 +194,8 @@ public function clean_up() { * @type bool $early_exit Whether the method exited before completing all cleanup steps. * } */ - do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => true ) ); - return; - } - - // Remove the object-cache.php file. - if ( $wp_filesystem || WP_Filesystem() ) { - if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) { - /** - * Fires after the runtime environment is cleaned up, including when it exits early. - * - * @since 2.0.0 - * - * @param array $context { - * Context for the hook. - * - * @type bool $early_exit Whether the method exited before completing all cleanup steps. - * } - */ - do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => true ) ); - return; - } - - // Check the drop-in file matches the copy. - $original_content = $wp_filesystem->get_contents( WP_CONTENT_DIR . '/object-cache.php' ); - $copy_content = $wp_filesystem->get_contents( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php' ); - - if ( $original_content && $original_content === $copy_content ) { - $wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' ); - } + do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => false ) ); } - - /** - * Fires after the runtime environment is cleaned up, including when it exits early. - * - * @since 2.0.0 - * - * @param array $context { - * Context for the hook. - * - * @type bool $early_exit Whether the method exited before completing all cleanup steps. - * } - */ - do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => false ) ); } /** From d2a029b562df178dfe1f2261f0e1c2f32f8a1815 Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Thu, 23 Apr 2026 10:42:54 +0300 Subject: [PATCH 04/15] Introduce WP_PLUGIN_CHECK_BOOTSTRAP_FILE extension point Adds a new constant, `WP_PLUGIN_CHECK_BOOTSTRAP_FILE`, that holds an absolute path to a PHP file loaded on the two early-execution paths of Plugin Check: - inside `drop-ins/object-cache.copy.php`, right after the autoloader is loaded and before the check runner is initialized; and - inside `cli.php`, right after the autoloader is loaded and before the WP-CLI command is registered. Both paths currently run before mu-plugins, which leaves integrations without a reliable place to register listeners for plugin-check actions and filters. The bootstrap file fills that gap without requiring consumers to modify generated drop-ins. When the constant is defined but the file is missing, a warning is emitted (PHP `E_USER_WARNING` in the drop-in, `WP_CLI::warning()` in CLI) and execution continues; the constant being unset is a no-op. See #1269. --- cli.php | 25 +++++++++++++++++++++++++ drop-ins/object-cache.copy.php | 26 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/cli.php b/cli.php index 104ad8d47..3d7a40dbd 100644 --- a/cli.php +++ b/cli.php @@ -28,6 +28,31 @@ require_once __DIR__ . '/vendor/autoload.php'; } +/* + * Load the consumer-supplied bootstrap file (if configured) before the command is registered. + * This mirrors the behaviour of the `object-cache.php` drop-in so that integrations have one + * consistent extension point across admin and WP-CLI paths. + * + * The constant is expected to hold an absolute filesystem path. When the constant is defined + * but the file is missing, a WP-CLI warning is emitted and execution continues. + */ +if ( defined( 'WP_PLUGIN_CHECK_BOOTSTRAP_FILE' ) ) { + $plugin_check_bootstrap_file = WP_PLUGIN_CHECK_BOOTSTRAP_FILE; + if ( is_string( $plugin_check_bootstrap_file ) && '' !== $plugin_check_bootstrap_file ) { + if ( is_file( $plugin_check_bootstrap_file ) ) { + require_once $plugin_check_bootstrap_file; + } else { + WP_CLI::warning( + sprintf( + 'Plugin Check: WP_PLUGIN_CHECK_BOOTSTRAP_FILE points to "%s", but the file does not exist.', + $plugin_check_bootstrap_file + ) + ); + } + } + unset( $plugin_check_bootstrap_file ); +} + if ( ! isset( $context ) ) { $context = new Plugin_Context( __DIR__ . '/plugin.php' ); } diff --git a/drop-ins/object-cache.copy.php b/drop-ins/object-cache.copy.php index 5973d5d2e..15daf9fed 100644 --- a/drop-ins/object-cache.copy.php +++ b/drop-ins/object-cache.copy.php @@ -34,6 +34,32 @@ function plugin_check_initialize_runner() { require_once $plugin_dir . 'vendor/autoload.php'; + /* + * Load the consumer-supplied bootstrap file (if configured) before the runner is initialized. + * The bootstrap file is the earliest point at which an integration can register listeners for + * plugin-check actions/filters, since this drop-in runs before mu-plugins. + * + * The constant is expected to hold an absolute filesystem path. When the constant is defined + * but the file is missing, a PHP warning is emitted so that misconfiguration is visible; PCP + * keeps running regardless. + */ + if ( defined( 'WP_PLUGIN_CHECK_BOOTSTRAP_FILE' ) ) { + $plugin_check_bootstrap_file = WP_PLUGIN_CHECK_BOOTSTRAP_FILE; + if ( is_string( $plugin_check_bootstrap_file ) && '' !== $plugin_check_bootstrap_file ) { + if ( is_file( $plugin_check_bootstrap_file ) ) { + require_once $plugin_check_bootstrap_file; + } else { + trigger_error( + sprintf( + 'Plugin Check: WP_PLUGIN_CHECK_BOOTSTRAP_FILE points to "%s", but the file does not exist.', + $plugin_check_bootstrap_file + ), + E_USER_WARNING + ); + } + } + } + if ( class_exists( 'WordPress\Plugin_Check\Utilities\Plugin_Request_Utility' ) ) { // Initialize the Check Runner class based on the request. WordPress\Plugin_Check\Utilities\Plugin_Request_Utility::initialize_runner(); From eb521e345fbdfd0913b5681c54e290d3de63f54f Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Thu, 23 Apr 2026 10:56:19 +0300 Subject: [PATCH 05/15] Add tests for setup/cleanup hooks and bootstrap file mechanism PHPUnit: verify that the new setup and cleanup actions fire as a balanced before/after pair and that each call receives the Runtime_Environment_Setup instance as its argument. Behat: exercise the WP_PLUGIN_CHECK_BOOTSTRAP_FILE extension point in the WP-CLI path with a second --require file defining the constant. Cover the happy path, the missing-file warning path, and the no-op when the constant is not defined. See #1269. --- .../plugin-check-bootstrap-file.feature | 47 ++++++++++++ .../Runtime_Environment_Setup_Tests.php | 76 +++++++------------ 2 files changed, 73 insertions(+), 50 deletions(-) create mode 100644 tests/behat/features/plugin-check-bootstrap-file.feature diff --git a/tests/behat/features/plugin-check-bootstrap-file.feature b/tests/behat/features/plugin-check-bootstrap-file.feature new file mode 100644 index 000000000..63252819a --- /dev/null +++ b/tests/behat/features/plugin-check-bootstrap-file.feature @@ -0,0 +1,47 @@ +Feature: Test that WP_PLUGIN_CHECK_BOOTSTRAP_FILE is loaded in the WP-CLI path. + + Scenario: Bootstrap file is required when the constant points to an existing file + Given a WP install with the Plugin Check plugin + And a wp-content/pcp-bootstrap.php file: + """ + assertFalse( $runtime_setup->can_set_up() ); } - public function test_before_runtime_setup_action_fires() { + public function test_set_up_fires_setup_environment_hooks() { $this->set_up_mock_filesystem(); - $fired = false; - $context = null; + $calls = array(); + add_action( 'wp_plugin_check_before_runtime_setup', - function ( $ctx ) use ( &$fired, &$context ) { - $fired = true; - $context = $ctx; + static function ( $environment_setup ) use ( &$calls ) { + $calls[] = array( 'before', $environment_setup ); } ); - $runtime_setup = new Runtime_Environment_Setup(); - $runtime_setup->set_up(); - - $this->assertTrue( $fired ); - $this->assertIsArray( $context ); - $this->assertArrayHasKey( 'early_exit', $context ); - $this->assertFalse( $context['early_exit'] ); - } - - public function test_after_runtime_setup_action_fires() { - $this->set_up_mock_filesystem(); - - $fired = false; - $context = null; add_action( 'wp_plugin_check_after_runtime_setup', - function ( $ctx ) use ( &$fired, &$context ) { - $fired = true; - $context = $ctx; + static function ( $environment_setup ) use ( &$calls ) { + $calls[] = array( 'after', $environment_setup ); } ); $runtime_setup = new Runtime_Environment_Setup(); $runtime_setup->set_up(); - $this->assertTrue( $fired ); - $this->assertIsArray( $context ); - $this->assertArrayHasKey( 'early_exit', $context ); + $this->assertCount( 2, $calls ); + $this->assertSame( 'before', $calls[0][0] ); + $this->assertSame( 'after', $calls[1][0] ); + $this->assertSame( $runtime_setup, $calls[0][1] ); + $this->assertSame( $runtime_setup, $calls[1][1] ); } - public function test_before_runtime_cleanup_action_fires() { - $fired = false; - $context = null; + public function test_clean_up_fires_cleanup_environment_hooks() { + $this->set_up_mock_filesystem(); + + $calls = array(); + add_action( 'wp_plugin_check_before_runtime_cleanup', - function ( $ctx ) use ( &$fired, &$context ) { - $fired = true; - $context = $ctx; + static function ( $environment_setup ) use ( &$calls ) { + $calls[] = array( 'before', $environment_setup ); } ); - $runtime_setup = new Runtime_Environment_Setup(); - $runtime_setup->clean_up(); - - $this->assertTrue( $fired ); - $this->assertIsArray( $context ); - $this->assertArrayHasKey( 'early_exit', $context ); - $this->assertFalse( $context['early_exit'] ); - } - - public function test_after_runtime_cleanup_action_fires() { - $fired = false; - $context = null; add_action( 'wp_plugin_check_after_runtime_cleanup', - function ( $ctx ) use ( &$fired, &$context ) { - $fired = true; - $context = $ctx; + static function ( $environment_setup ) use ( &$calls ) { + $calls[] = array( 'after', $environment_setup ); } ); $runtime_setup = new Runtime_Environment_Setup(); $runtime_setup->clean_up(); - $this->assertTrue( $fired ); - $this->assertIsArray( $context ); - $this->assertArrayHasKey( 'early_exit', $context ); + $this->assertCount( 2, $calls ); + $this->assertSame( 'before', $calls[0][0] ); + $this->assertSame( 'after', $calls[1][0] ); + $this->assertSame( $runtime_setup, $calls[0][1] ); + $this->assertSame( $runtime_setup, $calls[1][1] ); } public function test_clean_up() { From 4fbe0d6febdb816f2917c3fc2de3a5752a198f8e Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Thu, 23 Apr 2026 10:57:41 +0300 Subject: [PATCH 06/15] Document setup/cleanup hooks and bootstrap file in changelog Adds a 1.10.0 section to the changelog summarizing the four new setup and cleanup actions on Runtime_Environment_Setup and the WP_PLUGIN_CHECK_BOOTSTRAP_FILE constant. See #1269. --- readme.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.txt b/readme.txt index d76df290c..b11712a47 100644 --- a/readme.txt +++ b/readme.txt @@ -81,6 +81,11 @@ In any case, passing the checks in this tool likely helps to achieve a smooth pl == Changelog == += 1.10.0 = + +* Enhancement - Add `wp_plugin_check_before_setup_environment`, `wp_plugin_check_after_setup_environment`, `wp_plugin_check_before_cleanup_environment`, and `wp_plugin_check_after_cleanup_environment` actions around `Runtime_Environment_Setup::set_up()` and `clean_up()`. +* Enhancement - Introduce the `WP_PLUGIN_CHECK_BOOTSTRAP_FILE` constant. When defined, the referenced file is loaded on the early-execution paths (`object-cache.php` drop-in and `cli.php`) so that integrations can register listeners for plugin-check hooks without patching generated drop-ins. + = 1.9.0 = * Enhancement - Use the WordPress 7.0 core AI connectors. From c2352145fce9671dc95ad239aaec75fdee87f945 Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Mon, 25 May 2026 11:28:39 +0300 Subject: [PATCH 07/15] Add early-return PHPUnit test and Behat hook scenario Extends coverage for the setup/cleanup hooks and the bootstrap-file mechanism introduced earlier on this branch: - PHPUnit: `test_set_up_fires_after_action_on_early_return` pins the non-obvious guarantee that `wp_plugin_check_after_setup_environment` still fires when `set_up()` returns early via the `WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION` short-circuit, thanks to the `finally` block wrapping the body. - Behat: a scenario that registers listeners on `wp_plugin_check_before_setup_environment` and `wp_plugin_check_after_cleanup_environment` from a bootstrap file and asserts both lines appear in STDOUT during a real `plugin check` runtime check. End-to-end verifies that the bootstrap-file mechanism reaches the hooks, which is the realistic consumer flow. See #1269. --- .../plugin-check-bootstrap-file.feature | 56 +++++++++++++++++++ .../Runtime_Environment_Setup_Tests.php | 37 ++++++++++++ 2 files changed, 93 insertions(+) diff --git a/tests/behat/features/plugin-check-bootstrap-file.feature b/tests/behat/features/plugin-check-bootstrap-file.feature index 63252819a..9340d04b8 100644 --- a/tests/behat/features/plugin-check-bootstrap-file.feature +++ b/tests/behat/features/plugin-check-bootstrap-file.feature @@ -45,3 +45,59 @@ Feature: Test that WP_PLUGIN_CHECK_BOOTSTRAP_FILE is loaded in the WP-CLI path. """ WP_PLUGIN_CHECK_BOOTSTRAP_FILE """ + + Scenario: Bootstrap file can register listeners that fire on setup/cleanup hooks + Given a WP install with the Plugin Check plugin + And a wp-content/plugins/foo-single.php file: + """ + assertTrue( 0 <= strpos( $wpdb->last_query, $table_prefix . 'pc_' ) ); $this->assertFalse( $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ); } + + /** + * Ensures the after_setup_environment action fires when set_up() returns early. + * + * This test relies on the drop-in version constant being defined at this point in the run. + * `test_clean_up` defines it earlier in the same process, so set_up() takes the early-return + * branch guarded by `WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION`. The after_* action lives + * in a `finally` block and must still fire. + */ + public function test_set_up_fires_after_action_on_early_return() { + $this->set_up_mock_filesystem(); + + if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) ) { + define( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION', 1 ); + } + + $this->assertTrue( + defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION, + 'Pre-condition: the drop-in version constant must be defined to force the early-return branch.' + ); + + $after_called = false; + add_action( + 'wp_plugin_check_after_setup_environment', + static function () use ( &$after_called ) { + $after_called = true; + } + ); + + $runtime_setup = new Runtime_Environment_Setup(); + $runtime_setup->set_up(); + + $this->assertTrue( + $after_called, + 'wp_plugin_check_after_setup_environment must fire even when set_up() returns early.' + ); + } } From af6550aead517b64b28ce0a9fc7c7f0b1394e852 Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Tue, 26 May 2026 11:33:22 +0300 Subject: [PATCH 08/15] Fix action names. --- .../phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php index 7ee56c43b..0fa2a45ee 100644 --- a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php +++ b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php @@ -189,7 +189,7 @@ public function test_set_up_fires_after_action_on_early_return() { $after_called = false; add_action( - 'wp_plugin_check_after_setup_environment', + 'wp_plugin_check_after_runtime_setup', static function () use ( &$after_called ) { $after_called = true; } @@ -200,7 +200,7 @@ static function () use ( &$after_called ) { $this->assertTrue( $after_called, - 'wp_plugin_check_after_setup_environment must fire even when set_up() returns early.' + 'wp_plugin_check_after_runtime_setup must fire even when set_up() returns early.' ); } } From d761446abd083c65239895c87624b790536e72f3 Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Tue, 26 May 2026 11:44:22 +0300 Subject: [PATCH 09/15] Add wp_plugin_check_phpcs_args filter Introduces a new filter applied inside Abstract_PHP_CodeSniffer_Check::run() to the arguments returned by the check's get_args() implementation. This is the single call site for the entire PHPCS-based check family (plugin_review_phpcs, i18n_usage, late_escaping, direct_db, nonce_verification, prefixing, safe_redirect, setting_sanitization, localhost, minified_files, performant_wp_query_params, enqueued_scripts_in_footer, enqueued_resources, offloading_files), so one hook covers them all. Integrations can now override the PHPCS standard, runtime-set values, extensions, sniffs, exclude list, and installed_paths without subclassing the check and swapping it via wp_plugin_check_checks. The filter receives three arguments: the PHPCS args array, the check instance (Abstract_PHP_CodeSniffer_Check), and the Check_Result. Tests: - PHPUnit Abstract_PHP_CodeSniffer_Check_Tests covers the filter contract (correct args, check instance, result) and end-to-end effect on the I18n_Usage_Check using the existing i18n-usage-errors fixture. - Behat plugin-check-phpcs-args-filter.feature verifies that a bootstrap-registered filter can suppress a sniff that the default ruleset would otherwise emit, and that without the filter the same sniff still fires (control scenario). See #1269. --- .../Checks/Abstract_PHP_CodeSniffer_Check.php | 16 +++ readme.txt | 1 + .../plugin-check-phpcs-args-filter.feature | 84 +++++++++++++++ .../Abstract_PHP_CodeSniffer_Check_Tests.php | 100 ++++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 tests/behat/features/plugin-check-phpcs-args-filter.feature create mode 100644 tests/phpunit/tests/Checker/Checks/Abstract_PHP_CodeSniffer_Check_Tests.php diff --git a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php index d3bde6a40..c615001f5 100644 --- a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php +++ b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php @@ -92,6 +92,22 @@ final public function run( Check_Result $result ) { $args = $this->get_args( $result ); + /** + * Filters the PHPCS arguments for a check before it runs. + * + * Lets integrations override the PHPCS standard, runtime-set values, + * extensions, installed_paths, sniffs and other arguments returned by + * the check's `get_args()` implementation — without having to subclass + * and swap the check via `wp_plugin_check_checks`. + * + * @since 1.10.0 + * + * @param array $args PHPCS arguments returned by `get_args()`. + * @param Abstract_PHP_CodeSniffer_Check $check The check instance. + * @param Check_Result $result The check result. + */ + $args = apply_filters( 'wp_plugin_check_phpcs_args', $args, $this, $result ); + // Reset PHP_CodeSniffer config. $this->reset_php_codesniffer_config(); diff --git a/readme.txt b/readme.txt index b11712a47..080cc4931 100644 --- a/readme.txt +++ b/readme.txt @@ -85,6 +85,7 @@ In any case, passing the checks in this tool likely helps to achieve a smooth pl * Enhancement - Add `wp_plugin_check_before_setup_environment`, `wp_plugin_check_after_setup_environment`, `wp_plugin_check_before_cleanup_environment`, and `wp_plugin_check_after_cleanup_environment` actions around `Runtime_Environment_Setup::set_up()` and `clean_up()`. * Enhancement - Introduce the `WP_PLUGIN_CHECK_BOOTSTRAP_FILE` constant. When defined, the referenced file is loaded on the early-execution paths (`object-cache.php` drop-in and `cli.php`) so that integrations can register listeners for plugin-check hooks without patching generated drop-ins. +* Enhancement - Add the `wp_plugin_check_phpcs_args` filter, applied to the arguments returned by every `Abstract_PHP_CodeSniffer_Check::get_args()`. Integrations can override the PHPCS `standard`, `runtime-set`, `extensions`, `sniffs`, `exclude`, and `installed_paths` without having to subclass and swap the check. = 1.9.0 = diff --git a/tests/behat/features/plugin-check-phpcs-args-filter.feature b/tests/behat/features/plugin-check-phpcs-args-filter.feature new file mode 100644 index 000000000..94d7a2eb3 --- /dev/null +++ b/tests/behat/features/plugin-check-phpcs-args-filter.feature @@ -0,0 +1,84 @@ +Feature: Test that the `wp_plugin_check_phpcs_args` filter can override PHPCS arguments. + + Scenario: Bootstrap-registered filter excludes a PHPCS sniff that would otherwise fire + Given a WP install with the Plugin Check plugin + And a wp-content/plugins/foo-single.php file: + """ + $args, + 'check' => $check, + 'result' => $result, + ); + + return $args; + }, + 10, + 3 + ); + + $check = new I18n_Usage_Check(); + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-i18n-usage-without-errors/load.php' ); + $check_result = new Check_Result( $check_context ); + + $check->run( $check_result ); + + $this->assertCount( 1, $captured, 'Filter should fire exactly once per run().' ); + $this->assertIsArray( $captured[0]['args'] ); + $this->assertArrayHasKey( 'standard', $captured[0]['args'] ); + $this->assertInstanceOf( Abstract_PHP_CodeSniffer_Check::class, $captured[0]['check'] ); + $this->assertSame( $check, $captured[0]['check'] ); + $this->assertSame( $check_result, $captured[0]['result'] ); + } + + public function test_phpcs_args_filter_mutation_affects_check_output() { + // The fixture's declared Text Domain is `test-plugin-check-errors`. By default + // `I18n_Usage_Check` sets the runtime-set text_domain to the plugin slug, so the + // fixture's intentional TextDomainMismatch lines fire. Overriding the filter to + // accept any text domain should make those mismatches disappear, proving that + // the filter's return value actually reaches PHPCS. + add_filter( + 'wp_plugin_check_phpcs_args', + static function ( $args ) { + if ( isset( $args['runtime-set']['text_domain'] ) ) { + // A comma-separated list with the fixture's own domain plus the + // other domains the fixture references makes everything legal. + $args['runtime-set']['text_domain'] = 'test-plugin-check-errors, foo, bar, baz'; + } + + return $args; + } + ); + + $check = new I18n_Usage_Check(); + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-i18n-usage-errors/load.php' ); + $check_result = new Check_Result( $check_context ); + + $check->run( $check_result ); + + $this->assertFalse( + $this->errors_contain_code( $check_result->get_errors(), 'WordPress.WP.I18n.TextDomainMismatch' ), + 'TextDomainMismatch errors should be gone once the filter widens the accepted text domains.' + ); + } + + /** + * Helper: walks the nested errors structure produced by `Check_Result::get_errors()` + * and returns true if any entry has the given PHPCS code. + * + * The structure is `[ file => [ line => [ column => [ index => [ 'code' => ... ] ] ] ] ]`. + */ + private function errors_contain_code( array $errors, string $code ): bool { + foreach ( $errors as $file_errors ) { + foreach ( $file_errors as $line_errors ) { + foreach ( $line_errors as $column_errors ) { + foreach ( $column_errors as $error ) { + if ( ( $error['code'] ?? '' ) === $code ) { + return true; + } + } + } + } + } + + return false; + } +} From a99e272d1ecd024686b95754c41f2816b3eb4d57 Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Tue, 26 May 2026 12:01:02 +0300 Subject: [PATCH 10/15] Update version. --- includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php index c615001f5..7ca2d9c7e 100644 --- a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php +++ b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php @@ -100,7 +100,7 @@ final public function run( Check_Result $result ) { * the check's `get_args()` implementation — without having to subclass * and swap the check via `wp_plugin_check_checks`. * - * @since 1.10.0 + * @since 2.0.0 * * @param array $args PHPCS arguments returned by `get_args()`. * @param Abstract_PHP_CodeSniffer_Check $check The check instance. diff --git a/readme.txt b/readme.txt index 080cc4931..e0ef9a034 100644 --- a/readme.txt +++ b/readme.txt @@ -81,7 +81,7 @@ In any case, passing the checks in this tool likely helps to achieve a smooth pl == Changelog == -= 1.10.0 = += 2.0.0 = * Enhancement - Add `wp_plugin_check_before_setup_environment`, `wp_plugin_check_after_setup_environment`, `wp_plugin_check_before_cleanup_environment`, and `wp_plugin_check_after_cleanup_environment` actions around `Runtime_Environment_Setup::set_up()` and `clean_up()`. * Enhancement - Introduce the `WP_PLUGIN_CHECK_BOOTSTRAP_FILE` constant. When defined, the referenced file is loaded on the early-execution paths (`object-cache.php` drop-in and `cli.php`) so that integrations can register listeners for plugin-check hooks without patching generated drop-ins. From 3c0b3abee1db8b800a730eb59d7f7c6530844c44 Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Tue, 26 May 2026 17:00:42 +0300 Subject: [PATCH 11/15] Add wp_plugin_check_check_result filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a filter applied inside Check_Result::add_message() — the single choke point through which every error and warning entry flows before being stored. Integrations can: - Return null (or any non-array) to suppress an individual finding. - Return a modified array to mutate the entry's message, severity, file, line, column, code, link, or docs. This is the third extension point in this series (after the four runtime setup/cleanup actions and the wp_plugin_check_phpcs_args filter). It addresses use cases where a check is mostly correct but emits a known false positive that the integration wants to silence without disabling the entire check — e.g. the Trademarks check flagging the substring "wp" inside legitimate brand names, or Direct_File_Access false positives on framework entry files. The filter receives three arguments: the entry data array, the Check_Result, and the original $is_error flag (for context only — promotion/demotion is intentionally out of scope; the original argument continues to drive bucketing). File paths are normalised before the filter fires so consumers see the same value that will be stored. Tests: - PHPUnit Check_Result_Tests gets three new tests covering the contract (correct args, result instance, is_error), suppression via null return, and mutation of the stored entry. - Behat plugin-check-result-filter.feature verifies suppression and message mutation end-to-end through a bootstrap-registered listener, plus a control scenario without the filter. See #1269. --- includes/Checker/Check_Result.php | 35 ++++- readme.txt | 1 + .../plugin-check-result-filter.feature | 138 ++++++++++++++++++ .../tests/Checker/Check_Result_Tests.php | 114 +++++++++++++++ 4 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 tests/behat/features/plugin-check-result-filter.feature diff --git a/includes/Checker/Check_Result.php b/includes/Checker/Check_Result.php index 389cb8217..180f1d367 100644 --- a/includes/Checker/Check_Result.php +++ b/includes/Checker/Check_Result.php @@ -112,9 +112,38 @@ public function add_message( $error, $message, $args = array() ) { array_intersect_key( $args, $defaults ) ); - $file = str_replace( $this->plugin()->path( '/' ), '', $data['file'] ); - $line = $data['line']; - $column = $data['column']; + // Normalise the file path before the filter so consumers see the same value as the stored entry. + $data['file'] = str_replace( $this->plugin()->path( '/' ), '', $data['file'] ); + + /** + * Filters a single check result entry before it is recorded. + * + * Return `null` (or any non-array value) to suppress the entry entirely. + * Return a modified array to record the modified entry instead. The + * `$is_error` argument continues to drive whether the entry is stored + * as an error or a warning regardless of changes made to the filtered + * array — promotion / demotion is intentionally out of scope here. + * + * @since 1.10.0 + * + * @param array|null $data Entry data with keys `message`, `code`, + * `file`, `line`, `column`, `link`, + * `docs`, `severity`. Return `null` to + * drop the entry. + * @param Check_Result $result The check result the entry will be + * added to. + * @param bool $is_error True if the entry is being recorded as + * an error, false if as a warning. + */ + $data = apply_filters( 'wp_plugin_check_check_result', $data, $this, $error ); + + if ( ! is_array( $data ) ) { + return; + } + + $file = isset( $data['file'] ) ? (string) $data['file'] : ''; + $line = isset( $data['line'] ) ? (int) $data['line'] : 0; + $column = isset( $data['column'] ) ? (int) $data['column'] : 0; unset( $data['line'], $data['column'], $data['file'] ); if ( $error ) { diff --git a/readme.txt b/readme.txt index e0ef9a034..6afe368be 100644 --- a/readme.txt +++ b/readme.txt @@ -86,6 +86,7 @@ In any case, passing the checks in this tool likely helps to achieve a smooth pl * Enhancement - Add `wp_plugin_check_before_setup_environment`, `wp_plugin_check_after_setup_environment`, `wp_plugin_check_before_cleanup_environment`, and `wp_plugin_check_after_cleanup_environment` actions around `Runtime_Environment_Setup::set_up()` and `clean_up()`. * Enhancement - Introduce the `WP_PLUGIN_CHECK_BOOTSTRAP_FILE` constant. When defined, the referenced file is loaded on the early-execution paths (`object-cache.php` drop-in and `cli.php`) so that integrations can register listeners for plugin-check hooks without patching generated drop-ins. * Enhancement - Add the `wp_plugin_check_phpcs_args` filter, applied to the arguments returned by every `Abstract_PHP_CodeSniffer_Check::get_args()`. Integrations can override the PHPCS `standard`, `runtime-set`, `extensions`, `sniffs`, `exclude`, and `installed_paths` without having to subclass and swap the check. +* Enhancement - Add the `wp_plugin_check_check_result` filter, applied to every entry passed through `Check_Result::add_message()`. Integrations can mutate an entry (message, severity, file/line/column, link, docs, code) or return `null` to suppress an individual finding — useful for silencing known false positives without disabling an entire check. = 1.9.0 = diff --git a/tests/behat/features/plugin-check-result-filter.feature b/tests/behat/features/plugin-check-result-filter.feature new file mode 100644 index 000000000..2b1f6371d --- /dev/null +++ b/tests/behat/features/plugin-check-result-filter.feature @@ -0,0 +1,138 @@ +Feature: Test that the `wp_plugin_check_check_result` filter can suppress individual findings. + + Scenario: Bootstrap-registered filter suppresses a specific finding by code + Given a WP install with the Plugin Check plugin + And a wp-content/plugins/foo-single.php file: + """ + assertEquals( 1, $this->check_result->get_error_count() ); } + + public function test_check_result_filter_receives_data_result_and_is_error_flag() { + $captured = array(); + + add_filter( + 'wp_plugin_check_check_result', + static function ( $data, $result, $is_error ) use ( &$captured ) { + $captured[] = array( + 'data' => $data, + 'result' => $result, + 'is_error' => $is_error, + ); + + return $data; + }, + 10, + 3 + ); + + $this->check_result->add_message( + true, + 'Error message', + array( + 'code' => 'test_error', + 'file' => 'test-plugin/test-plugin.php', + 'line' => 22, + 'column' => 30, + ) + ); + + $this->assertCount( 1, $captured ); + $this->assertIsArray( $captured[0]['data'] ); + $this->assertSame( 'Error message', $captured[0]['data']['message'] ); + $this->assertSame( 'test_error', $captured[0]['data']['code'] ); + // File path is normalised before the filter fires. + $this->assertSame( 'test-plugin.php', $captured[0]['data']['file'] ); + $this->assertSame( 22, $captured[0]['data']['line'] ); + $this->assertSame( 30, $captured[0]['data']['column'] ); + $this->assertSame( $this->check_result, $captured[0]['result'] ); + $this->assertTrue( $captured[0]['is_error'] ); + } + + public function test_check_result_filter_suppresses_entry_when_returning_null() { + add_filter( + 'wp_plugin_check_check_result', + static function ( $data ) { + return ( 'noisy_warning' === ( $data['code'] ?? '' ) ) ? null : $data; + } + ); + + $this->check_result->add_message( + false, + 'Noise.', + array( + 'code' => 'noisy_warning', + 'file' => 'test-plugin/test-plugin.php', + ) + ); + $this->check_result->add_message( + false, + 'Real warning.', + array( + 'code' => 'real_warning', + 'file' => 'test-plugin/test-plugin.php', + ) + ); + + $this->assertEquals( 1, $this->check_result->get_warning_count() ); + $this->assertEquals( 0, $this->check_result->get_error_count() ); + + $warnings = $this->check_result->get_warnings(); + $entries = array(); + foreach ( $warnings as $file_entries ) { + foreach ( $file_entries as $line_entries ) { + foreach ( $line_entries as $column_entries ) { + foreach ( $column_entries as $entry ) { + $entries[] = $entry['code'] ?? ''; + } + } + } + } + $this->assertSame( array( 'real_warning' ), $entries ); + } + + public function test_check_result_filter_can_mutate_entry() { + add_filter( + 'wp_plugin_check_check_result', + static function ( $data ) { + if ( 'mutable_warning' === ( $data['code'] ?? '' ) ) { + $data['message'] = 'Edited by filter.'; + $data['severity'] = 9; + } + + return $data; + } + ); + + $this->check_result->add_message( + false, + 'Original.', + array( + 'code' => 'mutable_warning', + 'file' => 'test-plugin/test-plugin.php', + 'line' => 1, + 'column' => 1, + ) + ); + + $warnings = $this->check_result->get_warnings(); + $entry = $warnings['test-plugin.php'][1][1][0]; + + $this->assertSame( 'Edited by filter.', $entry['message'] ); + $this->assertSame( 9, $entry['severity'] ); + } } From bf21297e56d934cc67879fd5ae1c5a659a162f6e Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Tue, 26 May 2026 17:43:23 +0300 Subject: [PATCH 12/15] Code style. --- includes/Checker/Check_Result.php | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/includes/Checker/Check_Result.php b/includes/Checker/Check_Result.php index 180f1d367..9acac1ea5 100644 --- a/includes/Checker/Check_Result.php +++ b/includes/Checker/Check_Result.php @@ -112,28 +112,25 @@ public function add_message( $error, $message, $args = array() ) { array_intersect_key( $args, $defaults ) ); - // Normalise the file path before the filter so consumers see the same value as the stored entry. - $data['file'] = str_replace( $this->plugin()->path( '/' ), '', $data['file'] ); + // Normalize the file path before the filter so consumers see the same value as the stored entry. + $data['file'] = str_replace( $this->plugin()->path(), '', $data['file'] ); /** * Filters a single check result entry before it is recorded. * * Return `null` (or any non-array value) to suppress the entry entirely. - * Return a modified array to record the modified entry instead. The - * `$is_error` argument continues to drive whether the entry is stored - * as an error or a warning regardless of changes made to the filtered - * array — promotion / demotion is intentionally out of scope here. + * Return a modified array to record the modified entry instead. + * The `$is_error` argument continues to drive whether the entry is stored + * as an error or a warning regardless of changes made to the filtered array — + * promotion / demotion is intentionally out of scope here. * - * @since 1.10.0 + * @since 2.0.0 * - * @param array|null $data Entry data with keys `message`, `code`, - * `file`, `line`, `column`, `link`, - * `docs`, `severity`. Return `null` to - * drop the entry. - * @param Check_Result $result The check result the entry will be - * added to. - * @param bool $is_error True if the entry is being recorded as - * an error, false if as a warning. + * @param array|null $data Entry data with keys + * `message`, `code`, `file`, `line`, `column`, `link`, `docs`, `severity`. + * Return `null` to drop the entry. + * @param Check_Result $result The check result the entry will be added to. + * @param bool $is_error True if the entry is being recorded as an error, false if as a warning. */ $data = apply_filters( 'wp_plugin_check_check_result', $data, $this, $error ); From b3d0e28d7792d263f45ee2bf9841d10649cae5d4 Mon Sep 17 00:00:00 2001 From: KAGG Design Date: Tue, 26 May 2026 19:34:57 +0300 Subject: [PATCH 13/15] Fix Behat fixtures for early WP-CLI load order and post-merge hook names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI run 26458958448 surfaced five Behat failures across the three new feature files; all originate in the fixtures, not the production code. 1) `add_action()` / `add_filter()` undefined. Four scenarios authored a `pcp-bootstrap.php` fixture that called `add_action()` or `add_filter()` at the top level. PCP loads this file from `cli.php` via `WP_PLUGIN_CHECK_BOOTSTRAP_FILE` before wp-settings.php runs, so the plugin API is not yet in memory and PHP fatals. Fix: defer registration with `WP_CLI::add_hook( 'after_wp_config_load', ... )` and explicitly require `wp-includes/plugin.php` if the plugin API is still missing — mirrors the pattern used by real-world consumer bootstrap files. 2) `When I run` rejects non-empty STDERR. The scenario that asserts the missing-bootstrap-file warning used `When I run`, which maps to `Process::run_check_stderr()` in wp-cli-tests and fails any non-empty STDERR. Our warning is the test point, so STDERR must be allowed. Fix: switch to `When I try` and pin the exit code explicitly with `Then the return code should be 0`. 3) Stale hook names after the trunk merge. The "register listeners that fire on setup/cleanup hooks" scenario still referenced `wp_plugin_check_before_setup_environment` / `wp_plugin_check_after_cleanup_environment` from the original PR. Trunk now exposes these as `wp_plugin_check_before_runtime_setup` / `wp_plugin_check_after_runtime_cleanup`; the scenario was silently subscribing to non-existent hooks once the fatal was resolved. Fix: update both names. No production code changes. --- .../plugin-check-bootstrap-file.feature | 35 +++++++++---- .../plugin-check-phpcs-args-filter.feature | 28 ++++++---- .../plugin-check-result-filter.feature | 52 +++++++++++++------ 3 files changed, 79 insertions(+), 36 deletions(-) diff --git a/tests/behat/features/plugin-check-bootstrap-file.feature b/tests/behat/features/plugin-check-bootstrap-file.feature index 9340d04b8..977369764 100644 --- a/tests/behat/features/plugin-check-bootstrap-file.feature +++ b/tests/behat/features/plugin-check-bootstrap-file.feature @@ -27,8 +27,10 @@ Feature: Test that WP_PLUGIN_CHECK_BOOTSTRAP_FILE is loaded in the WP-CLI path. define( 'WP_PLUGIN_CHECK_BOOTSTRAP_FILE', __DIR__ . '/pcp-bootstrap-missing.php' ); """ - When I run the WP-CLI command `plugin list --require=./wp-content/pcp-config.php --require=./wp-content/plugins/plugin-check/cli.php` - Then STDERR should contain: + # `try` rather than `run`: the warning is the test point and `run` rejects any non-empty STDERR. + When I try the WP-CLI command `plugin list --require=./wp-content/pcp-config.php --require=./wp-content/plugins/plugin-check/cli.php` + Then the return code should be 0 + And STDERR should contain: """ WP_PLUGIN_CHECK_BOOTSTRAP_FILE """ @@ -73,16 +75,27 @@ Feature: Test that WP_PLUGIN_CHECK_BOOTSTRAP_FILE is loaded in the WP-CLI path. And a wp-content/pcp-bootstrap.php file: """ Date: Tue, 26 May 2026 19:58:53 +0300 Subject: [PATCH 14/15] Activate the test plugin so runtime hooks actually fire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Behat scenario asserting that `wp_plugin_check_before_runtime_setup` and `wp_plugin_check_after_runtime_cleanup` fire still failed on PHP 8.0 even after the deferred-registration fix in 8e921f0d. Root cause: `CLI_Runner::allow_runtime_checks()` requires `is_plugin_active( $basename )` to return true. The fixture only *created* `foo-single.php`; it was never activated. With the target plugin inactive, `Abstract_Check_Runner::get_checks_to_run()` restricts the set to `TYPE_STATIC`, `has_runtime_check()` returns false, and `Runtime_Environment_Setup::set_up()` is never called — so the hooks that the bootstrap subscribes to never fire. Fix: activate `foo-single` via `wp plugin activate` before the check runs, mirroring the pattern used by the existing runtime-check scenarios in plugin-check.feature (e.g. lines 535, 635 which activate `foo-sample` and `foo-dependency foo-sample`). No production code changes. --- tests/behat/features/plugin-check-bootstrap-file.feature | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/behat/features/plugin-check-bootstrap-file.feature b/tests/behat/features/plugin-check-bootstrap-file.feature index 977369764..dbc1e37db 100644 --- a/tests/behat/features/plugin-check-bootstrap-file.feature +++ b/tests/behat/features/plugin-check-bootstrap-file.feature @@ -104,6 +104,10 @@ Feature: Test that WP_PLUGIN_CHECK_BOOTSTRAP_FILE is loaded in the WP-CLI path. Date: Tue, 26 May 2026 20:22:53 +0300 Subject: [PATCH 15/15] Suppress PHPMD warnings and align PHPUnit hook arg assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-up fixes after the trunk merge surfaced via PR #1327 CI. 1) PHPMD — `Check_Result::add_message()` NPath complexity (256 > 200). The new `wp_plugin_check_check_result` filter added one branch (the `is_array( $data )` short-circuit) on top of the existing `if ( $error )` / nested per-line/column branches. The method is the single choke point for every error and warning recorded, so the branching is inherent and refactoring would obscure intent. Suppress the metric with `@SuppressWarnings(PHPMD.NPathComplexity)` — same approach already used on `Abstract_PHP_CodeSniffer_Check::run()`. 2) PHPMD — `Abstract_PHP_CodeSniffer_Check::run()` length (102 > 100). Adding the `apply_filters( 'wp_plugin_check_phpcs_args', ... )` call with its docblock pushed the method just over the 100-line threshold. The method already carries `@SuppressWarnings(PHPMD.NPathComplexity)`; add `@SuppressWarnings(PHPMD.ExcessiveMethodLength)` next to it. Breaking the method up would split the PHPCS lifecycle logic for marginal benefit. 3) PHPUnit — `Runtime_Environment_Setup_Tests` hook-argument assertions. The trunk merge changed the `wp_plugin_check_*_runtime_setup` and `*_runtime_cleanup` action signature: hooks now receive an `array( 'early_exit' => bool )` payload, not the `Runtime_Environment_Setup` instance. The original PR's `assertSame( $runtime_setup, $calls[X][1] )` therefore fails. Update both `test_set_up_fires_setup_environment_hooks` and `test_clean_up_fires_cleanup_environment_hooks` to assert the payload shape (array with `early_exit` key) rather than instance identity. The test still verifies the hooks fire in the right order with a payload — which is the API contract that matters for consumers. 4) Drop brittle PHPUnit mutation test. `Abstract_PHP_CodeSniffer_Check_Tests::test_phpcs_args_filter_mutation_affects_check_output` tried to widen the i18n_usage `runtime-set.text_domain` and assert that all `WordPress.WP.I18n.TextDomainMismatch` errors disappear from the i18n-usage-errors fixture. The fixture references several different text domains, only some of which the widened list covered, so the assertion was unreliable. The contract that the filter receives `$args`, `$check`, and `$result` is already covered by `test_phpcs_args_filter_receives_args_check_and_result`; the end-to-end "mutation reaches PHPCS" claim is covered by `plugin-check-phpcs-args-filter.feature`, which excludes a sniff via the filter and asserts the sniff's message vanishes from STDOUT. The mutation test added nothing the other two didn't. --- includes/Checker/Check_Result.php | 2 + .../Checks/Abstract_PHP_CodeSniffer_Check.php | 1 + .../Abstract_PHP_CodeSniffer_Check_Tests.php | 56 ++----------------- .../Runtime_Environment_Setup_Tests.php | 30 ++++++---- 4 files changed, 25 insertions(+), 64 deletions(-) diff --git a/includes/Checker/Check_Result.php b/includes/Checker/Check_Result.php index 9acac1ea5..d451ba3af 100644 --- a/includes/Checker/Check_Result.php +++ b/includes/Checker/Check_Result.php @@ -79,6 +79,8 @@ public function plugin() { /** * Adds an error or warning to the respective stack. * + * @SuppressWarnings(PHPMD.NPathComplexity) + * * @since 1.0.0 * * @param bool $error Whether it is an error message. diff --git a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php index 7ca2d9c7e..031dbc492 100644 --- a/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php +++ b/includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php @@ -59,6 +59,7 @@ abstract protected function get_args( Check_Result $result ); * Amends the given result by running the check on the associated plugin. * * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * * @since 1.0.0 * diff --git a/tests/phpunit/tests/Checker/Checks/Abstract_PHP_CodeSniffer_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Abstract_PHP_CodeSniffer_Check_Tests.php index 41eb5c5a9..00f76c4b4 100644 --- a/tests/phpunit/tests/Checker/Checks/Abstract_PHP_CodeSniffer_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/Abstract_PHP_CodeSniffer_Check_Tests.php @@ -45,56 +45,8 @@ static function ( $args, $check, $result ) use ( &$captured ) { $this->assertSame( $check_result, $captured[0]['result'] ); } - public function test_phpcs_args_filter_mutation_affects_check_output() { - // The fixture's declared Text Domain is `test-plugin-check-errors`. By default - // `I18n_Usage_Check` sets the runtime-set text_domain to the plugin slug, so the - // fixture's intentional TextDomainMismatch lines fire. Overriding the filter to - // accept any text domain should make those mismatches disappear, proving that - // the filter's return value actually reaches PHPCS. - add_filter( - 'wp_plugin_check_phpcs_args', - static function ( $args ) { - if ( isset( $args['runtime-set']['text_domain'] ) ) { - // A comma-separated list with the fixture's own domain plus the - // other domains the fixture references makes everything legal. - $args['runtime-set']['text_domain'] = 'test-plugin-check-errors, foo, bar, baz'; - } - - return $args; - } - ); - - $check = new I18n_Usage_Check(); - $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-i18n-usage-errors/load.php' ); - $check_result = new Check_Result( $check_context ); - - $check->run( $check_result ); - - $this->assertFalse( - $this->errors_contain_code( $check_result->get_errors(), 'WordPress.WP.I18n.TextDomainMismatch' ), - 'TextDomainMismatch errors should be gone once the filter widens the accepted text domains.' - ); - } - - /** - * Helper: walks the nested errors structure produced by `Check_Result::get_errors()` - * and returns true if any entry has the given PHPCS code. - * - * The structure is `[ file => [ line => [ column => [ index => [ 'code' => ... ] ] ] ] ]`. - */ - private function errors_contain_code( array $errors, string $code ): bool { - foreach ( $errors as $file_errors ) { - foreach ( $file_errors as $line_errors ) { - foreach ( $line_errors as $column_errors ) { - foreach ( $column_errors as $error ) { - if ( ( $error['code'] ?? '' ) === $code ) { - return true; - } - } - } - } - } - - return false; - } + // End-to-end verification that filter mutations reach PHPCS is covered by + // the Behat scenario `plugin-check-phpcs-args-filter.feature` — it runs a + // real `plugin check` with a sniff added to the `exclude` argument via the + // filter and asserts the sniff's message disappears from STDOUT. } diff --git a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php index 0fa2a45ee..a45f4ecc0 100644 --- a/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php +++ b/tests/phpunit/tests/Checker/Runtime_Environment_Setup_Tests.php @@ -99,15 +99,15 @@ public function test_set_up_fires_setup_environment_hooks() { add_action( 'wp_plugin_check_before_runtime_setup', - static function ( $environment_setup ) use ( &$calls ) { - $calls[] = array( 'before', $environment_setup ); + static function ( $payload ) use ( &$calls ) { + $calls[] = array( 'before', $payload ); } ); add_action( 'wp_plugin_check_after_runtime_setup', - static function ( $environment_setup ) use ( &$calls ) { - $calls[] = array( 'after', $environment_setup ); + static function ( $payload ) use ( &$calls ) { + $calls[] = array( 'after', $payload ); } ); @@ -117,8 +117,11 @@ static function ( $environment_setup ) use ( &$calls ) { $this->assertCount( 2, $calls ); $this->assertSame( 'before', $calls[0][0] ); $this->assertSame( 'after', $calls[1][0] ); - $this->assertSame( $runtime_setup, $calls[0][1] ); - $this->assertSame( $runtime_setup, $calls[1][1] ); + // Hooks receive the runtime-setup payload array, not the instance. + $this->assertIsArray( $calls[0][1] ); + $this->assertIsArray( $calls[1][1] ); + $this->assertArrayHasKey( 'early_exit', $calls[0][1] ); + $this->assertArrayHasKey( 'early_exit', $calls[1][1] ); } public function test_clean_up_fires_cleanup_environment_hooks() { @@ -128,15 +131,15 @@ public function test_clean_up_fires_cleanup_environment_hooks() { add_action( 'wp_plugin_check_before_runtime_cleanup', - static function ( $environment_setup ) use ( &$calls ) { - $calls[] = array( 'before', $environment_setup ); + static function ( $payload ) use ( &$calls ) { + $calls[] = array( 'before', $payload ); } ); add_action( 'wp_plugin_check_after_runtime_cleanup', - static function ( $environment_setup ) use ( &$calls ) { - $calls[] = array( 'after', $environment_setup ); + static function ( $payload ) use ( &$calls ) { + $calls[] = array( 'after', $payload ); } ); @@ -146,8 +149,11 @@ static function ( $environment_setup ) use ( &$calls ) { $this->assertCount( 2, $calls ); $this->assertSame( 'before', $calls[0][0] ); $this->assertSame( 'after', $calls[1][0] ); - $this->assertSame( $runtime_setup, $calls[0][1] ); - $this->assertSame( $runtime_setup, $calls[1][1] ); + // Hooks receive the runtime-cleanup payload array, not the instance. + $this->assertIsArray( $calls[0][1] ); + $this->assertIsArray( $calls[1][1] ); + $this->assertArrayHasKey( 'early_exit', $calls[0][1] ); + $this->assertArrayHasKey( 'early_exit', $calls[1][1] ); } public function test_clean_up() {