Skip to content

Commit 21c5694

Browse files
committed
Respect .moodle-plugin-ci.yml config from subdirectories (#379)
When running checks on a plugin, discover .moodle-plugin-ci.yml files in subdirectories (e.g., subplugins) and merge their notPaths/notNames filters, so subplugins can manage their own CI exclusions independently.
1 parent 198ee6c commit 21c5694

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

src/Bridge/MoodlePlugin.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,70 @@ public function getIgnores(): array
256256
return array_key_exists('filter', $config) ? $config['filter'] : [];
257257
}
258258

259+
/**
260+
* Get ignore file information from subdirectory config files.
261+
*
262+
* Discovers .moodle-plugin-ci.yml files in all subdirectories of the plugin
263+
* and merges their filter rules. notPaths entries are prefixed with the
264+
* relative subdirectory path so they apply correctly when Finder is rooted
265+
* at the main plugin directory.
266+
*
267+
* @return array{notPaths?: string[], notNames?: string[]}
268+
*/
269+
private function getSubdirectoryIgnores(): array
270+
{
271+
$merged = [];
272+
273+
$configFiles = Finder::create()
274+
->files()
275+
->ignoreDotFiles(false)
276+
->in($this->directory)
277+
->name('.moodle-plugin-ci.yml')
278+
->depth('> 0')
279+
->ignoreUnreadableDirs();
280+
281+
foreach ($configFiles as $file) {
282+
$config = Yaml::parse(file_get_contents($file->getRealPath()));
283+
if (!is_array($config)) {
284+
continue;
285+
}
286+
287+
// Determine filter section: context-specific or generic (same logic as getIgnores).
288+
$ignores = [];
289+
if (!empty($this->context) && array_key_exists('filter-' . $this->context, $config)) {
290+
$ignores = $config['filter-' . $this->context];
291+
} elseif (array_key_exists('filter', $config)) {
292+
$ignores = $config['filter'];
293+
}
294+
295+
if (empty($ignores)) {
296+
continue;
297+
}
298+
299+
// Relative directory from the plugin root to this config file's directory.
300+
$relativeDir = rtrim(substr(
301+
dirname($file->getRealPath()),
302+
strlen($this->directory) + 1
303+
), '/');
304+
305+
// Prefix notPaths with the relative subdirectory path.
306+
if (!empty($ignores['notPaths'])) {
307+
foreach ($ignores['notPaths'] as $notPath) {
308+
$merged['notPaths'][] = $relativeDir . '/' . $notPath;
309+
}
310+
}
311+
312+
// notNames are filename patterns, applied globally.
313+
if (!empty($ignores['notNames'])) {
314+
foreach ($ignores['notNames'] as $notName) {
315+
$merged['notNames'][] = $notName;
316+
}
317+
}
318+
}
319+
320+
return $merged;
321+
}
322+
259323
/**
260324
* Get a list of plugin files.
261325
*
@@ -286,6 +350,20 @@ public function getFiles(Finder $finder): array
286350
}
287351
}
288352

353+
// Merge ignores from subdirectory config files (e.g., subplugins).
354+
$subIgnores = $this->getSubdirectoryIgnores();
355+
356+
if (!empty($subIgnores['notPaths'])) {
357+
foreach ($subIgnores['notPaths'] as $notPath) {
358+
$finder->notPath($notPath);
359+
}
360+
}
361+
if (!empty($subIgnores['notNames'])) {
362+
foreach ($subIgnores['notNames'] as $notName) {
363+
$finder->notName($notName);
364+
}
365+
}
366+
289367
$files = [];
290368
foreach ($finder as $file) {
291369
/* @var \SplFileInfo $file */

tests/Bridge/MoodlePluginTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,96 @@ public function testGetFiles()
140140
$this->assertSame($expected, $files);
141141
}
142142

