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/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 @@
+
-
+
+
+ */mu-plugins/[^/]+\.php$
+ */client-mu-plugins/[^/]+\.php$
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e0037471..688db45f 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,6 +11,7 @@
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 @@
+