hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
637 lines (582 loc) • 19.2 kB
text/typescript
import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import {
DirectoryNotEmptyError,
FileAlreadyExistsError,
FileNotFoundError,
FileSystemAccessError,
InvalidFileFormatError,
IsDirectoryError,
NotADirectoryError,
} from "@nomicfoundation/hardhat-utils/fs";
import {
PackageJsonNotFoundError,
PackageJsonReadError,
} from "@nomicfoundation/hardhat-utils/package";
import {
DispatcherError,
RequestError,
ResponseStatusCodeError,
} from "@nomicfoundation/hardhat-utils/request";
import {
SubprocessFileNotFoundError,
SubprocessPathIsDirectoryError,
} from "@nomicfoundation/hardhat-utils/subprocess";
import {
EdrProviderStackTraceGenerationError,
SolidityTestStackTraceGenerationError,
} from "../../../builtin-plugins/network-manager/edr/stack-traces/stack-trace-generation-errors.js";
import {
ProviderError,
UnknownError,
} from "../../../builtin-plugins/network-manager/provider-errors.js";
import { UsingHardhat2PluginError } from "../../../using-hardhat2-plugin-errors.js";
import {
getHookExecutionFrame,
getTaskExecutionFrame,
isConfigLoadingBoundaryFrame,
isConsoleEvaluationBoundaryFrame,
isEdrFrame,
isFirstPartyPluginFrame,
isHookHandlerBoundaryFrame,
isMochaTestExecutionBoundaryFrame,
isNodeTestExecutionBoundaryFrame,
isRunningInsideHardhatMonorepo,
isScriptExecutionBoundaryFrame,
isTaskActionBoundaryFrame,
isThirdPartyFrame,
} from "./codebase-dependent-helpers.js";
import {
type ErrorContext,
type StackFrame,
FrameOrigin,
createErrorContext,
getNodeErrorCode,
hasErrorClassName,
includesAny,
} from "./helpers.js";
/**
* Classifies the error based on a set of heuristics.
*
* This classification is later used to select different criteria to decide if
* the error should be reported or not, and in some cases, how to display it in
* the CLI.
*
* @param error The error to classify.
* @param ignoreDevelopmentTimeFilter If true, the classifier will ignore the
* development-time filter, which is used to exclude errors that happen during
* development of Hardhat itself. This is only meant to be used for testing.
* @returns The error category.
*/
export function classifyError(
error: Error,
ignoreDevelopmentTimeFilter = false,
): ErrorCategory {
const context = createErrorContext(error);
for (const matcher of ERROR_CATEGORY_MATCHERS) {
if (ignoreDevelopmentTimeFilter && matcher === isDevelopmentTimeError) {
continue;
}
const category = matcher(context);
if (category !== undefined) {
return category;
}
}
return ErrorCategory.UNEXPECTED_ERROR;
}
export enum ErrorCategory {
CJS_TO_ESM_MIGRATION_ERROR = "CJS_TO_ESM_MIGRATION_ERROR",
HH2_TO_HH3_MIGRATION_ERROR = "HH2_TO_HH3_MIGRATION_ERROR",
TYPESCRIPT_SUPPORT_ERROR = "TYPESCRIPT_SUPPORT_ERROR",
DEVELOPMENT_TIME_ERROR = "DEVELOPMENT_TIME_ERROR",
HARDHAT_ERROR = "HARDHAT_ERROR",
CONFIG_LOADING_ERROR = "CONFIG_LOADING_ERROR",
CONSOLE_EVALUATION_ERROR = "CONSOLE_EVALUATION_ERROR",
SCRIPT_EXECUTION_ERROR = "SCRIPT_EXECUTION_ERROR",
NODE_TEST_EXECUTION_ERROR = "NODE_TEST_EXECUTION_ERROR",
MOCHA_TEST_EXECUTION_ERROR = "MOCHA_TEST_EXECUTION_ERROR",
TASK_ACTION_ERROR = "TASK_ACTION_ERROR",
PLUGIN_TASK_ACTION_ERROR = "PLUGIN_TASK_ACTION_ERROR",
USER_TASK_ACTION_ERROR = "USER_TASK_ACTION_ERROR",
PLUGIN_HOOK_HANDLER_ERROR = "PLUGIN_HOOK_HANDLER_ERROR",
PROVIDER_INTERACTION_ERROR = "PROVIDER_INTERACTION_ERROR",
EDR_ERROR = "EDR_ERROR",
NETWORK_INTERACTION_ERROR = "NETWORK_INTERACTION_ERROR",
RUNTIME_ENVIRONMENT_ERROR = "RUNTIME_ENVIRONMENT_ERROR",
FILESYSTEM_INTERACTION_ERROR = "FILESYSTEM_INTERACTION_ERROR",
UNEXPECTED_ERROR = "UNEXPECTED_ERROR",
}
type ErrorCategoryMatcher = (
context: ErrorContext,
) => ErrorCategory | undefined;
export type UserCodeBoundaryCategory =
| ErrorCategory.CONFIG_LOADING_ERROR
| ErrorCategory.CONSOLE_EVALUATION_ERROR
| ErrorCategory.SCRIPT_EXECUTION_ERROR
| ErrorCategory.NODE_TEST_EXECUTION_ERROR
| ErrorCategory.MOCHA_TEST_EXECUTION_ERROR
| ErrorCategory.PLUGIN_TASK_ACTION_ERROR
| ErrorCategory.USER_TASK_ACTION_ERROR
| ErrorCategory.PLUGIN_HOOK_HANDLER_ERROR;
export const USER_CODE_BOUNDARY_FRAME_MATCHERS: Record<
UserCodeBoundaryCategory,
(frame: StackFrame) => boolean
> = {
[ErrorCategory.CONFIG_LOADING_ERROR]: isConfigLoadingBoundaryFrame,
[ErrorCategory.CONSOLE_EVALUATION_ERROR]: isConsoleEvaluationBoundaryFrame,
[ErrorCategory.SCRIPT_EXECUTION_ERROR]: isScriptExecutionBoundaryFrame,
[ErrorCategory.NODE_TEST_EXECUTION_ERROR]: isNodeTestExecutionBoundaryFrame,
[ErrorCategory.MOCHA_TEST_EXECUTION_ERROR]: isMochaTestExecutionBoundaryFrame,
[ErrorCategory.PLUGIN_TASK_ACTION_ERROR]: isTaskActionBoundaryFrame,
[ErrorCategory.USER_TASK_ACTION_ERROR]: isTaskActionBoundaryFrame,
[ErrorCategory.PLUGIN_HOOK_HANDLER_ERROR]: isHookHandlerBoundaryFrame,
};
// These are categories that only need the boundary check for classification
const BOUNDARY_ONLY_ERROR_CATEGORIES = [
ErrorCategory.CONFIG_LOADING_ERROR,
ErrorCategory.SCRIPT_EXECUTION_ERROR,
ErrorCategory.NODE_TEST_EXECUTION_ERROR,
ErrorCategory.MOCHA_TEST_EXECUTION_ERROR,
ErrorCategory.CONSOLE_EVALUATION_ERROR,
] as const;
// IMPORTANT: The order here matters, as the first matcher that returns a
// category wins
const ERROR_CATEGORY_MATCHERS: ErrorCategoryMatcher[] = [
isDevelopmentTimeError,
isESMMigrationError,
isHH3MigrationError,
isTypescriptSupportError,
isHardhatError,
isProviderInteractionError,
isEdrError,
isNetworkInteractionError,
isRuntimeEnvironmentError,
isFilesystemInteractionError,
isTaskActionError,
isBoundaryOnlyError,
isPluginTaskActionError,
isUserTaskActionError,
isPluginHookHandlerError,
];
const ESM_MIGRATION_MARKERS = [
"is not defined in es module scope",
"cannot use import statement outside a module",
];
/**
* Classifies common CommonJS/ESM migration failures by matching standard Node
* runtime markers.
*/
function isESMMigrationError(
context: ErrorContext,
): ErrorCategory.CJS_TO_ESM_MIGRATION_ERROR | undefined {
for (const lowercaseMessage of context.lowercaseMessageByError.values()) {
if (
includesAny(lowercaseMessage, ...ESM_MIGRATION_MARKERS) ||
/require\(\) of es module/.test(lowercaseMessage)
) {
return ErrorCategory.CJS_TO_ESM_MIGRATION_ERROR;
}
}
}
const HH3_MIGRATION_MARKERS = [
"class extends value undefined is not a constructor or null",
"the requested module 'hardhat' does not provide an export named",
'the requested module "hardhat" does not provide an export named',
"the requested module 'hardhat/config' does not provide an export named",
'the requested module "hardhat/config" does not provide an export named',
"the requested module 'hardhat/plugins' does not provide an export named",
'the requested module "hardhat/plugins" does not provide an export named',
"the requested module 'hardhat/builtin-tasks/task-names' does not provide an export named",
'the requested module "hardhat/builtin-tasks/task-names" does not provide an export named',
"the requested module 'hardhat/types/runtime' does not provide an export named",
'the requested module "hardhat/types/runtime" does not provide an export named',
];
const HH2_PLUGIN_ERROR_MARKER =
"you are trying to use a hardhat 2 plugin in a hardhat 3 project";
/**
* Classifies Hardhat 2 to Hardhat 3 migration failures by checking for known
* migration error types and message patterns anywhere in the cause chain.
*
* This is temporary, and will be removed once the HH2 to HH3 migration by the
* community is in a solid state. The matcher is broad, but right now mostly
* correct.
*/
function isHH3MigrationError(
context: ErrorContext,
): ErrorCategory.HH2_TO_HH3_MIGRATION_ERROR | undefined {
if (
context.errorChain.some(
(candidate) =>
hasErrorClassName(candidate, UsingHardhat2PluginError) ||
(context.lowercaseMessageByError.get(candidate) ?? "").includes(
HH2_PLUGIN_ERROR_MARKER,
),
)
) {
return ErrorCategory.HH2_TO_HH3_MIGRATION_ERROR;
}
for (const lowercaseMessage of context.lowercaseMessageByError.values()) {
if (includesAny(lowercaseMessage, ...HH3_MIGRATION_MARKERS)) {
return ErrorCategory.HH2_TO_HH3_MIGRATION_ERROR;
}
}
if (
context.errorChain.some(
(candidate) => candidate.stack?.includes("@nomiclabs") === true,
)
) {
return ErrorCategory.HH2_TO_HH3_MIGRATION_ERROR;
}
}
/**
* If Hardhat is being run from the monorepo, we don't report the error.
*/
function isDevelopmentTimeError(
_context: ErrorContext,
): ErrorCategory.DEVELOPMENT_TIME_ERROR | undefined {
if (isRunningInsideHardhatMonorepo()) {
return ErrorCategory.DEVELOPMENT_TIME_ERROR;
}
return undefined;
}
const TYPESCRIPT_SUPPORT_ERROR_CODES = new Set([
"ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX",
"ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING",
"ERR_NO_TYPESCRIPT",
"ERR_UNKNOWN_FILE_EXTENSION",
]);
/**
* Classifies Node.js TypeScript support failures by matching Node's stable
* error codes. ERR_UNKNOWN_FILE_EXTENSION also happens for non-TypeScript
* extensions, so require a TypeScript extension in that error's message.
*/
function isTypescriptSupportError(
context: ErrorContext,
): ErrorCategory.TYPESCRIPT_SUPPORT_ERROR | undefined {
if (
context.errorChain.some((candidate) => {
const code = getTypescriptSupportErrorCode(candidate);
if (code === undefined) {
return false;
}
return (
code !== "ERR_UNKNOWN_FILE_EXTENSION" ||
includesAny(
context.lowercaseMessageByError.get(candidate),
".ts",
".mts",
".cts",
)
);
})
) {
return ErrorCategory.TYPESCRIPT_SUPPORT_ERROR;
}
}
/**
* Classifies top-level HardhatError instances that were not captured by a more
* specific matcher earlier in the chain.
*/
function isHardhatError(
context: ErrorContext,
): ErrorCategory.HARDHAT_ERROR | undefined {
if (HardhatError.isHardhatError(context.error)) {
return ErrorCategory.HARDHAT_ERROR;
}
}
function isBoundaryOnlyError(
context: ErrorContext,
): (typeof BOUNDARY_ONLY_ERROR_CATEGORIES)[number] | undefined {
for (const category of BOUNDARY_ONLY_ERROR_CATEGORIES) {
const boundaryMatcher = USER_CODE_BOUNDARY_FRAME_MATCHERS[category];
if (context.allStackFrames.some(boundaryMatcher)) {
return category;
}
}
}
/**
* Classifies task-action failures routed through ResolvedTask when the nearest
* task-action frame belongs to first-party code.
*/
function isTaskActionError(
context: ErrorContext,
): ErrorCategory.TASK_ACTION_ERROR | undefined {
const taskActionFrame = getTaskExecutionFrame(context.allStackFrames);
if (
taskActionFrame !== undefined &&
isFirstPartyPluginFrame(taskActionFrame.location)
) {
return ErrorCategory.TASK_ACTION_ERROR;
}
}
/**
* Classifies task-action failures routed through ResolvedTask when the nearest
* task-action frame belongs to a third-party plugin.
*/
function isPluginTaskActionError(
context: ErrorContext,
): ErrorCategory.PLUGIN_TASK_ACTION_ERROR | undefined {
const taskActionFrame = getTaskExecutionFrame(context.allStackFrames);
if (
taskActionFrame !== undefined &&
isThirdPartyFrame(taskActionFrame.location)
) {
return ErrorCategory.PLUGIN_TASK_ACTION_ERROR;
}
}
/**
* Classifies task-action failures routed through ResolvedTask when the nearest
* task-action frame belongs to user project code.
*/
function isUserTaskActionError(
context: ErrorContext,
): ErrorCategory.USER_TASK_ACTION_ERROR | undefined {
const taskActionFrame = getTaskExecutionFrame(context.allStackFrames);
if (
taskActionFrame !== undefined &&
taskActionFrame.origin === FrameOrigin.USER_PROJECT
) {
return ErrorCategory.USER_TASK_ACTION_ERROR;
}
}
/**
* Classifies hook execution failures routed through HookManager when the
* nearest hook-handler frame belongs to a third-party plugin.
*/
function isPluginHookHandlerError(
context: ErrorContext,
): ErrorCategory.PLUGIN_HOOK_HANDLER_ERROR | undefined {
const hookExecutionFrame = getHookExecutionFrame(context.allStackFrames);
if (
hookExecutionFrame !== undefined &&
isThirdPartyFrame(hookExecutionFrame.location)
) {
return ErrorCategory.PLUGIN_HOOK_HANDLER_ERROR;
}
}
const PROVIDER_INTERACTION_ERROR_WRAPPED_IN_UNKNOWN_EDR_ERROR_MARKERS = [
"unauthorized",
"rate limit",
"too many requests",
"historical state unavailable",
];
/**
* Classifies provider-facing failures, including Solidity errors, expected
* provider errors, and selected UnknownError cases with provider-like causes.
*/
function isProviderInteractionError(
context: ErrorContext,
): ErrorCategory.PROVIDER_INTERACTION_ERROR | undefined {
if (
context.errorChain.some(
(candidate) =>
candidate.name === "SolidityError" ||
(ProviderError.isProviderError(candidate) &&
isUnknownEdrError(candidate, context) === false),
)
) {
return ErrorCategory.PROVIDER_INTERACTION_ERROR;
}
if (
isUnknownEdrError(context.errorChain[0], context) &&
context.errorChain[1] !== undefined &&
includesAny(
context.lowercaseMessageByError.get(context.errorChain[1]),
...PROVIDER_INTERACTION_ERROR_WRAPPED_IN_UNKNOWN_EDR_ERROR_MARKERS,
)
) {
return ErrorCategory.PROVIDER_INTERACTION_ERROR;
}
}
/**
* Classifies EDR-specific failures, including stack-trace generation errors
* and remaining UnknownError cases from the provider layer.
*/
function isEdrError(
context: ErrorContext,
): ErrorCategory.EDR_ERROR | undefined {
if (
context.errorChain.some(
(candidate) =>
hasErrorClassName(candidate, EdrProviderStackTraceGenerationError) ||
hasErrorClassName(candidate, SolidityTestStackTraceGenerationError) ||
isUnknownEdrError(candidate, context),
)
) {
return ErrorCategory.EDR_ERROR;
}
}
/**
* Classifies network-related failures by collapsing request setup, request
* transport, response status, and telemetry transport errors into one bucket.
*/
function isNetworkInteractionError(
context: ErrorContext,
): ErrorCategory.NETWORK_INTERACTION_ERROR | undefined {
if (
context.errorChain.some((candidate) =>
hasErrorClassName(candidate, ResponseStatusCodeError),
) ||
context.errorChain.some((candidate) =>
hasErrorClassName(candidate, DispatcherError),
) ||
context.errorChain.some((candidate) =>
hasErrorClassName(candidate, RequestError),
) ||
(context.lowercaseMessageByError.get(context.error) ?? "").includes(
"fetch failed",
)
) {
return ErrorCategory.NETWORK_INTERACTION_ERROR;
}
}
/**
* Classifies runtime-environment incompatibilities by matching a small set of
* capability-related error messages.
*/
function isRuntimeEnvironmentError(
context: ErrorContext,
): ErrorCategory.RUNTIME_ENVIRONMENT_ERROR | undefined {
if (
Array.from(context.lowercaseMessageByError.values()).some((message) =>
includesAny(
message,
"toreversed is not a function",
"flatmap is not a function",
"crypto is not defined",
),
)
) {
return ErrorCategory.RUNTIME_ENVIRONMENT_ERROR;
}
}
/**
* Classifies project-data and filesystem-related failures by matching a known
* filesystem/project-data error type or a raw Node.js filesystem error code
* anywhere in the cause chain.
*/
function isFilesystemInteractionError(
context: ErrorContext,
): ErrorCategory.FILESYSTEM_INTERACTION_ERROR | undefined {
if (
context.errorChain.some(
(candidate) =>
isKnownFilesystemOrProjectDataError(candidate) ||
isNodeFilesystemError(candidate),
)
) {
return ErrorCategory.FILESYSTEM_INTERACTION_ERROR;
}
}
function isUnknownEdrError(error: Error, context: ErrorContext): boolean {
const errorIndex = context.errorChain.indexOf(error);
assertHardhatInvariant(
errorIndex !== -1,
"isUnknownEdrError must be called with an error from the error chain",
);
return (
ProviderError.isProviderError(error) &&
(error.code === UnknownError.CODE || error.name === "UnknownError") &&
context.errorChain
.slice(errorIndex)
.some((candidate) =>
(context.stackFramesByError.get(candidate) ?? []).some(isEdrFrame),
)
);
}
// This list should be kept up to date with hardhat-utils/fs errors
const HARDHAT_UTILS_FILESYSTEM_ERROR_CLASSES = [
PackageJsonReadError,
PackageJsonNotFoundError,
InvalidFileFormatError,
FileNotFoundError,
IsDirectoryError,
NotADirectoryError,
FileAlreadyExistsError,
DirectoryNotEmptyError,
] as const;
// This list should be kept up to date with hardhat-utils/subprocess errors
const HARDHAT_UTILS_SUBPROCESS_ERROR_CLASSES = [
SubprocessFileNotFoundError,
SubprocessPathIsDirectoryError,
] as const;
const NODE_FILESYSTEM_ERROR_CODES = new Set([
"EACCES",
"EAGAIN",
"EBADF",
"EBUSY",
"EEXIST",
"EFBIG",
"EINTR",
"EINVAL",
"EIO",
"EISDIR",
"ELOOP",
"EMFILE",
"ENAMETOOLONG",
"ENFILE",
"ENODEV",
"ENOENT",
"ENOSPC",
"ENOTDIR",
"ENOTEMPTY",
"ENOTSUP",
"ENXIO",
"EOPNOTSUPP",
"EOVERFLOW",
"EPERM",
"EROFS",
"ESPIPE",
"ETXTBSY",
"EXDEV",
]);
/**
* Returns `true` for any of the filesystem/project-data error classes the
* classifier knows about. This is the gate for the FILESYSTEM_INTERACTION_ERROR
* category.
*/
export function isKnownFilesystemOrProjectDataError(error: Error): boolean {
return (
isHardhatUtilsFilesystemError(error) ||
isSubprocessFilesystemError(error) ||
hasErrorClassName(error, FileSystemAccessError)
);
}
/**
* Returns `true` for filesystem errors that callers commonly expect to surface
* during normal operation (e.g. missing files, format errors). Used by both
* the classifier (as part of the filesystem-interaction gate) and the filter
* (to drop these errors from reporting).
*/
export function isHardhatUtilsFilesystemError(error: Error): boolean {
return HARDHAT_UTILS_FILESYSTEM_ERROR_CLASSES.some((cls) =>
hasErrorClassName(error, cls),
);
}
/**
* Returns `true` for filesystem errors raised by the subprocess-spawning
* helpers. These are unexpected enough to be worth reporting on their own.
*/
export function isSubprocessFilesystemError(error: Error): boolean {
return HARDHAT_UTILS_SUBPROCESS_ERROR_CLASSES.some((cls) =>
hasErrorClassName(error, cls),
);
}
/**
* Returns `true` for the node errors with fs related codes.
*/
function isNodeFilesystemError(error: Error): boolean {
const code = getNodeErrorCode(error);
return code !== undefined && NODE_FILESYSTEM_ERROR_CODES.has(code);
}
function getTypescriptSupportErrorCode(error: Error): string | undefined {
if (
"code" in error &&
typeof error.code === "string" &&
TYPESCRIPT_SUPPORT_ERROR_CODES.has(error.code)
) {
return error.code;
}
}