From 55ef776f012808a1e4b33a0f25d343a670e431de Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Fri, 4 Sep 2026 17:06:25 +0200 Subject: [PATCH 1/5] enforce --- .../Plugin_Repo/Plugin_Readme_Check.php | 66 +++++++++++-------- .../Checks/Plugin_Readme_Check_Tests.php | 50 ++++++-------- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php index 395234a46..c1ecf8bc2 100644 --- a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php @@ -980,7 +980,12 @@ private function check_language( Check_Result $result, string $readme_file, $par /** - * Checks for mismatched "Tested up to" header between plugin header and readme. + * Checks for a "Tested up to" header declared in the main plugin file. + * + * The "Tested up to" field is readme metadata and must be declared + * exclusively in the plugin readme file. This reports an error whenever + * the main plugin file also declares it, whether the readme value is + * missing, equal, or different. * * @since 1.8.0 * @@ -995,13 +1000,6 @@ private function check_tested_up_to_mismatch( Check_Result $result, $parser, str return; } - // Get the "Tested up to" value from the readme. - $readme_tested = isset( $parser->tested ) ? $parser->tested : ''; - - if ( empty( $readme_tested ) ) { - return; - } - // Get the "Tested up to" value from the plugin header. $tested_header = get_file_data( $plugin_main_file, @@ -1014,28 +1012,42 @@ private function check_tested_up_to_mismatch( Check_Result $result, $parser, str return; } - // Normalize versions by removing any suffixes (like -RC1, -beta1). - $readme_tested_normalized = strtok( $readme_tested, '-' ); - $plugin_tested_normalized = strtok( $plugin_tested, '-' ); + // Get the "Tested up to" value from the readme, if available. + $readme_tested = isset( $parser->tested ) ? $parser->tested : ''; - // Compare the two values. - if ( $readme_tested_normalized !== $plugin_tested_normalized ) { - $this->add_result_error_for_file( - $result, - sprintf( - /* translators: 1: Tested up to value from readme, 2: Tested up to value from plugin header */ - __( 'Mismatched "Tested up to": %1$s != %2$s.
The "Tested up to" value in the readme file must match the "Tested up to" value in the plugin header. If the plugin header has a "Tested up to" value, it will override the readme value, which can cause confusion.', 'plugin-check' ), - esc_html( $readme_tested_normalized ), + if ( ! empty( $readme_tested ) ) { + // Normalize versions by removing any suffixes (like -RC1, -beta1). + $readme_tested_normalized = strtok( $readme_tested, '-' ); + $plugin_tested_normalized = strtok( $plugin_tested, '-' ); + + if ( $readme_tested_normalized !== $plugin_tested_normalized ) { + $message = sprintf( + /* translators: 1: Tested up to value from plugin header, 2: Tested up to value from readme */ + __( 'The "Tested up to" field must be declared only in the plugin readme file: %1$s != %2$s.
Remove the "Tested up to" line from the main plugin PHP header and keep it only in readme.txt.', 'plugin-check' ), + esc_html( $plugin_tested_normalized ), + esc_html( $readme_tested_normalized ) + ); + } else { + $message = sprintf( + /* translators: %s: Tested up to value */ + __( 'The "Tested up to" field must be declared only in the plugin readme file (currently: %s).
Keeping the value in both places can cause them to silently get out of sync in a future release. Remove the "Tested up to" line from the main plugin PHP header and keep it only in readme.txt.', 'plugin-check' ), esc_html( $plugin_tested_normalized ) - ), - 'mismatched_tested_up_to_header', - $plugin_main_file, - 0, - 0, - 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information', - 7 - ); + ); + } + } else { + $message = __( 'The "Tested up to" field must be declared only in the plugin readme file.
Remove the "Tested up to" line from the main plugin PHP header and keep it only in readme.txt.', 'plugin-check' ); } + + $this->add_result_error_for_file( + $result, + $message, + 'plugin_header_tested_up_to_not_allowed', + $plugin_main_file, + 0, + 0, + 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information', + 7 + ); } /** diff --git a/tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php index bc7ffda8b..02800174f 100644 --- a/tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php @@ -767,11 +767,11 @@ public function test_run_with_mismatch() { $this->assertNotEmpty( $errors ); $this->assertArrayHasKey( 'load.php', $errors ); - // Check for mismatched "Tested up to" error. - $this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) ); + // Check for "Tested up to" declared in the plugin header error. + $this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) ); // Verify the error message contains the correct versions. - $error_items = wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ); + $error_items = wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ); $error_message = reset( $error_items )['message']; $this->assertStringContainsString( '6.7', $error_message ); $this->assertStringContainsString( '6.5', $error_message ); @@ -786,18 +786,15 @@ public function test_run_with_match() { $errors = $check_result->get_errors(); - // Should not have mismatched tested up to error when values match. - // Note: Other readme errors may still be present. - if ( ! empty( $errors ) ) { - foreach ( $errors as $file => $file_errors ) { - if ( isset( $file_errors[0][0] ) ) { - $this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) ); - } - } - } + $this->assertNotEmpty( $errors ); + $this->assertArrayHasKey( 'load.php', $errors ); - // Explicitly assert that we checked for the error code. - $this->assertTrue( true ); + // Even when the values match, declaring "Tested up to" in the header must be reported. + $this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) ); + + $error_items = wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ); + $error_message = reset( $error_items )['message']; + $this->assertStringContainsString( '6.7', $error_message ); } public function test_run_with_readme_only() { @@ -809,12 +806,12 @@ public function test_run_with_readme_only() { $errors = $check_result->get_errors(); - // Should not have mismatched tested up to error when only readme has the value. + // Should not report an error when "Tested up to" is only declared in the readme. // Note: Other readme errors may still be present. if ( ! empty( $errors ) ) { foreach ( $errors as $file => $file_errors ) { if ( isset( $file_errors[0][0] ) ) { - $this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) ); + $this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) ); } } } @@ -832,18 +829,11 @@ public function test_run_with_header_only() { $errors = $check_result->get_errors(); - // Should not have mismatched tested up to error when only header has the value. - // Note: Other readme errors may still be present. - if ( ! empty( $errors ) ) { - foreach ( $errors as $file => $file_errors ) { - if ( isset( $file_errors[0][0] ) ) { - $this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) ); - } - } - } + $this->assertNotEmpty( $errors ); + $this->assertArrayHasKey( 'load.php', $errors ); - // Explicitly assert that we checked for the error code. - $this->assertTrue( true ); + // "Tested up to" declared only in the header must still be reported. + $this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) ); } public function test_run_with_single_file_plugin() { @@ -855,12 +845,12 @@ public function test_run_with_single_file_plugin() { $errors = $check_result->get_errors(); - // Should not have mismatched tested up to errors for single-file plugins. + // Should not report "Tested up to" header errors for single-file plugins. // Note: Other header field errors may still be present. if ( ! empty( $errors ) ) { foreach ( $errors as $file => $file_errors ) { if ( isset( $file_errors[0][0] ) ) { - $this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) ); + $this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) ); } } } @@ -879,6 +869,6 @@ public function test_run_with_no_readme() { $errors = $check_result->get_errors(); // Should not have tested up to errors when readme doesn't exist. - $this->assertEmpty( wp_list_filter( $errors, array( 'code' => 'mismatched_tested_up_to_header' ) ) ); + $this->assertEmpty( wp_list_filter( $errors, array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) ); } } From 14e2633f5922a8b036d5fe1d87385c200d1bed7b Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Fri, 4 Sep 2026 17:13:06 +0200 Subject: [PATCH 2/5] update checks --- docs/checks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/checks.md b/docs/checks.md index 6ab4a3a4a..acb211f3d 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -9,7 +9,7 @@ | plugin_content | plugin_repo | Detects content that does not comply with the WordPress.org plugin guidelines. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) | | direct_file_access | security, plugin_repo | Checks that plugin files include proper security validation using the ABSPATH constant to prevent direct file access. | [Learn more](https://developer.wordpress.org/plugins/plugin-basics/best-practices/#file-security) | | file_type | plugin_repo | Detects the usage of hidden and compressed files, VCS directories, application files, badly named files, AI development directories (.cursor, .claude, .aider, .continue, .windsurf, .ai, .github), and unexpected markdown files in plugin root. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) | -| plugin_header_fields | plugin_repo | Checks adherence to the Headers requirements, including validation of "Tested up to" header matching between plugin file and readme.txt. | [Learn more](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) | +| plugin_header_fields | plugin_repo | Checks adherence to the Headers requirements. | [Learn more](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) | | late_escaping | security, plugin_repo | Checks that all output is escaped before being sent to the browser. | [Learn more](https://developer.wordpress.org/apis/security/escaping/) | | safe_redirect | security, plugin_repo | Checks that redirects use wp_safe_redirect() instead of wp_redirect() for security. | [Learn more](https://developer.wordpress.org/reference/functions/wp_safe_redirect/) | | plugin_updater | plugin_repo | Prevents altering WordPress update routines or using custom updaters, which are not allowed on WordPress.org. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) | From e39d399d409704c88cfacac88e65b5f3d515a016 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Fri, 4 Sep 2026 17:45:14 +0200 Subject: [PATCH 3/5] suggest Fran --- .../Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php index c1ecf8bc2..8211dda47 100644 --- a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php @@ -983,7 +983,7 @@ private function check_language( Check_Result $result, string $readme_file, $par * Checks for a "Tested up to" header declared in the main plugin file. * * The "Tested up to" field is readme metadata and must be declared - * exclusively in the plugin readme file. This reports an error whenever + * exclusively in the readme file. This reports an error whenever * the main plugin file also declares it, whether the readme value is * missing, equal, or different. * @@ -1023,19 +1023,19 @@ private function check_tested_up_to_mismatch( Check_Result $result, $parser, str if ( $readme_tested_normalized !== $plugin_tested_normalized ) { $message = sprintf( /* translators: 1: Tested up to value from plugin header, 2: Tested up to value from readme */ - __( 'The "Tested up to" field must be declared only in the plugin readme file: %1$s != %2$s.
Remove the "Tested up to" line from the main plugin PHP header and keep it only in readme.txt.', 'plugin-check' ), + __( 'The "Tested up to" field must be declared only in the readme file: %1$s != %2$s.
Remove the "Tested up to" line from the main plugin PHP header and keep it only in the readme file.', 'plugin-check' ), esc_html( $plugin_tested_normalized ), esc_html( $readme_tested_normalized ) ); } else { $message = sprintf( /* translators: %s: Tested up to value */ - __( 'The "Tested up to" field must be declared only in the plugin readme file (currently: %s).
Keeping the value in both places can cause them to silently get out of sync in a future release. Remove the "Tested up to" line from the main plugin PHP header and keep it only in readme.txt.', 'plugin-check' ), + __( 'The "Tested up to" field must be declared only in the readme file (currently: %s).
Keeping the value in both places can cause them to silently get out of sync in a future release. Remove the "Tested up to" line from the main plugin PHP header and keep it only in the readme file.', 'plugin-check' ), esc_html( $plugin_tested_normalized ) ); } } else { - $message = __( 'The "Tested up to" field must be declared only in the plugin readme file.
Remove the "Tested up to" line from the main plugin PHP header and keep it only in readme.txt.', 'plugin-check' ); + $message = __( 'The "Tested up to" field must be declared only in the readme file.
Remove the "Tested up to" line from the main plugin PHP header and keep it only in the readme file.', 'plugin-check' ); } $this->add_result_error_for_file( From fad86dfa61ecb76065fc595e3e705041a9a5fd8a Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 5 Sep 2026 08:54:32 +0200 Subject: [PATCH 4/5] update names --- includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php | 4 ++-- .../tests/Checker/Checks/Plugin_Header_Fields_Check_Tests.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php index 8211dda47..2804e2525 100644 --- a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php @@ -127,7 +127,7 @@ protected function check_files( Check_Result $result, array $files ) { $this->check_language( $result, $readme_file, $parser ); // Check for mismatched "Tested up to" header between plugin header and readme. - $this->check_tested_up_to_mismatch( $result, $parser, $result->plugin()->main_file() ); + $this->check_tested_up_to_in_header( $result, $parser, $result->plugin()->main_file() ); } /** @@ -993,7 +993,7 @@ private function check_language( Check_Result $result, string $readme_file, $par * @param DotorgParser|PCPParser $parser The Parser object. * @param string $plugin_main_file The main plugin file path. */ - private function check_tested_up_to_mismatch( Check_Result $result, $parser, string $plugin_main_file ) { + private function check_tested_up_to_in_header( Check_Result $result, $parser, string $plugin_main_file ) { // Check if single file plugin, then bail early. if ( $result->plugin()->is_single_file_plugin() ) { diff --git a/tests/phpunit/tests/Checker/Checks/Plugin_Header_Fields_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Plugin_Header_Fields_Check_Tests.php index b9ab7acbe..80ce14dae 100644 --- a/tests/phpunit/tests/Checker/Checks/Plugin_Header_Fields_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/Plugin_Header_Fields_Check_Tests.php @@ -244,7 +244,7 @@ public function test_run_with_mismatched_tested_up_to() { // The "Tested up to" mismatch check has been moved to Plugin_Readme_Check. // This test now verifies that Plugin_Header_Fields_Check does NOT report this error. - $error_items = wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'mismatched_tested_up_to_header' ) ); + $error_items = wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ); $this->assertCount( 0, $error_items ); } @@ -258,7 +258,7 @@ public function test_run_with_matching_tested_up_to() { $errors = $check_result->get_errors(); // Should not have mismatched tested up to error. - $error_items = wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'mismatched_tested_up_to_header' ) ); + $error_items = wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ); $this->assertCount( 0, $error_items ); } From 6b9df811ce95d40d4ed365a902102ad4ba821b01 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 5 Sep 2026 11:02:31 +0200 Subject: [PATCH 5/5] fix --- includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php index 2804e2525..a8a8d45d8 100644 --- a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php @@ -126,7 +126,7 @@ protected function check_files( Check_Result $result, array $files ) { // Check the readme for language. $this->check_language( $result, $readme_file, $parser ); - // Check for mismatched "Tested up to" header between plugin header and readme. + // Check for a "Tested up to" header declared in the main plugin file. $this->check_tested_up_to_in_header( $result, $parser, $result->plugin()->main_file() ); }