Skip to content
Merged
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
11 changes: 9 additions & 2 deletions src/File/SkipPathMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
* An absolute path therefore matches only that absolute location; it is not
* re-anchored under the base path.
*
* A glob pattern is matched against both the absolute path and the base-relative path.
* A glob pattern is matched against both the absolute path and the base-relative path,
* and matching directories also skip their descendants.
*
* Instances are cached per (base path, skip paths) pair, ignoring skip path
* order and duplicates, and memoise per-path
Expand Down Expand Up @@ -139,11 +140,17 @@ private function computeIsSkipped(string $path): bool
: $normalisedPath;

foreach ($this->patterns as $pattern) {
if (fnmatch($pattern, $normalisedPath) || fnmatch($pattern, $relativePath)) {
if ($this->matchesPattern($pattern, $normalisedPath) || $this->matchesPattern($pattern, $relativePath)) {
return true;
}
}

return false;
}

private function matchesPattern(string $pattern, string $path): bool
{
// Without FNM_PATHNAME, the appended wildcard also matches nested descendants.
return fnmatch($pattern, $path) || fnmatch($pattern . '/*', $path);
}
}
35 changes: 35 additions & 0 deletions tests/Analyser/AnalyserSkipPathsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,49 @@
use Boundwize\StructArmed\Analyser\Analyser;
use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\File\SkipPathMatcher;
use Boundwize\StructArmed\Preset\Preset;
use Boundwize\StructArmed\Preset\Presets\Psr4Preset;
use Boundwize\StructArmed\Rule\Rules\Class_\MustBeFinalRule;
use Boundwize\StructArmed\Tests\Support\TemporaryDirectoryCleanupTrait;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

use function file_put_contents;
use function mkdir;

#[CoversClass(Analyser::class)]
#[CoversClass(SkipPathMatcher::class)]
final class AnalyserSkipPathsTest extends TestCase
{
use TemporaryDirectoryCleanupTrait;

public function testPsr4RuleSpecificDirectoryGlobSkipsDescendants(): void
{
$basePath = $this->makeTemporaryDirectory('structarmed-psr4-skips');
file_put_contents($basePath . '/composer.json', '{"autoload-dev":{"psr-4":{"App\\\\":"tests/"}}}');

foreach (['functional/Core/Nested', 'unit', 'integration', 'functional/CoreExtra'] as $directory) {
mkdir($basePath . '/tests/' . $directory, 0777, true);
file_put_contents($basePath . '/tests/' . $directory . '/Wrong.php', '<?php class Mismatch {}');
}

$architecture = Architecture::define()
->skip([
Psr4Preset::CLASSES_MUST_MATCH_COMPOSER => [
'tests/**/Core',
'tests/unit',
'tests/integration',
],
])
->withPreset(Preset::PSR4());

$violations = (new Analyser($basePath))->analyse($architecture)
->forRule(Psr4Preset::CLASSES_MUST_MATCH_COMPOSER);

$this->assertCount(1, $violations);
$this->assertStringEndsWith('/tests/functional/CoreExtra/Wrong.php', $violations[0]->file);
}

public function testAnalyserComposesGlobalAndRuleSpecificSkipsForClassRules(): void
{
$architecture = Architecture::define()
Expand Down
13 changes: 13 additions & 0 deletions tests/File/SkipPathMatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ public function testCompileReturnsDifferentInstanceForDifferentBasePath(): void
);
}

public function testDirectoryGlobSkipsDescendantsWithoutMatchingSiblingPrefixes(): void
{
foreach (['tests/**/Core', '/project/tests/**/Core'] as $pattern) {
$matcher = SkipPathMatcher::compile('/project', [$pattern]);

$this->assertTrue($matcher->isSkipped('/project/tests/functional/Core'));
$this->assertTrue($matcher->isSkipped('/project/tests/functional/Core/Foo.php'));
$this->assertTrue($matcher->isSkipped('/project/tests/functional/Core/Nested/Foo.php'));
$this->assertFalse($matcher->isSkipped('/project/tests/functional/CoreExtra/Foo.php'));
$this->assertFalse($matcher->isSkipped('/other/tests/functional/Core/Foo.php'));
}
}

public function testLeadingSlashSkipPathMatchesOnlyTheAbsoluteLocation(): void
{
$skipPathMatcher = SkipPathMatcher::compile('/project', ['/vendor']);
Expand Down