const COMPILED_GLOBS = new Map(); const NEVER_MATCH = /a^/; function escapeRegex(value: string): string { return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&"); } function translateGlob(glob: string): string { const normalized = glob.replace(/\\/g, "/"); let source = ""; for (let i = 0; i < normalized.length; i += 1) { const char = normalized[i]; if (char === "*") { if (normalized[i + 1] === "*") { i += 1; if (normalized[i + 1] === "/") { i += 1; source += "(?:.*\/)?"; } else { source += ".*"; } } else { source += "[^/]*"; } continue; } if (char === "?") { source += "[^/]"; continue; } if (char === "{") { const end = normalized.indexOf("}", i + 1); if (end === -1) throw new Error("malformed brace expansion"); const choices = normalized .slice(i + 1, end) .split(",") .map((choice) => escapeRegex(choice)); source += `(?:${choices.join("|")})`; i = end; continue; } if (char === "}") throw new Error("malformed brace expansion"); source += escapeRegex(char); } return `^${source}$`; } /** * Compile a small, dependency-free glob subset into a RegExp. Malformed or * otherwise untranslatable globs compile to a never-matching regex; this module * intentionally emits no diagnostics because callers own rule validation. */ export function globToRegex(glob: string): RegExp { const cached = COMPILED_GLOBS.get(glob); if (cached) return cached; try { const compiled = new RegExp(translateGlob(glob)); COMPILED_GLOBS.set(glob, compiled); return compiled; } catch { COMPILED_GLOBS.set(glob, NEVER_MATCH); return NEVER_MATCH; } } export function matchesAny(path: string, globs: string[]): boolean { const normalized = path.replace(/\\/g, "/").replace(/^\.\//, ""); return globs.some((glob) => globToRegex(glob).test(normalized)); }