Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
| --- | --- | --- | --- |
| i18n_usage | general, plugin_repo | Checks for various internationalization best practices. | [Learn more](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/) |
| code_obfuscation | plugin_repo | Detects the usage of code obfuscation tools. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) |
| file_type | plugin_repo | Detects the usage of hidden and compressed files, VCS directories, application files and badly named files. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) |
| file_type | plugin_repo | Detects the usage of hidden and compressed files, VCS directories, application files, badly named files, AI development directories (.cursor, .claude, .aider, .continue, .windsurf, .ai, .github), and unexpected markdown files in plugin root. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) |
| plugin_header_fields | plugin_repo | Checks adherence to the Headers requirements. | [Learn more](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) |
| late_escaping | security, plugin_repo | Checks that all output is escaped before being sent to the browser. | [Learn more](https://developer.wordpress.org/apis/security/escaping/) |
| safe_redirect | security, plugin_repo | Checks that redirects use wp_safe_redirect() instead of wp_redirect() for security. | [Learn more](https://developer.wordpress.org/reference/functions/wp_safe_redirect/) |
Expand Down
157 changes: 147 additions & 10 deletions includes/Checker/Checks/Plugin_Repo/File_Type_Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ class File_Type_Check extends Abstract_File_Check {
use Amend_Check_Result;
use Stable_Check;

const TYPE_COMPRESSED = 1;
const TYPE_PHAR = 2;
const TYPE_VCS = 4;
const TYPE_HIDDEN = 8;
const TYPE_APPLICATION = 16;
const TYPE_BADLY_NAMED = 32;
const TYPE_LIBRARY_CORE = 64;
const TYPE_COMPOSER = 128;
const TYPE_ALL = 255; // Same as all of the above with bitwise OR.
const TYPE_COMPRESSED = 1;
const TYPE_PHAR = 2;
const TYPE_VCS = 4;
const TYPE_HIDDEN = 8;
const TYPE_APPLICATION = 16;
const TYPE_BADLY_NAMED = 32;
const TYPE_LIBRARY_CORE = 64;
const TYPE_COMPOSER = 128;
const TYPE_AI_INSTRUCTIONS = 256;
const TYPE_ALL = 511; // Same as all of the above with bitwise OR.

/**
* Bitwise flags to control check behavior.
Expand Down Expand Up @@ -106,6 +107,9 @@ protected function check_files( Check_Result $result, array $files ) {
if ( $this->flags & self::TYPE_COMPOSER ) {
$this->look_for_composer_files( $result, $files );
}
if ( $this->flags & self::TYPE_AI_INSTRUCTIONS ) {
$this->look_for_ai_instructions( $result, $files );
}
}

/**
Expand Down Expand Up @@ -474,6 +478,139 @@ protected function look_for_composer_files( Check_Result $result, array $files )
}
}

/**
* Looks for AI instruction files and directories.
*
* @since 1.8.0
*
* @param Check_Result $result The check result to amend, including the plugin context to check.
* @param array $files List of absolute file paths.
*/
protected function look_for_ai_instructions( Check_Result $result, array $files ) {
$this->check_ai_directories( $result, $files );
$this->check_github_directory( $result, $files );
$this->check_unexpected_markdown_files( $result, $files );
}

/**
* Checks for AI instruction directories.
*
* @since 1.8.0
*
* @param Check_Result $result Check result to amend.
* @param array $files List of file paths.
*/
protected function check_ai_directories( Check_Result $result, array $files ) {
$plugin_path = $result->plugin()->path();
$ai_directories = array( '.cursor', '.claude', '.aider', '.continue', '.windsurf', '.ai' );
$found_ai_dirs = array();

foreach ( $files as $file ) {
$relative_path = str_replace( $plugin_path, '', $file );

foreach ( $ai_directories as $ai_dir ) {
if ( strpos( $relative_path, '/' . $ai_dir . '/' ) !== false || strpos( $relative_path, $ai_dir . '/' ) === 0 ) {
$found_ai_dirs[ $ai_dir ] = true;
break;
}
}
}

foreach ( array_keys( $found_ai_dirs ) as $ai_dir ) {
$this->add_result_warning_for_file(
$result,
sprintf(
/* translators: %s: directory name */
__( 'AI instruction directory "%s" detected. These directories should not be included in production plugins.', 'plugin-check' ),
$ai_dir
),
'ai_instruction_directory',
$plugin_path . $ai_dir,
0,
0,
'',
9
);
}
}

/**
* Checks for GitHub workflow directory.
*
* @since 1.8.0
*
* @param Check_Result $result Check result to amend.
* @param array $files List of file paths.
*/
protected function check_github_directory( Check_Result $result, array $files ) {
$plugin_path = $result->plugin()->path();
$found_github = false;

foreach ( $files as $file ) {
$relative_path = str_replace( $plugin_path, '', $file );
if ( strpos( $relative_path, '/.github/' ) !== false || strpos( $relative_path, '.github/' ) === 0 ) {
$found_github = true;
break;
}
}

if ( $found_github ) {
$this->add_result_warning_for_file(
$result,
__( 'GitHub workflow directory ".github" detected. This directory should not be included in production plugins.', 'plugin-check' ),
'github_directory',
$plugin_path . '.github',
0,
0,
'',
9
);
}
}

/**
* Checks for unexpected markdown files.
*
* @since 1.8.0
*
* @param Check_Result $result Check result to amend.
* @param array $files List of file paths.
*/
protected function check_unexpected_markdown_files( Check_Result $result, array $files ) {
$plugin_path = $result->plugin()->path();
$allowed_root_md_files = array( 'README.md', 'readme.txt', 'LICENSE', 'LICENSE.md', 'CHANGELOG.md', 'CONTRIBUTING.md', 'SECURITY.md' );
$root_md_files = array();

foreach ( $files as $file ) {
$relative_path = str_replace( $plugin_path, '', $file );
$relative_path = ltrim( $relative_path, '/' );
$basename = basename( $file );

if ( substr_count( $relative_path, '/' ) === 0 && pathinfo( $file, PATHINFO_EXTENSION ) === 'md' ) {
if ( ! in_array( $basename, $allowed_root_md_files, true ) ) {
$root_md_files[] = $file;
}
}
}

foreach ( $root_md_files as $file ) {
$this->add_result_warning_for_file(
$result,
sprintf(
/* translators: %s: file name */
__( 'Unexpected markdown file "%s" detected in plugin root. Only specific markdown files are expected in production plugins.', 'plugin-check' ),
basename( $file )
),
'unexpected_markdown_file',
$file,
0,
0,
'',
9
);
}
}

/**
* Gets the description for the check.
*
Expand All @@ -484,7 +621,7 @@ protected function look_for_composer_files( Check_Result $result, array $files )
* @return string Description.
*/
public function get_description(): string {
return __( 'Detects the usage of hidden and compressed files, VCS directories, application files, badly named files and Library Core Files.', 'plugin-check' );
return __( 'Detects the usage of hidden and compressed files, VCS directories, application files, badly named files, Library Core Files, AI development directories, and unexpected markdown files.', 'plugin-check' );
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Cursor Rules

This file contains AI instructions for the Cursor editor.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: Test
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Contributing Guide

This is an unexpected markdown file in the plugin root.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Development Guide

This file contains development instructions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php
/**
* Plugin Name: Test Plugin AI Instructions Errors
*/

function test_plugin_init() {
return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Test Plugin

This plugin is for testing purposes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# API Documentation

This markdown file is in a subfolder and should not be flagged.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Developer Guide

This is a markdown file in a subfolder, which should be allowed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php
/**
* Plugin Name: Test Plugin AI Instructions Without Errors
*/

function test_plugin_init() {
return true;
}
99 changes: 99 additions & 0 deletions tests/phpunit/tests/Checker/Checks/File_Type_Check_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,103 @@ public function test_run_with_distignore_shows_error_in_production() {
$this->assertTrue( isset( $errors['.hidden-test'][0][0][0] ) );
$this->assertSame( 'hidden_files', $errors['.hidden-test'][0][0][0]['code'] );
}

public function test_run_with_ai_instructions_errors() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-ai-instructions-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check = new File_Type_Check( File_Type_Check::TYPE_AI_INSTRUCTIONS );
$check->run( $check_result );

$problems = $check_result->get_warnings();
$problem_count = $check_result->get_warning_count();
$errors = $check_result->get_errors();

$this->assertNotEmpty( $problems );
$this->assertGreaterThanOrEqual( 3, $problem_count );
$this->assertEmpty( $errors );

$found_cursor = false;
$found_github = false;
$found_dev = false;

foreach ( $problems as $file => $messages ) {
if ( strpos( $file, '.cursor' ) !== false ) {
$found_cursor = true;
$this->assertTrue( isset( $messages[0][0][0] ) );
$this->assertSame( 'ai_instruction_directory', $messages[0][0][0]['code'] );
}
if ( strpos( $file, '.github' ) !== false ) {
$found_github = true;
$this->assertTrue( isset( $messages[0][0][0] ) );
$this->assertSame( 'github_directory', $messages[0][0][0]['code'] );
}
if ( strpos( $file, 'DEVELOPMENT.md' ) !== false ) {
$found_dev = true;
$this->assertTrue( isset( $messages[0][0][0] ) );
$this->assertSame( 'unexpected_markdown_file', $messages[0][0][0]['code'] );
}
}

$this->assertTrue( $found_cursor, 'Expected .cursor directory to be detected' );
$this->assertTrue( $found_github, 'Expected .github directory to be detected' );
$this->assertTrue( $found_dev, 'Expected DEVELOPMENT.md to be detected as unexpected' );
}

public function test_run_with_ai_instructions_in_local_dev() {
$filter_callback = function () {
return 'local';
};
add_filter( 'wp_get_environment_type', $filter_callback );

$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-ai-instructions-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check = new File_Type_Check( File_Type_Check::TYPE_AI_INSTRUCTIONS );
$check->run( $check_result );

$warnings = $check_result->get_warnings();
$warning_count = $check_result->get_warning_count();
$errors = $check_result->get_errors();

$this->assertGreaterThanOrEqual( 3, $warning_count );
$this->assertNotEmpty( $warnings );
$this->assertEmpty( $errors );

remove_filter( 'wp_get_environment_type', $filter_callback );
}

public function test_run_without_ai_instructions_errors() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-ai-instructions-without-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check = new File_Type_Check( File_Type_Check::TYPE_AI_INSTRUCTIONS );
$check->run( $check_result );

$errors = $check_result->get_errors();
$warnings = $check_result->get_warnings();

$this->assertEmpty( $errors );
$this->assertEmpty( $warnings );
}

public function test_markdown_files_in_subfolders_allowed() {
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-ai-instructions-without-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check = new File_Type_Check( File_Type_Check::TYPE_AI_INSTRUCTIONS );
$check->run( $check_result );

$errors = $check_result->get_errors();
$warnings = $check_result->get_warnings();

$this->assertEmpty( $errors, 'Markdown files in subfolders should not trigger errors' );
$this->assertEmpty( $warnings, 'Markdown files in subfolders should not trigger warnings' );

foreach ( array_merge( $errors, $warnings ) as $file => $messages ) {
$this->assertStringNotContainsString( 'docs/', $file, 'Files in docs/ subfolder should not be flagged' );
$this->assertStringNotContainsString( 'GUIDE.md', $file, 'GUIDE.md in subfolder should not be flagged' );
$this->assertStringNotContainsString( 'API.md', $file, 'API.md in subfolder should not be flagged' );
}
}
}
Loading