143+
public function testGetFilesWithSubdirectoryNotPaths()
144+
{
145+
// Create a subplugin directory with its own config.
146+
$subDir = $this->pluginDir . '/subtype/mysub';
147+
$this->fs->mkdir($subDir . '/vendor');
148+
$this->fs->dumpFile($subDir . '/lib.php', '<?php // Subplugin lib.');
149+
$this->fs->dumpFile($subDir . '/vendor/dep.php', '<?php // Vendor file to exclude.');
150+
151+
// Subplugin config excludes 'vendor' path.
152+
$subConfig = ['filter' => ['notPaths' => ['vendor']]];
153+
$this->fs->dumpFile($subDir . '/.moodle-plugin-ci.yml', Yaml::dump($subConfig));
154+
155+
// Main plugin config excludes 'ignore' path and 'ignore_name.php' name.
156+
$mainConfig = ['filter' => ['notNames' => ['ignore_name.php'], 'notPaths' => ['ignore']]];
157+
$this->fs->dumpFile($this->pluginDir . '/.moodle-plugin-ci.yml', Yaml::dump($mainConfig));
158+
159+
$finder = new Finder();
160+
$finder->name('*.php');
161+
162+
$plugin = new MoodlePlugin($this->pluginDir);
163+
$files = $plugin->getFiles($finder);
164+
165+
// The subplugin's lib.php should be present.
166+
$this->assertContains(realpath($subDir . '/lib.php'), $files);
167+
168+
// The subplugin's vendor/dep.php should be excluded by the subplugin config.
169+
$this->assertNotContains(realpath($subDir . '/vendor/dep.php'), $files);
170+
}
171+
172+
public function testGetFilesWithSubdirectoryContextFilter()
173+
{
174+
$subDir = $this->pluginDir . '/subtype/mysub';
175+
$this->fs->mkdir($subDir);
176+
$this->fs->dumpFile($subDir . '/excluded.php', '<?php // Should be excluded.');
177+
$this->fs->dumpFile($subDir . '/included.php', '<?php // Should be included.');
178+
179+
// Context-specific filter for 'phpcs' command.
180+
$subConfig = [
181+
'filter' => ['notPaths' => ['nonexistent']],
182+
'filter-phpcs' => ['notNames' => ['excluded.php']],
183+
];
184+
$this->fs->dumpFile($subDir . '/.moodle-plugin-ci.yml', Yaml::dump($subConfig));
185+
186+
// Main plugin config excludes 'ignore' path and 'ignore_name.php' name.
187+
$mainConfig = ['filter' => ['notNames' => ['ignore_name.php'], 'notPaths' => ['ignore']]];
188+
$this->fs->dumpFile($this->pluginDir . '/.moodle-plugin-ci.yml', Yaml::dump($mainConfig));
189+
190+
$finder = new Finder();
191+
$finder->name('*.php');
192+
193+
$plugin = new MoodlePlugin($this->pluginDir);
194+
$plugin->context = 'phpcs';
195+
$files = $plugin->getFiles($finder);
196+
197+
$this->assertNotContains(realpath($subDir . '/excluded.php'), $files);
198+
$this->assertContains(realpath($subDir . '/included.php'), $files);
199+
}
200+
201+
public function testGetFilesWithMultipleSubdirectoryConfigs()
202+
{
203+
$sub1Dir = $this->pluginDir . '/subtype1/sub1';
204+
$sub2Dir = $this->pluginDir . '/subtype2/sub2';
205+
$this->fs->mkdir($sub1Dir . '/generated');
206+
$this->fs->mkdir($sub2Dir . '/tmp');
207+
$this->fs->dumpFile($sub1Dir . '/lib.php', '<?php // Sub1 lib.');
208+
$this->fs->dumpFile($sub1Dir . '/generated/out.php', '<?php // Generated.');
209+
$this->fs->dumpFile($sub2Dir . '/lib.php', '<?php // Sub2 lib.');
210+
$this->fs->dumpFile($sub2Dir . '/tmp/cache.php', '<?php // Cached.');
211+
212+
$this->fs->dumpFile($sub1Dir . '/.moodle-plugin-ci.yml',
213+
Yaml::dump(['filter' => ['notPaths' => ['generated']]]));
214+
$this->fs->dumpFile($sub2Dir . '/.moodle-plugin-ci.yml',
215+
Yaml::dump(['filter' => ['notPaths' => ['tmp']]]));
216+
217+
// Main plugin config excludes 'ignore' path and 'ignore_name.php' name.
218+
$mainConfig = ['filter' => ['notNames' => ['ignore_name.php'], 'notPaths' => ['ignore']]];
219+
$this->fs->dumpFile($this->pluginDir . '/.moodle-plugin-ci.yml', Yaml::dump($mainConfig));
220+
221+
$finder = new Finder();
222+
$finder->name('*.php');
223+
224+
$plugin = new MoodlePlugin($this->pluginDir);
225+
$files = $plugin->getFiles($finder);
226+
227+
$this->assertNotContains(realpath($sub1Dir . '/generated/out.php'), $files);
228+
$this->assertNotContains(realpath($sub2Dir . '/tmp/cache.php'), $files);
229+
$this->assertContains(realpath($sub1Dir . '/lib.php'), $files);
230+
$this->assertContains(realpath($sub2Dir . '/lib.php'), $files);
231+
}
232+
143233
public function testGetRelativeFiles()
144234
{
145235
// Ignore some files for better testing.

0 commit comments

Comments
 (0)