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 @@ +