diff --git a/src/Bridge/MoodlePlugin.php b/src/Bridge/MoodlePlugin.php index 61678786..9f6efa09 100644 --- a/src/Bridge/MoodlePlugin.php +++ b/src/Bridge/MoodlePlugin.php @@ -256,6 +256,60 @@ public function getIgnores(): array return array_key_exists('filter', $config) ? $config['filter'] : []; } + /** + * Get ignore file information from subdirectory config files. + * + * Discovers .moodle-plugin-ci.yml files in all subdirectories of the plugin + * and merges their filter rules. notPaths entries are prefixed with the + * relative subdirectory path so they apply correctly when Finder is rooted + * at the main plugin directory. + * + * @return array{notPaths?: string[], notNames?: string[]} + */ + private function getSubdirectoryIgnores(): array + { + $merged = []; + + $configFiles = Finder::create() + ->files() + ->ignoreDotFiles(false) + ->in($this->directory) + ->name('.moodle-plugin-ci.yml') + ->depth('> 0') + ->ignoreUnreadableDirs(); + + foreach ($configFiles as $file) { + $config = Yaml::parse(file_get_contents($file->getRealPath())); + + // Determine filter section: context-specific or generic (same logic as getIgnores). + $ignores = []; + if (!empty($this->context) && array_key_exists('filter-' . $this->context, $config)) { + $ignores = $config['filter-' . $this->context]; + } elseif (array_key_exists('filter', $config)) { + $ignores = $config['filter']; + } + + // Relative directory from the plugin root to this config file's directory. + $relativeDir = $file->getRelativePath(); + + // Prefix notPaths with the relative subdirectory path. + if (!empty($ignores['notPaths'])) { + foreach ($ignores['notPaths'] as $notPath) { + $merged['notPaths'][] = $relativeDir . '/' . $notPath; + } + } + + // notNames are filename patterns, applied globally. + if (!empty($ignores['notNames'])) { + foreach ($ignores['notNames'] as $notName) { + $merged['notNames'][] = $notName; + } + } + } + + return $merged; + } + /** * Get a list of plugin files. * @@ -286,6 +340,20 @@ public function getFiles(Finder $finder): array } } + // Merge ignores from subdirectory config files (e.g., subplugins). + $subIgnores = $this->getSubdirectoryIgnores(); + + if (!empty($subIgnores['notPaths'])) { + foreach ($subIgnores['notPaths'] as $notPath) { + $finder->notPath($notPath); + } + } + if (!empty($subIgnores['notNames'])) { + foreach ($subIgnores['notNames'] as $notName) { + $finder->notName($notName); + } + } + $files = []; foreach ($finder as $file) { /* @var \SplFileInfo $file */ diff --git a/tests/Bridge/MoodlePluginTest.php b/tests/Bridge/MoodlePluginTest.php index 1df95de7..c3a6e2ef 100644 --- a/tests/Bridge/MoodlePluginTest.php +++ b/tests/Bridge/MoodlePluginTest.php @@ -140,6 +140,96 @@ public function testGetFiles() $this->assertSame($expected, $files); } + public function testGetFilesWithSubdirectoryNotPaths() + { + // Create a subplugin directory with its own config. + $subDir = $this->pluginDir . '/subtype/mysub'; + $this->fs->mkdir($subDir . '/vendor'); + $this->fs->dumpFile($subDir . '/lib.php', 'fs->dumpFile($subDir . '/vendor/dep.php', ' ['notPaths' => ['vendor']]]; + $this->fs->dumpFile($subDir . '/.moodle-plugin-ci.yml', Yaml::dump($subConfig)); + + // Main plugin config excludes 'ignore' path and 'ignore_name.php' name. + $mainConfig = ['filter' => ['notNames' => ['ignore_name.php'], 'notPaths' => ['ignore']]]; + $this->fs->dumpFile($this->pluginDir . '/.moodle-plugin-ci.yml', Yaml::dump($mainConfig)); + + $finder = new Finder(); + $finder->name('*.php'); + + $plugin = new MoodlePlugin($this->pluginDir); + $files = $plugin->getFiles($finder); + + // The subplugin's lib.php should be present. + $this->assertContains(realpath($subDir . '/lib.php'), $files); + + // The subplugin's vendor/dep.php should be excluded by the subplugin config. + $this->assertNotContains(realpath($subDir . '/vendor/dep.php'), $files); + } + + public function testGetFilesWithSubdirectoryContextFilter() + { + $subDir = $this->pluginDir . '/subtype/mysub'; + $this->fs->mkdir($subDir); + $this->fs->dumpFile($subDir . '/excluded.php', 'fs->dumpFile($subDir . '/included.php', ' ['notPaths' => ['nonexistent']], + 'filter-phpcs' => ['notNames' => ['excluded.php']], + ]; + $this->fs->dumpFile($subDir . '/.moodle-plugin-ci.yml', Yaml::dump($subConfig)); + + // Main plugin config excludes 'ignore' path and 'ignore_name.php' name. + $mainConfig = ['filter' => ['notNames' => ['ignore_name.php'], 'notPaths' => ['ignore']]]; + $this->fs->dumpFile($this->pluginDir . '/.moodle-plugin-ci.yml', Yaml::dump($mainConfig)); + + $finder = new Finder(); + $finder->name('*.php'); + + $plugin = new MoodlePlugin($this->pluginDir); + $plugin->context = 'phpcs'; + $files = $plugin->getFiles($finder); + + $this->assertNotContains(realpath($subDir . '/excluded.php'), $files); + $this->assertContains(realpath($subDir . '/included.php'), $files); + } + + public function testGetFilesWithMultipleSubdirectoryConfigs() + { + $sub1Dir = $this->pluginDir . '/subtype1/sub1'; + $sub2Dir = $this->pluginDir . '/subtype2/sub2'; + $this->fs->mkdir($sub1Dir . '/generated'); + $this->fs->mkdir($sub2Dir . '/tmp'); + $this->fs->dumpFile($sub1Dir . '/lib.php', 'fs->dumpFile($sub1Dir . '/generated/out.php', 'fs->dumpFile($sub2Dir . '/lib.php', 'fs->dumpFile($sub2Dir . '/tmp/cache.php', 'fs->dumpFile($sub1Dir . '/.moodle-plugin-ci.yml', + Yaml::dump(['filter' => ['notPaths' => ['generated']]])); + $this->fs->dumpFile($sub2Dir . '/.moodle-plugin-ci.yml', + Yaml::dump(['filter' => ['notPaths' => ['tmp']]])); + + // Main plugin config excludes 'ignore' path and 'ignore_name.php' name. + $mainConfig = ['filter' => ['notNames' => ['ignore_name.php'], 'notPaths' => ['ignore']]]; + $this->fs->dumpFile($this->pluginDir . '/.moodle-plugin-ci.yml', Yaml::dump($mainConfig)); + + $finder = new Finder(); + $finder->name('*.php'); + + $plugin = new MoodlePlugin($this->pluginDir); + $files = $plugin->getFiles($finder); + + $this->assertNotContains(realpath($sub1Dir . '/generated/out.php'), $files); + $this->assertNotContains(realpath($sub2Dir . '/tmp/cache.php'), $files); + $this->assertContains(realpath($sub1Dir . '/lib.php'), $files); + $this->assertContains(realpath($sub2Dir . '/lib.php'), $files); + } + public function testGetRelativeFiles() { // Ignore some files for better testing.