hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
681 lines (607 loc) • 19.7 kB
text/typescript
import type {
HardhatUserConfig,
ProjectPathsUserConfig,
} from "../../types/config.js";
import type {
HardhatUserConfigValidationError,
HookManager,
} from "../../types/hooks.js";
import type { HardhatPlugin } from "../../types/plugins.js";
import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
import {
ArgumentType,
type OptionDefinition,
type PositionalArgumentDefinition,
} from "../../types/arguments.js";
import {
type EmptyTaskDefinition,
type NewTaskDefinition,
type TaskDefinition,
TaskDefinitionType,
type TaskOverrideDefinition,
} from "../../types/tasks.js";
function isValidEnumValue(
theEnum: Record<string, string>,
value: string,
): boolean {
// Enums are objects that have entries that map:
// 1) keys to values
// 2) values to keys
const key = theEnum[value];
if (key === undefined) {
return false;
}
return theEnum[key] === value;
}
/**
* Returns true if `potential` is a `TaskDefinition`.
*/
function isTaskDefinition(potential: unknown): potential is TaskDefinition {
return (
typeof potential === "object" &&
potential !== null &&
"type" in potential &&
typeof potential.type === "string" &&
isValidEnumValue(TaskDefinitionType, potential.type)
);
}
/**
* Returns true if `potential` is a `PositionalArgumentDefinition`.
*/
function isPositionalArgumentDefinition(
potential: unknown,
): potential is PositionalArgumentDefinition {
return (
typeof potential === "object" &&
potential !== null &&
"type" in potential &&
typeof potential.type === "string" &&
isValidEnumValue(ArgumentType, potential.type) &&
"isVariadic" in potential
);
}
export async function validateUserConfig(
hooks: HookManager,
config: HardhatUserConfig,
): Promise<HardhatUserConfigValidationError[]> {
const validationErrors: HardhatUserConfigValidationError[] =
collectValidationErrorsForUserConfig(config);
const results = await hooks.runParallelHandlers(
"config",
"validateUserConfig",
[config],
);
return [...validationErrors, ...results.flat(1)];
}
export function collectValidationErrorsForUserConfig(
config: HardhatUserConfig,
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
if (config.paths !== undefined) {
if (isObject(config.paths)) {
validationErrors.push(...validatePaths(config.paths));
} else {
validationErrors.push({
path: ["paths"],
message: "paths must be an object",
});
}
}
if (config.tasks !== undefined) {
if (Array.isArray(config.tasks)) {
validationErrors.push(...validateTasksConfig(config.tasks));
} else {
validationErrors.push({
path: ["tasks"],
message: "tasks must be an array",
});
}
}
if (config.plugins !== undefined) {
if (Array.isArray(config.plugins)) {
validationErrors.push(...validatePluginsConfig(config.plugins));
} else {
validationErrors.push({
path: ["plugins"],
message: "plugins must be an array",
});
}
}
return validationErrors;
}
export function validatePaths(
paths: ProjectPathsUserConfig,
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
if (paths.cache !== undefined) {
validationErrors.push(...validatePath(paths.cache, "cache"));
}
if (paths.artifacts !== undefined) {
validationErrors.push(...validatePath(paths.artifacts, "artifacts"));
}
if (paths.tests !== undefined) {
// paths.tests of type TestPathsUserConfig is not validated because it is customizable by the user
if (!isObject(paths.tests)) {
validationErrors.push(...validatePath(paths.tests, "tests"));
}
}
if (paths.sources !== undefined) {
if (Array.isArray(paths.sources)) {
for (const [index, source] of paths.sources.entries()) {
validationErrors.push(...validatePath(source, "sources", index));
}
// paths.sources of type SourcePathsUserConfig is not validated because it is customizable by the user
} else if (!isObject(paths.sources)) {
validationErrors.push(...validatePath(paths.sources, "sources"));
}
}
return validationErrors;
}
function validatePath(
filePath: unknown,
pathName: "cache" | "artifacts" | "tests" | "sources",
index?: number,
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
if (typeof filePath !== "string") {
const messagePrefix =
index !== undefined
? `paths.${pathName} at index ${index}`
: `paths.${pathName}`;
validationErrors.push({
path:
index !== undefined ? ["paths", pathName, index] : ["paths", pathName],
message: `${messagePrefix} must be a string`,
});
}
return validationErrors;
}
export function validateTasksConfig(
tasks: TaskDefinition[],
path: Array<string | number> = [],
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
for (const [index, task] of tasks.entries()) {
if (!isTaskDefinition(task)) {
validationErrors.push({
path: [...path, "tasks", index],
message: "tasks must be an array of TaskDefinitions",
});
continue;
}
switch (task.type) {
case TaskDefinitionType.EMPTY_TASK: {
validationErrors.push(
...validateEmptyTask(task, [...path, "tasks", index]),
);
break;
}
case TaskDefinitionType.NEW_TASK: {
validationErrors.push(
...validateNewTask(task, [...path, "tasks", index]),
);
break;
}
case TaskDefinitionType.TASK_OVERRIDE: {
validationErrors.push(
...validateTaskOverride(task, [...path, "tasks", index]),
);
break;
}
}
}
return validationErrors;
}
export function validateEmptyTask(
task: EmptyTaskDefinition,
path: Array<string | number>,
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
if (
!Array.isArray(task.id) ||
!task.id.every((id) => typeof id === "string")
) {
validationErrors.push({
path: [...path, "id"],
message: "task id must be an array of strings",
});
}
if (typeof task.description !== "string") {
validationErrors.push({
path: [...path, "description"],
message: "task description must be a string",
});
}
return validationErrors;
}
export function validateNewTask(
task: NewTaskDefinition,
path: Array<string | number>,
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
if (
!Array.isArray(task.id) ||
!task.id.every((id) => typeof id === "string")
) {
validationErrors.push({
path: [...path, "id"],
message: "task id must be an array of strings",
});
}
if (typeof task.description !== "string") {
validationErrors.push({
path: [...path, "description"],
message: "task description must be a string",
});
}
if (typeof task.action !== "function") {
validationErrors.push({
path: [...path, "action"],
message:
"task action must be a lazy import function returning a module with a default export",
});
}
if (isObject(task.options)) {
validationErrors.push(
...validateOptions(task.options, [...path, "options"]),
);
} else {
validationErrors.push({
path: [...path, "options"],
message: "task options must be an object",
});
}
if (Array.isArray(task.positionalArguments)) {
validationErrors.push(
...validatePositionalArguments(task.positionalArguments, path),
);
} else {
validationErrors.push({
path: [...path, "positionalArguments"],
message: "task positionalArguments must be an array",
});
}
return validationErrors;
}
export function validateTaskOverride(
task: TaskOverrideDefinition,
path: Array<string | number>,
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
if (
!Array.isArray(task.id) ||
!task.id.every((id) => typeof id === "string")
) {
validationErrors.push({
path: [...path, "id"],
message: "task id must be an array of strings",
});
}
if (task.description !== undefined && typeof task.description !== "string") {
validationErrors.push({
path: [...path, "description"],
message: "task description must be a string",
});
}
if (typeof task.action !== "function") {
validationErrors.push({
path: [...path, "action"],
message:
"task action must be a lazy import function returning a module with a default export",
});
}
if (isObject(task.options)) {
validationErrors.push(
...validateOptions(task.options, [...path, "options"]),
);
} else {
validationErrors.push({
path: [...path, "options"],
message: "task options must be an object",
});
}
return validationErrors;
}
export function validateOptions(
options: Record<string, OptionDefinition>,
path: Array<string | number>,
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
for (const [name, option] of Object.entries(options)) {
if (typeof option.name !== "string") {
validationErrors.push({
path: [...path, name, "name"],
message: "option name must be a string",
});
}
if (typeof option.description !== "string") {
validationErrors.push({
path: [...path, name, "description"],
message: "option description must be a string",
});
}
if (ArgumentType[option.type] === undefined) {
validationErrors.push({
path: [...path, name, "type"],
message: "option type must be a valid ArgumentType",
});
}
if (
option.type !== ArgumentType.STRING_WITHOUT_DEFAULT &&
option.type !== ArgumentType.FILE_WITHOUT_DEFAULT &&
option.defaultValue === undefined
) {
validationErrors.push({
path: [...path, name, "defaultValue"],
message: "option defaultValue must be defined",
});
} else {
switch (option.type) {
case ArgumentType.STRING:
case ArgumentType.FILE: {
if (typeof option.defaultValue !== "string") {
validationErrors.push({
path: [...path, name, "defaultValue"],
message: "option defaultValue must be a string",
});
}
break;
}
case ArgumentType.FILE_WITHOUT_DEFAULT:
case ArgumentType.STRING_WITHOUT_DEFAULT: {
if (
typeof option.defaultValue !== "string" &&
option.defaultValue !== undefined
) {
validationErrors.push({
path: [...path, name, "defaultValue"],
message: "option defaultValue must be a string or undefined",
});
}
break;
}
case ArgumentType.FLAG:
case ArgumentType.BOOLEAN: {
if (typeof option.defaultValue !== "boolean") {
validationErrors.push({
path: [...path, name, "defaultValue"],
message: "option defaultValue must be a boolean",
});
}
break;
}
case ArgumentType.INT:
case ArgumentType.FLOAT: {
if (typeof option.defaultValue !== "number") {
validationErrors.push({
path: [...path, name, "defaultValue"],
message: "option defaultValue must be a number",
});
}
break;
}
case ArgumentType.LEVEL:
if (
typeof option.defaultValue !== "number" ||
option.defaultValue < 0
) {
validationErrors.push({
path: [...path, name, "defaultValue"],
message: "option defaultValue must be a non-negative number",
});
}
break;
case ArgumentType.BIGINT: {
if (typeof option.defaultValue !== "bigint") {
validationErrors.push({
path: [...path, name, "defaultValue"],
message: "option defaultValue must be a bigint",
});
}
break;
}
}
}
}
return validationErrors;
}
export function validatePositionalArguments(
positionalArgs: PositionalArgumentDefinition[],
path: Array<string | number>,
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
for (const [index, arg] of positionalArgs.entries()) {
if (typeof arg.name !== "string") {
validationErrors.push({
path: [...path, "positionalArguments", index, "name"],
message: "positional argument name must be a string",
});
}
if (typeof arg.description !== "string") {
validationErrors.push({
path: [...path, "positionalArguments", index, "description"],
message: "positional argument description must be a string",
});
}
if (!isPositionalArgumentDefinition(arg)) {
validationErrors.push({
path: [...path, "positionalArguments", index, "type"],
message: "positional argument type must be a valid ArgumentType",
});
}
if (arg.defaultValue !== undefined) {
switch (arg.type) {
case ArgumentType.STRING_WITHOUT_DEFAULT:
case ArgumentType.FILE_WITHOUT_DEFAULT:
case ArgumentType.STRING:
case ArgumentType.FILE: {
if (
typeof arg.defaultValue !== "string" &&
(!Array.isArray(arg.defaultValue) ||
arg.defaultValue.some((v) => typeof v !== "string"))
) {
validationErrors.push({
path: [...path, "positionalArguments", index, "defaultValue"],
message:
"positional argument defaultValue must be a string or an array of strings",
});
}
break;
}
case ArgumentType.BOOLEAN: {
if (
typeof arg.defaultValue !== "boolean" &&
(!Array.isArray(arg.defaultValue) ||
arg.defaultValue.some((v) => typeof v !== "boolean"))
) {
validationErrors.push({
path: [...path, "positionalArguments", index, "defaultValue"],
message:
"positional argument defaultValue must be a boolean or an array of booleans",
});
}
break;
}
case ArgumentType.INT:
case ArgumentType.FLOAT: {
if (
typeof arg.defaultValue !== "number" &&
(!Array.isArray(arg.defaultValue) ||
arg.defaultValue.some((v) => typeof v !== "number"))
) {
validationErrors.push({
path: [...path, "positionalArguments", index, "defaultValue"],
message:
"positional argument defaultValue must be a number or an array of numbers",
});
}
break;
}
case ArgumentType.BIGINT: {
if (
typeof arg.defaultValue !== "bigint" &&
(!Array.isArray(arg.defaultValue) ||
arg.defaultValue.some((v) => typeof v !== "bigint"))
) {
validationErrors.push({
path: [...path, "positionalArguments", index, "defaultValue"],
message:
"positional argument defaultValue must be a bigint or an array of bigints",
});
}
break;
}
case ArgumentType.FLAG:
case ArgumentType.LEVEL:
throw new HardhatError(
HardhatError.ERRORS.CORE.INTERNAL.ASSERTION_ERROR,
{
message: `Argument type ${arg.type} cannot be used as a positional argument`,
},
);
}
}
if (typeof arg.isVariadic !== "boolean") {
validationErrors.push({
path: [...path, "positionalArguments", index, "isVariadic"],
message: "positional argument isVariadic must be a boolean",
});
} else if (arg.isVariadic === true && index !== positionalArgs.length - 1) {
validationErrors.push({
path: [...path, "positionalArguments", index, "isVariadic"],
message: "variadic positional argument must be the last one",
});
}
}
return validationErrors;
}
export function validatePluginsConfig(
plugins: HardhatPlugin[],
path: Array<string | number> = [],
): HardhatUserConfigValidationError[] {
const validationErrors: HardhatUserConfigValidationError[] = [];
for (const [index, plugin] of plugins.entries()) {
if (typeof plugin !== "object" || plugin === null) {
validationErrors.push({
path: [...path, "plugins", index],
message: "plugins must be an array of PluginDefinitions",
});
continue;
}
if (typeof plugin.id !== "string") {
validationErrors.push({
path: [...path, "plugins", index, "id"],
message: "plugin id must be a string",
});
}
if (
plugin.npmPackage !== undefined &&
typeof plugin.npmPackage !== "string" &&
plugin.npmPackage !== null
) {
validationErrors.push({
path: [...path, "plugins", index, "npmPackage"],
message: "plugin npmPackage must be a string",
});
}
if (plugin.dependencies !== undefined) {
if (typeof plugin.dependencies !== "function") {
validationErrors.push({
path: [...path, "plugins", index, "dependencies"],
message:
"plugin dependencies must be a function returning an array of functions, each importing a module with a default export",
});
}
}
if (plugin.hookHandlers !== undefined) {
if (
typeof plugin.hookHandlers === "object" &&
plugin.hookHandlers !== null
) {
for (const [hookName, handler] of Object.entries(plugin.hookHandlers)) {
if (typeof handler !== "function") {
validationErrors.push({
path: [...path, "plugins", index, "hookHandlers", hookName],
message:
"plugin hookHandlers must be a lazy import function returning a module with a default export",
});
}
}
} else {
validationErrors.push({
path: [...path, "plugins", index, "hookHandlers"],
message: "plugin hookHandlers must be an object",
});
}
}
if (plugin.globalOptions !== undefined) {
if (Array.isArray(plugin.globalOptions)) {
validationErrors.push(
...validateOptions(
Object.fromEntries(plugin.globalOptions.entries()),
[...path, "plugins", index, "globalOptions"],
),
);
} else {
validationErrors.push({
path: [...path, "plugins", index, "globalOptions"],
message: "plugin globalOptions must be an array",
});
}
}
if (plugin.tasks !== undefined) {
if (Array.isArray(plugin.tasks)) {
validationErrors.push(
...validateTasksConfig(plugin.tasks, [...path, "plugins", index]),
);
} else {
validationErrors.push({
path: [...path, "plugins", index, "tasks"],
message: "plugin tasks must be an array",
});
}
}
}
return validationErrors;
}