diff --git a/src/File/SkipPathMatcher.php b/src/File/SkipPathMatcher.php index ce122bd1..1b893009 100644 --- a/src/File/SkipPathMatcher.php +++ b/src/File/SkipPathMatcher.php @@ -49,7 +49,12 @@ final class SkipPathMatcher */ private readonly array $pathPrefixesWithSlash; - /** @var list */ + /** + * Glob patterns with "/*" appended, matched against "$path/" so a single + * fnmatch() covers both the exact path and its descendants. + * + * @var list + */ private readonly array $patterns; private readonly bool $hasMatchers; @@ -85,7 +90,8 @@ private function __construct(string $basePath, array $skipPaths) $normalisedSkipPath = Path::normalise($skipPath); if (strpbrk($skipPath, '*?[') !== false) { - $patterns[] = $normalisedSkipPath; + // Without FNM_PATHNAME, the appended wildcard also matches nested descendants. + $patterns[] = $normalisedSkipPath . '/*'; continue; } @@ -135,22 +141,16 @@ private function computeIsSkipped(string $path): bool return false; } - $relativePath = str_starts_with($normalisedPath, $this->normalisedBasePathWithSlash) - ? substr($normalisedPath, strlen($this->normalisedBasePathWithSlash)) - : $normalisedPath; + $relativePathWithSlash = str_starts_with($pathWithSlash, $this->normalisedBasePathWithSlash) + ? substr($pathWithSlash, strlen($this->normalisedBasePathWithSlash)) + : $pathWithSlash; foreach ($this->patterns as $pattern) { - if ($this->matchesPattern($pattern, $normalisedPath) || $this->matchesPattern($pattern, $relativePath)) { + if (fnmatch($pattern, $pathWithSlash) || fnmatch($pattern, $relativePathWithSlash)) { 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); - } } diff --git a/tests/File/SkipPathMatcherTest.php b/tests/File/SkipPathMatcherTest.php index bb4d18e1..3a850beb 100644 --- a/tests/File/SkipPathMatcherTest.php +++ b/tests/File/SkipPathMatcherTest.php @@ -72,6 +72,16 @@ public function testDirectoryGlobSkipsDescendantsWithoutMatchingSiblingPrefixes( } } + public function testTrailingWildcardGlobSkipsDescendants(): void + { + $skipPathMatcher = SkipPathMatcher::compile('/project', ['*tests*', 'build*']); + + $this->assertTrue($skipPathMatcher->isSkipped('/project/tests/Foo.php')); + $this->assertTrue($skipPathMatcher->isSkipped('/project/src/unittests/Nested/Foo.php')); + $this->assertTrue($skipPathMatcher->isSkipped('/project/build-cache/Nested/Foo.php')); + $this->assertFalse($skipPathMatcher->isSkipped('/project/src/Foo.php')); + } + public function testLeadingSlashSkipPathMatchesOnlyTheAbsoluteLocation(): void { $skipPathMatcher = SkipPathMatcher::compile('/project', ['/vendor']);