Skip to content

Commit 16a860c

Browse files
authored
Merge pull request #5344 from NomicFoundation/tasks-run
Task run implementation
2 parents b2ff3e6 + 3aabb01 commit 16a860c

File tree

11 files changed

+939
-37
lines changed

11 files changed

+939
-37
lines changed

v-next/core/src/internal/tasks/builders.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,12 @@ export class NewTaskDefinitionBuilderImplementation
101101
!isParameterValueValid(parameterType, defaultValue)
102102
) {
103103
throw new HardhatError(
104-
HardhatError.ERRORS.ARGUMENTS.INVALID_VALUE_FOR_TYPE,
104+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
105105
{
106106
value: defaultValue,
107107
name: "defaultValue",
108108
type: parameterType,
109+
task: formatTaskId(this.#id),
109110
},
110111
);
111112
}
@@ -225,11 +226,12 @@ export class NewTaskDefinitionBuilderImplementation
225226
if (defaultValue !== undefined) {
226227
if (!isParameterValueValid(parameterType, defaultValue, isVariadic)) {
227228
throw new HardhatError(
228-
HardhatError.ERRORS.ARGUMENTS.INVALID_VALUE_FOR_TYPE,
229+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
229230
{
230231
value: defaultValue,
231232
name: "defaultValue",
232233
type: parameterType,
234+
task: formatTaskId(this.#id),
233235
},
234236
);
235237
}
@@ -349,11 +351,12 @@ export class TaskOverrideDefinitionBuilderImplementation
349351
!isParameterValueValid(parameterType, defaultValue)
350352
) {
351353
throw new HardhatError(
352-
HardhatError.ERRORS.ARGUMENTS.INVALID_VALUE_FOR_TYPE,
354+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
353355
{
354356
value: defaultValue,
355357
name: "defaultValue",
356358
type: parameterType,
359+
task: formatTaskId(this.#id),
357360
},
358361
);
359362
}

v-next/core/src/internal/tasks/resolved-task.ts

+193-16
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
import { HardhatRuntimeEnvironment } from "../../types/hre.js";
2-
import {
1+
import type { ParameterValue } from "../../types/common.js";
2+
import type { HardhatRuntimeEnvironment } from "../../types/hre.js";
3+
import type {
34
NamedTaskParameter,
45
NewTaskActionFunction,
56
PositionalTaskParameter,
67
Task,
78
TaskActions,
89
TaskArguments,
10+
TaskOverrideActionFunction,
11+
TaskParameter,
912
} from "../../types/tasks.js";
1013

14+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
15+
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
16+
17+
import { isParameterValueValid } from "../../types/common.js";
18+
1119
import { formatTaskId } from "./utils.js";
1220

1321
export class ResolvedTask implements Task {
@@ -69,24 +77,193 @@ export class ResolvedTask implements Task {
6977
return this.actions.length === 1 && this.actions[0].action === undefined;
7078
}
7179

80+
/**
81+
* This method runs the task with the given arguments.
82+
* It validates the arguments, resolves the file actions, and runs the task
83+
* actions by calling them in order.
84+
*
85+
* @param taskArguments The arguments to run the task with.
86+
* @returns The result of running the task.
87+
* @throws HardhatError if the task is empty, a required parameter is missing,
88+
* a parameter has an invalid type, or the file actions can't be resolved.
89+
*/
7290
public async run(taskArguments: TaskArguments): Promise<any> {
73-
// TODO: Run the task
74-
// - Validate the argument types
75-
// - Validate that there are no missing required arguments
76-
// - Resolve defaults for optional arguments
77-
// - Run the tasks actions with a chain of `runSuper`s
78-
console.log(`Running task "${formatTaskId(this.id)}"`);
79-
for (const action of this.actions) {
80-
if (action.pluginId !== undefined) {
81-
console.log(
82-
` Running action from plugin ${action.pluginId}: ${action.action?.toString()}`,
91+
if (this.isEmpty) {
92+
throw new HardhatError(HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK, {
93+
task: formatTaskId(this.id),
94+
});
95+
}
96+
97+
// Normalize parameters into a single iterable
98+
const allParameters: TaskParameter[] = [
99+
...this.namedParameters.values(),
100+
...this.positionalParameters,
101+
];
102+
103+
const providedArgumentNames = new Set(Object.keys(taskArguments));
104+
for (const parameter of allParameters) {
105+
const value = taskArguments[parameter.name];
106+
107+
this.#validateRequiredParameter(parameter, value);
108+
this.#validateParameterType(parameter, value);
109+
110+
// resolve defaults for optional parameters
111+
if (value === undefined && parameter.defaultValue !== undefined) {
112+
taskArguments[parameter.name] = parameter.defaultValue;
113+
}
114+
115+
// Remove processed parameter from the set
116+
providedArgumentNames.delete(parameter.name);
117+
}
118+
119+
// At this point, the set should be empty as all the task parameters have
120+
// been processed. If there are any extra parameters, an error is thrown
121+
this.#validateExtraArguments(providedArgumentNames);
122+
123+
const next = async (
124+
nextTaskArguments: TaskArguments,
125+
currentIndex = this.actions.length - 1,
126+
): Promise<any> => {
127+
// The first action may be empty if the task was originally an empty task
128+
const currentAction = this.actions[currentIndex].action ?? (() => {});
129+
130+
const actionFn =
131+
typeof currentAction === "function"
132+
? currentAction
133+
: await this.#resolveFileAction(currentAction, this.id);
134+
135+
if (currentIndex === 0) {
136+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions --
137+
We know that the first action in the array is a NewTaskActionFunction */
138+
return (actionFn as NewTaskActionFunction)(
139+
nextTaskArguments,
140+
this.#hre,
83141
);
84-
} else {
85-
console.log(` Running action: ${action.action?.toString()}`);
86142
}
143+
144+
return actionFn(
145+
nextTaskArguments,
146+
this.#hre,
147+
async (newTaskArguments: TaskArguments) => {
148+
return next(newTaskArguments, currentIndex - 1);
149+
},
150+
);
151+
};
152+
153+
return next(taskArguments);
154+
}
155+
156+
/**
157+
* Validates that a required parameter has a value. A parameter is required if
158+
* it doesn't have a default value.
159+
*
160+
* @throws HardhatError if the parameter is required and doesn't have a value.
161+
*/
162+
#validateRequiredParameter(
163+
parameter: TaskParameter,
164+
value: ParameterValue | ParameterValue[],
165+
) {
166+
if (parameter.defaultValue === undefined && value === undefined) {
167+
throw new HardhatError(
168+
HardhatError.ERRORS.TASK_DEFINITIONS.MISSING_VALUE_FOR_PARAMETER,
169+
{
170+
parameter: parameter.name,
171+
task: formatTaskId(this.id),
172+
},
173+
);
174+
}
175+
}
176+
177+
/**
178+
* Validates that a parameter has the correct type. If the parameter is optional
179+
* and doesn't have a value, the type is not validated as it will be resolved
180+
* to the default value.
181+
*
182+
* @throws HardhatError if the parameter has an invalid type.
183+
*/
184+
#validateParameterType(
185+
parameter: TaskParameter,
186+
value: ParameterValue | ParameterValue[],
187+
) {
188+
// skip type validation for optional parameters with undefined value
189+
if (value === undefined && parameter.defaultValue !== undefined) {
190+
return;
191+
}
192+
193+
// check if the parameter is variadic
194+
const isPositionalParameter = (
195+
param: TaskParameter,
196+
): param is PositionalTaskParameter => "isVariadic" in param;
197+
const isVariadic = isPositionalParameter(parameter) && parameter.isVariadic;
198+
199+
// check if the value is valid for the parameter type
200+
if (!isParameterValueValid(parameter.parameterType, value, isVariadic)) {
201+
throw new HardhatError(
202+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
203+
{
204+
value,
205+
name: parameter.name,
206+
type: parameter.parameterType,
207+
task: formatTaskId(this.id),
208+
},
209+
);
210+
}
211+
}
212+
213+
/**
214+
* Validates that no extra arguments were provided in the task arguments.
215+
*
216+
* @throws HardhatError if extra arguments were provided. The error message
217+
* includes the name of the first extra argument.
218+
*/
219+
#validateExtraArguments(providedArgumentNames: Set<string>) {
220+
if (providedArgumentNames.size > 0) {
221+
throw new HardhatError(
222+
HardhatError.ERRORS.TASK_DEFINITIONS.UNRECOGNIZED_NAMED_PARAM,
223+
{
224+
parameter: [...providedArgumentNames][0],
225+
task: formatTaskId(this.id),
226+
},
227+
);
228+
}
229+
}
230+
231+
/**
232+
* Resolves the action file for a task. The action file is imported and the
233+
* default export function is returned.
234+
*
235+
* @throws HardhatError if the module can't be imported or doesn't have a
236+
* default export function.
237+
*/
238+
async #resolveFileAction(
239+
actionFileUrl: string,
240+
taskId: string[],
241+
): Promise<NewTaskActionFunction | TaskOverrideActionFunction> {
242+
let resolvedActionFn;
243+
try {
244+
resolvedActionFn = await import(actionFileUrl);
245+
} catch (error) {
246+
ensureError(error);
247+
throw new HardhatError(
248+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_ACTION_URL,
249+
{
250+
action: actionFileUrl,
251+
task: formatTaskId(taskId),
252+
},
253+
error,
254+
);
255+
}
256+
257+
if (typeof resolvedActionFn.default !== "function") {
258+
throw new HardhatError(
259+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_ACTION,
260+
{
261+
action: actionFileUrl,
262+
task: formatTaskId(taskId),
263+
},
264+
);
87265
}
88266

89-
void taskArguments;
90-
void this.#hre;
267+
return resolvedActionFn.default;
91268
}
92269
}

v-next/core/src/types/tasks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export type NewTaskActionFunction = (
8989
export type TaskOverrideActionFunction = (
9090
taskArguments: TaskArguments,
9191
hre: HardhatRuntimeEnvironment,
92-
runSuper: (taskArguments?: TaskArguments) => Promise<any>,
92+
runSuper: (taskArguments: TaskArguments) => Promise<any>,
9393
) => any;
9494

9595
/**

v-next/core/test/internal/tasks/builders.ts

+29-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import assert from "node:assert/strict";
22
import { describe, it } from "node:test";
33

4+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
5+
46
import { RESERVED_PARAMETER_NAMES } from "../../../src/internal/parameters.js";
57
import {
68
NewTaskDefinitionBuilderImplementation,
@@ -643,11 +645,15 @@ describe("Task builders", () => {
643645
defaultValue: 123 as any,
644646
type: ParameterType.STRING,
645647
}),
646-
{
647-
name: "HardhatError",
648-
message:
649-
"HHE300: Invalid value 123 for argument defaultValue of type STRING",
650-
},
648+
new HardhatError(
649+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
650+
{
651+
value: 123,
652+
name: "defaultValue",
653+
type: ParameterType.STRING,
654+
task: "task-id",
655+
},
656+
),
651657
);
652658
});
653659

@@ -660,11 +666,15 @@ describe("Task builders", () => {
660666
defaultValue: [123, 456, 789] as any,
661667
type: ParameterType.STRING,
662668
}),
663-
{
664-
name: "HardhatError",
665-
message:
666-
"HHE300: Invalid value [123,456,789] for argument defaultValue of type STRING",
667-
},
669+
new HardhatError(
670+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
671+
{
672+
value: [123, 456, 789],
673+
name: "defaultValue",
674+
type: ParameterType.STRING,
675+
task: "task-id",
676+
},
677+
),
668678
);
669679
});
670680
});
@@ -1097,11 +1107,15 @@ describe("Task builders", () => {
10971107
defaultValue: 123 as any,
10981108
type: ParameterType.STRING,
10991109
}),
1100-
{
1101-
name: "HardhatError",
1102-
message:
1103-
"HHE300: Invalid value 123 for argument defaultValue of type STRING",
1104-
},
1110+
new HardhatError(
1111+
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
1112+
{
1113+
value: 123,
1114+
name: "defaultValue",
1115+
type: ParameterType.STRING,
1116+
task: "task-id",
1117+
},
1118+
),
11051119
);
11061120
});
11071121
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default (args, _, runSuper) => runSuper(args);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const a = 1;
2+
3+
export default a;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const a = 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default (args) => `action fn called with args: ${JSON.stringify(args)}`;

0 commit comments

Comments
 (0)