From 2fb3c576fbc04f2ffbef07ddbbab84d0231f8e33 Mon Sep 17 00:00:00 2001 From: Ishita Jagtap <143935560+ishitaj34@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:41:20 +0530 Subject: [PATCH 01/14] feat: integrate Plugin Check Namer as standard AI Name Check static check --- assets/js/plugin-check-namer.js | 432 ---------------- includes/Admin/Namer_Page.php | 460 ------------------ includes/Checker/Abstract_Check_Runner.php | 13 +- .../Checks/Plugin_Repo/AI_Name_Check.php | 179 +++++++ includes/Checker/Default_Check_Repository.php | 1 + includes/Plugin_Main.php | 8 +- .../Checker/Checks/AI_Name_Check_Tests.php | 33 ++ 7 files changed, 228 insertions(+), 898 deletions(-) delete mode 100644 assets/js/plugin-check-namer.js delete mode 100644 includes/Admin/Namer_Page.php create mode 100644 includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php create mode 100644 tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php diff --git a/assets/js/plugin-check-namer.js b/assets/js/plugin-check-namer.js deleted file mode 100644 index f8e3d90cb..000000000 --- a/assets/js/plugin-check-namer.js +++ /dev/null @@ -1,432 +0,0 @@ -/** - * Plugin Check Namer tool. - */ - -/* global pluginCheckNamer */ - -( function () { - 'use strict'; - - function setText( el, text ) { - if ( ! el ) { - return; - } - el.textContent = text; - } - - function setHtml( el, html ) { - if ( ! el ) { - return; - } - el.innerHTML = html; - } - - function escapeHtml( text ) { - const div = document.createElement( 'div' ); - div.textContent = text; - return div.innerHTML; - } - - document.addEventListener( 'DOMContentLoaded', function () { - const form = document.getElementById( 'plugin-check-namer-form' ); - const input = document.getElementById( 'plugin_check_namer_input' ); - - if ( ! form || ! input || ! window.pluginCheckNamer ) { - return; - } - - const authorInput = document.getElementById( - 'plugin_check_namer_author' - ); - const modelSelect = document.getElementById( - 'plugin_check_namer_model' - ); - - const submitBtn = document.getElementById( - 'plugin-check-namer-submit' - ); - const spinner = document.getElementById( 'plugin-check-namer-spinner' ); - - const resultWrap = document.getElementById( - 'plugin-check-namer-result' - ); - const verdictEl = document.getElementById( - 'plugin-check-namer-verdict' - ); - const explainEl = document.getElementById( - 'plugin-check-namer-explanation' - ); - const verdictContainer = document.getElementById( - 'plugin-check-namer-verdict-container' - ); - const confusionPluginsDiv = document.getElementById( - 'plugin-check-namer-confusion-plugins' - ); - const confusionPluginsList = document.getElementById( - 'plugin-check-namer-confusion-plugins-list' - ); - const confusionOthersDiv = document.getElementById( - 'plugin-check-namer-confusion-others' - ); - const confusionOthersList = document.getElementById( - 'plugin-check-namer-confusion-others-list' - ); - const timingDiv = document.getElementById( - 'plugin-check-namer-timing' - ); - const timingValue = document.getElementById( - 'plugin-check-namer-timing-value' - ); - const tokensDiv = document.getElementById( - 'plugin-check-namer-tokens' - ); - const tokensValue = document.getElementById( - 'plugin-check-namer-tokens-value' - ); - const errorDiv = document.getElementById( 'plugin-check-namer-error' ); - const errorEl = errorDiv ? errorDiv.querySelector( 'p' ) : null; - - function setLoading( isLoading ) { - if ( spinner ) { - spinner.classList.toggle( 'is-active', isLoading ); - } - submitBtn.disabled = isLoading; - } - - form.addEventListener( 'submit', function ( event ) { - event.preventDefault(); - - const name = ( input.value || '' ).trim(); - if ( ! name ) { - setText( errorEl, pluginCheckNamer.messages.missingName ); - if ( errorEl ) { - errorEl.classList.remove( 'plugin-check-namer-hidden' ); - } - return; - } - - // Clear previous results. - if ( resultWrap ) { - resultWrap.classList.add( 'plugin-check-namer-hidden' ); - } - if ( verdictEl ) { - setText( verdictEl, '' ); - } - if ( explainEl ) { - setHtml( explainEl, '' ); - } - if ( errorDiv ) { - errorDiv.classList.add( 'plugin-check-namer-hidden' ); - } - if ( errorEl ) { - setText( errorEl, '' ); - } - if ( confusionPluginsDiv ) { - confusionPluginsDiv.classList.add( - 'plugin-check-namer-hidden' - ); - } - if ( confusionPluginsList ) { - confusionPluginsList.innerHTML = ''; - } - if ( confusionOthersDiv ) { - confusionOthersDiv.classList.add( 'plugin-check-namer-hidden' ); - } - if ( confusionOthersList ) { - confusionOthersList.innerHTML = ''; - } - if ( timingDiv ) { - timingDiv.classList.add( 'plugin-check-namer-hidden' ); - } - if ( timingValue ) { - setText( timingValue, '' ); - } - if ( tokensDiv ) { - tokensDiv.classList.add( 'plugin-check-namer-hidden' ); - } - if ( tokensValue ) { - setText( tokensValue, '' ); - } - if ( verdictContainer ) { - verdictContainer.classList.add( 'plugin-check-namer-hidden' ); - } - - // Record start time. - const startTime = Date.now(); - - setLoading( true ); - - const formData = new FormData(); - formData.append( 'action', 'plugin_check_namer_analyze' ); - formData.append( 'nonce', pluginCheckNamer.nonce ); - formData.append( 'plugin_name', name ); - if ( authorInput ) { - formData.append( - 'author_name', - ( authorInput.value || '' ).trim() - ); - } - if ( modelSelect ) { - formData.append( - 'model_preference', - ( modelSelect.value || '' ).trim() - ); - } - - fetch( pluginCheckNamer.ajaxUrl, { - method: 'POST', - credentials: 'same-origin', - body: formData, - } ) - .then( function ( response ) { - return response.json(); - } ) - .then( function ( payload ) { - if ( ! payload || ! payload.success ) { - throw new Error( - payload && payload.data && payload.data.message - ? payload.data.message - : pluginCheckNamer.messages.genericError - ); - } - - setText( verdictEl, payload.data.verdict || '' ); - setHtml( explainEl, payload.data.explanation || '' ); - - // Set border color based on verdict. - if ( verdictContainer ) { - const verdict = ( - payload.data.verdict || '' - ).toLowerCase(); - let borderColor = '#2271b1'; // Default blue. - - if ( verdict.indexOf( 'disallowed' ) !== -1 ) { - borderColor = '#d63638'; // Red for disallowed. - } else if ( - verdict.indexOf( 'good' ) !== -1 || - verdict.indexOf( 'low' ) !== -1 || - verdict.indexOf( 'no issues' ) !== -1 - ) { - borderColor = '#00a32a'; // Green for good. - } else if ( - verdict.indexOf( 'generally allowable' ) !== -1 || - verdict.indexOf( 'allowable' ) !== -1 - ) { - borderColor = '#2271b1'; // Blue for generally allowable. - } else if ( - verdict.indexOf( 'review' ) !== -1 || - verdict.indexOf( 'medium' ) !== -1 || - verdict.indexOf( 'issues found' ) !== -1 - ) { - borderColor = '#dba617'; // Yellow/orange for needs review. - } else if ( - verdict.indexOf( 'problematic' ) !== -1 || - verdict.indexOf( 'high' ) !== -1 - ) { - borderColor = '#d63638'; // Red for problematic. - } - - verdictContainer.style.borderLeftColor = borderColor; - verdictContainer.classList.remove( - 'plugin-check-namer-hidden' - ); - } - - // Calculate and display elapsed time. - if ( timingDiv && timingValue ) { - const endTime = Date.now(); - const elapsedSeconds = Math.round( - ( endTime - startTime ) / 1000 - ); - timingValue.textContent = - elapsedSeconds + ' ' + 'seconds'; - timingDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } - - // Display token usage if available. - if ( - tokensDiv && - tokensValue && - payload.data.token_usage - ) { - const tokenUsage = payload.data.token_usage; - let tokensText = ''; - - // Add AI provider and model info if available. - if ( payload.data.ai_info ) { - const parts = []; - if ( payload.data.ai_info.provider ) { - parts.push( payload.data.ai_info.provider ); - } - if ( payload.data.ai_info.model ) { - parts.push( payload.data.ai_info.model ); - } - if ( parts.length ) { - tokensText = parts.join( ' / ' ) + ' - '; - } - } - - const totalTokens = - tokenUsage.total_tokens || - ( tokenUsage.prompt_tokens && - tokenUsage.completion_tokens - ? tokenUsage.prompt_tokens + - tokenUsage.completion_tokens - : null ); - - if ( totalTokens ) { - tokensText += totalTokens + ' total'; - } - - // Add breakdown with prompt and completion tokens. - if ( - tokenUsage.prompt_tokens && - tokenUsage.completion_tokens - ) { - tokensText += - ' (' + - tokenUsage.prompt_tokens + - ' prompt + ' + - tokenUsage.completion_tokens + - ' completion)'; - } - - // Add similar name query tokens if available. - if ( - tokenUsage.similar_name && - tokenUsage.similar_name.total_tokens - ) { - tokensText += - ' [Similar name query: ' + - tokenUsage.similar_name.total_tokens + - ' tokens]'; - } - - tokensValue.textContent = tokensText; - tokensDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } - - // Display confusion_existing_plugins if available. - if ( - confusionPluginsDiv && - confusionPluginsList && - payload.data.confusion_existing_plugins && - payload.data.confusion_existing_plugins.length > 0 - ) { - confusionPluginsList.innerHTML = ''; - payload.data.confusion_existing_plugins.forEach( - function ( plugin ) { - const div = document.createElement( 'div' ); - div.className = - 'plugin-check-namer-confusion-item'; - div.innerHTML = - '' + - escapeHtml( plugin.name || '' ) + - '' + - ( plugin.active_installations - ? ' (' + - escapeHtml( - plugin.active_installations - ) + - ' ' + - 'active installs' + - ')' - : '' ) + - ( plugin.owner_username - ? ' - ' + - escapeHtml( plugin.owner_username ) + - '' - : '' ) + - '
' + - '' + - escapeHtml( plugin.explanation || '' ) + - '' + - ( plugin.link - ? '
' + - escapeHtml( plugin.link ) + - '' - : '' ); - confusionPluginsList.appendChild( div ); - } - ); - confusionPluginsDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } else if ( confusionPluginsDiv ) { - confusionPluginsDiv.classList.add( - 'plugin-check-namer-hidden' - ); - } - - // Display confusion_existing_others if available. - if ( - confusionOthersDiv && - confusionOthersList && - payload.data.confusion_existing_others && - payload.data.confusion_existing_others.length > 0 - ) { - confusionOthersList.innerHTML = ''; - payload.data.confusion_existing_others.forEach( - function ( item ) { - const div = document.createElement( 'div' ); - div.className = - 'plugin-check-namer-confusion-item plugin-check-namer-confusion-item-others'; - div.innerHTML = - '' + - escapeHtml( item.name || '' ) + - '' + - '
' + - '' + - escapeHtml( item.explanation || '' ) + - '' + - ( item.link - ? '
' + - escapeHtml( item.link ) + - '' - : '' ); - confusionOthersList.appendChild( div ); - } - ); - confusionOthersDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } else if ( confusionOthersDiv ) { - confusionOthersDiv.classList.add( - 'plugin-check-namer-hidden' - ); - } - - if ( resultWrap ) { - resultWrap.classList.remove( - 'plugin-check-namer-hidden' - ); - } - } ) - .catch( function ( err ) { - setText( - errorEl, - err && err.message - ? err.message - : pluginCheckNamer.messages.genericError - ); - - if ( errorDiv ) { - errorDiv.classList.remove( - 'plugin-check-namer-hidden' - ); - } - } ) - .finally( function () { - setLoading( false ); - } ); - } ); - } ); -} )(); diff --git a/includes/Admin/Namer_Page.php b/includes/Admin/Namer_Page.php deleted file mode 100644 index cb4b0d415..000000000 --- a/includes/Admin/Namer_Page.php +++ /dev/null @@ -1,460 +0,0 @@ -hook_suffix = add_management_page( - __( 'Plugin Check Namer', 'plugin-check' ), - __( 'Plugin Check Namer', 'plugin-check' ), - 'manage_options', - self::MENU_SLUG, - array( $this, 'render_page' ) - ); - } - - /** - * Enqueues scripts for the tools page. - * - * @since 1.8.0 - * - * @param string $hook_suffix Current admin page hook suffix. - */ - public function enqueue_scripts( $hook_suffix ) { - if ( $hook_suffix !== $this->hook_suffix ) { - return; - } - - wp_enqueue_style( - 'plugin-check-admin', - plugins_url( 'assets/css/plugin-check-admin.css', WP_PLUGIN_CHECK_MAIN_FILE ), - array(), - WP_PLUGIN_CHECK_VERSION - ); - - wp_enqueue_script( - 'plugin-check-namer', - plugins_url( 'assets/js/plugin-check-namer.js', WP_PLUGIN_CHECK_MAIN_FILE ), - array(), - WP_PLUGIN_CHECK_VERSION, - true - ); - - wp_localize_script( - 'plugin-check-namer', - 'pluginCheckNamer', - array( - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'plugin_check_namer_ajax' ), - 'messages' => array( - 'missingName' => __( 'Please enter a plugin name.', 'plugin-check' ), - 'genericError' => __( 'An unexpected error occurred.', 'plugin-check' ), - ), - ) - ); - } - - /** - * AJAX handler to analyze a plugin name. - * - * @since 1.8.0 - */ - public function ajax_analyze() { - check_ajax_referer( 'plugin_check_namer_ajax', 'nonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'plugin-check' ) ) ); - } - - $name = $this->get_plugin_name_from_request(); - if ( empty( $name ) ) { - wp_send_json_error( array( 'message' => __( 'Please enter a plugin name.', 'plugin-check' ) ) ); - } - - $author = $this->get_author_name_from_request(); - - $model_preference = $this->get_model_preference_from_request(); - $ai_config = $this->get_ai_config( $model_preference ); - if ( is_wp_error( $ai_config ) ) { - wp_send_json_error( array( 'message' => $ai_config->get_error_message() ) ); - } - - $analysis = $this->run_name_analysis( $ai_config['model_preference'], $name, $author ); - if ( is_wp_error( $analysis ) ) { - wp_send_json_error( array( 'message' => $analysis->get_error_message() ) ); - } - - $parsed = $this->parse_analysis( $analysis ); - $response = $this->build_ajax_response( $parsed, $analysis, $ai_config ); - - wp_send_json_success( $response ); - } - - /** - * Builds AJAX response from parsed analysis. - * - * @since 1.8.0 - * - * @param array $parsed Parsed analysis. - * @param string|array $analysis Raw analysis. - * @param array $ai_config AI configuration with model preference info. - * @return array Response array. - */ - protected function build_ajax_response( $parsed, $analysis, $ai_config = array() ) { - $raw_output = $this->get_raw_output( $parsed, $analysis ); - $raw_output = $this->format_json_output( $raw_output ); - - $response = array( - 'verdict' => $parsed['verdict'], - 'explanation' => $parsed['explanation'], - 'raw' => $raw_output, - ); - - if ( ! empty( $parsed['confusion_existing_plugins'] ) ) { - $response['confusion_existing_plugins'] = $parsed['confusion_existing_plugins']; - } - if ( ! empty( $parsed['confusion_existing_others'] ) ) { - $response['confusion_existing_others'] = $parsed['confusion_existing_others']; - } - if ( ! empty( $parsed['token_usage'] ) ) { - $response['token_usage'] = $parsed['token_usage']; - } - - // Add AI model preference information. - if ( ! empty( $ai_config['model_preference'] ) ) { - $response['ai_info'] = array( - 'model' => $ai_config['model_preference'], - ); - } - - return $response; - } - - /** - * Gets plugin name from request. - * - * @since 1.8.0 - * - * @return string Plugin name or empty string. - */ - protected function get_plugin_name_from_request() { - $name = isset( $_POST['plugin_name'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_name'] ) ) : ''; - return trim( $name ); - } - - /** - * Gets author name from request. - * - * @since 1.8.0 - * - * @return string Author name or empty string. - */ - protected function get_author_name_from_request() { - $author = isset( $_POST['author_name'] ) ? sanitize_text_field( wp_unslash( $_POST['author_name'] ) ) : ''; - return trim( $author ); - } - - /** - * Renders the page. - * - * @since 1.8.0 - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function render_page() { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - $ai_config = $this->get_ai_config(); - ?> -
-

- - -
-

- get_error_message() ); - ?> -

-
- - check_ai_prerequisites() && is_wp_error( $this->check_ai_connectors() ) ) { - ?> -

- - - -

- - -
- - - - - - - - - - - - - - - - - -

- -
- -
- -

-

- - -

-
- -

- -
-

-
-

- - -

-

- - -

-

- - -

-

- - -

-
-
-

-
-
- -
-

-
-
-
-
- get_model_preference_from_request(); - $user_id = get_current_user_id(); - - if ( empty( $input ) ) { - $this->handle_analyze_error( $user_id, '', new WP_Error( 'missing_input', __( 'Please enter a plugin name.', 'plugin-check' ) ) ); - return; - } - - $ai_config = $this->get_ai_config( $model_preference ); - if ( is_wp_error( $ai_config ) ) { - $this->handle_analyze_error( $user_id, $input, $ai_config ); - return; - } - - $analysis = $this->run_name_analysis( $ai_config['model_preference'], $input, $author ); - - if ( is_wp_error( $analysis ) ) { - $this->handle_analyze_error( $user_id, $input, $analysis ); - return; - } - - $this->store_result( - $user_id, - array( - 'input' => $input, - 'analysis' => $analysis, - ) - ); - wp_safe_redirect( $this->get_page_url() ); - exit; - } - - /** - * Handles analyze error and redirects. - * - * @since 1.8.0 - * - * @param int $user_id User ID. - * @param string $input Input value. - * @param WP_Error $error Error object. - */ - protected function handle_analyze_error( $user_id, $input, $error ) { - $this->store_result( - $user_id, - array( - 'input' => $input, - 'error' => $error, - ) - ); - wp_safe_redirect( $this->get_page_url() ); - exit; - } - - /** - * Gets the page URL. - * - * @since 1.8.0 - * - * @return string - */ - protected function get_page_url() { - return add_query_arg( array( 'page' => self::MENU_SLUG ), admin_url( 'tools.php' ) ); - } -} diff --git a/includes/Checker/Abstract_Check_Runner.php b/includes/Checker/Abstract_Check_Runner.php index 8f66010ac..5040bec8b 100644 --- a/includes/Checker/Abstract_Check_Runner.php +++ b/includes/Checker/Abstract_Check_Runner.php @@ -342,11 +342,22 @@ final public function set_ai_model_preference( $model_preference ) { * * @return bool True if AI analysis should be used, false otherwise. */ - protected function should_use_ai() { + public function should_use_ai() { // Check if explicitly set via setter (e.g., CLI flag or checkbox). return $this->use_ai; } + /** + * Gets the AI model preference for analysis. + * + * @since 2.1.0 + * + * @return string Model preference. + */ + final public function get_ai_model_preference() { + return $this->ai_model_preference; + } + /** * Sets categories for filtering the checks. * diff --git a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php new file mode 100644 index 000000000..5e5a4d109 --- /dev/null +++ b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php @@ -0,0 +1,179 @@ +should_use_ai() ) { + return; + } + + if ( is_wp_error( $this->check_ai_prerequisites() ) || is_wp_error( $this->check_ai_connectors() ) ) { + return; + } + + // Retrieve model preference. + $model_preference = ''; + if ( method_exists( $runner, 'get_ai_model_preference' ) ) { + $model_preference = $runner->get_ai_model_preference(); + } + if ( empty( $model_preference ) && class_exists( Settings_Page::class ) ) { + $model_preference = Settings_Page::get_model_preference(); + } + + if ( ! function_exists( 'get_plugin_data' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugin_main_file = $result->plugin()->main_file(); + $plugin_header = get_plugin_data( $plugin_main_file ); + $plugin_name = isset( $plugin_header['Name'] ) ? $plugin_header['Name'] : ''; + $author_name = isset( $plugin_header['AuthorName'] ) ? $plugin_header['AuthorName'] : ''; + + if ( empty( $plugin_name ) ) { + return; + } + + $analysis = $this->run_name_analysis( $model_preference, $plugin_name, $author_name ); + if ( is_wp_error( $analysis ) ) { + $this->add_result_warning_for_file( + $result, + sprintf( + /* translators: %s: Error message. */ + __( 'AI plugin name check failed: %s', 'plugin-check' ), + $analysis->get_error_message() + ), + 'ai_name_check_failed', + $plugin_main_file + ); + return; + } + + $parsed = $this->parse_analysis( $analysis ); + + // If there is a verdict indicating issues: + if ( isset( $parsed['processed_data'] ) ) { + $data = $parsed['processed_data']; + + // 1. If disallowed: + if ( ! empty( $data['disallowed'] ) ) { + $msg = isset( $data['disallowed_explanation'] ) ? $data['disallowed_explanation'] : __( 'The plugin name is disallowed.', 'plugin-check' ); + $this->add_result_error_for_file( + $result, + $msg, + 'plugin_name_disallowed', + $plugin_main_file + ); + } + + // 2. If possible naming issues: + if ( ! empty( $data['possible_naming_issues'] ) ) { + $msg = isset( $data['naming_explanation'] ) ? $data['naming_explanation'] : __( 'The plugin name has possible naming issues.', 'plugin-check' ); + $this->add_result_warning_for_file( + $result, + $msg, + 'plugin_name_issue', + $plugin_main_file + ); + } + + // 3. If possible owner/trademark issues: + if ( ! empty( $data['possible_owner_issues'] ) ) { + $msg = isset( $data['owner_explanation'] ) ? $data['owner_explanation'] : __( 'The plugin name has possible trademark or ownership issues.', 'plugin-check' ); + $this->add_result_warning_for_file( + $result, + $msg, + 'plugin_name_trademark_issue', + $plugin_main_file + ); + } + + // 4. Group list of similar plugins/trademarks into recommendations: + if ( ! empty( $parsed['confusion_existing_plugins'] ) && is_array( $parsed['confusion_existing_plugins'] ) ) { + $plugins_list = array(); + foreach ( $parsed['confusion_existing_plugins'] as $plugin ) { + $plugins_list[] = sprintf( '%s (%s, %s active installs)', $plugin['name'], $plugin['similarity_level'], $plugin['active_installations'] ); + } + $this->add_result_warning_for_file( + $result, + sprintf( + /* translators: %s: List of similar plugins. */ + __( 'Plugin name is similar to existing plugins: %s', 'plugin-check' ), + implode( '; ', $plugins_list ) + ), + 'plugin_name_similarity', + $plugin_main_file + ); + } + } + } + + /** + * Gets the description for the check. + * + * @since 2.1.0 + * + * @return string Description. + */ + public function get_description(): string { + return __( 'Checks the plugin name for guideline compliance, trademark conflicts, and similarity with existing plugins using AI.', 'plugin-check' ); + } + + /** + * Gets the documentation URL for the check. + * + * @since 2.1.0 + * + * @return string The documentation URL. + */ + public function get_documentation_url(): string { + return __( 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/', 'plugin-check' ); + } +} diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php index d4d5c548d..00778a080 100644 --- a/includes/Checker/Default_Check_Repository.php +++ b/includes/Checker/Default_Check_Repository.php @@ -105,6 +105,7 @@ private function register_default_checks() { 'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(), 'wp_functions_compatibility' => new Checks\Plugin_Repo\WP_Functions_Compatibility_Check(), 'ai_provider' => new Checks\General\AI_Provider_Check(), + 'ai_name' => new Checks\Plugin_Repo\AI_Name_Check(), ) ); diff --git a/includes/Plugin_Main.php b/includes/Plugin_Main.php index 210034611..cfe422fc3 100644 --- a/includes/Plugin_Main.php +++ b/includes/Plugin_Main.php @@ -78,14 +78,12 @@ public function add_hooks() { // Create the Settings page. $settings_page = new Settings_Page(); $settings_page->add_hooks(); - - // Create the Plugin Check Namer tool page. - $namer_page_class = '\\WordPress\\Plugin_Check\\Admin\\Namer_Page'; - $namer_page = new $namer_page_class(); - $namer_page->add_hooks(); +<<<<<<< HEAD // Create the SVN Checker page. $svn_checker_page = new SVN_Checker_Page(); $svn_checker_page->add_hooks(); +======= +>>>>>>> 443d8a22 (feat: integrate Plugin Check Namer as standard AI Name Check static check) } } diff --git a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php new file mode 100644 index 000000000..3bc47a50c --- /dev/null +++ b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php @@ -0,0 +1,33 @@ +createMock( AJAX_Runner::class ); + $runner->method( 'should_use_ai' )->willReturn( false ); + + $reflection = new \ReflectionClass( Plugin_Request_Utility::class ); + $property = $reflection->getProperty( 'runner' ); + $property->setAccessible( true ); + $property->setValue( null, $runner ); + + $check = new AI_Name_Check(); + $context = $this->createMock( Check_Context::class ); + $result = new Check_Result( $context ); + + $check->run( $result ); + + $this->assertEmpty( $result->get_errors() ); + $this->assertEmpty( $result->get_warnings() ); + + $property->setValue( null, null ); + } +} From e002a1c5ebde3f23c911c609cfa9527e08a90720 Mon Sep 17 00:00:00 2001 From: Ishita Jagtap <143935560+ishitaj34@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:24:36 +0530 Subject: [PATCH 02/14] fix: resolve PHPCS formatting --- includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php | 8 ++++---- .../phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php index 5e5a4d109..aba4a0d27 100644 --- a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php @@ -7,15 +7,15 @@ namespace WordPress\Plugin_Check\Checker\Checks\Plugin_Repo; +use WordPress\Plugin_Check\Admin\Settings_Page; use WordPress\Plugin_Check\Checker\Check_Categories; use WordPress\Plugin_Check\Checker\Check_Result; use WordPress\Plugin_Check\Checker\Static_Check; -use WordPress\Plugin_Check\Traits\Amend_Check_Result; -use WordPress\Plugin_Check\Traits\Stable_Check; use WordPress\Plugin_Check\Traits\AI_Check_Names; use WordPress\Plugin_Check\Traits\AI_Utils; +use WordPress\Plugin_Check\Traits\Amend_Check_Result; +use WordPress\Plugin_Check\Traits\Stable_Check; use WordPress\Plugin_Check\Utilities\Plugin_Request_Utility; -use WordPress\Plugin_Check\Admin\Settings_Page; /** * Check for plugin name issues using AI. @@ -98,7 +98,7 @@ public function run( Check_Result $result ) { $parsed = $this->parse_analysis( $analysis ); - // If there is a verdict indicating issues: + // If there is a verdict indicating issues. if ( isset( $parsed['processed_data'] ) ) { $data = $parsed['processed_data']; diff --git a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php index 3bc47a50c..54864aa55 100644 --- a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php @@ -1,12 +1,12 @@ - Date: Mon, 13 Jul 2026 12:30:39 +0530 Subject: [PATCH 03/14] fix: resolve sorting --- .../phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php index 54864aa55..759cd6d1f 100644 --- a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php @@ -1,11 +1,10 @@ - setValue( null, $runner ); $check = new AI_Name_Check(); - $context = $this->createMock( Check_Context::class ); + $context = new Check_Context( WP_PLUGIN_CHECK_MAIN_FILE ); $result = new Check_Result( $context ); $check->run( $result ); From 589006b82c6e2ae54f934b4b5fb0e4bc96a681be Mon Sep 17 00:00:00 2001 From: Ishita Jagtap <143935560+ishitaj34@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:37:41 +0530 Subject: [PATCH 04/14] refactor: reduce method length --- .../Checks/Plugin_Repo/AI_Name_Check.php | 143 ++++++++++++------ 1 file changed, 93 insertions(+), 50 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php index aba4a0d27..30c5bde6c 100644 --- a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php @@ -97,61 +97,104 @@ public function run( Check_Result $result ) { } $parsed = $this->parse_analysis( $analysis ); + $this->process_analysis_results( $result, $parsed, $plugin_main_file ); + } - // If there is a verdict indicating issues. - if ( isset( $parsed['processed_data'] ) ) { - $data = $parsed['processed_data']; - - // 1. If disallowed: - if ( ! empty( $data['disallowed'] ) ) { - $msg = isset( $data['disallowed_explanation'] ) ? $data['disallowed_explanation'] : __( 'The plugin name is disallowed.', 'plugin-check' ); - $this->add_result_error_for_file( - $result, - $msg, - 'plugin_name_disallowed', - $plugin_main_file - ); - } + /** + * Process analysis results and add appropriate warnings/errors. + * + * @since 2.1.0 + * + * @param Check_Result $result The check result. + * @param array $parsed The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function process_analysis_results( Check_Result $result, array $parsed, string $plugin_main_file ) { + if ( ! isset( $parsed['processed_data'] ) ) { + return; + } - // 2. If possible naming issues: - if ( ! empty( $data['possible_naming_issues'] ) ) { - $msg = isset( $data['naming_explanation'] ) ? $data['naming_explanation'] : __( 'The plugin name has possible naming issues.', 'plugin-check' ); - $this->add_result_warning_for_file( - $result, - $msg, - 'plugin_name_issue', - $plugin_main_file - ); - } + $data = $parsed['processed_data']; - // 3. If possible owner/trademark issues: - if ( ! empty( $data['possible_owner_issues'] ) ) { - $msg = isset( $data['owner_explanation'] ) ? $data['owner_explanation'] : __( 'The plugin name has possible trademark or ownership issues.', 'plugin-check' ); - $this->add_result_warning_for_file( - $result, - $msg, - 'plugin_name_trademark_issue', - $plugin_main_file - ); - } + $this->check_disallowed_name( $result, $data, $plugin_main_file ); + $this->check_naming_issues( $result, $data, $plugin_main_file ); + $this->check_owner_issues( $result, $data, $plugin_main_file ); + $this->check_similar_plugins( $result, $parsed, $plugin_main_file ); + } - // 4. Group list of similar plugins/trademarks into recommendations: - if ( ! empty( $parsed['confusion_existing_plugins'] ) && is_array( $parsed['confusion_existing_plugins'] ) ) { - $plugins_list = array(); - foreach ( $parsed['confusion_existing_plugins'] as $plugin ) { - $plugins_list[] = sprintf( '%s (%s, %s active installs)', $plugin['name'], $plugin['similarity_level'], $plugin['active_installations'] ); - } - $this->add_result_warning_for_file( - $result, - sprintf( - /* translators: %s: List of similar plugins. */ - __( 'Plugin name is similar to existing plugins: %s', 'plugin-check' ), - implode( '; ', $plugins_list ) - ), - 'plugin_name_similarity', - $plugin_main_file - ); + /** + * Check if the plugin name is disallowed. + * + * @since 2.1.0 + * + * @param Check_Result $result The check result. + * @param array $data The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function check_disallowed_name( Check_Result $result, array $data, string $plugin_main_file ) { + if ( ! empty( $data['disallowed'] ) ) { + $msg = isset( $data['disallowed_explanation'] ) ? $data['disallowed_explanation'] : __( 'The plugin name is disallowed.', 'plugin-check' ); + $this->add_result_error_for_file( $result, $msg, 'plugin_name_disallowed', $plugin_main_file ); + } + } + + /** + * Check for possible naming issues. + * + * @since 2.1.0 + * + * @param Check_Result $result The check result. + * @param array $data The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function check_naming_issues( Check_Result $result, array $data, string $plugin_main_file ) { + if ( ! empty( $data['possible_naming_issues'] ) ) { + $msg = isset( $data['naming_explanation'] ) ? $data['naming_explanation'] : __( 'The plugin name has possible naming issues.', 'plugin-check' ); + $this->add_result_warning_for_file( $result, $msg, 'plugin_name_issue', $plugin_main_file ); + } + } + + /** + * Check for possible owner issues. + * + * @since 2.1.0 + * + * @param Check_Result $result The check result. + * @param array $data The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function check_owner_issues( Check_Result $result, array $data, string $plugin_main_file ) { + if ( ! empty( $data['possible_owner_issues'] ) ) { + $msg = isset( $data['owner_explanation'] ) ? $data['owner_explanation'] : __( 'The plugin name has possible trademark or ownership issues.', 'plugin-check' ); + $this->add_result_warning_for_file( $result, $msg, 'plugin_name_trademark_issue', $plugin_main_file ); + } + } + + /** + * Check for similar plugins. + * + * @since 2.1.0 + * + * @param Check_Result $result The check result. + * @param array $parsed The parsed analysis data. + * @param string $plugin_main_file The plugin main file path. + */ + private function check_similar_plugins( Check_Result $result, array $parsed, string $plugin_main_file ) { + if ( ! empty( $parsed['confusion_existing_plugins'] ) && is_array( $parsed['confusion_existing_plugins'] ) ) { + $plugins_list = array(); + foreach ( $parsed['confusion_existing_plugins'] as $plugin ) { + $plugins_list[] = sprintf( '%s (%s, %s active installs)', $plugin['name'], $plugin['similarity_level'], $plugin['active_installations'] ); } + $this->add_result_warning_for_file( + $result, + sprintf( + /* translators: %s: List of similar plugins. */ + __( 'Plugin name is similar to existing plugins: %s', 'plugin-check' ), + implode( '; ', $plugins_list ) + ), + 'plugin_name_similarity', + $plugin_main_file + ); } } From a3d94069690d11a649e7ed83776242f28fde2bf5 Mon Sep 17 00:00:00 2001 From: Ishita Jagtap <143935560+ishitaj34@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:41:50 +0530 Subject: [PATCH 05/14] refactor: reduce run method NPath complexity to satisfy PHPMD --- .../Checks/Plugin_Repo/AI_Name_Check.php | 100 +++++++++++++++--- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php index 30c5bde6c..384fe68cf 100644 --- a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php @@ -49,39 +49,105 @@ public function get_categories() { */ public function run( Check_Result $result ) { $runner = Plugin_Request_Utility::get_runner(); - - // Only run when AI analysis is enabled. - if ( ! $runner || ! method_exists( $runner, 'should_use_ai' ) || ! $runner->should_use_ai() ) { + if ( ! $this->should_run_ai_check( $runner ) ) { return; } - if ( is_wp_error( $this->check_ai_prerequisites() ) || is_wp_error( $this->check_ai_connectors() ) ) { + $model_preference = $this->get_model_preference( $runner ); + $plugin_data = $this->get_plugin_name_and_author( $result ); + if ( ! $plugin_data ) { return; } - // Retrieve model preference. - $model_preference = ''; - if ( method_exists( $runner, 'get_ai_model_preference' ) ) { - $model_preference = $runner->get_ai_model_preference(); + $analysis = $this->run_name_analysis( $model_preference, $plugin_data['name'], $plugin_data['author'] ); + $this->handle_analysis_response( $result, $analysis, $plugin_data['file'] ); + } + + /** + * Determines if the AI check should run. + * + * @since 2.1.0 + * + * @param mixed $runner The active runner. + * @return bool True if the check should run, false otherwise. + */ + private function should_run_ai_check( $runner ) { + if ( ! $runner ) { + return false; + } + if ( ! method_exists( $runner, 'should_use_ai' ) ) { + return false; + } + if ( ! $runner->should_use_ai() ) { + return false; + } + if ( is_wp_error( $this->check_ai_prerequisites() ) ) { + return false; + } + if ( is_wp_error( $this->check_ai_connectors() ) ) { + return false; + } + return true; + } + + /** + * Gets the selected AI model preference. + * + * @since 2.1.0 + * + * @param mixed $runner The active runner. + * @return string The model preference. + */ + private function get_model_preference( $runner ) { + $model = ''; + if ( $runner && method_exists( $runner, 'get_ai_model_preference' ) ) { + $model = $runner->get_ai_model_preference(); } - if ( empty( $model_preference ) && class_exists( Settings_Page::class ) ) { - $model_preference = Settings_Page::get_model_preference(); + if ( empty( $model ) && class_exists( Settings_Page::class ) ) { + $model = Settings_Page::get_model_preference(); } + return $model; + } + /** + * Gets the plugin name and author from headers. + * + * @since 2.1.0 + * + * @param Check_Result $result The check result. + * @return array|null Name and author if found, null otherwise. + */ + private function get_plugin_name_and_author( Check_Result $result ) { if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } - $plugin_main_file = $result->plugin()->main_file(); - $plugin_header = get_plugin_data( $plugin_main_file ); - $plugin_name = isset( $plugin_header['Name'] ) ? $plugin_header['Name'] : ''; - $author_name = isset( $plugin_header['AuthorName'] ) ? $plugin_header['AuthorName'] : ''; + $file = $result->plugin()->main_file(); + $header = get_plugin_data( $file ); + $name = isset( $header['Name'] ) ? $header['Name'] : ''; + $author = isset( $header['AuthorName'] ) ? $header['AuthorName'] : ''; - if ( empty( $plugin_name ) ) { - return; + if ( empty( $name ) ) { + return null; } - $analysis = $this->run_name_analysis( $model_preference, $plugin_name, $author_name ); + return array( + 'name' => $name, + 'author' => $author, + 'file' => $file, + ); + } + + /** + * Handles the AI analysis response and parses results. + * + * @since 2.1.0 + * + * @param Check_Result $result The check result. + * @param mixed $analysis The analysis response or WP_Error. + * @param string $plugin_main_file The plugin main file path. + */ + private function handle_analysis_response( Check_Result $result, $analysis, string $plugin_main_file ) { if ( is_wp_error( $analysis ) ) { $this->add_result_warning_for_file( $result, From 462fabf8256571f5d23e7d346506bc070ea18ebb Mon Sep 17 00:00:00 2001 From: Ishita Jagtap <143935560+ishitaj34@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:57:51 +0530 Subject: [PATCH 06/14] refactor: separate AI name check option --- assets/js/plugin-check-admin.js | 19 +++++++++++ includes/Admin/Admin_AJAX.php | 15 +++++---- includes/CLI/Plugin_Check_Command.php | 5 +++ includes/Checker/Abstract_Check_Runner.php | 32 ++++++++++++++++++- .../Checks/Plugin_Repo/AI_Name_Check.php | 32 +++++++++---------- templates/admin-page.php | 3 ++ .../Checker/Checks/AI_Name_Check_Tests.php | 2 +- 7 files changed, 84 insertions(+), 24 deletions(-) diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index 9b17043b3..d0578264f 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -38,6 +38,7 @@ 'plugin-check__include-experimental' ); const useAi = document.getElementById( 'plugin-check__use-ai' ); + const useAiName = document.getElementById( 'plugin-check__use-ai-name' ); // Handle disabling the Check it button when a plugin is not selected. function canRunChecks() { @@ -140,6 +141,9 @@ if ( useAi ) { useAi.disabled = true; } + if ( useAiName ) { + useAiName.disabled = true; + } if ( includeExperimental ) { includeExperimental.disabled = true; } @@ -220,6 +224,9 @@ if ( useAi ) { useAi.disabled = false; } + if ( useAiName ) { + useAiName.disabled = false; + } if ( includeExperimental ) { includeExperimental.disabled = false; } @@ -654,6 +661,10 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); + pluginCheckData.append( + 'use-ai-name', + useAiName && useAiName.checked ? 1 : 0 + ); for ( let i = 0; i < checks.length; i++ ) { pluginCheckData.append( 'checks[]', checks[ i ] ); @@ -718,6 +729,10 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); + pluginCheckData.append( + 'use-ai-name', + useAiName && useAiName.checked ? 1 : 0 + ); for ( let i = 0; i < categories.length; i++ ) { pluginCheckData.append( 'categories[]', categories[ i ] ); @@ -1010,6 +1025,10 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); + pluginCheckData.append( + 'use-ai-name', + useAiName && useAiName.checked ? 1 : 0 + ); for ( let i = 0; i < types.length; i++ ) { pluginCheckData.append( 'types[]', types[ i ] ); diff --git a/includes/Admin/Admin_AJAX.php b/includes/Admin/Admin_AJAX.php index 3d2b42216..7a2a15979 100644 --- a/includes/Admin/Admin_AJAX.php +++ b/includes/Admin/Admin_AJAX.php @@ -218,12 +218,13 @@ public function clean_up_environment() { public function get_checks_to_run() { $this->check_request_validity(); - $categories = filter_input( INPUT_POST, 'categories', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); - $categories = is_null( $categories ) ? array() : $categories; - $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); - $checks = is_null( $checks ) ? array() : $checks; - $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); - $runner = $this->get_ajax_runner(); + $categories = filter_input( INPUT_POST, 'categories', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $categories = is_null( $categories ) ? array() : $categories; + $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $checks = is_null( $checks ) ? array() : $checks; + $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); + $use_ai_name = 1 === filter_input( INPUT_POST, 'use-ai-name', FILTER_VALIDATE_INT ); + $runner = $this->get_ajax_runner(); if ( is_wp_error( $runner ) ) { wp_send_json_error( $runner, 500 ); @@ -285,6 +286,7 @@ public function run_checks() { $include_experimental = 1 === filter_input( INPUT_POST, 'include-experimental', FILTER_VALIDATE_INT ); $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); + $use_ai_name = 1 === filter_input( INPUT_POST, 'use-ai-name', FILTER_VALIDATE_INT ); $types = filter_input( INPUT_POST, 'types', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); $types = is_null( $types ) ? array( 'error', 'warning' ) : $types; @@ -293,6 +295,7 @@ public function run_checks() { $runner->set_check_slugs( $checks ); $runner->set_plugin( $plugin ); $runner->set_use_ai( $use_ai ); + $runner->set_use_ai_name( $use_ai_name ); $results = $runner->run(); } catch ( Exception $error ) { wp_send_json_error( diff --git a/includes/CLI/Plugin_Check_Command.php b/includes/CLI/Plugin_Check_Command.php index fd10abbef..a792e0bb7 100644 --- a/includes/CLI/Plugin_Check_Command.php +++ b/includes/CLI/Plugin_Check_Command.php @@ -157,6 +157,9 @@ public function __construct( Plugin_Context $plugin_context ) { * [--ai-model=] * : AI model preference for analysis (e.g., 'openai::gpt-4o'). Requires --ai. * + * [--ai-name] + * : Enable AI-based plugin name checking. + * * ## EXAMPLES * * wp plugin check akismet @@ -201,6 +204,7 @@ public function check( $args, $assoc_args ) { 'mode' => 'new', 'ai' => false, 'ai-model' => '', + 'ai-name' => false, ) ); @@ -262,6 +266,7 @@ static function ( $dirs ) use ( $excluded_files ) { $runner->set_slug( $options['slug'] ); $runner->set_mode( $options['mode'] ); $runner->set_use_ai( $options['ai'] ); + $runner->set_use_ai_name( $options['ai-name'] ); if ( ! empty( $options['ai-model'] ) ) { $runner->set_ai_model_preference( $options['ai-model'] ); } diff --git a/includes/Checker/Abstract_Check_Runner.php b/includes/Checker/Abstract_Check_Runner.php index 5040bec8b..510e628b0 100644 --- a/includes/Checker/Abstract_Check_Runner.php +++ b/includes/Checker/Abstract_Check_Runner.php @@ -42,6 +42,14 @@ abstract class Abstract_Check_Runner implements Check_Runner { */ protected $use_ai = false; + /** + * Whether AI plugin name checking is enabled. + * + * @since x.x.x + * @var bool + */ + protected $use_ai_name = false; + /** * AI model preference for analysis. * @@ -324,6 +332,17 @@ final public function set_use_ai( $use_ai ) { $this->use_ai = (bool) $use_ai; } + /** + * Sets whether to use AI plugin name check. + * + * @since x.x.x + * + * @param bool $use_ai_name True to enable AI name check, false to disable. + */ + final public function set_use_ai_name( $use_ai_name ) { + $this->use_ai_name = (bool) $use_ai_name; + } + /** * Sets the AI model preference for analysis. * @@ -347,10 +366,21 @@ public function should_use_ai() { return $this->use_ai; } + /** + * Determines if AI plugin name check should be used. + * + * @since x.x.x + * + * @return bool True if AI name check should be used, false otherwise. + */ + public function should_use_ai_name() { + return $this->use_ai_name; + } + /** * Gets the AI model preference for analysis. * - * @since 2.1.0 + * @since x.x.x * * @return string Model preference. */ diff --git a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php index 384fe68cf..10aa17eab 100644 --- a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php @@ -20,7 +20,7 @@ /** * Check for plugin name issues using AI. * - * @since 2.1.0 + * @since x.x.x */ class AI_Name_Check implements Static_Check { @@ -32,7 +32,7 @@ class AI_Name_Check implements Static_Check { /** * Gets the categories for the check. * - * @since 2.1.0 + * @since x.x.x * * @return array The categories for the check. */ @@ -43,7 +43,7 @@ public function get_categories() { /** * Runs the AI Name Check. * - * @since 2.1.0 + * @since x.x.x * * @param Check_Result $result The check result to amend. */ @@ -66,7 +66,7 @@ public function run( Check_Result $result ) { /** * Determines if the AI check should run. * - * @since 2.1.0 + * @since x.x.x * * @param mixed $runner The active runner. * @return bool True if the check should run, false otherwise. @@ -75,10 +75,10 @@ private function should_run_ai_check( $runner ) { if ( ! $runner ) { return false; } - if ( ! method_exists( $runner, 'should_use_ai' ) ) { + if ( ! method_exists( $runner, 'should_use_ai_name' ) ) { return false; } - if ( ! $runner->should_use_ai() ) { + if ( ! $runner->should_use_ai_name() ) { return false; } if ( is_wp_error( $this->check_ai_prerequisites() ) ) { @@ -93,7 +93,7 @@ private function should_run_ai_check( $runner ) { /** * Gets the selected AI model preference. * - * @since 2.1.0 + * @since x.x.x * * @param mixed $runner The active runner. * @return string The model preference. @@ -112,7 +112,7 @@ private function get_model_preference( $runner ) { /** * Gets the plugin name and author from headers. * - * @since 2.1.0 + * @since x.x.x * * @param Check_Result $result The check result. * @return array|null Name and author if found, null otherwise. @@ -141,7 +141,7 @@ private function get_plugin_name_and_author( Check_Result $result ) { /** * Handles the AI analysis response and parses results. * - * @since 2.1.0 + * @since x.x.x * * @param Check_Result $result The check result. * @param mixed $analysis The analysis response or WP_Error. @@ -169,7 +169,7 @@ private function handle_analysis_response( Check_Result $result, $analysis, stri /** * Process analysis results and add appropriate warnings/errors. * - * @since 2.1.0 + * @since x.x.x * * @param Check_Result $result The check result. * @param array $parsed The parsed analysis data. @@ -191,7 +191,7 @@ private function process_analysis_results( Check_Result $result, array $parsed, /** * Check if the plugin name is disallowed. * - * @since 2.1.0 + * @since x.x.x * * @param Check_Result $result The check result. * @param array $data The parsed analysis data. @@ -207,7 +207,7 @@ private function check_disallowed_name( Check_Result $result, array $data, strin /** * Check for possible naming issues. * - * @since 2.1.0 + * @since x.x.x * * @param Check_Result $result The check result. * @param array $data The parsed analysis data. @@ -223,7 +223,7 @@ private function check_naming_issues( Check_Result $result, array $data, string /** * Check for possible owner issues. * - * @since 2.1.0 + * @since x.x.x * * @param Check_Result $result The check result. * @param array $data The parsed analysis data. @@ -239,7 +239,7 @@ private function check_owner_issues( Check_Result $result, array $data, string $ /** * Check for similar plugins. * - * @since 2.1.0 + * @since x.x.x * * @param Check_Result $result The check result. * @param array $parsed The parsed analysis data. @@ -267,7 +267,7 @@ private function check_similar_plugins( Check_Result $result, array $parsed, str /** * Gets the description for the check. * - * @since 2.1.0 + * @since x.x.x * * @return string Description. */ @@ -278,7 +278,7 @@ public function get_description(): string { /** * Gets the documentation URL for the check. * - * @since 2.1.0 + * @since x.x.x * * @return string The documentation URL. */ diff --git a/templates/admin-page.php b/templates/admin-page.php index d8bfe935e..26183e659 100644 --- a/templates/admin-page.php +++ b/templates/admin-page.php @@ -85,6 +85,9 @@

+

+ +

diff --git a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php index 759cd6d1f..29be2abc0 100644 --- a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php @@ -11,7 +11,7 @@ class AI_Name_Check_Tests extends \WP_UnitTestCase { public function test_run_bails_early_when_ai_disabled() { $runner = $this->createMock( AJAX_Runner::class ); - $runner->method( 'should_use_ai' )->willReturn( false ); + $runner->method( 'should_use_ai_name' )->willReturn( false ); $reflection = new \ReflectionClass( Plugin_Request_Utility::class ); $property = $reflection->getProperty( 'runner' ); From 6c8ecc01a431eb48d99812d5a05ab4aee2082f58 Mon Sep 17 00:00:00 2001 From: Ishita Jagtap <143935560+ishitaj34@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:04:53 +0530 Subject: [PATCH 07/14] fix: resolve phpmd warning --- includes/Admin/Admin_AJAX.php | 1 + includes/Checker/Abstract_Check_Runner.php | 1 + 2 files changed, 2 insertions(+) diff --git a/includes/Admin/Admin_AJAX.php b/includes/Admin/Admin_AJAX.php index 7a2a15979..cf9f87544 100644 --- a/includes/Admin/Admin_AJAX.php +++ b/includes/Admin/Admin_AJAX.php @@ -234,6 +234,7 @@ public function get_checks_to_run() { $this->configure_runner( $runner ); $runner->set_categories( $categories ); $runner->set_use_ai( $use_ai ); + $runner->set_use_ai_name( $use_ai_name ); $checks_to_run = $runner->get_checks_to_run(); } catch ( Exception $error ) { diff --git a/includes/Checker/Abstract_Check_Runner.php b/includes/Checker/Abstract_Check_Runner.php index 510e628b0..69c1ecf19 100644 --- a/includes/Checker/Abstract_Check_Runner.php +++ b/includes/Checker/Abstract_Check_Runner.php @@ -21,6 +21,7 @@ * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.TooManyFields) */ abstract class Abstract_Check_Runner implements Check_Runner { From ce4359f2ac32e55be103a281528f45be29cc1247 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 22 Aug 2026 11:09:53 +0200 Subject: [PATCH 08/14] fix: remove leftover conflict markers --- includes/Plugin_Main.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/includes/Plugin_Main.php b/includes/Plugin_Main.php index cfe422fc3..4296f1aad 100644 --- a/includes/Plugin_Main.php +++ b/includes/Plugin_Main.php @@ -78,12 +78,8 @@ public function add_hooks() { // Create the Settings page. $settings_page = new Settings_Page(); $settings_page->add_hooks(); -<<<<<<< HEAD - // Create the SVN Checker page. $svn_checker_page = new SVN_Checker_Page(); $svn_checker_page->add_hooks(); -======= ->>>>>>> 443d8a22 (feat: integrate Plugin Check Namer as standard AI Name Check static check) } } From eb7084ff24ba0ef29d98e2948ee15277b09de425 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 22 Aug 2026 11:59:24 +0200 Subject: [PATCH 09/14] finished --- includes/Admin/Admin_AJAX.php | 18 ++++-------------- includes/Utilities/Plugin_Request_Utility.php | 15 +++++++++++++++ templates/admin-page.php | 6 +++--- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/includes/Admin/Admin_AJAX.php b/includes/Admin/Admin_AJAX.php index cf9f87544..b26fd2959 100644 --- a/includes/Admin/Admin_AJAX.php +++ b/includes/Admin/Admin_AJAX.php @@ -143,6 +143,10 @@ private function get_ajax_runner() { return new WP_Error( 'invalid-runner', __( 'AJAX Runner was not initialized correctly.', 'plugin-check' ) ); } + // Register the runner so that checks relying on Plugin_Request_Utility::get_runner() + // (e.g. AI_Name_Check) can access it, even when no runtime checks are involved. + Plugin_Request_Utility::set_runner( $runner ); + return $runner; } @@ -267,20 +271,6 @@ public function run_checks() { wp_send_json_error( $runner, 500 ); } - $runner = Plugin_Request_Utility::get_runner(); - - if ( is_null( $runner ) ) { - $runner = new AJAX_Runner(); - } - - // Make sure we are using the correct runner instance. - if ( ! ( $runner instanceof AJAX_Runner ) ) { - wp_send_json_error( - new WP_Error( 'invalid-runner', __( 'AJAX Runner was not initialized correctly.', 'plugin-check' ) ), - 500 - ); - } - $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); $checks = is_null( $checks ) ? array() : $checks; $plugin = filter_input( INPUT_POST, 'plugin', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); diff --git a/includes/Utilities/Plugin_Request_Utility.php b/includes/Utilities/Plugin_Request_Utility.php index 3451f8052..8958a982c 100644 --- a/includes/Utilities/Plugin_Request_Utility.php +++ b/includes/Utilities/Plugin_Request_Utility.php @@ -143,6 +143,21 @@ public static function get_runner() { return null; } + /** + * Sets the Runner class for the current request. + * + * This is needed for requests (e.g. AJAX requests running only static checks) + * where the runner is not registered early via the object-cache.php drop-in, + * so that checks relying on get_runner() (such as AI_Name_Check) can access it. + * + * @since x.x.x + * + * @param Abstract_Check_Runner $runner The runner instance for the current request. + */ + public static function set_runner( Abstract_Check_Runner $runner ) { + static::$runner = $runner; + } + /** * Runs the cleanup functions and destroys the runner. * diff --git a/templates/admin-page.php b/templates/admin-page.php index 26183e659..e3ff17ee6 100644 --- a/templates/admin-page.php +++ b/templates/admin-page.php @@ -81,12 +81,12 @@
-

+

- +

- +

From 685e020b48b3239da8a32a856e0774fedc8b9637 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 22 Aug 2026 12:16:13 +0200 Subject: [PATCH 10/14] fix: suppress PHPMD TooManyPublicMethods warning in Plugin_Request_Utility The new set_runner() method (added to support AI Name Check in AJAX-only static check requests) pushed the public method count above PHPMD's threshold. Suppressing the rule here follows the same pattern already used in Admin_Page, Settings_Page, Abstract_Check_Runner and Check_Result. --- includes/Utilities/Plugin_Request_Utility.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/Utilities/Plugin_Request_Utility.php b/includes/Utilities/Plugin_Request_Utility.php index 8958a982c..fd05763ec 100644 --- a/includes/Utilities/Plugin_Request_Utility.php +++ b/includes/Utilities/Plugin_Request_Utility.php @@ -16,6 +16,8 @@ * Class providing utility methods to return plugin information based on the request. * * @since 1.0.0 + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class Plugin_Request_Utility { From c68b1e6afc2126707383672fd86e2faecb9e3a23 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Fri, 4 Sep 2026 16:23:15 +0200 Subject: [PATCH 11/14] recover page --- assets/js/plugin-check-admin.js | 19 - assets/js/plugin-check-namer.js | 432 ++++++++++++++++ includes/Admin/Admin_AJAX.php | 4 - includes/Admin/Namer_Page.php | 460 ++++++++++++++++++ includes/CLI/Plugin_Check_Command.php | 5 - includes/Checker/Abstract_Check_Runner.php | 30 -- .../Checks/Plugin_Repo/AI_Name_Check.php | 4 +- includes/Plugin_Main.php | 6 + templates/admin-page.php | 7 +- .../Checker/Checks/AI_Name_Check_Tests.php | 2 +- 10 files changed, 903 insertions(+), 66 deletions(-) create mode 100644 assets/js/plugin-check-namer.js create mode 100644 includes/Admin/Namer_Page.php diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index d0578264f..9b17043b3 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -38,7 +38,6 @@ 'plugin-check__include-experimental' ); const useAi = document.getElementById( 'plugin-check__use-ai' ); - const useAiName = document.getElementById( 'plugin-check__use-ai-name' ); // Handle disabling the Check it button when a plugin is not selected. function canRunChecks() { @@ -141,9 +140,6 @@ if ( useAi ) { useAi.disabled = true; } - if ( useAiName ) { - useAiName.disabled = true; - } if ( includeExperimental ) { includeExperimental.disabled = true; } @@ -224,9 +220,6 @@ if ( useAi ) { useAi.disabled = false; } - if ( useAiName ) { - useAiName.disabled = false; - } if ( includeExperimental ) { includeExperimental.disabled = false; } @@ -661,10 +654,6 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); - pluginCheckData.append( - 'use-ai-name', - useAiName && useAiName.checked ? 1 : 0 - ); for ( let i = 0; i < checks.length; i++ ) { pluginCheckData.append( 'checks[]', checks[ i ] ); @@ -729,10 +718,6 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); - pluginCheckData.append( - 'use-ai-name', - useAiName && useAiName.checked ? 1 : 0 - ); for ( let i = 0; i < categories.length; i++ ) { pluginCheckData.append( 'categories[]', categories[ i ] ); @@ -1025,10 +1010,6 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); - pluginCheckData.append( - 'use-ai-name', - useAiName && useAiName.checked ? 1 : 0 - ); for ( let i = 0; i < types.length; i++ ) { pluginCheckData.append( 'types[]', types[ i ] ); diff --git a/assets/js/plugin-check-namer.js b/assets/js/plugin-check-namer.js new file mode 100644 index 000000000..f8e3d90cb --- /dev/null +++ b/assets/js/plugin-check-namer.js @@ -0,0 +1,432 @@ +/** + * Plugin Check Namer tool. + */ + +/* global pluginCheckNamer */ + +( function () { + 'use strict'; + + function setText( el, text ) { + if ( ! el ) { + return; + } + el.textContent = text; + } + + function setHtml( el, html ) { + if ( ! el ) { + return; + } + el.innerHTML = html; + } + + function escapeHtml( text ) { + const div = document.createElement( 'div' ); + div.textContent = text; + return div.innerHTML; + } + + document.addEventListener( 'DOMContentLoaded', function () { + const form = document.getElementById( 'plugin-check-namer-form' ); + const input = document.getElementById( 'plugin_check_namer_input' ); + + if ( ! form || ! input || ! window.pluginCheckNamer ) { + return; + } + + const authorInput = document.getElementById( + 'plugin_check_namer_author' + ); + const modelSelect = document.getElementById( + 'plugin_check_namer_model' + ); + + const submitBtn = document.getElementById( + 'plugin-check-namer-submit' + ); + const spinner = document.getElementById( 'plugin-check-namer-spinner' ); + + const resultWrap = document.getElementById( + 'plugin-check-namer-result' + ); + const verdictEl = document.getElementById( + 'plugin-check-namer-verdict' + ); + const explainEl = document.getElementById( + 'plugin-check-namer-explanation' + ); + const verdictContainer = document.getElementById( + 'plugin-check-namer-verdict-container' + ); + const confusionPluginsDiv = document.getElementById( + 'plugin-check-namer-confusion-plugins' + ); + const confusionPluginsList = document.getElementById( + 'plugin-check-namer-confusion-plugins-list' + ); + const confusionOthersDiv = document.getElementById( + 'plugin-check-namer-confusion-others' + ); + const confusionOthersList = document.getElementById( + 'plugin-check-namer-confusion-others-list' + ); + const timingDiv = document.getElementById( + 'plugin-check-namer-timing' + ); + const timingValue = document.getElementById( + 'plugin-check-namer-timing-value' + ); + const tokensDiv = document.getElementById( + 'plugin-check-namer-tokens' + ); + const tokensValue = document.getElementById( + 'plugin-check-namer-tokens-value' + ); + const errorDiv = document.getElementById( 'plugin-check-namer-error' ); + const errorEl = errorDiv ? errorDiv.querySelector( 'p' ) : null; + + function setLoading( isLoading ) { + if ( spinner ) { + spinner.classList.toggle( 'is-active', isLoading ); + } + submitBtn.disabled = isLoading; + } + + form.addEventListener( 'submit', function ( event ) { + event.preventDefault(); + + const name = ( input.value || '' ).trim(); + if ( ! name ) { + setText( errorEl, pluginCheckNamer.messages.missingName ); + if ( errorEl ) { + errorEl.classList.remove( 'plugin-check-namer-hidden' ); + } + return; + } + + // Clear previous results. + if ( resultWrap ) { + resultWrap.classList.add( 'plugin-check-namer-hidden' ); + } + if ( verdictEl ) { + setText( verdictEl, '' ); + } + if ( explainEl ) { + setHtml( explainEl, '' ); + } + if ( errorDiv ) { + errorDiv.classList.add( 'plugin-check-namer-hidden' ); + } + if ( errorEl ) { + setText( errorEl, '' ); + } + if ( confusionPluginsDiv ) { + confusionPluginsDiv.classList.add( + 'plugin-check-namer-hidden' + ); + } + if ( confusionPluginsList ) { + confusionPluginsList.innerHTML = ''; + } + if ( confusionOthersDiv ) { + confusionOthersDiv.classList.add( 'plugin-check-namer-hidden' ); + } + if ( confusionOthersList ) { + confusionOthersList.innerHTML = ''; + } + if ( timingDiv ) { + timingDiv.classList.add( 'plugin-check-namer-hidden' ); + } + if ( timingValue ) { + setText( timingValue, '' ); + } + if ( tokensDiv ) { + tokensDiv.classList.add( 'plugin-check-namer-hidden' ); + } + if ( tokensValue ) { + setText( tokensValue, '' ); + } + if ( verdictContainer ) { + verdictContainer.classList.add( 'plugin-check-namer-hidden' ); + } + + // Record start time. + const startTime = Date.now(); + + setLoading( true ); + + const formData = new FormData(); + formData.append( 'action', 'plugin_check_namer_analyze' ); + formData.append( 'nonce', pluginCheckNamer.nonce ); + formData.append( 'plugin_name', name ); + if ( authorInput ) { + formData.append( + 'author_name', + ( authorInput.value || '' ).trim() + ); + } + if ( modelSelect ) { + formData.append( + 'model_preference', + ( modelSelect.value || '' ).trim() + ); + } + + fetch( pluginCheckNamer.ajaxUrl, { + method: 'POST', + credentials: 'same-origin', + body: formData, + } ) + .then( function ( response ) { + return response.json(); + } ) + .then( function ( payload ) { + if ( ! payload || ! payload.success ) { + throw new Error( + payload && payload.data && payload.data.message + ? payload.data.message + : pluginCheckNamer.messages.genericError + ); + } + + setText( verdictEl, payload.data.verdict || '' ); + setHtml( explainEl, payload.data.explanation || '' ); + + // Set border color based on verdict. + if ( verdictContainer ) { + const verdict = ( + payload.data.verdict || '' + ).toLowerCase(); + let borderColor = '#2271b1'; // Default blue. + + if ( verdict.indexOf( 'disallowed' ) !== -1 ) { + borderColor = '#d63638'; // Red for disallowed. + } else if ( + verdict.indexOf( 'good' ) !== -1 || + verdict.indexOf( 'low' ) !== -1 || + verdict.indexOf( 'no issues' ) !== -1 + ) { + borderColor = '#00a32a'; // Green for good. + } else if ( + verdict.indexOf( 'generally allowable' ) !== -1 || + verdict.indexOf( 'allowable' ) !== -1 + ) { + borderColor = '#2271b1'; // Blue for generally allowable. + } else if ( + verdict.indexOf( 'review' ) !== -1 || + verdict.indexOf( 'medium' ) !== -1 || + verdict.indexOf( 'issues found' ) !== -1 + ) { + borderColor = '#dba617'; // Yellow/orange for needs review. + } else if ( + verdict.indexOf( 'problematic' ) !== -1 || + verdict.indexOf( 'high' ) !== -1 + ) { + borderColor = '#d63638'; // Red for problematic. + } + + verdictContainer.style.borderLeftColor = borderColor; + verdictContainer.classList.remove( + 'plugin-check-namer-hidden' + ); + } + + // Calculate and display elapsed time. + if ( timingDiv && timingValue ) { + const endTime = Date.now(); + const elapsedSeconds = Math.round( + ( endTime - startTime ) / 1000 + ); + timingValue.textContent = + elapsedSeconds + ' ' + 'seconds'; + timingDiv.classList.remove( + 'plugin-check-namer-hidden' + ); + } + + // Display token usage if available. + if ( + tokensDiv && + tokensValue && + payload.data.token_usage + ) { + const tokenUsage = payload.data.token_usage; + let tokensText = ''; + + // Add AI provider and model info if available. + if ( payload.data.ai_info ) { + const parts = []; + if ( payload.data.ai_info.provider ) { + parts.push( payload.data.ai_info.provider ); + } + if ( payload.data.ai_info.model ) { + parts.push( payload.data.ai_info.model ); + } + if ( parts.length ) { + tokensText = parts.join( ' / ' ) + ' - '; + } + } + + const totalTokens = + tokenUsage.total_tokens || + ( tokenUsage.prompt_tokens && + tokenUsage.completion_tokens + ? tokenUsage.prompt_tokens + + tokenUsage.completion_tokens + : null ); + + if ( totalTokens ) { + tokensText += totalTokens + ' total'; + } + + // Add breakdown with prompt and completion tokens. + if ( + tokenUsage.prompt_tokens && + tokenUsage.completion_tokens + ) { + tokensText += + ' (' + + tokenUsage.prompt_tokens + + ' prompt + ' + + tokenUsage.completion_tokens + + ' completion)'; + } + + // Add similar name query tokens if available. + if ( + tokenUsage.similar_name && + tokenUsage.similar_name.total_tokens + ) { + tokensText += + ' [Similar name query: ' + + tokenUsage.similar_name.total_tokens + + ' tokens]'; + } + + tokensValue.textContent = tokensText; + tokensDiv.classList.remove( + 'plugin-check-namer-hidden' + ); + } + + // Display confusion_existing_plugins if available. + if ( + confusionPluginsDiv && + confusionPluginsList && + payload.data.confusion_existing_plugins && + payload.data.confusion_existing_plugins.length > 0 + ) { + confusionPluginsList.innerHTML = ''; + payload.data.confusion_existing_plugins.forEach( + function ( plugin ) { + const div = document.createElement( 'div' ); + div.className = + 'plugin-check-namer-confusion-item'; + div.innerHTML = + '' + + escapeHtml( plugin.name || '' ) + + '' + + ( plugin.active_installations + ? ' (' + + escapeHtml( + plugin.active_installations + ) + + ' ' + + 'active installs' + + ')' + : '' ) + + ( plugin.owner_username + ? ' - ' + + escapeHtml( plugin.owner_username ) + + '' + : '' ) + + '
' + + '' + + escapeHtml( plugin.explanation || '' ) + + '' + + ( plugin.link + ? '
' + + escapeHtml( plugin.link ) + + '' + : '' ); + confusionPluginsList.appendChild( div ); + } + ); + confusionPluginsDiv.classList.remove( + 'plugin-check-namer-hidden' + ); + } else if ( confusionPluginsDiv ) { + confusionPluginsDiv.classList.add( + 'plugin-check-namer-hidden' + ); + } + + // Display confusion_existing_others if available. + if ( + confusionOthersDiv && + confusionOthersList && + payload.data.confusion_existing_others && + payload.data.confusion_existing_others.length > 0 + ) { + confusionOthersList.innerHTML = ''; + payload.data.confusion_existing_others.forEach( + function ( item ) { + const div = document.createElement( 'div' ); + div.className = + 'plugin-check-namer-confusion-item plugin-check-namer-confusion-item-others'; + div.innerHTML = + '' + + escapeHtml( item.name || '' ) + + '' + + '
' + + '' + + escapeHtml( item.explanation || '' ) + + '' + + ( item.link + ? '
' + + escapeHtml( item.link ) + + '' + : '' ); + confusionOthersList.appendChild( div ); + } + ); + confusionOthersDiv.classList.remove( + 'plugin-check-namer-hidden' + ); + } else if ( confusionOthersDiv ) { + confusionOthersDiv.classList.add( + 'plugin-check-namer-hidden' + ); + } + + if ( resultWrap ) { + resultWrap.classList.remove( + 'plugin-check-namer-hidden' + ); + } + } ) + .catch( function ( err ) { + setText( + errorEl, + err && err.message + ? err.message + : pluginCheckNamer.messages.genericError + ); + + if ( errorDiv ) { + errorDiv.classList.remove( + 'plugin-check-namer-hidden' + ); + } + } ) + .finally( function () { + setLoading( false ); + } ); + } ); + } ); +} )(); diff --git a/includes/Admin/Admin_AJAX.php b/includes/Admin/Admin_AJAX.php index b26fd2959..aa529be85 100644 --- a/includes/Admin/Admin_AJAX.php +++ b/includes/Admin/Admin_AJAX.php @@ -227,7 +227,6 @@ public function get_checks_to_run() { $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); $checks = is_null( $checks ) ? array() : $checks; $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); - $use_ai_name = 1 === filter_input( INPUT_POST, 'use-ai-name', FILTER_VALIDATE_INT ); $runner = $this->get_ajax_runner(); if ( is_wp_error( $runner ) ) { @@ -238,7 +237,6 @@ public function get_checks_to_run() { $this->configure_runner( $runner ); $runner->set_categories( $categories ); $runner->set_use_ai( $use_ai ); - $runner->set_use_ai_name( $use_ai_name ); $checks_to_run = $runner->get_checks_to_run(); } catch ( Exception $error ) { @@ -277,7 +275,6 @@ public function run_checks() { $include_experimental = 1 === filter_input( INPUT_POST, 'include-experimental', FILTER_VALIDATE_INT ); $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); - $use_ai_name = 1 === filter_input( INPUT_POST, 'use-ai-name', FILTER_VALIDATE_INT ); $types = filter_input( INPUT_POST, 'types', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); $types = is_null( $types ) ? array( 'error', 'warning' ) : $types; @@ -286,7 +283,6 @@ public function run_checks() { $runner->set_check_slugs( $checks ); $runner->set_plugin( $plugin ); $runner->set_use_ai( $use_ai ); - $runner->set_use_ai_name( $use_ai_name ); $results = $runner->run(); } catch ( Exception $error ) { wp_send_json_error( diff --git a/includes/Admin/Namer_Page.php b/includes/Admin/Namer_Page.php new file mode 100644 index 000000000..cb4b0d415 --- /dev/null +++ b/includes/Admin/Namer_Page.php @@ -0,0 +1,460 @@ +hook_suffix = add_management_page( + __( 'Plugin Check Namer', 'plugin-check' ), + __( 'Plugin Check Namer', 'plugin-check' ), + 'manage_options', + self::MENU_SLUG, + array( $this, 'render_page' ) + ); + } + + /** + * Enqueues scripts for the tools page. + * + * @since 1.8.0 + * + * @param string $hook_suffix Current admin page hook suffix. + */ + public function enqueue_scripts( $hook_suffix ) { + if ( $hook_suffix !== $this->hook_suffix ) { + return; + } + + wp_enqueue_style( + 'plugin-check-admin', + plugins_url( 'assets/css/plugin-check-admin.css', WP_PLUGIN_CHECK_MAIN_FILE ), + array(), + WP_PLUGIN_CHECK_VERSION + ); + + wp_enqueue_script( + 'plugin-check-namer', + plugins_url( 'assets/js/plugin-check-namer.js', WP_PLUGIN_CHECK_MAIN_FILE ), + array(), + WP_PLUGIN_CHECK_VERSION, + true + ); + + wp_localize_script( + 'plugin-check-namer', + 'pluginCheckNamer', + array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'plugin_check_namer_ajax' ), + 'messages' => array( + 'missingName' => __( 'Please enter a plugin name.', 'plugin-check' ), + 'genericError' => __( 'An unexpected error occurred.', 'plugin-check' ), + ), + ) + ); + } + + /** + * AJAX handler to analyze a plugin name. + * + * @since 1.8.0 + */ + public function ajax_analyze() { + check_ajax_referer( 'plugin_check_namer_ajax', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'plugin-check' ) ) ); + } + + $name = $this->get_plugin_name_from_request(); + if ( empty( $name ) ) { + wp_send_json_error( array( 'message' => __( 'Please enter a plugin name.', 'plugin-check' ) ) ); + } + + $author = $this->get_author_name_from_request(); + + $model_preference = $this->get_model_preference_from_request(); + $ai_config = $this->get_ai_config( $model_preference ); + if ( is_wp_error( $ai_config ) ) { + wp_send_json_error( array( 'message' => $ai_config->get_error_message() ) ); + } + + $analysis = $this->run_name_analysis( $ai_config['model_preference'], $name, $author ); + if ( is_wp_error( $analysis ) ) { + wp_send_json_error( array( 'message' => $analysis->get_error_message() ) ); + } + + $parsed = $this->parse_analysis( $analysis ); + $response = $this->build_ajax_response( $parsed, $analysis, $ai_config ); + + wp_send_json_success( $response ); + } + + /** + * Builds AJAX response from parsed analysis. + * + * @since 1.8.0 + * + * @param array $parsed Parsed analysis. + * @param string|array $analysis Raw analysis. + * @param array $ai_config AI configuration with model preference info. + * @return array Response array. + */ + protected function build_ajax_response( $parsed, $analysis, $ai_config = array() ) { + $raw_output = $this->get_raw_output( $parsed, $analysis ); + $raw_output = $this->format_json_output( $raw_output ); + + $response = array( + 'verdict' => $parsed['verdict'], + 'explanation' => $parsed['explanation'], + 'raw' => $raw_output, + ); + + if ( ! empty( $parsed['confusion_existing_plugins'] ) ) { + $response['confusion_existing_plugins'] = $parsed['confusion_existing_plugins']; + } + if ( ! empty( $parsed['confusion_existing_others'] ) ) { + $response['confusion_existing_others'] = $parsed['confusion_existing_others']; + } + if ( ! empty( $parsed['token_usage'] ) ) { + $response['token_usage'] = $parsed['token_usage']; + } + + // Add AI model preference information. + if ( ! empty( $ai_config['model_preference'] ) ) { + $response['ai_info'] = array( + 'model' => $ai_config['model_preference'], + ); + } + + return $response; + } + + /** + * Gets plugin name from request. + * + * @since 1.8.0 + * + * @return string Plugin name or empty string. + */ + protected function get_plugin_name_from_request() { + $name = isset( $_POST['plugin_name'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_name'] ) ) : ''; + return trim( $name ); + } + + /** + * Gets author name from request. + * + * @since 1.8.0 + * + * @return string Author name or empty string. + */ + protected function get_author_name_from_request() { + $author = isset( $_POST['author_name'] ) ? sanitize_text_field( wp_unslash( $_POST['author_name'] ) ) : ''; + return trim( $author ); + } + + /** + * Renders the page. + * + * @since 1.8.0 + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function render_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $ai_config = $this->get_ai_config(); + ?> +
+

+ + +
+

+ get_error_message() ); + ?> +

+
+ + check_ai_prerequisites() && is_wp_error( $this->check_ai_connectors() ) ) { + ?> +

+ + + +

+ + +
+ + + + + + + + + + + + + + + + + +

+ +
+ +
+ +

+

+ + +

+
+ +

+ +
+

+
+

+ + +

+

+ + +

+

+ + +

+

+ + +

+
+
+

+
+
+ +
+

+
+
+
+
+ get_model_preference_from_request(); + $user_id = get_current_user_id(); + + if ( empty( $input ) ) { + $this->handle_analyze_error( $user_id, '', new WP_Error( 'missing_input', __( 'Please enter a plugin name.', 'plugin-check' ) ) ); + return; + } + + $ai_config = $this->get_ai_config( $model_preference ); + if ( is_wp_error( $ai_config ) ) { + $this->handle_analyze_error( $user_id, $input, $ai_config ); + return; + } + + $analysis = $this->run_name_analysis( $ai_config['model_preference'], $input, $author ); + + if ( is_wp_error( $analysis ) ) { + $this->handle_analyze_error( $user_id, $input, $analysis ); + return; + } + + $this->store_result( + $user_id, + array( + 'input' => $input, + 'analysis' => $analysis, + ) + ); + wp_safe_redirect( $this->get_page_url() ); + exit; + } + + /** + * Handles analyze error and redirects. + * + * @since 1.8.0 + * + * @param int $user_id User ID. + * @param string $input Input value. + * @param WP_Error $error Error object. + */ + protected function handle_analyze_error( $user_id, $input, $error ) { + $this->store_result( + $user_id, + array( + 'input' => $input, + 'error' => $error, + ) + ); + wp_safe_redirect( $this->get_page_url() ); + exit; + } + + /** + * Gets the page URL. + * + * @since 1.8.0 + * + * @return string + */ + protected function get_page_url() { + return add_query_arg( array( 'page' => self::MENU_SLUG ), admin_url( 'tools.php' ) ); + } +} diff --git a/includes/CLI/Plugin_Check_Command.php b/includes/CLI/Plugin_Check_Command.php index a792e0bb7..fd10abbef 100644 --- a/includes/CLI/Plugin_Check_Command.php +++ b/includes/CLI/Plugin_Check_Command.php @@ -157,9 +157,6 @@ public function __construct( Plugin_Context $plugin_context ) { * [--ai-model=] * : AI model preference for analysis (e.g., 'openai::gpt-4o'). Requires --ai. * - * [--ai-name] - * : Enable AI-based plugin name checking. - * * ## EXAMPLES * * wp plugin check akismet @@ -204,7 +201,6 @@ public function check( $args, $assoc_args ) { 'mode' => 'new', 'ai' => false, 'ai-model' => '', - 'ai-name' => false, ) ); @@ -266,7 +262,6 @@ static function ( $dirs ) use ( $excluded_files ) { $runner->set_slug( $options['slug'] ); $runner->set_mode( $options['mode'] ); $runner->set_use_ai( $options['ai'] ); - $runner->set_use_ai_name( $options['ai-name'] ); if ( ! empty( $options['ai-model'] ) ) { $runner->set_ai_model_preference( $options['ai-model'] ); } diff --git a/includes/Checker/Abstract_Check_Runner.php b/includes/Checker/Abstract_Check_Runner.php index 69c1ecf19..664c2aafd 100644 --- a/includes/Checker/Abstract_Check_Runner.php +++ b/includes/Checker/Abstract_Check_Runner.php @@ -43,14 +43,6 @@ abstract class Abstract_Check_Runner implements Check_Runner { */ protected $use_ai = false; - /** - * Whether AI plugin name checking is enabled. - * - * @since x.x.x - * @var bool - */ - protected $use_ai_name = false; - /** * AI model preference for analysis. * @@ -333,17 +325,6 @@ final public function set_use_ai( $use_ai ) { $this->use_ai = (bool) $use_ai; } - /** - * Sets whether to use AI plugin name check. - * - * @since x.x.x - * - * @param bool $use_ai_name True to enable AI name check, false to disable. - */ - final public function set_use_ai_name( $use_ai_name ) { - $this->use_ai_name = (bool) $use_ai_name; - } - /** * Sets the AI model preference for analysis. * @@ -367,17 +348,6 @@ public function should_use_ai() { return $this->use_ai; } - /** - * Determines if AI plugin name check should be used. - * - * @since x.x.x - * - * @return bool True if AI name check should be used, false otherwise. - */ - public function should_use_ai_name() { - return $this->use_ai_name; - } - /** * Gets the AI model preference for analysis. * diff --git a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php index 10aa17eab..c1433ff28 100644 --- a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php @@ -75,10 +75,10 @@ private function should_run_ai_check( $runner ) { if ( ! $runner ) { return false; } - if ( ! method_exists( $runner, 'should_use_ai_name' ) ) { + if ( ! method_exists( $runner, 'should_use_ai' ) ) { return false; } - if ( ! $runner->should_use_ai_name() ) { + if ( ! $runner->should_use_ai() ) { return false; } if ( is_wp_error( $this->check_ai_prerequisites() ) ) { diff --git a/includes/Plugin_Main.php b/includes/Plugin_Main.php index 4296f1aad..518c53a59 100644 --- a/includes/Plugin_Main.php +++ b/includes/Plugin_Main.php @@ -9,6 +9,7 @@ use WordPress\Plugin_Check\Admin\Admin_AJAX; use WordPress\Plugin_Check\Admin\Admin_Page; +use WordPress\Plugin_Check\Admin\Namer_Page; use WordPress\Plugin_Check\Admin\Settings_Page; use WordPress\Plugin_Check\Admin\SVN_Checker_Page; @@ -78,6 +79,11 @@ public function add_hooks() { // Create the Settings page. $settings_page = new Settings_Page(); $settings_page->add_hooks(); + + // Create the Plugin Check Namer tool page. + $namer_page = new Namer_Page(); + $namer_page->add_hooks(); + // Create the SVN Checker page. $svn_checker_page = new SVN_Checker_Page(); $svn_checker_page->add_hooks(); diff --git a/templates/admin-page.php b/templates/admin-page.php index e3ff17ee6..d8bfe935e 100644 --- a/templates/admin-page.php +++ b/templates/admin-page.php @@ -81,12 +81,9 @@
-

+

- -

-

- +

diff --git a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php index 29be2abc0..759cd6d1f 100644 --- a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php @@ -11,7 +11,7 @@ class AI_Name_Check_Tests extends \WP_UnitTestCase { public function test_run_bails_early_when_ai_disabled() { $runner = $this->createMock( AJAX_Runner::class ); - $runner->method( 'should_use_ai_name' )->willReturn( false ); + $runner->method( 'should_use_ai' )->willReturn( false ); $reflection = new \ReflectionClass( Plugin_Request_Utility::class ); $property = $reflection->getProperty( 'runner' ); From 4e1759c59978f8a743b69bdbbcd29b81211425f8 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Tue, 15 Sep 2026 18:24:26 +0200 Subject: [PATCH 12/14] Fix lint alignment in AJAX runner setup --- includes/Admin/Admin_AJAX.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/Admin/Admin_AJAX.php b/includes/Admin/Admin_AJAX.php index aa529be85..011db275b 100644 --- a/includes/Admin/Admin_AJAX.php +++ b/includes/Admin/Admin_AJAX.php @@ -222,12 +222,12 @@ public function clean_up_environment() { public function get_checks_to_run() { $this->check_request_validity(); - $categories = filter_input( INPUT_POST, 'categories', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); - $categories = is_null( $categories ) ? array() : $categories; - $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); - $checks = is_null( $checks ) ? array() : $checks; - $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); - $runner = $this->get_ajax_runner(); + $categories = filter_input( INPUT_POST, 'categories', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $categories = is_null( $categories ) ? array() : $categories; + $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $checks = is_null( $checks ) ? array() : $checks; + $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); + $runner = $this->get_ajax_runner(); if ( is_wp_error( $runner ) ) { wp_send_json_error( $runner, 500 ); From 04e12ff11574468a74e2e2bc8adf3b86917956fa Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sat, 19 Sep 2026 11:05:30 +0200 Subject: [PATCH 13/14] refactor: separate AI name check option Restores the dedicated 'Name Check' checkbox under the AI section that was reverted in c68b1e6 ("recover page"). The AI Name Check now has its own use-ai-name toggle independent from the general AI Analysis checkbox, both in the admin UI, AJAX handlers, and WP-CLI (--ai-name flag). --- assets/js/plugin-check-admin.js | 39 +++++++++++++++---- includes/Admin/Admin_AJAX.php | 16 +++++--- includes/CLI/Plugin_Check_Command.php | 5 +++ includes/Checker/Abstract_Check_Runner.php | 30 ++++++++++++++ .../Checks/Plugin_Repo/AI_Name_Check.php | 4 +- templates/admin-page.php | 7 +++- .../Checker/Checks/AI_Name_Check_Tests.php | 2 +- 7 files changed, 84 insertions(+), 19 deletions(-) diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index 9b17043b3..27c0c4a14 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -38,6 +38,7 @@ 'plugin-check__include-experimental' ); const useAi = document.getElementById( 'plugin-check__use-ai' ); + const useAiName = document.getElementById( 'plugin-check__use-ai-name' ); // Handle disabling the Check it button when a plugin is not selected. function canRunChecks() { @@ -140,6 +141,9 @@ if ( useAi ) { useAi.disabled = true; } + if ( useAiName ) { + useAiName.disabled = true; + } if ( includeExperimental ) { includeExperimental.disabled = true; } @@ -148,6 +152,7 @@ const categories = getSelectedValues( categoriesList ); const types = getSelectedValues( typesList ); const useAiChecked = useAi && useAi.checked ? 1 : 0; + const useAiNameChecked = useAiName && useAiName.checked ? 1 : 0; const includeExperimentalChecked = includeExperimental && includeExperimental.checked ? 1 : 0; let currentChecks; @@ -156,7 +161,8 @@ plugin, categories, includeExperimentalChecked, - useAiChecked + useAiChecked, + useAiNameChecked ) .then( ( data ) => { currentChecks = data.checks; @@ -164,7 +170,8 @@ plugin, currentChecks, includeExperimentalChecked, - useAiChecked + useAiChecked, + useAiNameChecked ); } ) .then( () => @@ -173,7 +180,8 @@ currentChecks, types, includeExperimentalChecked, - useAiChecked + useAiChecked, + useAiNameChecked ) ) .then( () => cleanUpEnvironment() ) @@ -220,6 +228,9 @@ if ( useAi ) { useAi.disabled = false; } + if ( useAiName ) { + useAiName.disabled = false; + } if ( includeExperimental ) { includeExperimental.disabled = false; } @@ -635,13 +646,15 @@ * @param {Array} checks Check slugs that will run. * @param {number} includeExperimentalInput Whether to include experimental checks. * @param {number} useAiInput Whether to enable AI analysis. + * @param {number} useAiNameInput Whether to enable AI name check. * @return {Promise} Resolves with the response message. */ function setUpEnvironment( plugin, checks, includeExperimentalInput, - useAiInput + useAiInput, + useAiNameInput ) { const pluginCheckData = new FormData(); pluginCheckData.append( 'plugin', plugin ); @@ -654,6 +667,7 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); + pluginCheckData.append( 'use-ai-name', useAiNameInput ); for ( let i = 0; i < checks.length; i++ ) { pluginCheckData.append( 'checks[]', checks[ i ] ); @@ -702,13 +716,15 @@ * @param {Array} categories Selected category slugs. * @param {number} includeExperimentalInput Whether to include experimental checks. * @param {number} useAiInput Whether to enable AI analysis. + * @param {number} useAiNameInput Whether to enable AI name check. * @return {Promise} Resolves with the response containing plugin and checks. */ function getChecksToRun( plugin, categories, includeExperimentalInput, - useAiInput + useAiInput, + useAiNameInput ) { const pluginCheckData = new FormData(); pluginCheckData.append( 'plugin', plugin ); @@ -718,6 +734,7 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); + pluginCheckData.append( 'use-ai-name', useAiNameInput ); for ( let i = 0; i < categories.length; i++ ) { pluginCheckData.append( 'categories[]', categories[ i ] ); @@ -742,13 +759,15 @@ * @param {Array} types Result types to include (error, warning). * @param {number} includeExperimentalInput Whether to include experimental checks. * @param {number} useAiInput Whether to enable AI analysis. + * @param {number} useAiNameInput Whether to enable AI name check. */ async function runChecks( plugin, checks, types, includeExperimentalInput, - useAiInput + useAiInput, + useAiNameInput ) { let isSuccessMessage = true; let aiStats = null; @@ -759,7 +778,8 @@ checks[ i ], types, includeExperimentalInput, - useAiInput + useAiInput, + useAiNameInput ); const splitResults = splitResultsByFalsePositive( results ); const errorsLength = countResultTree( @@ -992,6 +1012,7 @@ * @param {Array} types Result types to include (error, warning). * @param {number} includeExperimentalInput Whether to include experimental checks. * @param {number} useAiInput Whether to enable AI analysis. + * @param {number} useAiNameInput Whether to enable AI name check. * @return {Promise} The check results. */ function runCheck( @@ -999,7 +1020,8 @@ check, types, includeExperimentalInput, - useAiInput + useAiInput, + useAiNameInput ) { const pluginCheckData = new FormData(); pluginCheckData.append( 'plugin', plugin ); @@ -1010,6 +1032,7 @@ includeExperimentalInput ); pluginCheckData.append( 'use-ai', useAiInput ); + pluginCheckData.append( 'use-ai-name', useAiNameInput ); for ( let i = 0; i < types.length; i++ ) { pluginCheckData.append( 'types[]', types[ i ] ); diff --git a/includes/Admin/Admin_AJAX.php b/includes/Admin/Admin_AJAX.php index 011db275b..b26fd2959 100644 --- a/includes/Admin/Admin_AJAX.php +++ b/includes/Admin/Admin_AJAX.php @@ -222,12 +222,13 @@ public function clean_up_environment() { public function get_checks_to_run() { $this->check_request_validity(); - $categories = filter_input( INPUT_POST, 'categories', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); - $categories = is_null( $categories ) ? array() : $categories; - $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); - $checks = is_null( $checks ) ? array() : $checks; - $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); - $runner = $this->get_ajax_runner(); + $categories = filter_input( INPUT_POST, 'categories', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $categories = is_null( $categories ) ? array() : $categories; + $checks = filter_input( INPUT_POST, 'checks', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); + $checks = is_null( $checks ) ? array() : $checks; + $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); + $use_ai_name = 1 === filter_input( INPUT_POST, 'use-ai-name', FILTER_VALIDATE_INT ); + $runner = $this->get_ajax_runner(); if ( is_wp_error( $runner ) ) { wp_send_json_error( $runner, 500 ); @@ -237,6 +238,7 @@ public function get_checks_to_run() { $this->configure_runner( $runner ); $runner->set_categories( $categories ); $runner->set_use_ai( $use_ai ); + $runner->set_use_ai_name( $use_ai_name ); $checks_to_run = $runner->get_checks_to_run(); } catch ( Exception $error ) { @@ -275,6 +277,7 @@ public function run_checks() { $include_experimental = 1 === filter_input( INPUT_POST, 'include-experimental', FILTER_VALIDATE_INT ); $use_ai = 1 === filter_input( INPUT_POST, 'use-ai', FILTER_VALIDATE_INT ); + $use_ai_name = 1 === filter_input( INPUT_POST, 'use-ai-name', FILTER_VALIDATE_INT ); $types = filter_input( INPUT_POST, 'types', FILTER_DEFAULT, FILTER_FORCE_ARRAY ); $types = is_null( $types ) ? array( 'error', 'warning' ) : $types; @@ -283,6 +286,7 @@ public function run_checks() { $runner->set_check_slugs( $checks ); $runner->set_plugin( $plugin ); $runner->set_use_ai( $use_ai ); + $runner->set_use_ai_name( $use_ai_name ); $results = $runner->run(); } catch ( Exception $error ) { wp_send_json_error( diff --git a/includes/CLI/Plugin_Check_Command.php b/includes/CLI/Plugin_Check_Command.php index fd10abbef..a792e0bb7 100644 --- a/includes/CLI/Plugin_Check_Command.php +++ b/includes/CLI/Plugin_Check_Command.php @@ -157,6 +157,9 @@ public function __construct( Plugin_Context $plugin_context ) { * [--ai-model=] * : AI model preference for analysis (e.g., 'openai::gpt-4o'). Requires --ai. * + * [--ai-name] + * : Enable AI-based plugin name checking. + * * ## EXAMPLES * * wp plugin check akismet @@ -201,6 +204,7 @@ public function check( $args, $assoc_args ) { 'mode' => 'new', 'ai' => false, 'ai-model' => '', + 'ai-name' => false, ) ); @@ -262,6 +266,7 @@ static function ( $dirs ) use ( $excluded_files ) { $runner->set_slug( $options['slug'] ); $runner->set_mode( $options['mode'] ); $runner->set_use_ai( $options['ai'] ); + $runner->set_use_ai_name( $options['ai-name'] ); if ( ! empty( $options['ai-model'] ) ) { $runner->set_ai_model_preference( $options['ai-model'] ); } diff --git a/includes/Checker/Abstract_Check_Runner.php b/includes/Checker/Abstract_Check_Runner.php index 664c2aafd..d51604b7a 100644 --- a/includes/Checker/Abstract_Check_Runner.php +++ b/includes/Checker/Abstract_Check_Runner.php @@ -43,6 +43,14 @@ abstract class Abstract_Check_Runner implements Check_Runner { */ protected $use_ai = false; + /** + * Whether AI plugin name checking is enabled. + * + * @since 2.2.0 + * @var bool + */ + protected $use_ai_name = false; + /** * AI model preference for analysis. * @@ -325,6 +333,17 @@ final public function set_use_ai( $use_ai ) { $this->use_ai = (bool) $use_ai; } + /** + * Sets whether to use AI plugin name check. + * + * @since 2.2.0 + * + * @param bool $use_ai_name True to enable AI name check, false to disable. + */ + final public function set_use_ai_name( $use_ai_name ) { + $this->use_ai_name = (bool) $use_ai_name; + } + /** * Sets the AI model preference for analysis. * @@ -348,6 +367,17 @@ public function should_use_ai() { return $this->use_ai; } + /** + * Determines if AI plugin name check should be used. + * + * @since 2.2.0 + * + * @return bool True if AI name check should be used, false otherwise. + */ + public function should_use_ai_name() { + return $this->use_ai_name; + } + /** * Gets the AI model preference for analysis. * diff --git a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php index c1433ff28..10aa17eab 100644 --- a/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/AI_Name_Check.php @@ -75,10 +75,10 @@ private function should_run_ai_check( $runner ) { if ( ! $runner ) { return false; } - if ( ! method_exists( $runner, 'should_use_ai' ) ) { + if ( ! method_exists( $runner, 'should_use_ai_name' ) ) { return false; } - if ( ! $runner->should_use_ai() ) { + if ( ! $runner->should_use_ai_name() ) { return false; } if ( is_wp_error( $this->check_ai_prerequisites() ) ) { diff --git a/templates/admin-page.php b/templates/admin-page.php index d8bfe935e..e3ff17ee6 100644 --- a/templates/admin-page.php +++ b/templates/admin-page.php @@ -81,9 +81,12 @@
-

+

- + +

+

+

diff --git a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php index 759cd6d1f..29be2abc0 100644 --- a/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php +++ b/tests/phpunit/tests/Checker/Checks/AI_Name_Check_Tests.php @@ -11,7 +11,7 @@ class AI_Name_Check_Tests extends \WP_UnitTestCase { public function test_run_bails_early_when_ai_disabled() { $runner = $this->createMock( AJAX_Runner::class ); - $runner->method( 'should_use_ai' )->willReturn( false ); + $runner->method( 'should_use_ai_name' )->willReturn( false ); $reflection = new \ReflectionClass( Plugin_Request_Utility::class ); $property = $reflection->getProperty( 'runner' ); From b3703fa9b96e6897a1e11daf361cc06505d34826 Mon Sep 17 00:00:00 2001 From: davidperezgar Date: Sun, 20 Sep 2026 12:32:44 +0200 Subject: [PATCH 14/14] feat: run AI Name Check first so its results appear at the top Reorders the checks-to-run list so 'ai_name' executes before any other check. Since results render sequentially as each check completes, this ensures the Name Check findings (plugin_name_disallowed, plugin_name_issue, etc.) appear as the first table in the results, instead of buried at the end after dozens of other check tables. --- assets/js/plugin-check-admin.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js index f2398e5bd..902205847 100644 --- a/assets/js/plugin-check-admin.js +++ b/assets/js/plugin-check-admin.js @@ -70,6 +70,27 @@ return values; } + /** + * Reorders a list of check slugs so that priority checks run (and thus + * render) first, in the given order, followed by the remaining checks + * in their original order. + * + * @since 2.2.0 + * + * @param {Array} checks Check slugs to run. + * @param {Array} priorityChecks Check slugs that should run first, in order. + * @return {Array} Reordered check slugs. + */ + function prioritizeChecks( checks, priorityChecks ) { + const remaining = checks.filter( + ( check ) => ! priorityChecks.includes( check ) + ); + const prioritized = priorityChecks.filter( ( check ) => + checks.includes( check ) + ); + return [ ...prioritized, ...remaining ]; + } + /** * Posts FormData to the plugin's AJAX endpoint. * @@ -175,7 +196,9 @@ usePcpignoreChecked ) .then( ( data ) => { - currentChecks = data.checks; + currentChecks = prioritizeChecks( data.checks, [ + 'ai_name', + ] ); return setUpEnvironment( plugin, currentChecks,