diff --git a/assets/js/plugin-check-admin.js b/assets/js/plugin-check-admin.js
index e75220958..9e79555ff 100644
--- a/assets/js/plugin-check-admin.js
+++ b/assets/js/plugin-check-admin.js
@@ -38,6 +38,9 @@
'plugin-check__include-experimental'
);
const useAi = document.getElementById( 'plugin-check__use-ai' );
+ const usePcpignore = document.getElementById(
+ 'plugin-check__use-pcpignore'
+ );
// Handle disabling the Check it button when a plugin is not selected.
function canRunChecks() {
@@ -141,6 +144,9 @@
if ( useAi ) {
useAi.disabled = true;
}
+ if ( usePcpignore ) {
+ usePcpignore.disabled = true;
+ }
if ( includeExperimental ) {
includeExperimental.disabled = true;
}
@@ -149,6 +155,8 @@
const categories = getSelectedValues( categoriesList );
const types = getSelectedValues( typesList );
const useAiChecked = useAi && useAi.checked ? 1 : 0;
+ const usePcpignoreChecked =
+ usePcpignore && usePcpignore.checked ? 1 : 0;
const includeExperimentalChecked =
includeExperimental && includeExperimental.checked ? 1 : 0;
let currentChecks;
@@ -157,7 +165,8 @@
plugin,
categories,
includeExperimentalChecked,
- useAiChecked
+ useAiChecked,
+ usePcpignoreChecked
)
.then( ( data ) => {
currentChecks = data.checks;
@@ -165,7 +174,8 @@
plugin,
currentChecks,
includeExperimentalChecked,
- useAiChecked
+ useAiChecked,
+ usePcpignoreChecked
);
} )
.then( () =>
@@ -174,7 +184,8 @@
currentChecks,
types,
includeExperimentalChecked,
- useAiChecked
+ useAiChecked,
+ usePcpignoreChecked
)
)
.then( () => cleanUpEnvironment() )
@@ -224,6 +235,9 @@
if ( useAi ) {
useAi.disabled = false;
}
+ if ( usePcpignore ) {
+ usePcpignore.disabled = false;
+ }
if ( includeExperimental ) {
includeExperimental.disabled = false;
}
@@ -639,13 +653,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} usePcpignoreInput Whether to apply .pcpignore exclusions.
* @return {Promise
+
diff --git a/tests/behat/features/plugin-check.feature b/tests/behat/features/plugin-check.feature
index 903b132bc..a3d188a23 100644
--- a/tests/behat/features/plugin-check.feature
+++ b/tests/behat/features/plugin-check.feature
@@ -314,6 +314,140 @@ Feature: Test that the WP-CLI command works.
FILE: subdirectory/error.php
"""
+ Scenario: Apply .pcpignore exclusions only when requested
+ Given a WP install with the Plugin Check plugin
+ And an empty wp-content/plugins/foo-plugin directory
+ And an empty wp-content/plugins/foo-plugin/docs directory
+ And a wp-content/plugins/foo-plugin/foo-plugin.php file:
+ """
+ assertEquals( $custom_ignore_files, $result );
}
+ public function test_get_pcpignore_exclusions() {
+ $plugin_directory = UNIT_TESTS_PLUGIN_DIR . 'test-plugin-pcpignore';
+
+ $exclusions = PCP_Ignore_Utility::get_exclusions( $plugin_directory );
+
+ $this->assertSame(
+ array( '/docs', '/tests/fixtures' ),
+ $exclusions['directories']
+ );
+ $this->assertSame(
+ array( '/development-only.php', '/*.map', '/.pcpignore' ),
+ $exclusions['files']
+ );
+ $this->assertSame( '', PCP_Ignore_Utility::get_warning() );
+ }
+
+ public function test_get_pcpignore_exclusions_without_ignore_file() {
+ $plugin_directory = UNIT_TESTS_PLUGIN_DIR . 'test-plugin-ignore-files';
+
+ $exclusions = PCP_Ignore_Utility::get_exclusions( $plugin_directory );
+
+ $this->assertSame(
+ array(
+ 'directories' => array(),
+ 'files' => array(),
+ ),
+ $exclusions
+ );
+ $this->assertSame( '', PCP_Ignore_Utility::get_warning() );
+ }
+
+ public function test_get_pcpignore_exclusions_for_single_file_plugin() {
+ $exclusions = PCP_Ignore_Utility::get_exclusions( WP_PLUGIN_DIR . '/foo-single.php' );
+
+ $this->assertSame(
+ array(
+ 'directories' => array(),
+ 'files' => array(),
+ ),
+ $exclusions
+ );
+ $this->assertSame( '', PCP_Ignore_Utility::get_warning() );
+ }
+
+ public function test_get_pcpignore_exclusions_with_unreadable_file() {
+ $plugin_directory = UNIT_TESTS_PLUGIN_DIR . 'test-plugin-pcpignore';
+ $ignore_file = trailingslashit( $plugin_directory ) . '.pcpignore';
+
+ $filter = static function ( $is_readable, $file ) use ( $ignore_file ) {
+ return $ignore_file === $file ? false : $is_readable;
+ };
+ add_filter( 'wp_plugin_check_pcpignore_is_readable', $filter, 10, 2 );
+ $this->cleanups[] = static function () use ( $filter ) {
+ remove_filter( 'wp_plugin_check_pcpignore_is_readable', $filter );
+ };
+
+ $exclusions = PCP_Ignore_Utility::get_exclusions( $plugin_directory );
+
+ $this->assertSame(
+ array(
+ 'directories' => array(),
+ 'files' => array(),
+ ),
+ $exclusions
+ );
+ $this->assertNotSame( '', PCP_Ignore_Utility::get_warning() );
+ }
+
+ public function test_pcpignore_wildcards_do_not_cross_directory_boundaries() {
+ $plugin_root = '/plugin';
+
+ $this->assertTrue( Ignore_Matcher::is_file_ignored( '/plugin/app.js.map', $plugin_root, array( '/*.map' ) ) );
+ $this->assertFalse( Ignore_Matcher::is_file_ignored( '/plugin/assets/app.js.map', $plugin_root, array( '/*.map' ) ) );
+ $this->assertTrue( Ignore_Matcher::is_file_in_ignored_directory( '/plugin/docs-new/readme.txt', $plugin_root, array( '/docs*' ) ) );
+ $this->assertFalse( Ignore_Matcher::is_file_in_ignored_directory( '/plugin/includes/docs-new/readme.txt', $plugin_root, array( '/docs*' ) ) );
+ }
+
+ public function test_pcpignore_php_codesniffer_patterns_match_glob_semantics() {
+ $plugin_root = '/plugin';
+
+ $this->assertSame( '^/plugin/[^/]{0,}\\.map$', Ignore_Matcher::get_php_codesniffer_file_ignore_pattern( $plugin_root, '/*.map' ) );
+ $this->assertSame( '^/plugin/file[^/]\\.php$', Ignore_Matcher::get_php_codesniffer_file_ignore_pattern( $plugin_root, '/file?.php' ) );
+ $this->assertSame( '^/plugin/data\\[1\\]\\.php$', Ignore_Matcher::get_php_codesniffer_file_ignore_pattern( $plugin_root, '/data[1].php' ) );
+ $this->assertSame( '^/plugin/docs[^/]{0,}/*', Ignore_Matcher::get_php_codesniffer_directory_ignore_pattern( $plugin_root, '/docs*' ) );
+ }
+
+ public function test_pcpignore_directory_exclusion_is_anchored_to_plugin_root() {
+ $checks_to_run = array(
+ new I18n_Usage_Check(),
+ );
+
+ add_filter(
+ 'wp_plugin_check_checks',
+ function () {
+ return array(
+ 'i18n_usage_check' => new I18n_Usage_Check(),
+ );
+ }
+ );
+
+ // Without exclusions, both docs/example.php (root) and
+ // includes/docs/real-code.php (nested) trigger an error.
+ $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-pcpignore/load.php' );
+ $results_without = ( new Checks() )->run_checks( $check_context, $checks_to_run );
+
+ $this->assertSame( 2, $results_without->get_error_count() );
+
+ // With .pcpignore exclusions applied, only the root-level docs/
+ // directory is excluded; the nested includes/docs/ directory, which
+ // merely shares the same directory name, must still be scanned.
+ PCP_Ignore_Utility::apply_exclusions( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-pcpignore' );
+
+ $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-pcpignore/load.php' );
+ $results_with = ( new Checks() )->run_checks( $check_context, $checks_to_run );
+
+ $this->assertSame( 1, $results_with->get_error_count() );
+ }
+
public function test_plugin_without_error_for_ignore_directories() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-ignore-directories/load.php' );