feat: Negation patterns (#24)
This commit is contained in:
parent
bf27cf3d67
commit
e192e488c3
2 changed files with 124 additions and 39 deletions
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue