From 571cfdfe1dc76651ad2f5e58b48129f4ab41a8b5 Mon Sep 17 00:00:00 2001
From: Faisal Ahammad
Date: Mon, 4 May 2026 13:24:51 +0600
Subject: [PATCH 1/3] feat(plugin-repo): add privacy policy content check
Add a new Privacy_Policy_Check that warns when a plugin uses
personal-data-handling APIs but does not call
wp_add_privacy_policy_content().
WordPress.org guidelines require plugins that collect, store,
or transmit personal data to a third party to suggest privacy
policy text to site administrators via this function.
The check scans PHP files for signals indicating potential personal
data handling:
- wp_remote_post() / wp_remote_get() (external data transmission)
- setcookie() / $_COOKIE (cookie-based tracking)
- wp_set_auth_cookie() (authentication cookies)
If any signal is detected and wp_add_privacy_policy_content() is
not called anywhere in the plugin, a single warning is emitted on
the plugin's main file pointing to the official WordPress privacy
developer documentation.
Plugins with no signals are completely unaffected by this check.
Fixes #1249
---
docs/checks.md | 1 +
.../Plugin_Repo/Privacy_Policy_Check.php | 139 ++++++++++++++++++
includes/Checker/Default_Check_Repository.php | 1 +
.../load.php | 27 ++++
.../load.php | 30 ++++
.../load.php | 41 ++++++
.../Checks/Privacy_Policy_Check_Tests.php | 71 +++++++++
7 files changed, 310 insertions(+)
create mode 100644 includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php
create mode 100644 tests/phpunit/testdata/plugins/test-plugin-privacy-policy-no-signals/load.php
create mode 100644 tests/phpunit/testdata/plugins/test-plugin-privacy-policy-with-errors/load.php
create mode 100644 tests/phpunit/testdata/plugins/test-plugin-privacy-policy-without-errors/load.php
create mode 100644 tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php
diff --git a/docs/checks.md b/docs/checks.md
index 11662a607..c85188147 100644
--- a/docs/checks.md
+++ b/docs/checks.md
@@ -35,3 +35,4 @@
| enqueued_styles_scope | performance | Checks whether any stylesheets are loaded on all pages, which is usually not desirable and can lead to performance issues. | [Learn more](https://developer.wordpress.org/plugins/) |
| enqueued_scripts_scope | performance | Checks whether any scripts are loaded on all pages, which is usually not desirable and can lead to performance issues. | [Learn more](https://developer.wordpress.org/plugins/) |
| non_blocking_scripts | performance | Checks whether scripts and styles are enqueued using a recommended loading strategy. | [Learn more](https://developer.wordpress.org/plugins/) |
+| privacy_policy | plugin_repo | Checks that plugins handling personal data call wp_add_privacy_policy_content() to suggest privacy policy text to site administrators. | [Learn more](https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/) |
diff --git a/includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php b/includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php
new file mode 100644
index 000000000..a5922f721
--- /dev/null
+++ b/includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php
@@ -0,0 +1,139 @@
+
+ */
+ const PERSONAL_DATA_PATTERNS = array(
+ 'wp_remote_post\s*\(' => 'wp_remote_post()',
+ 'wp_remote_get\s*\(' => 'wp_remote_get()',
+ 'setcookie\s*\(' => 'setcookie()',
+ '\$_COOKIE\b' => '$_COOKIE',
+ 'wp_set_auth_cookie\s*\(' => 'wp_set_auth_cookie()',
+ );
+
+ /**
+ * Gets the categories for the check.
+ *
+ * Every check must have at least one category.
+ *
+ * @since 1.7.0
+ *
+ * @return array The categories for the check.
+ */
+ public function get_categories() {
+ return array( Check_Categories::CATEGORY_PLUGIN_REPO );
+ }
+
+ /**
+ * Amends the given result by running the check on the given list of files.
+ *
+ * @since 1.7.0
+ *
+ * @param Check_Result $result The check result to amend, including the plugin context to check.
+ * @param array $files List of absolute file paths.
+ */
+ protected function check_files( Check_Result $result, array $files ) {
+ $php_files = self::filter_files_by_extension( $files, 'php' );
+
+ if ( empty( $php_files ) ) {
+ return;
+ }
+
+ // First, detect whether the plugin already calls wp_add_privacy_policy_content().
+ $has_privacy_call = (bool) self::file_preg_match(
+ '#\bwp_add_privacy_policy_content\s*\(#',
+ $php_files
+ );
+
+ // If the plugin already registers privacy policy content, nothing to warn about.
+ if ( $has_privacy_call ) {
+ return;
+ }
+
+ // Check for each personal-data-handling pattern.
+ foreach ( self::PERSONAL_DATA_PATTERNS as $pattern => $label ) {
+ $matches = array();
+ $matched_file = self::file_preg_match( '#' . $pattern . '#', $php_files, $matches );
+
+ if ( $matched_file ) {
+ $this->add_result_warning_for_file(
+ $result,
+ sprintf(
+ /* translators: %s: The detected function or variable name indicating personal data usage. */
+ __( 'Missing privacy policy content registration.
The plugin uses %s which may involve handling personal data, but does not call wp_add_privacy_policy_content(). Plugins that collect, store, or transmit personal data should suggest privacy policy text to site administrators.', 'plugin-check' ),
+ '' . esc_html( $label ) . ''
+ ),
+ 'missing_privacy_policy_content',
+ $result->plugin()->main_file(),
+ 0,
+ 0,
+ 'https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/',
+ 5
+ );
+
+ // One warning per plugin is sufficient — avoid duplicate messages.
+ return;
+ }
+ }
+ }
+
+ /**
+ * Gets the description for the check.
+ *
+ * Every check must have a short description explaining what the check does.
+ *
+ * @since 1.7.0
+ *
+ * @return string Description.
+ */
+ public function get_description(): string {
+ return __( 'Checks that plugins handling personal data call wp_add_privacy_policy_content() to suggest privacy policy text to site administrators.', 'plugin-check' );
+ }
+
+ /**
+ * Gets the documentation URL for the check.
+ *
+ * Every check must have a URL with further information about the check.
+ *
+ * @since 1.7.0
+ *
+ * @return string The documentation URL.
+ */
+ public function get_documentation_url(): string {
+ return __( 'https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/', 'plugin-check' );
+ }
+}
diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php
index c22371044..a8844bec1 100644
--- a/includes/Checker/Default_Check_Repository.php
+++ b/includes/Checker/Default_Check_Repository.php
@@ -102,6 +102,7 @@ private function register_default_checks() {
'direct_file_access' => new Checks\Plugin_Repo\Direct_File_Access_Check(),
'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(),
'wp_functions_compatibility' => new Checks\Plugin_Repo\WP_Functions_Compatibility_Check(),
+ 'privacy_policy' => new Checks\Plugin_Repo\Privacy_Policy_Check(),
)
);
diff --git a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-no-signals/load.php b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-no-signals/load.php
new file mode 100644
index 000000000..f0300534b
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-no-signals/load.php
@@ -0,0 +1,27 @@
+' . esc_html__( 'Hello from Test Plugin!', 'test-plugin-privacy-policy-no-signals' ) . '
';
+}
+
+add_action( 'admin_footer', 'test_plugin_privacy_no_signals_greet' );
diff --git a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-with-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-with-errors/load.php
new file mode 100644
index 000000000..795c6430e
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-with-errors/load.php
@@ -0,0 +1,30 @@
+ array(
+ 'email' => get_option( 'admin_email' ),
+ ),
+ )
+ );
+
+ return $response;
+}
diff --git a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-without-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-without-errors/load.php
new file mode 100644
index 000000000..34f399b59
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-without-errors/load.php
@@ -0,0 +1,41 @@
+ array(
+ 'site_url' => get_site_url(),
+ ),
+ )
+ );
+
+ return $response;
+}
diff --git a/tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php
new file mode 100644
index 000000000..91b29f6fe
--- /dev/null
+++ b/tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php
@@ -0,0 +1,71 @@
+run( $check_result );
+
+ $warnings = $check_result->get_warnings();
+
+ $this->assertNotEmpty( $warnings );
+
+ // Warning must be on the plugin's main file.
+ $this->assertArrayHasKey( 'load.php', $warnings );
+
+ // Verify the expected warning code is present.
+ $this->assertCount( 1, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'missing_privacy_policy_content' ) ) );
+ }
+
+ /**
+ * Tests that a plugin using wp_remote_post() WITH wp_add_privacy_policy_content()
+ * does not receive any warnings.
+ */
+ public function test_run_without_errors() {
+ $check = new Privacy_Policy_Check();
+ $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-without-errors/load.php' );
+ $check_result = new Check_Result( $check_context );
+
+ $check->run( $check_result );
+
+ $warnings = $check_result->get_warnings();
+ $errors = $check_result->get_errors();
+
+ $this->assertEmpty( $warnings );
+ $this->assertEmpty( $errors );
+ }
+
+ /**
+ * Tests that a plugin with no personal-data-handling patterns does not receive
+ * any warnings, even if it does not call wp_add_privacy_policy_content().
+ */
+ public function test_run_with_no_signals() {
+ $check = new Privacy_Policy_Check();
+ $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-no-signals/load.php' );
+ $check_result = new Check_Result( $check_context );
+
+ $check->run( $check_result );
+
+ $warnings = $check_result->get_warnings();
+ $errors = $check_result->get_errors();
+
+ $this->assertEmpty( $warnings );
+ $this->assertEmpty( $errors );
+ }
+}
From 47e42263826385d865f63e50c3b413f197141da2 Mon Sep 17 00:00:00 2001
From: Faisal Ahammad
Date: Mon, 4 May 2026 13:35:48 +0600
Subject: [PATCH 2/3] fix(tests): remove function name from test plugin
description
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The plugin description contained wp_add_privacy_policy_content()
with parentheses, which caused the check's detection regex to
match the comment string and return early as if the function was
already implemented — producing no warning and failing the test.
The description now reads 'does not register privacy policy
content' which avoids the false positive without changing the
intent of the test fixture.
---
.../plugins/test-plugin-privacy-policy-with-errors/load.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-with-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-with-errors/load.php
index 795c6430e..d32c6f211 100644
--- a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-with-errors/load.php
+++ b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-with-errors/load.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: Test Plugin Privacy Policy With Errors
* Plugin URI: https://github.com/WordPress/plugin-check
- * Description: A test plugin that handles personal data but does not call wp_add_privacy_policy_content().
+ * Description: A test plugin that handles personal data but does not register privacy policy content.
* Requires at least: 6.0
* Requires PHP: 7.4
* Version: 1.0.0
From d025f52cf90a19e18934ca4edf0dee82b9e62b80 Mon Sep 17 00:00:00 2001
From: Faisal Ahammad
Date: Mon, 15 Jun 2026 18:26:04 +0600
Subject: [PATCH 3/3] Use token parsing for privacy policy detection
Replace regex scanning with token_get_all() so function names and
variables in comments and string literals no longer trigger false
positives or hide real signals.
Add fixtures and tests for comment-only and string-only mentions.
---
.../Plugin_Repo/Privacy_Policy_Check.php | 262 +++++++++++++++---
.../load.php | 34 +++
.../load.php | 34 +++
.../load.php | 2 +-
.../Checks/Privacy_Policy_Check_Tests.php | 42 +++
5 files changed, 338 insertions(+), 36 deletions(-)
create mode 100644 tests/phpunit/testdata/plugins/test-plugin-privacy-policy-comment-only/load.php
create mode 100644 tests/phpunit/testdata/plugins/test-plugin-privacy-policy-string-only/load.php
diff --git a/includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php b/includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php
index a5922f721..e39554e75 100644
--- a/includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php
+++ b/includes/Checker/Checks/Plugin_Repo/Privacy_Policy_Check.php
@@ -21,6 +21,9 @@
* administrators via wp_add_privacy_policy_content(). This check detects common
* personal-data-handling patterns and warns if that function is not used.
*
+ * Uses token-based parsing to avoid false positives from function names appearing
+ * in comments or string literals.
+ *
* @since 1.7.0
*/
class Privacy_Policy_Check extends Abstract_File_Check {
@@ -29,20 +32,29 @@ class Privacy_Policy_Check extends Abstract_File_Check {
use Stable_Check;
/**
- * Regex patterns that indicate a plugin may handle personal data.
+ * Function names that indicate a plugin may handle personal data.
*
- * Each pattern is accompanied by a human-readable label used in the
+ * Each function name is accompanied by a human-readable label used in the
* warning message to help plugin authors understand why the check fired.
*
* @since 1.7.0
* @var array
*/
- const PERSONAL_DATA_PATTERNS = array(
- 'wp_remote_post\s*\(' => 'wp_remote_post()',
- 'wp_remote_get\s*\(' => 'wp_remote_get()',
- 'setcookie\s*\(' => 'setcookie()',
- '\$_COOKIE\b' => '$_COOKIE',
- 'wp_set_auth_cookie\s*\(' => 'wp_set_auth_cookie()',
+ const PERSONAL_DATA_FUNCTIONS = array(
+ 'wp_remote_post' => 'wp_remote_post()',
+ 'wp_remote_get' => 'wp_remote_get()',
+ 'setcookie' => 'setcookie()',
+ 'wp_set_auth_cookie' => 'wp_set_auth_cookie()',
+ );
+
+ /**
+ * Variable names that indicate a plugin may handle personal data.
+ *
+ * @since 1.7.0
+ * @var array
+ */
+ const PERSONAL_DATA_VARIABLES = array(
+ '$_COOKIE' => '$_COOKIE',
);
/**
@@ -74,41 +86,221 @@ protected function check_files( Check_Result $result, array $files ) {
}
// First, detect whether the plugin already calls wp_add_privacy_policy_content().
- $has_privacy_call = (bool) self::file_preg_match(
- '#\bwp_add_privacy_policy_content\s*\(#',
- $php_files
- );
+ $has_privacy_call = $this->find_function_in_files( $php_files, array( 'wp_add_privacy_policy_content' => 'wp_add_privacy_policy_content()' ) );
// If the plugin already registers privacy policy content, nothing to warn about.
if ( $has_privacy_call ) {
return;
}
- // Check for each personal-data-handling pattern.
- foreach ( self::PERSONAL_DATA_PATTERNS as $pattern => $label ) {
- $matches = array();
- $matched_file = self::file_preg_match( '#' . $pattern . '#', $php_files, $matches );
-
- if ( $matched_file ) {
- $this->add_result_warning_for_file(
- $result,
- sprintf(
- /* translators: %s: The detected function or variable name indicating personal data usage. */
- __( 'Missing privacy policy content registration.
The plugin uses %s which may involve handling personal data, but does not call wp_add_privacy_policy_content(). Plugins that collect, store, or transmit personal data should suggest privacy policy text to site administrators.', 'plugin-check' ),
- '' . esc_html( $label ) . ''
- ),
- 'missing_privacy_policy_content',
- $result->plugin()->main_file(),
- 0,
- 0,
- 'https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/',
- 5
- );
-
- // One warning per plugin is sufficient — avoid duplicate messages.
- return;
+ // Check for personal-data-handling function calls.
+ $matched_label = $this->find_function_in_files( $php_files, self::PERSONAL_DATA_FUNCTIONS );
+
+ // If no function call found, check for personal-data-handling variable usage.
+ if ( ! $matched_label ) {
+ $matched_label = $this->find_variable_in_files( $php_files, self::PERSONAL_DATA_VARIABLES );
+ }
+
+ if ( $matched_label ) {
+ $this->add_result_warning_for_file(
+ $result,
+ sprintf(
+ /* translators: %s: The detected function or variable name indicating personal data usage. */
+ __( 'Missing privacy policy content registration.
The plugin uses %s which may involve handling personal data, but does not call wp_add_privacy_policy_content(). Plugins that collect, store, or transmit personal data should suggest privacy policy text to site administrators.', 'plugin-check' ),
+ '' . esc_html( $matched_label ) . ''
+ ),
+ 'missing_privacy_policy_content',
+ $result->plugin()->main_file(),
+ 0,
+ 0,
+ 'https://developer.wordpress.org/plugins/privacy/suggesting-text-for-the-site-privacy-policy/',
+ 5
+ );
+ }
+ }
+
+ /**
+ * Searches PHP files for a global function call using token-based parsing.
+ *
+ * Uses token_get_all() to avoid false positives from function names appearing
+ * in comments or string literals.
+ *
+ * @since 1.7.0
+ *
+ * @param array $files List of absolute file paths.
+ * @param array $function_names Map of function names (lowercase) to human-readable labels.
+ * @return string|false Human-readable label of the first matched function, or false if none found.
+ */
+ private function find_function_in_files( array $files, array $function_names ) {
+ $lowercase_names = array();
+ foreach ( $function_names as $name => $label ) {
+ $lowercase_names[ strtolower( $name ) ] = $label;
+ }
+
+ foreach ( $files as $file ) {
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+ $source = file_get_contents( $file );
+ if ( false === $source || '' === $source ) {
+ continue;
+ }
+
+ $tokens = token_get_all( $source );
+
+ foreach ( $tokens as $index => $token ) {
+ if ( ! is_array( $token ) || T_STRING !== $token[0] ) {
+ continue;
+ }
+
+ $name = strtolower( $token[1] );
+ if ( ! isset( $lowercase_names[ $name ] ) ) {
+ continue;
+ }
+
+ $next_index = $this->get_next_significant_token_index( $tokens, $index );
+ if ( null === $next_index || '(' !== $tokens[ $next_index ] ) {
+ continue;
+ }
+
+ if ( ! $this->is_global_function_call( $tokens, $index ) ) {
+ continue;
+ }
+
+ return $lowercase_names[ $name ];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether a tokenized T_STRING is a global function call.
+ *
+ * Rejects method calls (->method), static calls (Class::method),
+ * function definitions (function name), instantiation (new name),
+ * and namespace-qualified calls (\NS\name or NS\name).
+ *
+ * @since 1.7.0
+ *
+ * @param array $tokens Token stream.
+ * @param int $index Current token index.
+ * @return bool True if the token represents a global function call.
+ */
+ private function is_global_function_call( array $tokens, int $index ): bool {
+ $previous_index = $this->get_previous_significant_token_index( $tokens, $index );
+ if ( null === $previous_index ) {
+ return true;
+ }
+
+ $previous_token = $tokens[ $previous_index ];
+
+ if ( is_array( $previous_token ) ) {
+ $disqualifying_tokens = array( T_FUNCTION, T_NEW, T_OBJECT_OPERATOR, T_DOUBLE_COLON );
+
+ // T_NULLSAFE_OBJECT_OPERATOR (?->) only exists on PHP 8.0+.
+ if ( defined( 'T_NULLSAFE_OBJECT_OPERATOR' ) ) {
+ $disqualifying_tokens[] = constant( 'T_NULLSAFE_OBJECT_OPERATOR' );
+ }
+
+ if ( in_array( $previous_token[0], $disqualifying_tokens, true ) ) {
+ return false;
+ }
+
+ if ( T_NS_SEPARATOR === $previous_token[0] ) {
+ $before_namespace_index = $this->get_previous_significant_token_index( $tokens, $previous_index );
+ if ( null === $before_namespace_index ) {
+ return true;
+ }
+
+ $before_namespace_token = $tokens[ $before_namespace_index ];
+ if ( is_array( $before_namespace_token ) && in_array( $before_namespace_token[0], array( T_STRING, T_NAMESPACE ), true ) ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Searches PHP files for a variable usage using token-based parsing.
+ *
+ * Uses token_get_all() to avoid false positives from variable names appearing
+ * in comments or string literals.
+ *
+ * @since 1.7.0
+ *
+ * @param array $files List of absolute file paths.
+ * @param array $variable_names Map of variable names to human-readable labels.
+ * @return string|false Human-readable label of the first matched variable, or false if none found.
+ */
+ private function find_variable_in_files( array $files, array $variable_names ) {
+ foreach ( $files as $file ) {
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+ $source = file_get_contents( $file );
+ if ( false === $source || '' === $source ) {
+ continue;
+ }
+
+ $tokens = token_get_all( $source );
+
+ foreach ( $tokens as $token ) {
+ if ( ! is_array( $token ) || T_VARIABLE !== $token[0] ) {
+ continue;
+ }
+
+ if ( isset( $variable_names[ $token[1] ] ) ) {
+ return $variable_names[ $token[1] ];
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Finds the next significant token index, skipping whitespace.
+ *
+ * @since 1.7.0
+ *
+ * @param array $tokens Token stream.
+ * @param int $index Current token index.
+ * @return int|null Index of the next significant token, or null if end of stream.
+ */
+ private function get_next_significant_token_index( array $tokens, int $index ): ?int {
+ $count = count( $tokens );
+ for ( $i = $index + 1; $i < $count; $i++ ) {
+ $token = $tokens[ $i ];
+ if ( is_array( $token ) && T_WHITESPACE === $token[0] ) {
+ continue;
+ }
+
+ return $i;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the previous significant token index, skipping whitespace and comments.
+ *
+ * @since 1.7.0
+ *
+ * @param array $tokens Token stream.
+ * @param int $index Current token index.
+ * @return int|null Index of the previous significant token, or null if start of stream.
+ */
+ private function get_previous_significant_token_index( array $tokens, int $index ): ?int {
+ for ( $i = $index - 1; $i >= 0; $i-- ) {
+ $token = $tokens[ $i ];
+
+ if ( is_array( $token ) && in_array( $token[0], array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ), true ) ) {
+ continue;
}
+
+ return $i;
}
+
+ return null;
}
/**
diff --git a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-comment-only/load.php b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-comment-only/load.php
new file mode 100644
index 000000000..a9cd50554
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-comment-only/load.php
@@ -0,0 +1,34 @@
+ array(
+ 'email' => get_option( 'admin_email' ),
+ ),
+ )
+ );
+}
diff --git a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-string-only/load.php b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-string-only/load.php
new file mode 100644
index 000000000..90f230688
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-string-only/load.php
@@ -0,0 +1,34 @@
+ array(
+ 'email' => get_option( 'admin_email' ),
+ ),
+ )
+ );
+}
diff --git a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-without-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-without-errors/load.php
index 34f399b59..93a3b05b1 100644
--- a/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-without-errors/load.php
+++ b/tests/phpunit/testdata/plugins/test-plugin-privacy-policy-without-errors/load.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: Test Plugin Privacy Policy Without Errors
* Plugin URI: https://github.com/WordPress/plugin-check
- * Description: A test plugin that handles personal data AND correctly calls wp_add_privacy_policy_content().
+ * Description: A test plugin that handles personal data AND correctly registers privacy policy content.
* Requires at least: 6.0
* Requires PHP: 7.4
* Version: 1.0.0
diff --git a/tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php
index 91b29f6fe..d0910ccd4 100644
--- a/tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php
+++ b/tests/phpunit/tests/Checker/Checks/Privacy_Policy_Check_Tests.php
@@ -68,4 +68,46 @@ public function test_run_with_no_signals() {
$this->assertEmpty( $warnings );
$this->assertEmpty( $errors );
}
+
+ /**
+ * Tests that function names appearing only in comments do not satisfy the
+ * wp_add_privacy_policy_content() check. The fixture contains a real
+ * wp_remote_post() signal plus a comment-only mention of the privacy
+ * function; the check must still emit a warning because the real code does
+ * not register privacy policy content.
+ */
+ public function test_run_with_comment_only_signals() {
+ $check = new Privacy_Policy_Check();
+ $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-comment-only/load.php' );
+ $check_result = new Check_Result( $check_context );
+
+ $check->run( $check_result );
+
+ $warnings = $check_result->get_warnings();
+
+ $this->assertNotEmpty( $warnings );
+ $this->assertArrayHasKey( 'load.php', $warnings );
+ $this->assertCount( 1, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'missing_privacy_policy_content' ) ) );
+ }
+
+ /**
+ * Tests that function names and variable names appearing only inside
+ * string literals do not satisfy the wp_add_privacy_policy_content() check.
+ * The fixture contains a real wp_remote_post() signal plus a string-only
+ * mention of the privacy function; the check must still emit a warning
+ * because the real code does not register privacy policy content.
+ */
+ public function test_run_with_string_only_signals() {
+ $check = new Privacy_Policy_Check();
+ $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-privacy-policy-string-only/load.php' );
+ $check_result = new Check_Result( $check_context );
+
+ $check->run( $check_result );
+
+ $warnings = $check_result->get_warnings();
+
+ $this->assertNotEmpty( $warnings );
+ $this->assertArrayHasKey( 'load.php', $warnings );
+ $this->assertCount( 1, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'missing_privacy_policy_content' ) ) );
+ }
}