feat: Negation patterns (#24)

This commit is contained in:
Felix Rieseberg 2025-06-28 16:11:32 -07:00 • committed by GitHub
commit e192e488c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 124 additions and 39 deletions

View file

@ -60,60 +60,89 @@ export function readDxtIgnorePatterns(baseDir: string): string[] {
} }
} }
/**
* Tests if a file path matches a given pattern
*/
function matchesPattern(
pattern: string,
filePath: string,
fileName: string,
): boolean {
if (pattern.includes("*")) {
// Enhanced glob matching to handle directory paths like "temp/*"
let patternToMatch = pattern;
// Handle patterns like "dir/" by converting to "dir/**"
if (pattern.endsWith("/")) {
patternToMatch = pattern + "**";
}
// Convert glob pattern to regex, with case sensitivity
const regexPattern =
"^" +
patternToMatch
.replace(/\./g, "\\.") // Escape dots
.replace(/\*\*/g, ".*") // ** matches anything including /
.replace(/\*/g, "[^/]*") + // * matches anything except /
"$";
const regex = new RegExp(regexPattern);
// Test full path
if (regex.test(filePath)) return true;
// Test filename
if (regex.test(fileName)) return true;
// Check if any part of the path matches for patterns like "node_modules"
const pathParts = filePath.split(sep);
for (const part of pathParts) {
if (regex.test(part)) return true;
}
} else {
// Exact match
if (fileName === pattern) return true;
// Check if any part of the path matches
if (filePath.includes(pattern)) return true;
// Handle directory patterns like "tests/"
if (pattern.endsWith("/") && filePath.startsWith(pattern)) {
return true;
}
}
return false;
}
export function shouldExclude( export function shouldExclude(
filePath: string, filePath: string,
additionalPatterns: string[] = [], additionalPatterns: string[] = [],
): boolean { ): boolean {
const fileName = basename(filePath); const fileName = basename(filePath);
const allPatterns = [...EXCLUDE_PATTERNS, ...additionalPatterns]; const allPatterns = [...EXCLUDE_PATTERNS, ...additionalPatterns];
const negationPatterns = allPatterns.filter((p) => p.startsWith("!"));
const exclusionPatterns = allPatterns.filter((p) => !p.startsWith("!"));
for (const pattern of allPatterns) { let shouldExcludeFile = false;
if (pattern.includes("*")) { for (const pattern of exclusionPatterns) {
// Enhanced glob matching to handle directory paths like "temp/*" if (matchesPattern(pattern, filePath, fileName)) {
let patternToMatch = pattern; shouldExcludeFile = true;
break;
}
}
// Handle patterns like "dir/" by converting to "dir/**" // If excluded, check if any negation pattern un-excludes it
if (pattern.endsWith("/")) { if (shouldExcludeFile) {
patternToMatch = pattern + "**"; for (const pattern of negationPatterns) {
} const negatedPattern = pattern.slice(1);
if (matchesPattern(negatedPattern, filePath, fileName)) {
// Convert glob pattern to regex, with case sensitivity // File should not be excluded (negation pattern matched)
const regexPattern = return false;
"^" +
patternToMatch
.replace(/\./g, "\\.") // Escape dots
.replace(/\*\*/g, ".*") // ** matches anything including /
.replace(/\*/g, "[^/]*") + // * matches anything except /
"$";
const regex = new RegExp(regexPattern);
// Test full path
if (regex.test(filePath)) return true;
// Test filename
if (regex.test(fileName)) return true;
// Check if any part of the path matches for patterns like "node_modules"
const pathParts = filePath.split(sep);
for (const part of pathParts) {
if (regex.test(part)) return true;
}
} else {
// Exact match
if (fileName === pattern) return true;
// Check if any part of the path matches
if (filePath.includes(pattern)) return true;
// Handle directory patterns like "tests/"
if (pattern.endsWith("/") && filePath.startsWith(pattern)) {
return true;
} }
} }
} }
return false; return shouldExcludeFile;
} }
export function getAllFiles( export function getAllFiles(

View file

@ -258,4 +258,60 @@ coverage/`;
); );
}); });
}); });
describe("Negation patterns", () => {
it("should include files that match negation patterns", () => {
const additionalPatterns = ["*.log", "!important.log"];
// debug.log should be excluded (matches *.log, no negation)
expect(shouldExclude("debug.log", additionalPatterns)).toBe(true);
// important.log should NOT be excluded (matches *.log, but negated by !important.log)
expect(shouldExclude("important.log", additionalPatterns)).toBe(false);
// Other files should not be excluded
expect(shouldExclude("main.js", additionalPatterns)).toBe(false);
});
it("should handle multiple negation patterns", () => {
const additionalPatterns = ["*.log", "!important.log", "!critical.log"];
expect(shouldExclude("debug.log", additionalPatterns)).toBe(true);
expect(shouldExclude("important.log", additionalPatterns)).toBe(false);
expect(shouldExclude("critical.log", additionalPatterns)).toBe(false);
expect(shouldExclude("other.log", additionalPatterns)).toBe(true);
});
it("should handle negation with glob patterns", () => {
const additionalPatterns = ["temp/*", "!temp/important.txt"];
expect(shouldExclude("temp/debug.txt", additionalPatterns)).toBe(true);
expect(shouldExclude("temp/important.txt", additionalPatterns)).toBe(
false,
);
expect(shouldExclude("other/file.txt", additionalPatterns)).toBe(false);
});
it("should handle negation with directory patterns", () => {
const additionalPatterns = ["tests/", "!tests/critical/"];
expect(shouldExclude("tests/unit/test.js", additionalPatterns)).toBe(
true,
);
expect(shouldExclude("tests/critical/test.js", additionalPatterns)).toBe(
false,
);
expect(shouldExclude("src/main.js", additionalPatterns)).toBe(false);
});
it("should not negate files that don't match exclusion patterns", () => {
const additionalPatterns = ["*.log", "!main.js"];
// main.js should not be excluded (doesn't match *.log, negation doesn't apply)
expect(shouldExclude("main.js", additionalPatterns)).toBe(false);
// debug.log should be excluded (matches *.log, no applicable negation)
expect(shouldExclude("debug.log", additionalPatterns)).toBe(true);
});
});
}); });