Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [Available Checks](checks.md)
* [AI-Powered Features & Configuration](ai-features.md)
* [WordPress Functions Compatibility Data](wp-functions-compatibility-data.md)
* [Plugin Check manifest](plugin-check-info.md)
* [CLI Commands](CLI.md)
* [Running Unit tests](running-unit-tests.md)
* [Releasing a New Version of Plugin](releasing.md)
34 changes: 34 additions & 0 deletions docs/plugin-check-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[Back to overview](./README.md)

# Plugin Check manifest

Plugin authors can add `plugin-check-info.json` to plugin root to identify bundled third-party code.

```json
{
"third_parties": [
"vendor/phpseclib",
"libraries/legacy"
]
}
```

Plugin Check keeps errors from declared paths, but hides warning-level findings for those paths. This reduces recommendations intended for plugin authors, such as replacing a library's native PHP function with a WordPress wrapper, without hiding possible errors. Findings outside declared paths remain unchanged.

Manifest is committed with plugin code, so reviewers can inspect declarations. Missing, malformed, or invalid manifest entries are ignored. Paths are relative to plugin root and use `/` separators. Entries match their declared path and files below it, not similarly named paths.

## Visibility

Suppressed warnings are reported, not silently dropped. In CLI table output, Plugin Check shows a notice with the number of suppressed warnings. The AJAX check response includes the `suppressed_warnings` count.

## Opting out

You can disable manifest-based suppression to show all warnings, including from declared third-party paths:

- **CLI**: `wp plugin check <plugin> --ignore-third-party-warnings`.
- **REST/AJAX**: pass `ignore-third-party-warnings=1` in the check request body.
- **Programmatic**: return `true` from the `wp_plugin_check_ignore_third_party_warnings` filter after the runner is created:

```php
add_filter( 'wp_plugin_check_ignore_third_party_warnings', '__return_true' );
```
9 changes: 6 additions & 3 deletions includes/Admin/Admin_AJAX.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,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 );
$ignore_third_party = 1 === filter_input( INPUT_POST, 'ignore-third-party-warnings', FILTER_VALIDATE_INT );
$types = filter_input( INPUT_POST, 'types', FILTER_DEFAULT, FILTER_FORCE_ARRAY );
$types = is_null( $types ) ? array( 'error', 'warning' ) : $types;

