JFIF $ d d 7 * Z qn )ZP 0 !1 A "#a$2Qr D8 a Ri [f \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H ~ , l s / 9 ć: [=/#f ; ) x { ٛE Q )~ = 𘙲r*2~ a _V = ' ku m F D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq 1dđ #̩ ͑ 0 ; H ] u 搂 @:~ ו K L N s h } O I R* 8 :2 ! lD JV o ( 3 = M ( z Ȱ+i *N A r6K n S l ) ! J J Ӂ* % ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c P A\IL Nj! J ۱+ O\ ͔ ] ޒ S J ȧ c%A N ol ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0 'G=i 2N +0 e 2 ]] 9 VT P O 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P < 0 x < !1AQ "Raq 02Br #S CTb ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV YT D I 6 2 '8Y2 7'\T P.6 d &˦ @V q i |8 -O Ε ] ʔ U= T L 8= ;6c | !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U#6w + +s &r+n ڐ { @ 29gL u " T Ù M=6 (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 ! OS r D 5 7 % | ?x> L | / nD 6?/8 w # [ ) L 7 +6〼T A T g ! %5 Mm Z /c -{1_Je "| ^$ 'O & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@ u6 BԒ i YN * f 0 _ GhKW u lT <nj- 0{ M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q $ H8o Q˼EV ~ k 0 h' L Pp M -M" m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @~p # !1QAa q ? AL0L x LU 4 d ; x " 16ad0 7.\ߋ 0 = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X &Q З non]я u ג./ ; gZ$ M QР 2M ]h ni 0 P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _ 1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3 6 #Ik+ v { v i ( ވ ? a e) X xy/*@Q Ǽ Ծ ) $ IWO l c G; !i k{ gOŀ L ('x T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ) X 5 c QɎMy W ? Q *!R > 6=7)Xj 5`J ] e 8%t ! + ' ! 1Q 5 !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $ v , ,m 㵜5u bV =sY+ @d { N ! dn O < .- B; _w J t 6;Q Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш e黅X ۢ F :)[ F G X ƹ / w _c MeD ,ʡc c . W D tA $ j@ : ) - #u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P 6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L ! ~ y5 g9T̅J kbM' +s :S +B) v @ M j e C f j E 0 Y\Qn z G 1 д~ W o{T 9?` Rm yhsy3 !HAD ] m c1 ~ 2 L S u7x T ;j$ `} 4-> L# vzŏ I L S ֭ T { r jGK C ; b pU= - ` Bs K. S F w4 M q] Z dH S 0 ) tLg
<?php
/**
* Class WordPress\Plugin_Check\CLI\Plugin_Check_Command
*
* @package plugin-check
*/
namespace WordPress\Plugin_Check\CLI;
use Exception;
use WordPress\Plugin_Check\Checker\Check;
use WordPress\Plugin_Check\Checker\Check_Categories;
use WordPress\Plugin_Check\Checker\Check_Repository;
use WordPress\Plugin_Check\Checker\CLI_Runner;
use WordPress\Plugin_Check\Checker\Default_Check_Repository;
use WordPress\Plugin_Check\Plugin_Context;
use WordPress\Plugin_Check\Utilities\Plugin_Request_Utility;
use WordPress\Plugin_Check\Utilities\Results_Exporter;
use WP_CLI;
/**
* Plugin check command.
*
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
final class Plugin_Check_Command {
/**
* Plugin context.
*
* @since 1.0.0
* @var Plugin_Context
*/
protected $plugin_context;
/**
* Output format type.
*
* @since 1.0.0
* @var string[]
*/
protected $output_formats = array(
'table',
'csv',
'json',
'ctrf',
'strict-table',
'strict-csv',
'strict-json',
'strict-ctrf',
);
/**
* Constructor.
*
* @since 1.0.0
*
* @param Plugin_Context $plugin_context Plugin context.
*/
public function __construct( Plugin_Context $plugin_context ) {
$this->plugin_context = $plugin_context;
}
/**
* Runs plugin check.
*
* ## OPTIONS
*
* <plugin>
* : The plugin to check. Plugin name.
*
* [--checks=<checks>]
* : Only runs checks provided as an argument in comma-separated values, e.g. i18n_usage, late_escaping. Otherwise runs all checks.
*
* [--exclude-checks=<checks>]
* : Exclude checks provided as an argument in comma-separated values, e.g. i18n_usage, late_escaping.
* Applies after evaluating `--checks`.
*
* [--ignore-codes=<codes>]
* : Ignore result codes provided as an argument in comma-separated values, e.g. missing_composer_json_file.
* Result codes are individual findings emitted by checks. They are different from check slugs used with
* `--checks` and `--exclude-checks`.
* Use an output format such as `--format=json` or `--format=csv` to inspect result codes.
* For example: `wp plugin check akismet --format=csv --fields=code,message`.
*
* [--format=<format>]
* : Format to display the results. Options are table, csv, json, ctrf, strict-table, strict-csv, strict-json, and strict-ctrf. The default will be a table.
* ---
* default: table
* options:
* - table
* - csv
* - json
* - ctrf
* - strict-table
* - strict-csv
* - strict-json
* - strict-ctrf
* ---
*
* [--categories]
* : Limit displayed results to include only specific categories Checks.
*
* [--fields=<fields>]
* : Limit displayed results to a subset of fields provided.
*
* [--ignore-warnings]
* : Limit displayed results to exclude warnings.
*
* [--ignore-errors]
* : Limit displayed results to exclude errors.
*
* [--include-experimental]
* : Include experimental checks.
*
* [--exclude-directories=<directories>]
* : Additional directories to exclude from checks.
* By default, `.git`, `vendor`, `vendor_prefixed`, `vendor-prefixed` and `node_modules` directories are excluded.
* This only excludes files from file-based scans. It does not suppress plugin-level findings such as
* missing_composer_json_file; use `--ignore-codes` for specific result codes.
*
* [--exclude-files=<files>]
* : Additional files to exclude from checks.
* This only excludes files from file-based scans. It does not suppress plugin-level findings such as
* missing_composer_json_file; use `--ignore-codes` for specific result codes.
*
* [--severity=<severity>]
* : Severity level.
*
* [--error-severity=<error-severity>]
* : Error severity level.
*
* [--warning-severity=<warning-severity>]
* : Warning severity level.
*
* [--include-low-severity-errors]
* : Include errors with lower severity than the threshold as other type.
*
* [--include-low-severity-warnings]
* : Include warnings with lower severity than the threshold as other type.
*
* [--slug=<slug>]
* : Slug to override the default.
*
* [--mode=<mode>]
* : Mode to run the checks in. Options are 'new' (default) or 'update'.
* ---
* default: new
* options:
* - new
* - update
* ---
*
* [--ai]
* : Enable AI-based analysis to detect false positives in check results.
*
* [--ai-model=<model>]
* : AI model preference for analysis (e.g., 'openai::gpt-4o'). Requires --ai.
*
* ## EXAMPLES
*
* wp plugin check akismet
* wp plugin check akismet --checks=late_escaping
* wp plugin check akismet --format=csv --fields=code,message
* wp plugin check akismet --ignore-codes=missing_composer_json_file
* wp plugin check akismet --format=json
* wp plugin check akismet --mode=update
* wp plugin check akismet --ai
* wp plugin check akismet --ai --ai-model=openai::gpt-4o
*
* @subcommand check
*
* @since 1.0.0
*
* @param array $args List of the positional arguments.
* @param array $assoc_args List of the associative arguments.
*
* @throws Exception Throws exception.
*
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function check( $args, $assoc_args ) {
// Get options based on the CLI arguments.
$options = $this->get_options(
$assoc_args,
array(
'checks' => '',
'format' => 'table',
'ignore-warnings' => false,
'ignore-errors' => false,
'include-experimental' => false,
'severity' => '',
'error-severity' => '',
'warning-severity' => '',
'include-low-severity-errors' => false,
'include-low-severity-warnings' => false,
'slug' => '',
'ignore-codes' => '',
'mode' => 'new',
'ai' => false,
'ai-model' => '',
)
);
// Create the plugin and checks array from CLI arguments.
$plugin = isset( $args[0] ) ? $args[0] : '';
$checks = wp_parse_list( $options['checks'] );
// Ignore codes.
$ignore_codes = isset( $options['ignore-codes'] ) ? wp_parse_list( $options['ignore-codes'] ) : array();
// Create the categories array from CLI arguments.
$categories = isset( $options['categories'] ) ? wp_parse_list( $options['categories'] ) : array();
$excluded_directories = isset( $options['exclude-directories'] ) ? wp_parse_list( $options['exclude-directories'] ) : array();
add_filter(
'wp_plugin_check_ignore_directories',
static function ( $dirs ) use ( $excluded_directories ) {
return array_unique( array_merge( $dirs, $excluded_directories ) );
}
);
$excluded_files = isset( $options['exclude-files'] ) ? wp_parse_list( $options['exclude-files'] ) : array();
add_filter(
'wp_plugin_check_ignore_files',
static function ( $dirs ) use ( $excluded_files ) {
return array_unique( array_merge( $dirs, $excluded_files ) );
}
);
// Get the CLI Runner.
$runner = Plugin_Request_Utility::get_runner();
// Create the runner if not already initialized.
if ( is_null( $runner ) ) {
$runner = new CLI_Runner();
}
// Make sure we are using the correct runner instance.
if ( ! ( $runner instanceof CLI_Runner ) ) {
WP_CLI::error(
__( 'CLI Runner was not initialized correctly.', 'plugin-check' )
);
}
// Ensure the correct slug.
if ( is_dir( $plugin ) && empty( $options['slug'] ) ) {
$options['slug'] = basename( $plugin );
} elseif ( filter_var( $plugin, FILTER_VALIDATE_URL ) && empty( $options['slug'] ) ) {
$options['slug'] = Plugin_Request_Utility::get_slug_from_url( $plugin );
}
try {
$runner->set_experimental_flag( $options['include-experimental'] );
$runner->set_check_slugs( $checks );
$runner->set_plugin( $plugin );
$runner->set_categories( $categories );
$runner->set_slug( $options['slug'] );
$runner->set_mode( $options['mode'] );
$runner->set_use_ai( $options['ai'] );
if ( ! empty( $options['ai-model'] ) ) {
$runner->set_ai_model_preference( $options['ai-model'] );
}
} catch ( Exception $error ) {
WP_CLI::error( $error->getMessage() );
}
$result = false;
// Run checks against the plugin.
try {
$result = $runner->run();
} catch ( Exception $error ) {
Plugin_Request_Utility::destroy_runner();
WP_CLI::error( $error->getMessage() );
}
Plugin_Request_Utility::destroy_runner();
// Get errors and warnings from the results.
$errors = array();
if ( $result && empty( $assoc_args['ignore-errors'] ) ) {
$errors = $result->get_errors();
}
$warnings = array();
if ( $result && empty( $assoc_args['ignore-warnings'] ) ) {
$warnings = $result->get_warnings();
}
// Get AI analysis results if available.
$ai_analysis = array();
if ( $result && $options['ai'] ) {
$ai_analysis = $result->get_ai_analysis();
}
// Get AI statistics if available.
$ai_stats = array();
if ( $result && $options['ai'] ) {
$ai_stats = $result->get_ai_stats();
}
if ( empty( $errors ) && empty( $warnings ) ) {
$message = __( 'Checks complete. No errors found.', 'plugin-check' );
// Add AI statistics to the message if available.
if ( ! empty( $ai_stats ) && isset( $ai_stats['false_positives'] ) && $ai_stats['false_positives'] > 0 ) {
$ai_info = sprintf(
// translators: %1$d: Number of possible false positives, %2$s: "possible false positive(s)" label.
__( ' AI detected %1$d %2$s', 'plugin-check' ),
$ai_stats['false_positives'],
_n( 'possible false positive', 'possible false positives', $ai_stats['false_positives'], 'plugin-check' )
);
if ( isset( $ai_stats['tokens_spent'] ) && $ai_stats['tokens_spent'] > 0 ) {
$ai_info .= sprintf(
// translators: %s: Tokens spent (formatted).
__( ' (Tokens spent: %s)', 'plugin-check' ),
number_format_i18n( $ai_stats['tokens_spent'] )
);
}
$message .= '.' . $ai_info;
}
WP_CLI::success( $message );
return;
}
// Default fields.
$default_fields = $this->get_check_default_fields( $assoc_args );
// Get formatter.
$formatter = $this->get_formatter( $assoc_args, $default_fields );
// Severity.
$error_severity = ! empty( $options['error-severity'] ) ? $options['error-severity'] : $options['severity'];
$warning_severity = ! empty( $options['warning-severity'] ) ? $options['warning-severity'] : $options['severity'];
$include_low_severity_errors = ! empty( $options['include-low-severity-errors'] ) ? true : false;
$include_low_severity_warnings = ! empty( $options['include-low-severity-warnings'] ) ? true : false;
$all_results = array();
// Collect all errors.
foreach ( $errors as $file_name => $file_errors ) {
$file_warnings = array();
if ( isset( $warnings[ $file_name ] ) ) {
$file_warnings = $warnings[ $file_name ];
unset( $warnings[ $file_name ] );
}
$file_results = Results_Exporter::flatten_file_results( $file_errors, $file_warnings );
if ( ! empty( $ignore_codes ) ) {
$file_results = $this->get_filtered_results_by_ignore_codes( $file_results, $ignore_codes );
}
if ( '' !== $error_severity || '' !== $warning_severity ) {
$file_results = $this->get_filtered_results_by_severity( $file_results, intval( $error_severity ), intval( $warning_severity ), $include_low_severity_errors, $include_low_severity_warnings );
}
foreach ( $file_results as $item ) {
$item['file'] = $file_name;
$all_results[] = $item;
}
}
// Collect remaining warnings.
foreach ( $warnings as $file_name => $file_warnings ) {
$file_results = Results_Exporter::flatten_file_results( array(), $file_warnings );
if ( ! empty( $ignore_codes ) ) {
$file_results = $this->get_filtered_results_by_ignore_codes( $file_results, $ignore_codes );
}
if ( '' !== $error_severity || '' !== $warning_severity ) {
$file_results = $this->get_filtered_results_by_severity( $file_results, intval( $error_severity ), intval( $warning_severity ), $include_low_severity_errors, $include_low_severity_warnings );
}
foreach ( $file_results as $item ) {
$item['file'] = $file_name;
$all_results[] = $item;
}
}
// Handle CTRF formats.
if ( Results_Exporter::FORMAT_CTRF === $options['format'] || 'strict-' . Results_Exporter::FORMAT_CTRF === $options['format'] ) {
$ctrf_report = Results_Exporter::to_ctrf_json(
$all_results,
array(
'timestamp_iso' => gmdate( 'c' ),
)
);
WP_CLI::line( $ctrf_report );
return;
}
// Handle strict-* formats.
if ( str_starts_with( $options['format'], 'strict-' ) ) {
$base_format = substr( $options['format'], 7 );
$formatter_args = $assoc_args;
$formatter_args['format'] = $base_format;
$formatter = $this->get_formatter( $formatter_args, $default_fields );
$formatter->display_items( $all_results );
return;
}
$false_positive_results = array();
if ( ! empty( $ai_analysis ) ) {
$split_results = $this->split_false_positive_results( $all_results, $ai_analysis );
$all_results = $split_results['actionable'];
$false_positive_results = $split_results['false_positives'];
}
// Group results by file.
$results_by_file = array();
foreach ( $all_results as $item ) {
$results_by_file[ $item['file'] ][] = $item;
}
foreach ( $results_by_file as $file_name => $file_results ) {
$this->display_results( $formatter, $file_name, $file_results );
}
// Display AI analysis summary if available.
if ( ! empty( $ai_analysis ) || ! empty( $ai_stats ) ) {
$this->display_ai_summary( $ai_analysis, $ai_stats, $false_positive_results );
}
}
/**
* Lists the available checks for plugins.
*
* ## OPTIONS
*
* [--fields=<fields>]
* : Limit displayed results to a subset of fields provided.
*
* [--format=<format>]
* : Format to display the results. Options are table, csv, and json. The default will be a table.
* ---
* default: table
* options:
* - table
* - csv
* - json
* ---
*
* [--categories]
* : Limit displayed results to include only specific categories.
*
* [--include-experimental]
* : Include experimental checks.
*
* ## EXAMPLES
*
* wp plugin list-checks
* wp plugin list-checks --format=json
*
* @subcommand list-checks
*
* @since 1.0.0
*
* @param array $args List of the positional arguments.
* @param array $assoc_args List of the associative arguments.
*
* @throws WP_CLI\ExitException Show error if invalid format argument.
*/
public function list_checks( $args, $assoc_args ) {
$check_repo = new Default_Check_Repository();
// Get options based on the CLI arguments.
$options = $this->get_options(
$assoc_args,
array(
'format' => 'table',
'categories' => '',
'include-experimental' => false,
)
);
$check_flags = Check_Repository::TYPE_ALL;
// Check whether to include experimental checks.
if ( $options['include-experimental'] ) {
$check_flags = $check_flags | Check_Repository::INCLUDE_EXPERIMENTAL;
}
$collection = $check_repo->get_checks( $check_flags );
// Filters the checks by specific categories.
if ( ! empty( $options['categories'] ) ) {
$categories = array_map( 'trim', explode( ',', $options['categories'] ) );
$collection = Check_Categories::filter_checks_by_categories( $collection, $categories );
}
$all_checks = array();
/**
* All checks to list.
*
* @var Check $check
*/
foreach ( $collection as $key => $check ) {
$item = array();
$item['slug'] = $key;
$item['stability'] = strtolower( $check->get_stability() );
$item['category'] = join( ', ', $check->get_categories() );
$item['description'] = $check->get_description();
$item['url'] = $check->get_documentation_url();
$all_checks[] = $item;
}
// Get formatter.
$formatter = $this->get_formatter(
$options,
array(
'slug',
'category',
'stability',
'description',
'url',
)
);
// Display results.
$formatter->display_items( $all_checks );
}
/**
* Lists the available check categories for plugins.
*
* ## OPTIONS
*
* [--fields=<fields>]
* : Limit displayed results to a subset of fields provided.
*
* [--format=<format>]
* : Format to display the results. Options are table, csv, and json. The default will be a table.
* ---
* default: table
* options:
* - table
* - csv
* - json
* ---
*
* ## EXAMPLES
*
* wp plugin list-check-categories
* wp plugin list-check-categories --format=json
*
* @subcommand list-check-categories
*
* @since 1.0.0
*
* @param array $args List of the positional arguments.
* @param array $assoc_args List of the associative arguments.
*
* @throws WP_CLI\ExitException Show error if invalid format argument.
*/
public function list_check_categories( $args, $assoc_args ) {
// Get options based on the CLI arguments.
$options = $this->get_options( $assoc_args, array( 'format' => 'table' ) );
// Get check categories details.
$categories = $this->get_check_categories();
// Get formatter.
$formatter = $this->get_formatter(
$options,
array(
'name',
'slug',
)
);
// Display results.
$formatter->display_items( $categories );
}
/**
* Returns check categories details.
*
* @since 1.0.0
*
* @return array List of the check categories.
*/
private function get_check_categories() {
$check_categories = new Check_Categories();
$all_categories = $check_categories->get_categories();
$categories = array();
foreach ( $all_categories as $slug => $label ) {
$categories[] = array(
'slug' => $slug,
'name' => $label,
);
}
return $categories;
}
/**
* Validates the associative arguments.
*
* @since 1.0.0
*
* @param array $assoc_args List of the associative arguments.
* @param array $defaults List of the default arguments.
* @return array List of the associative arguments.
*
* @throws WP_CLI\ExitException Show error if invalid format argument.
*/
private function get_options( $assoc_args, $defaults ) {
$options = wp_parse_args( $assoc_args, $defaults );
if ( ! in_array( $options['format'], $this->output_formats, true ) ) {
WP_CLI::error(
sprintf(
// translators: 1. Output formats.
__( 'Invalid format argument, valid value will be one of [%1$s]', 'plugin-check' ),
implode( ', ', $this->output_formats )
)
);
}
return $options;
}
/**
* Gets the formatter instance to format check results.
*
* @since 1.0.0
*
* @param array $assoc_args Associative arguments.
* @param array $default_fields Default fields.
* @return WP_CLI\Formatter The formatter instance.
*/
private function get_formatter( $assoc_args, $default_fields ) {
if ( isset( $assoc_args['fields'] ) ) {
$default_fields = wp_parse_args( $assoc_args['fields'], $default_fields );
}
return new WP_CLI\Formatter(
$assoc_args,
$default_fields
);
}
/**
* Returns check default fields.
*
* @since 1.0.0
*
* @param array $assoc_args Associative arguments.
* @return array Default fields.
*/
private function get_check_default_fields( $assoc_args ) {
$default_fields = array(
'line',
'column',
'code',
'message',
'docs',
);
// If both errors and warnings are included, display the type of each result too.
if ( empty( $assoc_args['ignore_errors'] ) && empty( $assoc_args['ignore_warnings'] ) ) {
$default_fields = array(
'line',
'column',
'type',
'code',
'message',
'docs',
);
}
return $default_fields;
}
/**
* Displays the results.
*
* @since 1.0.0
*
* @param WP_CLI\Formatter $formatter Formatter class.
* @param string $file_name File name.
* @param array $file_results Results.
*/
private function display_results( $formatter, $file_name, $file_results ) {
WP_CLI::line(
sprintf(
'FILE: %s',
$file_name
)
);
$formatter->display_items( $file_results );
WP_CLI::line();
WP_CLI::line();
}
/**
* Splits likely false positives out of the main check results.
*
* @since 2.0.0
*
* @param array $results Check results.
* @param array $ai_analysis AI analysis results.
* @return array Results split into actionable and false positive groups.
*/
private function split_false_positive_results( array $results, array $ai_analysis ) {
$split_results = array(
'actionable' => array(),
'false_positives' => array(),
);
foreach ( $results as $item ) {
$analysis = $this->find_ai_analysis_for_result( $item, $ai_analysis );
if ( ! empty( $analysis['is_false_positive'] ) ) {
if ( ! empty( $analysis['reasoning'] ) ) {
$item['reasoning'] = $analysis['reasoning'];
}
$split_results['false_positives'][] = $item;
continue;
}
$split_results['actionable'][] = $item;
}
return $split_results;
}
/**
* Finds the AI analysis entry for a result item.
*
* @since 2.0.0
*
* @param array $item Result item.
* @param array $ai_analysis AI analysis results.
* @return array|null AI analysis entry, or null if none is found.
*/
private function find_ai_analysis_for_result( array $item, array $ai_analysis ) {
foreach ( $ai_analysis as $analysis ) {
if ( ! is_array( $analysis ) ) {
continue;
}
if (
(string) ( $analysis['file'] ?? '' ) === (string) ( $item['file'] ?? '' ) &&
(int) ( $analysis['line'] ?? 0 ) === (int) ( $item['line'] ?? 0 ) &&
(int) ( $analysis['column'] ?? 0 ) === (int) ( $item['column'] ?? 0 ) &&
(string) ( $analysis['code'] ?? '' ) === (string) ( $item['code'] ?? '' )
) {
return $analysis;
}
}
return null;
}
/**
* Displays AI analysis summary.
*
* @since 2.0.0
*
* @param array $ai_analysis AI analysis results.
* @param array $ai_stats AI statistics.
* @param array $false_positive_results False positive results.
*/
private function display_ai_summary(
array $ai_analysis,
array $ai_stats,
array $false_positive_results = array()
) {
WP_CLI::line( '' );
WP_CLI::line( str_repeat( '─', 60 ) );
WP_CLI::line( '✨ ' . __( 'AI Possible False Positive Analysis', 'plugin-check' ) );
WP_CLI::line( str_repeat( '─', 60 ) );
if ( ! empty( $ai_stats ) ) {
$issues_analyzed = isset( $ai_stats['issues_analyzed'] ) ? (int) $ai_stats['issues_analyzed'] : 0;
$false_positives = isset( $ai_stats['false_positives'] ) ? (int) $ai_stats['false_positives'] : 0;
$tokens_spent = isset( $ai_stats['tokens_spent'] ) ? (int) $ai_stats['tokens_spent'] : 0;
WP_CLI::line(
sprintf(
/* translators: %d: Number of issues analyzed. */
_n( 'Issue analyzed: %d', 'Issues analyzed: %d', $issues_analyzed, 'plugin-check' ),
$issues_analyzed
)
);
WP_CLI::line(
sprintf(
/* translators: %d: Number of possible false positives detected. */
_n( 'Possible false positive detected: %d', 'Possible false positives detected: %d', $false_positives, 'plugin-check' ),
$false_positives
)
);
if ( $tokens_spent > 0 ) {
WP_CLI::line(
sprintf(
/* translators: %s: Number of tokens spent. */
_n( 'Token spent: %s', 'Tokens spent: %s', $tokens_spent, 'plugin-check' ),
number_format_i18n( $tokens_spent )
)
);
}
}
// Show individual false positive details.
if ( ! empty( $false_positive_results ) ) {
WP_CLI::line( '' );
WP_CLI::line( __( 'Possible false positives:', 'plugin-check' ) );
foreach ( $false_positive_results as $item ) {
$location = isset( $item['file'] ) ? $item['file'] : '';
if ( isset( $item['line'] ) ) {
$location .= ':' . $item['line'];
}
WP_CLI::line(
sprintf(
' %s - %s',
$location,
isset( $item['reasoning'] ) ? $item['reasoning'] : $item['message']
)
);
}
}
WP_CLI::line( '' );
}
/**
* Returns check results filtered by severity level.
*
* @since 1.1.0
*
* @param array $results Check results.
* @param int $error_severity Error severity level.
* @param int $warning_severity Warning severity level.
* @param bool $include_low_severity_errors Include less level of severity issues as warning.
* @param bool $include_low_severity_warnings Include less level of severity issues as warning.
*
* @SuppressWarnings(PHPMD.BooleanArgumentFlag)
* @return array Filtered results.
*/
private function get_filtered_results_by_severity( $results, $error_severity, $warning_severity, $include_low_severity_errors = false, $include_low_severity_warnings = false ) {
$errors = array();
$warnings = array();
foreach ( $results as $item ) {
if ( 'ERROR' === $item['type'] && $item['severity'] >= $error_severity ) {
$errors[] = $item;
} elseif ( $include_low_severity_errors && 'ERROR' === $item['type'] && $item['severity'] < $error_severity ) {
$item['type'] = 'ERROR_LOW_SEVERITY';
$errors[] = $item;
} elseif ( $include_low_severity_warnings && 'WARNING' === $item['type'] && $item['severity'] < $warning_severity ) {
$item['type'] = 'WARNING_LOW_SEVERITY';
$warnings[] = $item;
} elseif ( 'WARNING' === $item['type'] && $item['severity'] >= $warning_severity ) {
$warnings[] = $item;
}
}
return array_merge( $errors, $warnings );
}
/**
* Returns check results filtered by ignore codes.
*
* @since 1.4.0
*
* @param array $results Check results.
* @param array $ignore_codes Array of error codes to be ignored.
* @return array Filtered results.
*/
private function get_filtered_results_by_ignore_codes( $results, $ignore_codes ) {
return array_filter(
$results,
static function ( $result ) use ( $ignore_codes ) {
return ! in_array( $result['code'], $ignore_codes, true );
}
);
}
}