hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
315 lines (276 loc) • 9.29 kB
text/typescript
import type { PositionalArgumentDefinition } from "../../../types/arguments.js";
import type { GlobalOptionDefinitions } from "../../../types/global-options.js";
import type { HardhatRuntimeEnvironment } from "../../../types/hre.js";
import type {
Task,
TaskDefinition,
TaskManager,
NewTaskDefinition,
TaskOverrideDefinition,
} from "../../../types/tasks.js";
import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import { TaskDefinitionType } from "../../../types/tasks.js";
import { ResolvedTask } from "./resolved-task.js";
import { formatTaskId, getActorFragment } from "./utils.js";
import {
validateId,
validateOption,
validatePositionalArgument,
} from "./validations.js";
export class TaskManagerImplementation implements TaskManager {
readonly #hre: HardhatRuntimeEnvironment;
readonly #rootTasks = new Map<string, Task>();
constructor(
hre: HardhatRuntimeEnvironment,
globalOptionDefinitions: GlobalOptionDefinitions,
) {
this.#hre = hre;
// reduce plugin tasks
for (const plugin of this.#hre.config.plugins) {
if (plugin.tasks === undefined) {
continue;
}
for (const taskDefinition of plugin.tasks) {
this.#validateTaskDefinition(taskDefinition);
this.#reduceTaskDefinition(
globalOptionDefinitions,
taskDefinition,
plugin.id,
);
}
}
// reduce global user defined tasks
for (const taskDefinition of this.#hre.config.tasks) {
this.#validateTaskDefinition(taskDefinition);
this.#reduceTaskDefinition(globalOptionDefinitions, taskDefinition);
}
}
public get rootTasks(): Map<string, Task> {
return this.#rootTasks;
}
public getTask(taskId: string | string[]): Task {
taskId = Array.isArray(taskId) ? taskId : [taskId];
if (taskId.length === 0) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.TASK_NOT_FOUND,
{
task: formatTaskId(taskId),
},
);
}
let tasks = this.#rootTasks;
let task: Task | undefined;
for (let i = 0; i < taskId.length; i++) {
const idFragment = taskId[i];
const currentTask = tasks.get(idFragment);
if (currentTask === undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.TASK_NOT_FOUND,
{
task: formatTaskId(taskId.slice(0, i + 1)),
},
);
}
task = currentTask;
tasks = task.subtasks;
}
assertHardhatInvariant(
task !== undefined,
"Task is undefined despite it being always set by a non-empty loop",
);
return task;
}
#insertTask(taskId: string[], task: Task, pluginId?: string) {
if (taskId.length === 0) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.EMPTY_TASK_ID,
);
}
// Traverse all the parent tasks to check that they exist
let tasks = this.#rootTasks;
for (let i = 0; i < taskId.length - 1; i++) {
const idFragment = taskId[i];
const currentTask = tasks.get(idFragment);
if (currentTask === undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.SUBTASK_WITHOUT_PARENT,
{
task: formatTaskId(taskId.slice(0, i + 1)),
subtask: formatTaskId(taskId),
},
);
}
tasks = currentTask.subtasks;
}
// Check that the task doesn't already exist
const lastIdFragment = taskId[taskId.length - 1];
const existingTask = tasks.get(lastIdFragment);
if (existingTask !== undefined) {
const exPluginId = existingTask.pluginId;
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.TASK_ALREADY_DEFINED,
{
actorFragment: getActorFragment(pluginId),
task: formatTaskId(taskId),
definedByFragment:
exPluginId !== undefined ? ` by plugin ${exPluginId}` : "",
},
);
}
// Insert the task
tasks.set(lastIdFragment, task);
}
#reduceTaskDefinition(
globalOptionDefinitions: GlobalOptionDefinitions,
taskDefinition: TaskDefinition,
pluginId?: string,
) {
switch (taskDefinition.type) {
case TaskDefinitionType.EMPTY_TASK: {
const task = ResolvedTask.createEmptyTask(
this.#hre,
taskDefinition.id,
taskDefinition.description,
pluginId,
);
this.#insertTask(taskDefinition.id, task, pluginId);
break;
}
case TaskDefinitionType.NEW_TASK: {
this.#validateClashesWithGlobalOptions(
globalOptionDefinitions,
taskDefinition,
pluginId,
);
const task = ResolvedTask.createNewTask(
this.#hre,
taskDefinition.id,
taskDefinition.description,
taskDefinition.action,
taskDefinition.options,
taskDefinition.positionalArguments,
pluginId,
);
this.#insertTask(taskDefinition.id, task, pluginId);
break;
}
case TaskDefinitionType.TASK_OVERRIDE: {
this.#validateClashesWithGlobalOptions(
globalOptionDefinitions,
taskDefinition,
pluginId,
);
this.#processTaskOverride(taskDefinition, pluginId);
break;
}
}
}
#validateClashesWithGlobalOptions(
globalOptionDefinitions: GlobalOptionDefinitions,
taskDefinition: NewTaskDefinition | TaskOverrideDefinition,
pluginId?: string,
) {
const optionNames = Object.keys(taskDefinition.options);
const positionalArgNames =
"positionalArguments" in taskDefinition
? taskDefinition.positionalArguments.map(({ name }) => name)
: [];
[...optionNames, ...positionalArgNames].forEach((argName) => {
const globalOptionEntry = globalOptionDefinitions.get(argName);
if (globalOptionEntry !== undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.TASK_OPTION_ALREADY_DEFINED,
{
actorFragment: getActorFragment(pluginId),
task: formatTaskId(taskDefinition.id),
option: argName,
globalOptionPluginId: globalOptionEntry.pluginId,
},
);
}
});
const globalOptionDefinitionsByShortName: GlobalOptionDefinitions =
new Map();
for (const globalOptionEntry of globalOptionDefinitions.values()) {
if (globalOptionEntry.option.shortName !== undefined) {
globalOptionDefinitionsByShortName.set(
globalOptionEntry.option.shortName,
globalOptionEntry,
);
}
}
const optionShortNames = Object.values(taskDefinition.options)
.map((option) => option.shortName)
.filter((shortName) => shortName !== undefined);
optionShortNames.forEach((argName) => {
const globalOptionEntry = globalOptionDefinitionsByShortName.get(argName);
if (globalOptionEntry !== undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.TASK_OPTION_ALREADY_DEFINED,
{
actorFragment: getActorFragment(pluginId),
task: formatTaskId(taskDefinition.id),
option: argName,
globalOptionPluginId: globalOptionEntry.pluginId,
},
);
}
});
}
#processTaskOverride(
taskDefinition: TaskOverrideDefinition,
pluginId?: string,
) {
const task = this.getTask(taskDefinition.id);
for (const [optionName, optionValue] of Object.entries(
taskDefinition.options,
)) {
const hasArgument =
task.options.has(optionName) ||
task.positionalArguments.some((p) => p.name === optionName);
if (hasArgument) {
throw new HardhatError(
HardhatError.ERRORS.CORE.TASK_DEFINITIONS.TASK_OVERRIDE_OPTION_ALREADY_DEFINED,
{
actorFragment: getActorFragment(pluginId),
option: optionName,
task: formatTaskId(taskDefinition.id),
},
);
}
task.options.set(optionName, optionValue);
}
if (taskDefinition.description !== undefined) {
task.description = taskDefinition.description;
}
task.actions.push({ pluginId, action: taskDefinition.action });
}
#validateTaskDefinition(taskDefinition: TaskDefinition): void {
validateId(taskDefinition.id);
// Empty tasks don't have actions, options, or positional arguments
if (taskDefinition.type === TaskDefinitionType.EMPTY_TASK) {
return;
}
const usedNames = new Set<string>();
Object.values(taskDefinition.options).forEach((optionDefinition) =>
validateOption(optionDefinition, usedNames, taskDefinition.id),
);
// Override tasks don't have positional arguments
if (taskDefinition.type === TaskDefinitionType.TASK_OVERRIDE) {
return;
}
let lastArg: PositionalArgumentDefinition;
taskDefinition.positionalArguments.forEach((posArgDefinition) => {
validatePositionalArgument(
posArgDefinition,
usedNames,
taskDefinition.id,
lastArg,
);
lastArg = posArgDefinition;
});
}
}