hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
236 lines (207 loc) • 5.81 kB
text/typescript
export enum FrameOrigin {
FIRST_PARTY = "FIRST_PARTY",
THIRD_PARTY = "THIRD_PARTY",
USER_PROJECT = "USER_PROJECT",
NODE_INTERNAL = "NODE_INTERNAL",
OTHER = "OTHER",
}
export interface StackFrame {
functionName?: string;
location: string;
origin: FrameOrigin;
}
/**
* The error context encapsulates the shared derived data used by classification
* and filtering.
*/
export interface ErrorContext {
error: Error;
errorChain: Error[];
lowercaseMessageByError: Map<Error, string>;
stackFramesByError: Map<Error, StackFrame[]>;
allStackFrames: StackFrame[];
}
/**
* Builds the shared derived data used by classification and filtering.
*
* This keeps stack parsing and cause-chain traversal consistent across
* matchers, and avoids recomputing them for every category heuristic.
*/
export function createErrorContext(error: Error): ErrorContext {
const errorChain = getErrorChain(error);
const stackFramesByError = new Map(
errorChain.map((candidate) => [candidate, parseStackFrames(candidate)]),
);
return {
error,
errorChain,
lowercaseMessageByError: new Map(
errorChain.map((candidate) => [
candidate,
candidate.message.toLowerCase(),
]),
),
stackFramesByError,
allStackFrames: errorChain.flatMap(
(candidate) => stackFramesByError.get(candidate) ?? [],
),
};
}
/**
* This function should be used instead of instanceof because it is robust
* under the presence of multiple installations of the same package (e.g.
* multiple hardhat-utils versions).
*
* @param error The error
* @param errorClass The error class
* @returns true if the error has the same name as the error class
*/
export function hasErrorClassName(
error: Error,
errorClass: abstract new (...args: never[]) => Error,
): boolean {
return error.name === errorClass.name;
}
/**
* Returns true when `value` contains any of the supplied substrings.
*/
export function includesAny(
value: string | undefined,
...substrings: string[]
): boolean {
return (
value !== undefined &&
substrings.some((substring) => value.includes(substring))
);
}
/**
* Returns a Node-style `code` string from an error or any Error cause.
*
* Traversal stops when a cause is not an Error, a cycle is detected, or
* `maxCauseDepth` is reached.
*/
export function getNodeErrorCode(
error: Error,
maxCauseDepth = 10,
): string | undefined {
const seen = new Set<Error>();
let current: Error | undefined = error;
let depth = 0;
while (current !== undefined && depth < maxCauseDepth && !seen.has(current)) {
if ("code" in current && typeof current.code === "string") {
return current.code;
}
seen.add(current);
current = getCause(current);
depth++;
}
}
/**
* Returns the error and its nested causes in outer-to-inner order.
*
* Traversal stops when a cause is not an Error, a cycle is detected, or
* `maxCauseDepth` is reached.
*/
function getErrorChain(error: Error, maxCauseDepth = 10): Error[] {
const errors: Error[] = [];
const seen = new Set<Error>();
let current: Error | undefined = error;
while (
current !== undefined &&
errors.length < maxCauseDepth &&
seen.has(current) === false
) {
errors.push(current);
seen.add(current);
if (current.cause !== undefined && !(current.cause instanceof Error)) {
break;
}
current = getCause(current);
}
return errors;
}
/**
* Parses V8-style stack lines into normalized stack frames.
*
* Unrecognized lines are ignored, and path separators are normalized to `/`
* before the frame origin is inferred.
*/
function parseStackFrames(error: Error): StackFrame[] {
if (error.stack === undefined) {
return [];
}
return error.stack
.split("\n")
.slice(1)
.map((line) => line.trim())
.map(parseStackFrameLine)
.filter((frame): frame is StackFrame => frame !== undefined);
}
/**
* Parses a single V8 stack frame line.
*/
function parseStackFrameLine(line: string): StackFrame | undefined {
const match =
line.match(/^at (?:(.+?) \()?(.+?):\d+:\d+\)?$/) ??
line.match(/^at (?:(.+?) \()?(.+?)\)?$/);
if (match === null || match[2] === undefined) {
return;
}
const functionName = match[1] === undefined ? undefined : match[1].trim();
const location = normalizeLocation(match[2]);
return {
functionName,
location,
origin: getFrameOrigin(location),
};
}
/**
* Returns the Error-valued cause of an error, ignoring non-Error causes.
*/
function getCause(error: Error): Error | undefined {
if ("cause" in error && error.cause instanceof Error) {
return error.cause;
}
}
/**
* Normalizes Windows paths and file URLs enough for substring-based matchers.
*/
function normalizeLocation(location: string): string {
return location.replaceAll("\\", "/");
}
/**
* Infers who owns a stack frame from its normalized location.
*/
function getFrameOrigin(location: string): FrameOrigin {
if (
startsWithAny(location, "node:", "internal/") ||
includesAny(location, "node:internal/")
) {
return FrameOrigin.NODE_INTERNAL;
}
if (location.includes("/node_modules/")) {
if (
includesAny(
location,
"/node_modules/hardhat/",
"/node_modules/@nomicfoundation/",
)
) {
return FrameOrigin.FIRST_PARTY;
}
return FrameOrigin.THIRD_PARTY;
}
if (
startsWithAny(location, "/", "file://", "[eval]") ||
/^[A-Za-z]:\//.test(location)
) {
return FrameOrigin.USER_PROJECT;
}
return FrameOrigin.OTHER;
}
/**
* Returns true when `value` starts with any of the supplied prefixes.
*/
function startsWithAny(value: string, ...prefixes: string[]): boolean {
return prefixes.some((prefix) => value.startsWith(prefix));
}