Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/Bridge/MoodlePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 */
Expand Down
90 changes: 90 additions & 0 deletions tests/Bridge/MoodlePluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', '<?php // Subplugin lib.');
$this->fs->dumpFile($subDir . '/vendor/dep.php', '<?php // Vendor file to exclude.');

// Subplugin config excludes 'vendor' path.
$subConfig = ['filter' => ['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', '<?php // Should be excluded.');
$this->fs->dumpFile($subDir . '/included.php', '<?php // Should be included.');

// Context-specific filter for 'phpcs' command.
$subConfig = [
'filter' => ['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', '<?php // Sub1 lib.');
$this->fs->dumpFile($sub1Dir . '/generated/out.php', '<?php // Generated.');
$this->fs->dumpFile($sub2Dir . '/lib.php', '<?php // Sub2 lib.');
$this->fs->dumpFile($sub2Dir . '/tmp/cache.php', '<?php // Cached.');

$this->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.
Expand Down
Loading