hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
226 lines (196 loc) • 5.37 kB
text/typescript
// The same patterns are reused for every source file, so compile each one once.
const compiledGlobCache = new Map<string, RegExp>();
/**
* Returns true when `path` should be included given user-supplied include and
* exclude glob lists. `include` is the gate: an empty `include` matches
* nothing. `exclude` then narrows the included set.
*/
export function isPathSelected(
path: string,
include: string[],
exclude: string[],
): boolean {
if (include.length === 0) {
return false;
}
if (!matchesAny(path, include)) {
return false;
}
if (exclude.length > 0 && matchesAny(path, exclude)) {
return false;
}
return true;
}
/**
* Returns true if `value` matches at least one of the given glob patterns.
*/
function matchesAny(value: string, patterns: string[]): boolean {
for (const pattern of patterns) {
if (getCompiledGlob(pattern).test(value)) {
return true;
}
}
return false;
}
/**
* Compiles a glob pattern into a regular expression. Supports `*`, `**`,
* `?`, `[abc]` (including `[a-z]` ranges and `[!abc]` / `[^abc]` negation),
* and `{a,b,c}` alternation.
*/
function globToRegExp(pattern: string): RegExp {
return new RegExp(`^${translateGlob(pattern)}$`);
}
function translateGlob(pattern: string): string {
let regex = "";
let i = 0;
while (i < pattern.length) {
const c = pattern[i];
if (c === "*") {
if (pattern[i + 1] === "*") {
// `**` as a full path segment matches zero or more directories,
// e.g. `**/x.sol` matches both `x.sol` and `a/b/x.sol`.
const afterSlash = i === 0 || pattern[i - 1] === "/";
const beforeSlash = pattern[i + 2] === "/";
if (afterSlash && beforeSlash) {
regex += "(?:.*/)?";
i += 3;
} else {
regex += ".*";
i += 2;
}
} else {
regex += "[^/]*";
i += 1;
}
} else if (c === "?") {
regex += "[^/]";
i += 1;
} else if (c === "[") {
const end = findCharClassEnd(pattern, i);
if (end !== -1) {
regex += translateCharClass(pattern.slice(i + 1, end));
i = end + 1;
continue;
}
regex += "\\[";
i += 1;
} else if (c === "{") {
const end = findBraceEnd(pattern, i);
if (end !== -1) {
const alternatives = splitBraceAlternatives(
pattern.slice(i + 1, end),
).map(translateGlob);
regex += `(?:${alternatives.join("|")})`;
i = end + 1;
continue;
}
regex += "\\{";
i += 1;
} else if (/[.+^$()|\\\]}]/.test(c)) {
regex += `\\${c}`;
i += 1;
} else {
regex += c;
i += 1;
}
}
return regex;
}
/**
* Returns the index of the `]` that closes the character class opened at
* `start`, or -1 if none is found. E.g. for `a[bc]d` starting at 1, returns 4.
*/
function findCharClassEnd(pattern: string, start: number): number {
for (let i = start + 1; i < pattern.length; i++) {
if (pattern[i] === "]") {
return i;
}
}
return -1;
}
/**
* Returns the index of the `}` that closes the brace group opened at `start`,
* handling nested groups and skipping character classes, or -1 if unterminated.
* E.g. `{a,{b,c}}` returns the outer `}`; `{a[}]b}` skips the bracketed `}`.
*/
function findBraceEnd(pattern: string, start: number): number {
let depth = 1;
let i = start + 1;
while (i < pattern.length) {
const c = pattern[i];
if (c === "{") {
depth += 1;
} else if (c === "}") {
depth -= 1;
if (depth === 0) {
return i;
}
} else if (c === "[") {
const end = findCharClassEnd(pattern, i);
if (end !== -1) {
i = end;
}
}
i += 1;
}
return -1;
}
/**
* Splits a brace group's body on top-level commas, leaving commas inside
* nested braces or character classes untouched. E.g. `a,{b,c},[d,e]` →
* `["a", "{b,c}", "[d,e]"]`.
*/
function splitBraceAlternatives(inside: string): string[] {
const result: string[] = [];
let depth = 0;
let start = 0;
let i = 0;
while (i < inside.length) {
const c = inside[i];
if (c === "{") {
depth += 1;
} else if (c === "}") {
depth -= 1;
} else if (c === "[") {
const end = findCharClassEnd(inside, i);
if (end !== -1) {
i = end;
}
} else if (c === "," && depth === 0) {
result.push(inside.slice(start, i));
start = i + 1;
}
i += 1;
}
result.push(inside.slice(start));
return result;
}
/**
* Translates a glob character class body into a regex character class.
* E.g. `Mm` → `[Mm]`, `!ab` → `[^ab]`, `a-z` → `[a-z]`.
*/
function translateCharClass(content: string): string {
let negated = false;
let body = content;
if (body.startsWith("!") || body.startsWith("^")) {
negated = true;
body = body.slice(1);
}
let escaped = "";
for (const ch of body) {
if (ch === "\\" || ch === "]") {
escaped += `\\${ch}`;
} else {
escaped += ch;
}
}
return `(?!/)[${negated ? "^" : ""}${escaped}]`;
}
function getCompiledGlob(pattern: string): RegExp {
let compiled = compiledGlobCache.get(pattern);
if (compiled === undefined) {
compiled = globToRegExp(pattern);
compiledGlobCache.set(pattern, compiled);
}
return compiled;
}