Expand All @@ -303,6 +304,7 @@ public function run_checks() {
$runner->set_check_slugs( $checks );
$runner->set_plugin( $plugin );
$runner->set_use_ai( $use_ai );
$runner->set_ignore_third_party_warnings( $ignore_third_party );
$results = $runner->run();
} catch ( Exception $error ) {
wp_send_json_error(
Expand Down Expand Up @@ -339,9 +341,10 @@ public function run_checks() {
*/
private function prepare_results_response( $results, array $types ) {
$response = array(
'message' => __( 'Checks run successfully', 'plugin-check' ),
'errors' => array(),
'warnings' => array(),
'message' => __( 'Checks run successfully', 'plugin-check' ),
'errors' => array(),
'warnings' => array(),
'suppressed_warnings' => $results->get_third_party_warning_filtered_count(),
);

if ( in_array( 'error', $types, true ) ) {
Expand Down
24 changes: 24 additions & 0 deletions includes/CLI/Plugin_Check_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ public function __construct( Plugin_Context $plugin_context ) {
* [--ignore-errors]
* : Limit displayed results to exclude errors.
*
* [--ignore-third-party-warnings]
* : Do not suppress warnings from paths declared in plugin-check-info.json.
*
* [--include-experimental]
* : Include experimental checks.
*
Expand Down Expand Up @@ -190,6 +193,7 @@ public function check( $args, $assoc_args ) {
'format' => 'table',
'ignore-warnings' => false,
'ignore-errors' => false,
'ignore-third-party-warnings' => false,
'include-experimental' => false,
'severity' => '',
'error-severity' => '',
Expand Down Expand Up @@ -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_ignore_third_party_warnings( (bool) $options['ignore-third-party-warnings'] );
if ( ! empty( $options['ai-model'] ) ) {
$runner->set_ai_model_preference( $options['ai-model'] );
}
Expand All @@ -281,6 +286,25 @@ static function ( $dirs ) use ( $excluded_files ) {

Plugin_Request_Utility::destroy_runner();

// Warn about third-party warnings suppressed by the manifest in human-readable table output.
if ( $result && 'table' === $options['format'] && empty( $options['ignore-third-party-warnings'] ) ) {
$filtered_count = $result->get_third_party_warning_filtered_count();
if ( $filtered_count > 0 ) {
WP_CLI::warning(
sprintf(
/* translators: %d: number of suppressed warnings. */
_n(
'%d warning was suppressed from paths declared in plugin-check-info.json (use --ignore-third-party-warnings to show it).',
'%d warnings were suppressed from paths declared in plugin-check-info.json (use --ignore-third-party-warnings to show them).',
$filtered_count,
'plugin-check'
),
$filtered_count
)
);
}
}

// Get errors and warnings from the results.
$errors = array();
if ( $result && empty( $assoc_args['ignore-errors'] ) ) {
Expand Down
75 changes: 75 additions & 0 deletions includes/Checker/Abstract_Check_Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use WordPress\Plugin_Check\Checker\Exception\Invalid_Check_Slug_Exception;
use WordPress\Plugin_Check\Checker\Preparations\Universal_Runtime_Preparation;
use WordPress\Plugin_Check\Traits\AI_Analyzer;
use WordPress\Plugin_Check\Utilities\Plugin_Config;
use WordPress\Plugin_Check\Utilities\Plugin_Request_Utility;

/**
Expand Down Expand Up @@ -42,6 +43,14 @@ abstract class Abstract_Check_Runner implements Check_Runner {
*/
protected $use_ai = false;

/**
* Whether to ignore manifest-based suppression of third-party warnings.
*
* @since 2.1.0
* @var bool
*/
protected $ignore_third_party_warnings = false;

/**
* AI model preference for analysis.
*
Expand Down Expand Up @@ -324,6 +333,17 @@ final public function set_use_ai( $use_ai ) {
$this->use_ai = (bool) $use_ai;
}

/**
* Sets whether to ignore manifest-based suppression of third-party warnings.
*
* @since 2.1.0
*
* @param bool $ignore True to ignore the suppression and show all warnings, false to apply it.
*/
final public function set_ignore_third_party_warnings( $ignore ) {
$this->ignore_third_party_warnings = (bool) $ignore;
}

/**
* Sets the AI model preference for analysis.
*
Expand Down Expand Up @@ -444,6 +464,8 @@ final public function run() {

$results = $this->get_checks_instance()->run_checks( $this->get_check_context(), $checks, $this );

$this->filter_third_party_warnings( $results );

$ai_analysis = array();
$ai_stats = array();

Expand Down Expand Up @@ -473,6 +495,59 @@ final public function run() {
return $results;
}

/**
* Removes warning-level findings from declared third-party paths.
*
* Errors and findings outside declared paths are kept unchanged.
*
* @since 2.1.0
*
* @param Check_Result $results Check results to filter, modified in place.
*/
private function filter_third_party_warnings( Check_Result $results ) {
if ( $this->get_ignore_third_party_warnings() ) {
return;
}

$third_party_paths = Plugin_Config::get_third_party_paths( $this->get_check_context()->path() );

if ( empty( $third_party_paths ) ) {
return;
}

$filtered = 0;

$results->transform_messages(
function ( $message, $is_error, $file ) use ( $third_party_paths, &$filtered ) {
if ( ! $is_error && Plugin_Config::is_third_party_file( $file, $third_party_paths ) ) {
++$filtered;
return false;
}

return $message;
}
);

if ( $filtered > 0 ) {
$results->increment_third_party_warning_filtered_count( $filtered );
}
}

/**
* Determines whether manifest-based suppression of third-party warnings should be ignored.
*
* The suppression can be disabled via the runner setting or the
* `wp_plugin_check_ignore_third_party_warnings` filter.
*
* @since 2.1.0
*
* @return bool True to ignore the suppression and show all warnings, false to apply it.
*/
private function get_ignore_third_party_warnings() {
return $this->ignore_third_party_warnings
|| (bool) apply_filters( 'wp_plugin_check_ignore_third_party_warnings', false );
}

/**
* Determines if any of the checks are a runtime check.
*
Expand Down
30 changes: 30 additions & 0 deletions includes/Checker/Check_Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ final class Check_Result {
*/
protected $warning_count = 0;

/**
* Number of warnings suppressed from declared third-party paths.
*
* @since 2.1.0
* @var int
*/
protected $third_party_warning_filtered_count = 0;

/**
* AI analysis results for false positives.
*
Expand Down Expand Up @@ -260,6 +268,28 @@ public function get_warning_count() {
return $this->warning_count;
}

/**
* Increments the number of warnings suppressed from declared third-party paths.
*
* @since 2.1.0
*
* @param int $count Number of suppressed warnings to add. Default 1.
*/
public function increment_third_party_warning_filtered_count( $count = 1 ) {
$this->third_party_warning_filtered_count += (int) $count;
}

/**
* Returns the number of warnings suppressed from declared third-party paths.
*
* @since 2.1.0
*
* @return int Number of suppressed warnings.
*/
public function get_third_party_warning_filtered_count() {
return $this->third_party_warning_filtered_count;
}

/**
* Sets AI analysis results.
*
Expand Down
113 changes: 113 additions & 0 deletions includes/Utilities/Plugin_Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
/**
* Class WordPress\Plugin_Check\Utilities\Plugin_Config
*
* @package plugin-check
*/

namespace WordPress\Plugin_Check\Utilities;

/**
* Reads optional plugin configuration.
*
* @since 2.1.0
*/
final class Plugin_Config {

/**
* Configuration file name.
*
* @var string
*/
const FILE_NAME = 'plugin-check-info.json';

/**
* Returns paths declared as third-party code.
*
* @since 2.1.0
*
* @param string $plugin_path Absolute plugin path.
* @return string[] Relative third-party paths.
*/
public static function get_third_party_paths( $plugin_path ) {
$config_file = trailingslashit( $plugin_path ) . self::FILE_NAME;

if ( ! is_readable( $config_file ) ) {
return array();
}

// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Read plugin-local JSON configuration.
$contents = file_get_contents( $config_file );
if ( false === $contents ) {
return array();
}

$config = json_decode( $contents, true );

if ( ! is_array( $config ) || empty( $config['third_parties'] ) || ! is_array( $config['third_parties'] ) ) {
return array();
}

$paths = array();
foreach ( $config['third_parties'] as $path ) {
if ( ! is_string( $path ) ) {
continue;
}

$path = self::normalize_path( $path );
if ( '' !== $path ) {
$paths[] = $path;
}
}

return array_values( array_unique( $paths ) );
}

/**
* Checks whether a relative file is inside a declared path.
*
* @since 2.1.0
*
* @param string $file Relative file path.
* @param string[] $paths Relative third-party paths.
* @return bool Whether the file is inside a declared path.
*/
public static function is_third_party_file( $file, array $paths ) {
$file = self::normalize_path( $file );

if ( '' === $file ) {
return false;
}

foreach ( $paths as $path ) {
if ( $file === $path || 0 === strpos( $file, $path . '/' ) ) {
return true;
}
}

return false;
}

/**
* Normalizes and validates a relative plugin path.
*
* @param string $path Plugin-relative path.
* @return string Normalized path, or empty string when invalid.
*/
private static function normalize_path( $path ) {
$path = trim( str_replace( '\\', '/', $path ), " /\t\n\r\0\x0B" );

if ( '' === $path || '/' === substr( $path, 0, 1 ) || preg_match( '#^[A-Za-z]:/#', $path ) ) {
return '';
}

$segments = explode( '/', $path );
foreach ( $segments as $segment ) {
if ( '' === $segment || '.' === $segment || '..' === $segment ) {
return '';
}
}

return implode( '/', $segments );
}
}
Loading
Loading