From 69c905f2765e59c496af41a657af1dc4b719689a Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Mon, 22 Jun 2026 12:29:47 -0400 Subject: [PATCH 1/2] Add failing test for mu-plugin namespacing rules Sets up tests to demonstrate the issues in this ruleset that prevent easy use of single-file mu-plugin patterns, which have pragmatic uses in many codebases and should still require namespaces, etc. --- HM/Tests/Files/FunctionFileNameUnitTest.php | 12 ++- .../client-mu-plugins/my-client-plugin.php | 5 ++ .../mu-plugins/my-plugin.php | 5 ++ .../mu-plugins/nested/nested-plugin.php | 5 ++ .../Files/NamespaceDirectoryNameUnitTest.php | 3 + .../client-mu-plugins/client-mu-plugin.php | 3 + .../mu-plugins/mu-plugin.php | 3 + .../mu-plugins/nested/nested-mu-plugin.php | 3 + phpunit.xml.dist | 1 + tests/MuPluginSideEffectsTest.php | 77 +++++++++++++++++++ .../sideeffects/mu-plugins/example-plugin.php | 18 +++++ .../sideeffects/regular/example-plugin.php | 18 +++++ 12 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 HM/Tests/Files/FunctionFileNameUnitTest/client-mu-plugins/my-client-plugin.php create mode 100644 HM/Tests/Files/FunctionFileNameUnitTest/mu-plugins/my-plugin.php create mode 100644 HM/Tests/Files/FunctionFileNameUnitTest/mu-plugins/nested/nested-plugin.php create mode 100644 HM/Tests/Files/NamespaceDirectoryNameUnitTest/client-mu-plugins/client-mu-plugin.php create mode 100644 HM/Tests/Files/NamespaceDirectoryNameUnitTest/mu-plugins/mu-plugin.php create mode 100644 HM/Tests/Files/NamespaceDirectoryNameUnitTest/mu-plugins/nested/nested-mu-plugin.php create mode 100644 tests/MuPluginSideEffectsTest.php create mode 100644 tests/fixtures/sideeffects/mu-plugins/example-plugin.php create mode 100644 tests/fixtures/sideeffects/regular/example-plugin.php diff --git a/HM/Tests/Files/FunctionFileNameUnitTest.php b/HM/Tests/Files/FunctionFileNameUnitTest.php index f7b6951a..142d66c9 100644 --- a/HM/Tests/Files/FunctionFileNameUnitTest.php +++ b/HM/Tests/Files/FunctionFileNameUnitTest.php @@ -2,7 +2,8 @@ namespace HM\Tests\Files; -use DirectoryIterator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** @@ -20,10 +21,12 @@ protected function getTestFiles( $test_base_dir ) { $test_base_dir = rtrim( $test_base_dir, '.' ); $test_files = []; - $di = new DirectoryIterator( $test_base_dir ); + $di = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $test_base_dir ) + ); foreach ( $di as $file ) { - if ( $file->isDot() ) { + if ( ! $file->isFile() ) { continue; } @@ -46,6 +49,9 @@ public function getErrorList() { $pass = [ 'namespace.php', 'matching-namespace.php', + // Single-file mu-plugins can't all be named namespace.php. + 'my-plugin.php', + 'my-client-plugin.php', ]; if ( in_array( $file, $pass, true ) ) { return []; diff --git a/HM/Tests/Files/FunctionFileNameUnitTest/client-mu-plugins/my-client-plugin.php b/HM/Tests/Files/FunctionFileNameUnitTest/client-mu-plugins/my-client-plugin.php new file mode 100644 index 00000000..a927313b --- /dev/null +++ b/HM/Tests/Files/FunctionFileNameUnitTest/client-mu-plugins/my-client-plugin.php @@ -0,0 +1,5 @@ + tests/AllSniffs.php tests/FixtureTests.php + tests/MuPluginSideEffectsTest.php diff --git a/tests/MuPluginSideEffectsTest.php b/tests/MuPluginSideEffectsTest.php new file mode 100644 index 00000000..3be9d65b --- /dev/null +++ b/tests/MuPluginSideEffectsTest.php @@ -0,0 +1,77 @@ + in + * HM/ruleset.xml. + * + * We exercise it by running the phpcs binary in a subprocess rather than + * building the ruleset in-process: loading the full HM standard alongside the + * other test suites triggers sniff class redeclaration errors, and restricting + * the sniffs in-process makes PHPCS skip the ruleset processing that loads the + * exclude-patterns in the first place. + */ + +namespace HM\CodingStandards\Tests; + +use PHPUnit\Framework\TestCase; + +/** + * Class MuPluginSideEffectsTest + * + * @group fixtures + */ +class MuPluginSideEffectsTest extends TestCase { + const PSR1_SIDE_EFFECTS = 'PSR1.Files.SideEffects.FoundWithSymbols'; + + /** + * Run PSR1.Files.SideEffects over a fixture and return the message sources. + * + * @param string $relative_file Fixture path relative to the tests directory. + * @return string[] List of reported message sources. + */ + protected function get_sources( string $relative_file ) { + $root = dirname( __DIR__ ); + $phpcs = $root . '/vendor/bin/phpcs'; + $file = __DIR__ . '/' . $relative_file; + + $command = sprintf( + '%s %s --standard=HM --sniffs=PSR1.Files.SideEffects --report=json %s', + escapeshellarg( PHP_BINARY ), + escapeshellarg( $phpcs ), + escapeshellarg( $file ) + ); + + $output = shell_exec( $command ); + $report = json_decode( $output, true ); + + $this->assertSame( JSON_ERROR_NONE, json_last_error(), 'phpcs should return valid JSON' ); + + $sources = []; + foreach ( $report['files'] as $details ) { + foreach ( $details['messages'] as $message ) { + $sources[] = $message['source']; + } + } + + return $sources; + } + + /** + * Single-file mu-plugins should be exempt from the side-effects rule. + */ + public function test_mu_plugin_is_exempt() { + $sources = $this->get_sources( 'fixtures/sideeffects/mu-plugins/example-plugin.php' ); + $this->assertNotContains( static::PSR1_SIDE_EFFECTS, $sources ); + } + + /** + * The same code outside mu-plugins/ should still get flagged, to prove correct scoping. + */ + public function test_regular_file_is_flagged() { + $sources = $this->get_sources( 'fixtures/sideeffects/regular/example-plugin.php' ); + $this->assertContains( static::PSR1_SIDE_EFFECTS, $sources ); + } +} diff --git a/tests/fixtures/sideeffects/mu-plugins/example-plugin.php b/tests/fixtures/sideeffects/mu-plugins/example-plugin.php new file mode 100644 index 00000000..373ff141 --- /dev/null +++ b/tests/fixtures/sideeffects/mu-plugins/example-plugin.php @@ -0,0 +1,18 @@ + Date: Mon, 22 Jun 2026 12:40:52 -0400 Subject: [PATCH 2/2] Adjust sniff targeting to permit single-file mu-plugins Permits files which are direct children of mu-plugins or client-mu-plugins to have sideeffects and to not require a folder-based structure. Proper file organization is still recommended but in many cases a full inc folder is overkill for a small mu-plugin which adjusts third-party plugin functionality, etcetera. --- HM/Sniffs/Files/FunctionFileNameSniff.php | 7 +++++ HM/Sniffs/Files/MuPluginFileTrait.php | 27 +++++++++++++++++++ .../Files/NamespaceDirectoryNameSniff.php | 7 +++++ HM/ruleset.xml | 6 ++++- 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 HM/Sniffs/Files/MuPluginFileTrait.php diff --git a/HM/Sniffs/Files/FunctionFileNameSniff.php b/HM/Sniffs/Files/FunctionFileNameSniff.php index 53ca10f3..001cd013 100644 --- a/HM/Sniffs/Files/FunctionFileNameSniff.php +++ b/HM/Sniffs/Files/FunctionFileNameSniff.php @@ -8,6 +8,8 @@ * Sniff to check for namespaced functions are in `namespace.php`. */ class FunctionFileNameSniff implements Sniff { + use MuPluginFileTrait; + public function register() { return array( T_FUNCTION ); } @@ -25,6 +27,11 @@ public function process( File $phpcsFile, $stackPtr ) { return; } + if ( $this->is_single_file_mu_plugin( $phpcsFile->getFileName() ) ) { + // Single-file plugins cannot be split into a namespace.php file. + return; + } + $filename = basename( $phpcsFile->getFileName() ); if ( $filename === 'namespace.php' ) { diff --git a/HM/Sniffs/Files/MuPluginFileTrait.php b/HM/Sniffs/Files/MuPluginFileTrait.php new file mode 100644 index 00000000..2c5487ec --- /dev/null +++ b/HM/Sniffs/Files/MuPluginFileTrait.php @@ -0,0 +1,27 @@ +is_single_file_mu_plugin( $full ) ) { + // Single-file mu-plugins will naturally never have an inc/ directory. + return; + } + if ( $filename === 'plugin.php' || $filename === 'functions.php' || $filename === 'load.php' ) { // Ignore the main file. return; diff --git a/HM/ruleset.xml b/HM/ruleset.xml index c6ec9771..11c980d0 100644 --- a/HM/ruleset.xml +++ b/HM/ruleset.xml @@ -143,7 +143,11 @@ - + + + */mu-plugins/[^/]+\.php$ + */client-mu-plugins/[^/]+\.php$ +