UNPKG

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
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; } }