diff --git a/.changeset/smooth-socks-dance.md b/.changeset/smooth-socks-dance.md new file mode 100644 index 00000000000..88af99bea2a --- /dev/null +++ b/.changeset/smooth-socks-dance.md @@ -0,0 +1,7 @@ +--- +"@nomicfoundation/hardhat-errors": patch +"@nomicfoundation/hardhat-verify": patch +"hardhat": patch +--- + +Added support for `inline actions` in tasks [7851](https://github.com/NomicFoundation/hardhat/pull/7851). diff --git a/v-next/hardhat-errors/src/descriptors.ts b/v-next/hardhat-errors/src/descriptors.ts index a556f9d3036..95b33f04b3b 100644 --- a/v-next/hardhat-errors/src/descriptors.ts +++ b/v-next/hardhat-errors/src/descriptors.ts @@ -694,6 +694,22 @@ Please double check your arguments.`, Please check you have the correct subtask.`, }, + INLINE_ACTION_CANNOT_BE_USED_IN_PLUGINS: { + number: 417, + messageTemplate: `The task "{task}" defines an "inlineAction", which is not allowed in plugins.`, + websiteTitle: "Inline action not allowed in plugins", + websiteDescription: `Plugins are not allowed to use inline actions for tasks. + +Please define the action in a separate file and reference it.`, + }, + ACTION_ALREADY_SET: { + number: 418, + messageTemplate: `The action for task "{task}" has already been set. You can only call "setAction" or "setInlineAction" once per task definition.`, + websiteTitle: "Task action already set", + websiteDescription: `A task definition can only have one action. You cannot call "setAction" or "setInlineAction" more than once on the same task builder. + +Please remove the duplicate call.`, + }, }, ARGUMENTS: { INVALID_VALUE_FOR_TYPE: { diff --git a/v-next/hardhat-verify/src/internal/tasks/verify/blockscout/index.ts b/v-next/hardhat-verify/src/internal/tasks/verify/blockscout/index.ts index 7bd5d6915ce..e8de1d1053e 100644 --- a/v-next/hardhat-verify/src/internal/tasks/verify/blockscout/index.ts +++ b/v-next/hardhat-verify/src/internal/tasks/verify/blockscout/index.ts @@ -1,10 +1,10 @@ -import type { NewTaskDefinition } from "hardhat/types/tasks"; +import type { PluginTaskDefinition } from "hardhat/types/plugins"; import { task } from "hardhat/config"; import { extendWithVerificationArgs } from "../utils.js"; -const verifyBlockscoutTask: NewTaskDefinition = extendWithVerificationArgs( +const verifyBlockscoutTask: PluginTaskDefinition = extendWithVerificationArgs( task(["verify", "blockscout"], "Verify a contract on Blockscout"), ) .setAction(() => import("./task-action.js")) diff --git a/v-next/hardhat-verify/src/internal/tasks/verify/etherscan/index.ts b/v-next/hardhat-verify/src/internal/tasks/verify/etherscan/index.ts index e770afcc7ad..1e47cd923e1 100644 --- a/v-next/hardhat-verify/src/internal/tasks/verify/etherscan/index.ts +++ b/v-next/hardhat-verify/src/internal/tasks/verify/etherscan/index.ts @@ -1,10 +1,10 @@ -import type { NewTaskDefinition } from "hardhat/types/tasks"; +import type { PluginTaskDefinition } from "hardhat/types/plugins"; import { task } from "hardhat/config"; import { extendWithVerificationArgs } from "../utils.js"; -const verifyEtherscanTask: NewTaskDefinition = extendWithVerificationArgs( +const verifyEtherscanTask: PluginTaskDefinition = extendWithVerificationArgs( task(["verify", "etherscan"], "Verify a contract on Etherscan"), ) .setAction(() => import("./task-action.js")) diff --git a/v-next/hardhat-verify/src/internal/tasks/verify/index.ts b/v-next/hardhat-verify/src/internal/tasks/verify/index.ts index f34c177cbc9..81d276e7cb2 100644 --- a/v-next/hardhat-verify/src/internal/tasks/verify/index.ts +++ b/v-next/hardhat-verify/src/internal/tasks/verify/index.ts @@ -1,10 +1,10 @@ -import type { NewTaskDefinition } from "hardhat/types/tasks"; +import type { PluginTaskDefinition } from "hardhat/types/plugins"; import { task } from "hardhat/config"; import { extendWithSourcifyArgs, extendWithVerificationArgs } from "./utils.js"; -const verifyTask: NewTaskDefinition = extendWithSourcifyArgs( +const verifyTask: PluginTaskDefinition = extendWithSourcifyArgs( extendWithVerificationArgs( task("verify", "Verify a contract on all supported explorers"), ), diff --git a/v-next/hardhat-verify/src/internal/tasks/verify/sourcify/index.ts b/v-next/hardhat-verify/src/internal/tasks/verify/sourcify/index.ts index 0a98aa1d13d..7d0196bbb23 100644 --- a/v-next/hardhat-verify/src/internal/tasks/verify/sourcify/index.ts +++ b/v-next/hardhat-verify/src/internal/tasks/verify/sourcify/index.ts @@ -1,4 +1,4 @@ -import type { NewTaskDefinition } from "hardhat/types/tasks"; +import type { PluginTaskDefinition } from "hardhat/types/plugins"; import { task } from "hardhat/config"; @@ -7,7 +7,7 @@ import { extendWithVerificationArgs, } from "../utils.js"; -const verifySourcifyTask: NewTaskDefinition = extendWithSourcifyArgs( +const verifySourcifyTask: PluginTaskDefinition = extendWithSourcifyArgs( extendWithVerificationArgs( task(["verify", "sourcify"], "Verify a contract on Sourcify"), ), diff --git a/v-next/hardhat/src/internal/core/config-validation.ts b/v-next/hardhat/src/internal/core/config-validation.ts index baad10ac2d0..638bcdef088 100644 --- a/v-next/hardhat/src/internal/core/config-validation.ts +++ b/v-next/hardhat/src/internal/core/config-validation.ts @@ -18,9 +18,9 @@ import { } from "../../types/arguments.js"; import { type EmptyTaskDefinition, + TaskDefinitionType, type NewTaskDefinition, type TaskDefinition, - TaskDefinitionType, type TaskOverrideDefinition, } from "../../types/tasks.js"; @@ -272,13 +272,7 @@ export function validateNewTask( }); } - 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", - }); - } + validationErrors.push(...validateActionFields(task, path)); if (isObject(task.options)) { validationErrors.push( @@ -328,13 +322,7 @@ export function validateTaskOverride( }); } - 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", - }); - } + validationErrors.push(...validateActionFields(task, path)); if (isObject(task.options)) { validationErrors.push( @@ -350,6 +338,51 @@ export function validateTaskOverride( return validationErrors; } +function validateActionFields( + task: { action?: unknown; inlineAction?: unknown }, + path: Array, +): HardhatUserConfigValidationError[] { + const validationErrors: HardhatUserConfigValidationError[] = []; + + // Mutual exclusivity: cannot have both action and inlineAction + if (task.action !== undefined && task.inlineAction !== undefined) { + validationErrors.push({ + path: [...path], + message: 'task cannot define both "action" and "inlineAction"', + }); + } + + // At least one action must be defined + if (task.action === undefined && task.inlineAction === undefined) { + validationErrors.push({ + path: [...path, "action"], + message: 'task must define either "action" or "inlineAction"', + }); + } + + if (task.action !== undefined) { + 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 (task.inlineAction !== undefined) { + if (typeof task.inlineAction !== "function") { + validationErrors.push({ + path: [...path, "inlineAction"], + message: + "task inlineAction must be a function implementing the task's behavior", + }); + } + } + + return validationErrors; +} + export function validateOptions( options: Record, path: Array, diff --git a/v-next/hardhat/src/internal/core/tasks/builders.ts b/v-next/hardhat/src/internal/core/tasks/builders.ts index 074fc978180..5ddfaba407a 100644 --- a/v-next/hardhat/src/internal/core/tasks/builders.ts +++ b/v-next/hardhat/src/internal/core/tasks/builders.ts @@ -15,6 +15,8 @@ import type { ExtendTaskArguments, TaskArguments, LazyActionObject, + TaskAction, + TaskOverrideAction, } from "../../../types/tasks.js"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; @@ -54,7 +56,11 @@ export class EmptyTaskDefinitionBuilderImplementation export class NewTaskDefinitionBuilderImplementation< TaskArgumentsT extends TaskArguments = TaskArguments, -> implements NewTaskDefinitionBuilder + ActionTypeT extends + | "LAZY_ACTION" + | "INLINE_ACTION" + | "MISSING_ACTION" = "MISSING_ACTION", +> implements NewTaskDefinitionBuilder { readonly #id: string[]; readonly #usedNames: Set = new Set(); @@ -65,6 +71,7 @@ export class NewTaskDefinitionBuilderImplementation< #description: string; #action?: LazyActionObject>; + #inlineAction?: NewTaskActionFunction; constructor(id: string | string[], description: string = "") { validateId(id); @@ -80,10 +87,26 @@ export class NewTaskDefinitionBuilderImplementation< public setAction( action: LazyActionObject>, - ): this { + ): NewTaskDefinitionBuilder { + this.#ensureNoActionSet(); + this.#action = action; - return this; + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to update the ActionTypeT to "LAZY_ACTION". */ + return this as NewTaskDefinitionBuilder; + } + + public setInlineAction( + inlineAction: NewTaskActionFunction, + ): NewTaskDefinitionBuilder { + this.#ensureNoActionSet(); + + this.#inlineAction = inlineAction; + + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to update the ActionTypeT to "INLINE_ACTION". */ + return this as NewTaskDefinitionBuilder; } public addOption< @@ -104,7 +127,8 @@ export class NewTaskDefinitionBuilderImplementation< defaultValue: ArgumentTypeToValueType; hidden?: boolean; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { const argumentType = type ?? ArgumentType.STRING; @@ -121,7 +145,13 @@ export class NewTaskDefinitionBuilderImplementation< this.#options[name] = optionDefinition; - return this; + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to update the generic argument types. Propagate 'ActionTypeT' to preserve + the current action state for subsequent method calls. */ + return this as NewTaskDefinitionBuilder< + ExtendTaskArguments, + ActionTypeT + >; } public addFlag(flagConfig: { @@ -130,7 +160,8 @@ export class NewTaskDefinitionBuilderImplementation< description?: string; hidden?: boolean; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { return this.addOption({ ...flagConfig, @@ -146,7 +177,8 @@ export class NewTaskDefinitionBuilderImplementation< description?: string; defaultValue?: number; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { return this.addOption({ ...levelConfig, @@ -164,7 +196,8 @@ export class NewTaskDefinitionBuilderImplementation< type?: TypeT; defaultValue?: ArgumentTypeToValueType; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { return this.#addPositionalArgument({ ...argConfig, @@ -181,7 +214,8 @@ export class NewTaskDefinitionBuilderImplementation< type?: TypeT; defaultValue?: Array>; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { return this.#addPositionalArgument({ ...argConfig, @@ -189,8 +223,15 @@ export class NewTaskDefinitionBuilderImplementation< }); } - public build(): NewTaskDefinition { - if (this.#action === undefined) { + public build(): ActionTypeT extends "LAZY_ACTION" + ? Extract< + NewTaskDefinition, + { action: LazyActionObject } + > + : ActionTypeT extends "INLINE_ACTION" + ? Extract + : never { + if (this.#action === undefined && this.#inlineAction === undefined) { throw new HardhatError( HardhatError.ERRORS.CORE.TASK_DEFINITIONS.NO_ACTION, { @@ -199,18 +240,30 @@ export class NewTaskDefinitionBuilderImplementation< ); } + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast the return value because TypeScript cannot verify that the object matches + the conditional type. */ return { type: TaskDefinitionType.NEW_TASK, id: this.#id, description: this.#description, /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions - -- The type of the action is narrowed in the setAction function to + -- The type of the action is narrowed in the setAction function or setInlineAction to improve the argument types. Once the task is built, we use the more general type to avoid having to parameterize the NewTaskDefinition */ - action: this.#action as LazyActionObject, + ...((this.#action !== undefined + ? { action: this.#action } + : { inlineAction: this.#inlineAction }) as TaskAction), options: this.#options, positionalArguments: this.#positionalArgs, - }; + } as ActionTypeT extends "LAZY_ACTION" + ? Extract< + NewTaskDefinition, + { action: LazyActionObject } + > + : ActionTypeT extends "INLINE_ACTION" + ? Extract + : never; } #addPositionalArgument< @@ -231,7 +284,8 @@ export class NewTaskDefinitionBuilderImplementation< | Array>; isVariadic: boolean; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { const argumentType = type ?? ArgumentType.STRING; @@ -253,13 +307,34 @@ export class NewTaskDefinitionBuilderImplementation< this.#positionalArgs.push(positionalArgDef); - return this; + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to update the generic argument types. Propagate 'ActionTypeT' to preserve + the current action state for subsequent method calls. */ + return this as NewTaskDefinitionBuilder< + ExtendTaskArguments, + ActionTypeT + >; + } + + #ensureNoActionSet(): void { + if (this.#action !== undefined || this.#inlineAction !== undefined) { + throw new HardhatError( + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { + task: formatTaskId(this.#id), + }, + ); + } } } export class TaskOverrideDefinitionBuilderImplementation< TaskArgumentsT extends TaskArguments = TaskArguments, -> implements TaskOverrideDefinitionBuilder + ActionTypeT extends + | "LAZY_ACTION" + | "INLINE_ACTION" + | "MISSING_ACTION" = "MISSING_ACTION", +> implements TaskOverrideDefinitionBuilder { readonly #id: string[]; @@ -268,6 +343,7 @@ export class TaskOverrideDefinitionBuilderImplementation< #description?: string; #action?: LazyActionObject>; + #inlineAction?: TaskOverrideActionFunction; constructor(id: string | string[]) { validateId(id); @@ -282,10 +358,29 @@ export class TaskOverrideDefinitionBuilderImplementation< public setAction( action: LazyActionObject>, - ): this { + ): TaskOverrideDefinitionBuilder { + this.#ensureNoActionSet(); + this.#action = action; - return this; + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to update the ActionTypeT to "LAZY_ACTION". */ + return this as TaskOverrideDefinitionBuilder; + } + + public setInlineAction( + inlineAction: TaskOverrideActionFunction, + ): TaskOverrideDefinitionBuilder { + this.#ensureNoActionSet(); + + this.#inlineAction = inlineAction; + + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to update the ActionTypeT to "INLINE_ACTION". */ + return this as TaskOverrideDefinitionBuilder< + TaskArgumentsT, + "INLINE_ACTION" + >; } public addOption< @@ -306,7 +401,8 @@ export class TaskOverrideDefinitionBuilderImplementation< defaultValue: ArgumentTypeToValueType; hidden?: boolean; }): TaskOverrideDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { const argumentType = type ?? ArgumentType.STRING; @@ -331,16 +427,23 @@ export class TaskOverrideDefinitionBuilderImplementation< this.#options[name] = optionDefinition; - return this; + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to update the generic argument types. Propagate 'ActionTypeT' to preserve + the current action state for subsequent method calls. */ + return this as TaskOverrideDefinitionBuilder< + ExtendTaskArguments, + ActionTypeT + >; } public addFlag(flagConfig: { - name: string; + name: NameT; shortName?: string; description?: string; hidden?: boolean; }): TaskOverrideDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { return this.addOption({ ...flagConfig, @@ -351,12 +454,13 @@ export class TaskOverrideDefinitionBuilderImplementation< } public addLevel(levelConfig: { - name: string; + name: NameT; shortName?: string; description?: string; defaultValue?: number; }): TaskOverrideDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT > { return this.addOption({ ...levelConfig, @@ -365,8 +469,18 @@ export class TaskOverrideDefinitionBuilderImplementation< }); } - public build(): TaskOverrideDefinition { - if (this.#action === undefined) { + public build(): ActionTypeT extends "LAZY_ACTION" + ? Extract< + TaskOverrideDefinition, + { action: LazyActionObject } + > + : ActionTypeT extends "INLINE_ACTION" + ? Extract< + TaskOverrideDefinition, + { inlineAction: TaskOverrideActionFunction } + > + : never { + if (this.#action === undefined && this.#inlineAction === undefined) { throw new HardhatError( HardhatError.ERRORS.CORE.TASK_DEFINITIONS.NO_ACTION, { @@ -375,16 +489,46 @@ export class TaskOverrideDefinitionBuilderImplementation< ); } + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast the return value because TypeScript cannot verify that the object matches + the conditional type. */ return { type: TaskDefinitionType.TASK_OVERRIDE, id: this.#id, description: this.#description, /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions - -- The type of the action is narrowed in the setAction function to + -- The type of the action is narrowed in the setAction function or setInlineAction to improve the argument types. Once the task is built, we use the more general type to avoid having to parameterize the TaskOverrideDefinition */ - action: this.#action as LazyActionObject, + ...((this.#action !== undefined + ? { + action: this.#action, + } + : { + inlineAction: this.#inlineAction, + }) as TaskOverrideAction), options: this.#options, - }; + } as ActionTypeT extends "LAZY_ACTION" + ? Extract< + TaskOverrideDefinition, + { action: LazyActionObject } + > + : ActionTypeT extends "INLINE_ACTION" + ? Extract< + TaskOverrideDefinition, + { inlineAction: TaskOverrideActionFunction } + > + : never; + } + + #ensureNoActionSet(): void { + if (this.#action !== undefined || this.#inlineAction !== undefined) { + throw new HardhatError( + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { + task: formatTaskId(this.#id), + }, + ); + } } } diff --git a/v-next/hardhat/src/internal/core/tasks/resolved-task.ts b/v-next/hardhat/src/internal/core/tasks/resolved-task.ts index 55c68aad0b0..2b93f8c811b 100644 --- a/v-next/hardhat/src/internal/core/tasks/resolved-task.ts +++ b/v-next/hardhat/src/internal/core/tasks/resolved-task.ts @@ -8,6 +8,7 @@ import type { LazyActionObject, NewTaskActionFunction, Task, + TaskAction, TaskActions, TaskArguments, TaskOverrideActionFunction, @@ -49,7 +50,7 @@ export class ResolvedTask implements Task { hre: HardhatRuntimeEnvironment, id: string[], description: string, - action: LazyActionObject, + taskAction: TaskAction, options: Record, positionalArguments: PositionalArgumentDefinition[], pluginId?: string, @@ -57,7 +58,12 @@ export class ResolvedTask implements Task { return new ResolvedTask( id, description, - [{ pluginId, action }], + [ + { + pluginId, + ...taskAction, + }, + ], new Map(Object.entries(options)), positionalArguments, pluginId, @@ -80,7 +86,11 @@ export class ResolvedTask implements Task { } public get isEmpty(): boolean { - return this.actions.length === 1 && this.actions[0].action === undefined; + return ( + this.actions.length === 1 && + this.actions[0].action === undefined && + this.actions[0].inlineAction === undefined + ); } /** @@ -142,17 +152,25 @@ export class ResolvedTask implements Task { nextTaskArguments: TaskArguments, currentIndex = this.actions.length - 1, ): Promise => { + const currentTaskAction = this.actions[currentIndex]; + + let actionFn: NewTaskActionFunction | TaskOverrideActionFunction; + // The first action may be empty if the task was originally an empty task - const currentAction = - this.actions[currentIndex].action ?? - (async () => ({ - default: () => {}, - })); + if (currentTaskAction.inlineAction !== undefined) { + actionFn = currentTaskAction.inlineAction; + } else { + const lazyAction = + currentTaskAction.action ?? + (async () => ({ + default: () => {}, + })); - const actionFn = await this.#resolveImportAction( - currentAction, - this.actions[currentIndex].pluginId, - ); + actionFn = await this.#resolveImportAction( + lazyAction, + currentTaskAction.pluginId, + ); + } if (currentIndex === 0) { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- diff --git a/v-next/hardhat/src/internal/core/tasks/task-manager.ts b/v-next/hardhat/src/internal/core/tasks/task-manager.ts index 183fcc675ee..19ec5046896 100644 --- a/v-next/hardhat/src/internal/core/tasks/task-manager.ts +++ b/v-next/hardhat/src/internal/core/tasks/task-manager.ts @@ -19,6 +19,7 @@ import { TaskDefinitionType } from "../../../types/tasks.js"; import { ResolvedTask } from "./resolved-task.js"; import { formatTaskId, getActorFragment } from "./utils.js"; import { + validateAction, validateId, validateOption, validatePositionalArgument, @@ -41,7 +42,7 @@ export class TaskManagerImplementation implements TaskManager { } for (const taskDefinition of plugin.tasks) { - this.#validateTaskDefinition(taskDefinition); + this.#validateTaskDefinition(taskDefinition, true); this.#reduceTaskDefinition( globalOptionDefinitions, taskDefinition, @@ -52,7 +53,7 @@ export class TaskManagerImplementation implements TaskManager { // reduce global user defined tasks for (const taskDefinition of this.#hre.config.tasks) { - this.#validateTaskDefinition(taskDefinition); + this.#validateTaskDefinition(taskDefinition, false); this.#reduceTaskDefinition(globalOptionDefinitions, taskDefinition); } } @@ -171,7 +172,9 @@ export class TaskManagerImplementation implements TaskManager { this.#hre, taskDefinition.id, taskDefinition.description, - taskDefinition.action, + taskDefinition.action !== undefined + ? { action: taskDefinition.action } + : { inlineAction: taskDefinition.inlineAction }, taskDefinition.options, taskDefinition.positionalArguments, pluginId, @@ -278,10 +281,18 @@ export class TaskManagerImplementation implements TaskManager { task.description = taskDefinition.description; } - task.actions.push({ pluginId, action: taskDefinition.action }); + task.actions.push({ + pluginId, + ...(taskDefinition.action !== undefined + ? { action: taskDefinition.action } + : { inlineAction: taskDefinition.inlineAction }), + }); } - #validateTaskDefinition(taskDefinition: TaskDefinition): void { + #validateTaskDefinition( + taskDefinition: TaskDefinition, + isPlugin: boolean, + ): void { validateId(taskDefinition.id); // Empty tasks don't have actions, options, or positional arguments @@ -289,6 +300,13 @@ export class TaskManagerImplementation implements TaskManager { return; } + validateAction( + taskDefinition.action, + taskDefinition.inlineAction, + taskDefinition.id, + isPlugin, + ); + const usedNames = new Set(); Object.values(taskDefinition.options).forEach((optionDefinition) => diff --git a/v-next/hardhat/src/internal/core/tasks/validations.ts b/v-next/hardhat/src/internal/core/tasks/validations.ts index 1dc8c62ae29..87e0a8bf3cd 100644 --- a/v-next/hardhat/src/internal/core/tasks/validations.ts +++ b/v-next/hardhat/src/internal/core/tasks/validations.ts @@ -4,6 +4,12 @@ import type { OptionDefinition, PositionalArgumentDefinition, } from "../../../types/arguments.js"; +import type { + LazyActionObject, + NewTaskActionFunction, + TaskArguments, + TaskOverrideActionFunction, +} from "../../../types/tasks.js"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; @@ -23,6 +29,34 @@ export function validateId(id: string | string[]): void { } } +export function validateAction( + action: + | LazyActionObject> + | LazyActionObject> + | undefined, + inlineAction: + | NewTaskActionFunction + | TaskOverrideActionFunction + | undefined, + taskId: string[], + isPlugin: boolean, +): void { + if (action !== undefined && inlineAction !== undefined) { + throw new HardhatError( + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: formatTaskId(taskId) }, + ); + } + + if (isPlugin && inlineAction !== undefined) { + // Inline actions cannot be used in plugins, as they are only allowed in user tasks + throw new HardhatError( + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.INLINE_ACTION_CANNOT_BE_USED_IN_PLUGINS, + { task: formatTaskId(taskId) }, + ); + } +} + export function validateOption( { name, shortName, type, defaultValue }: OptionDefinition, usedNames: Set, diff --git a/v-next/hardhat/src/types/plugins.ts b/v-next/hardhat/src/types/plugins.ts index 61a683adcba..853d0e7274b 100644 --- a/v-next/hardhat/src/types/plugins.ts +++ b/v-next/hardhat/src/types/plugins.ts @@ -1,6 +1,13 @@ import type { GlobalOptionDefinition } from "./arguments.js"; import type { HardhatHooks } from "./hooks.js"; -import type { TaskDefinition } from "./tasks.js"; +import type { + EmptyTaskDefinition, + LazyActionObject, + NewTaskActionFunction, + NewTaskDefinition, + TaskOverrideActionFunction, + TaskOverrideDefinition, +} from "./tasks.js"; // NOTE: We import the builtin plugins in this module, so that their // type-extensions are loaded when the user imports `hardhat/types/plugins`. @@ -20,6 +27,20 @@ declare module "./config.js" { } } +/** + * A helper type to strictly enforce that plugins only use lazy-loaded file-based actions. + */ +export type PluginTaskDefinition = + | EmptyTaskDefinition + | Extract< + NewTaskDefinition, + { action: LazyActionObject } + > + | Extract< + TaskOverrideDefinition, + { action: LazyActionObject } + >; + /** * A Hardhat plugin. */ @@ -65,8 +86,8 @@ export interface HardhatPlugin { * returning an object with a handler for the `extendUserConfig` hook. * * You can define each factory in two ways: - * - As an inline function. - * - As a string with the path to a file that exports the factory as `default`. + * - As an inline function. + * - As a string with the path to a file that exports the factory as `default`. * * The first option should only be used for development. You MUST use the second * option for production. @@ -85,7 +106,7 @@ export interface HardhatPlugin { * have been defined before, either by a plugin you depend on or by Hardhat * itself. */ - tasks?: TaskDefinition[]; + tasks?: PluginTaskDefinition[]; } /** diff --git a/v-next/hardhat/src/types/tasks.ts b/v-next/hardhat/src/types/tasks.ts index 9e5ddd16921..6efef259ef9 100644 --- a/v-next/hardhat/src/types/tasks.ts +++ b/v-next/hardhat/src/types/tasks.ts @@ -75,6 +75,17 @@ export enum TaskDefinitionType { TASK_OVERRIDE = "TASK_OVERRIDE", } +export type TaskAction = + | { action: LazyActionObject; inlineAction?: never } + | { inlineAction: NewTaskActionFunction; action?: never }; + +export type TaskOverrideAction = + | { + action: LazyActionObject; + inlineAction?: never; + } + | { inlineAction: TaskOverrideActionFunction; action?: never }; + /** * Empty task definition. It is meant to be used as a placeholder task that only * prints information about its subtasks. @@ -90,37 +101,44 @@ export interface EmptyTaskDefinition { } /** - * The definition of a new task. + * The base definition of a new task. */ -export interface NewTaskDefinition { +export interface BaseTaskDefinition { type: TaskDefinitionType.NEW_TASK; id: string[]; description: string; - action: LazyActionObject; - options: Record; positionalArguments: PositionalArgumentDefinition[]; } /** - * An override of an existing task. + * The definition of a new task. + */ +export type NewTaskDefinition = BaseTaskDefinition & TaskAction; + +/** + * The base definition of an override of an existing task. */ -export interface TaskOverrideDefinition { +export interface BaseTaskOverrideDefinition { type: TaskDefinitionType.TASK_OVERRIDE; id: string[]; description?: string; - action: LazyActionObject; - options: Record; } +/** + * An override of an existing task. + */ +export type TaskOverrideDefinition = BaseTaskOverrideDefinition & + TaskOverrideAction; + /** * The definition of a task, as used in the plugins and user config. They are * declarative descriptions of the task, which are later processed to create the @@ -160,9 +178,23 @@ export interface EmptyTaskDefinitionBuilder { /** * A builder for creating NewTaskDefinitions. + * + * @template TaskArgumentsT The arguments of the task. + * @template ActionTypeT Tracks if the action is "LAZY_ACTION" (Plugin Safe) or "INLINE_ACTION". + * + * @remarks + * This builder validates action definitions at runtime. Attempting to: + * - Call setAction or setInlineAction multiple times + * - Call both setAction and setInlineAction on the same task + * - Build without setting an action + * will throw a HardhatError with a clear message. */ export interface NewTaskDefinitionBuilder< TaskArgumentsT extends TaskArguments = TaskArguments, + ActionTypeT extends + | "LAZY_ACTION" + | "INLINE_ACTION" + | "MISSING_ACTION" = "MISSING_ACTION", > { /** * Sets the description of the task. @@ -172,15 +204,42 @@ export interface NewTaskDefinitionBuilder< /** * Sets the action of the task. * - * It can be provided as a function, or as a `file://` URL pointing to a file + * It must be provided as a `file://` URL pointing to a file * that exports a default NewTaskActionFunction. * * Note that plugins can only use the inline function form for development * purposes. + * + * @remarks + * This method can only be called once per task definition. Calling it multiple + * times will result in a runtime error. + * + * This method cannot be used together with {@link setInlineAction} on the same + * task. Use one or the other. + * + * @throws {HardhatError} CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET */ setAction( action: LazyActionObject>, - ): this; + ): NewTaskDefinitionBuilder; + + /** + * Sets the inline action of the task. + * + * It must be provided as a function. + * + * @remarks + * This method can only be called once per task definition. Calling it multiple + * times will result in a runtime error. + * + * This method cannot be used together with {@link setAction} on the same + * task. Use one or the other. + * + * @throws {HardhatError} CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET + */ + setInlineAction( + inlineAction: NewTaskActionFunction, + ): NewTaskDefinitionBuilder; /** * Adds an option to the task. @@ -202,7 +261,8 @@ export interface NewTaskDefinitionBuilder< defaultValue: ArgumentTypeToValueType; hidden?: boolean; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT >; /** @@ -214,7 +274,8 @@ export interface NewTaskDefinitionBuilder< description?: string; hidden?: boolean; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT >; /** @@ -226,7 +287,8 @@ export interface NewTaskDefinitionBuilder< description?: string; defaultValue?: number; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT >; /** @@ -254,7 +316,8 @@ export interface NewTaskDefinitionBuilder< type?: TypeT; defaultValue?: ArgumentTypeToValueType; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT >; /** @@ -280,20 +343,44 @@ export interface NewTaskDefinitionBuilder< type?: TypeT; defaultValue?: Array>; }): NewTaskDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT >; /** * Builds the NewTaskDefinition. + * + * @throws {HardhatError} CORE.TASK_DEFINITIONS.NO_ACTION if no action was set */ - build(): NewTaskDefinition; + build(): ActionTypeT extends "LAZY_ACTION" + ? Extract< + NewTaskDefinition, + { action: LazyActionObject } + > + : ActionTypeT extends "INLINE_ACTION" + ? Extract + : never; } /** - * A builder for overriding existing tasks. + * A builder for overriding existing tasks + * + * @template TaskArgumentsT The arguments of the task. + * @template ActionTypeT Tracks if the action is "LAZY_ACTION" (Plugin Safe) or "INLINE_ACTION". + * + * @remarks + * This builder validates action definitions at runtime. Attempting to: + * - Call setAction or setInlineAction multiple times + * - Call both setAction and setInlineAction on the same task + * - Build without setting an action + * will throw a HardhatError with a clear message. */ export interface TaskOverrideDefinitionBuilder< TaskArgumentsT extends TaskArguments = TaskArguments, + ActionTypeT extends + | "LAZY_ACTION" + | "INLINE_ACTION" + | "MISSING_ACTION" = "MISSING_ACTION", > { /** * Sets a new description for the task. @@ -304,10 +391,21 @@ export interface TaskOverrideDefinitionBuilder< * Sets a new action for the task. * * @see NewTaskDefinitionBuilder.setAction + * @throws {HardhatError} CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET */ setAction( action: LazyActionObject>, - ): this; + ): TaskOverrideDefinitionBuilder; + + /** + * Sets a new inline action for the task. + * + * @see NewTaskDefinitionBuilder.setInlineAction + * @throws {HardhatError} CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET + */ + setInlineAction( + inlineAction: TaskOverrideActionFunction, + ): TaskOverrideDefinitionBuilder; /** * Adds a new option to the task. @@ -325,7 +423,8 @@ export interface TaskOverrideDefinitionBuilder< defaultValue: ArgumentTypeToValueType; hidden?: boolean; }): TaskOverrideDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT >; /** @@ -337,7 +436,8 @@ export interface TaskOverrideDefinitionBuilder< description?: string; hidden?: boolean; }): TaskOverrideDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT >; /** @@ -349,36 +449,57 @@ export interface TaskOverrideDefinitionBuilder< description?: string; defaultValue?: number; }): TaskOverrideDefinitionBuilder< - ExtendTaskArguments + ExtendTaskArguments, + ActionTypeT >; /** * Builds the TaskOverrideDefinition. + * + * @throws {HardhatError} CORE.TASK_DEFINITIONS.NO_ACTION if no action was set */ - build(): TaskOverrideDefinition; + build(): ActionTypeT extends "LAZY_ACTION" + ? Extract< + TaskOverrideDefinition, + { action: LazyActionObject } + > + : ActionTypeT extends "INLINE_ACTION" + ? Extract< + TaskOverrideDefinition, + { inlineAction: TaskOverrideActionFunction } + > + : never; } /** * The actions associated to the task, in order. * * Each of them has the pluginId of the plugin that defined it, if any, and the - * action itself. + * action itself. The action is stored either in `lazyAction` or `inlineAction`. + * Note that `inlineAction` is reserved for user tasks and is not allowed for plugins. * - * Note that the first action is a `NewTaskActionFunction`, `string`, or - * `undefined`. `undefined` is only used for empty tasks. + * Note that the first action is a `NewTaskActionFunction` or undefined. + * `undefined` is only used for empty tasks. * - * The rest of the actions always have a `TaskOverrideActionFunction` or a - * `string`. + * The rest of the actions always have a `TaskOverrideActionFunction`. */ export type TaskActions = [ + // The Task Definition { pluginId?: string; - action?: LazyActionObject; - }, - ...Array<{ - pluginId?: string; - action: LazyActionObject; - }>, + } & ( + | TaskAction + | { + action?: undefined; + inlineAction?: undefined; + } + ), + // The Task Overrides + ...Array< + { + pluginId?: string; + } & TaskOverrideAction + >, ]; /** diff --git a/v-next/hardhat/test/internal/core/config-validation.ts b/v-next/hardhat/test/internal/core/config-validation.ts index ecc6d9bb298..3d286d21f4b 100644 --- a/v-next/hardhat/test/internal/core/config-validation.ts +++ b/v-next/hardhat/test/internal/core/config-validation.ts @@ -1197,6 +1197,116 @@ describe("config validation", function () { }, ]); }); + + it("should return an error if both action and inlineAction are defined", function () { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Allow type assertions to simulate invalid task definitions */ + const task = { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "task description", + action: async () => ({ default: () => {} }), + inlineAction: () => {}, + options: {}, + positionalArguments: [], + } as unknown as NewTaskDefinition; + + const errors = validateNewTask(task, []); + assert.equal(errors.length, 1); + assert.equal( + errors[0].message, + 'task cannot define both "action" and "inlineAction"', + ); + }); + + it("should return an error if neither action nor inlineAction are defined", function () { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Allow type assertions to simulate invalid task definitions */ + const task = { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "task description", + options: {}, + positionalArguments: [], + } as unknown as NewTaskDefinition; + + const errors = validateNewTask(task, []); + assert.equal(errors.length, 1); + assert.equal( + errors[0].message, + 'task must define either "action" or "inlineAction"', + ); + }); + + it("should accept a task with only action", function () { + const task: NewTaskDefinition = { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "task description", + action: async () => ({ + default: () => {}, + }), + options: {}, + positionalArguments: [], + }; + + assert.deepEqual(validateNewTask(task, []), []); + }); + + it("should accept a task with only inlineAction", function () { + const task: NewTaskDefinition = { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "task description", + inlineAction: () => {}, + options: {}, + positionalArguments: [], + }; + + assert.deepEqual(validateNewTask(task, []), []); + }); + + it("should return an error if action is not a lazy import function", function () { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Allow type assertions to simulate invalid task definitions */ + const task: NewTaskDefinition = { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "task description", + action: "not a lazy import function", + options: {}, + positionalArguments: [], + } as unknown as NewTaskDefinition; + + const errors = validateNewTask(task, []); + assert.equal(errors.length, 1); + assert.equal( + errors[0].message, + "task action must be a lazy import function returning a module with a default export", + ); + assert.deepEqual(errors[0].path, ["action"]); + }); + + it("should return an error if inlineAction is not a function", function () { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Allow type assertions to simulate invalid task definitions */ + const task: NewTaskDefinition = { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "task description", + inlineAction: "not a function", + options: {}, + positionalArguments: [], + } as unknown as NewTaskDefinition; + + const errors = validateNewTask(task, []); + assert.equal(errors.length, 1); + assert.equal( + errors[0].message, + "task inlineAction must be a function implementing the task's behavior", + ); + assert.deepEqual(errors[0].path, ["inlineAction"]); + }); }); describe("validateTaskOverride", function () { @@ -1310,6 +1420,108 @@ describe("config validation", function () { }, ]); }); + + it("should return an error if both action and inlineAction are defined", function () { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Allow type assertions to simulate invalid task definitions */ + const task = { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "task description", + action: async () => ({ default: () => {} }), + inlineAction: () => {}, + options: {}, + } as unknown as TaskOverrideDefinition; + + const errors = validateTaskOverride(task, []); + assert.equal(errors.length, 1); + assert.equal( + errors[0].message, + 'task cannot define both "action" and "inlineAction"', + ); + }); + + it("should return an error if neither action nor inlineAction are defined", function () { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Allow type assertions to simulate invalid task definitions */ + const task = { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "task description", + options: {}, + } as unknown as TaskOverrideDefinition; + + const errors = validateTaskOverride(task, []); + assert.equal(errors.length, 1); + assert.equal( + errors[0].message, + 'task must define either "action" or "inlineAction"', + ); + }); + + it("should accept a task override with only action", function () { + const task: TaskOverrideDefinition = { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "task description", + action: async () => ({ default: () => {} }), + options: {}, + }; + + assert.deepEqual(validateTaskOverride(task, []), []); + }); + + it("should accept a task override with only inlineAction", function () { + const task: TaskOverrideDefinition = { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "task description", + inlineAction: () => {}, + options: {}, + }; + + assert.deepEqual(validateTaskOverride(task, []), []); + }); + + it("should return an error if action is not a lazy import function", function () { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Allow type assertions to simulate invalid task definitions */ + const task = { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "task description", + action: "not a lazy import function", + options: {}, + } as unknown as TaskOverrideDefinition; + + const errors = validateTaskOverride(task, []); + assert.equal(errors.length, 1); + assert.equal( + errors[0].message, + "task action must be a lazy import function returning a module with a default export", + ); + assert.deepEqual(errors[0].path, ["action"]); + }); + + it("should return an error if inlineAction is not a function", function () { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Allow type assertions to simulate invalid task definitions */ + const task = { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "task description", + inlineAction: "not a function", + options: {}, + } as unknown as TaskOverrideDefinition; + + const errors = validateTaskOverride(task, []); + assert.equal(errors.length, 1); + assert.equal( + errors[0].message, + "task inlineAction must be a function implementing the task's behavior", + ); + assert.deepEqual(errors[0].path, ["inlineAction"]); + }); }); describe("validatePaths", function () { diff --git a/v-next/hardhat/test/internal/core/tasks/builders.ts b/v-next/hardhat/test/internal/core/tasks/builders.ts index 4259b01366d..c3853037490 100644 --- a/v-next/hardhat/test/internal/core/tasks/builders.ts +++ b/v-next/hardhat/test/internal/core/tasks/builders.ts @@ -1078,6 +1078,85 @@ describe("Task builders", () => { ); }); }); + + describe("actions", () => { + const action: LazyActionObject = async () => ({ + default: () => {}, + }); + const inlineAction: NewTaskActionFunction = () => {}; + + it("should be valid with only action", () => { + const builder = new NewTaskDefinitionBuilderImplementation("task-id"); + + const result = builder.setAction(action).build(); + + assert.equal(result.type, TaskDefinitionType.NEW_TASK); + assert.equal(result.action, action); + assert.equal(result.inlineAction, undefined); + }); + + it("should be valid with only inline action", () => { + const builder = new NewTaskDefinitionBuilderImplementation("task-id"); + + const result = builder.setInlineAction(inlineAction).build(); + + assert.equal(result.type, TaskDefinitionType.NEW_TASK); + assert.equal(result.inlineAction, inlineAction); + assert.equal(result.action, undefined); + }); + + it("should be invalid with action + inline action", () => { + const builder = new NewTaskDefinitionBuilderImplementation("task-id"); + + builder.setAction(action); + + assertThrowsHardhatError( + () => builder.setInlineAction(inlineAction), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + + it("should be invalid with inline action + action", () => { + const builder = new NewTaskDefinitionBuilderImplementation("task-id"); + + builder.setInlineAction(inlineAction); + + assertThrowsHardhatError( + () => builder.setAction(action), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + + it("should be invalid with double action", () => { + const builder = new NewTaskDefinitionBuilderImplementation("task-id"); + const action2: LazyActionObject = async () => ({ + default: () => {}, + }); + + builder.setAction(action); + + assertThrowsHardhatError( + () => builder.setAction(action2), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + + it("should be invalid with double inline action", () => { + const builder = new NewTaskDefinitionBuilderImplementation("task-id"); + const inlineAction2: NewTaskActionFunction = () => {}; + + builder.setInlineAction(inlineAction); + + assertThrowsHardhatError( + () => builder.setInlineAction(inlineAction2), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + }); }); describe("TaskOverrideDefinitionBuilderImplementation", () => { @@ -1692,5 +1771,96 @@ describe("Task builders", () => { ); }); }); + + describe("actions", () => { + const action = async () => ({ + default: () => {}, + }); + const inlineAction = () => {}; + + it("should be valid with only action", () => { + const builder = new TaskOverrideDefinitionBuilderImplementation( + "task-id", + ); + + const result = builder.setAction(action).build(); + + assert.equal(result.type, TaskDefinitionType.TASK_OVERRIDE); + assert.equal(result.action, action); + assert.equal(result.inlineAction, undefined); + }); + + it("should be valid with only inline action", () => { + const builder = new TaskOverrideDefinitionBuilderImplementation( + "task-id", + ); + + const result = builder.setInlineAction(inlineAction).build(); + + assert.equal(result.type, TaskDefinitionType.TASK_OVERRIDE); + assert.equal(result.inlineAction, inlineAction); + assert.equal(result.action, undefined); + }); + + it("should be invalid with action + inline action", () => { + const builder = new TaskOverrideDefinitionBuilderImplementation( + "task-id", + ); + + builder.setAction(action); + + assertThrowsHardhatError( + () => builder.setInlineAction(inlineAction), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + + it("should be invalid with inline action + action", () => { + const builder = new TaskOverrideDefinitionBuilderImplementation( + "task-id", + ); + + builder.setInlineAction(inlineAction); + + assertThrowsHardhatError( + () => builder.setAction(action), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + + it("should be invalid with double action", () => { + const builder = new TaskOverrideDefinitionBuilderImplementation( + "task-id", + ); + const action2 = async () => ({ + default: () => {}, + }); + + builder.setAction(action); + + assertThrowsHardhatError( + () => builder.setAction(action2), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + + it("should be invalid with double inline action", () => { + const builder = new TaskOverrideDefinitionBuilderImplementation( + "task-id", + ); + const inlineAction2 = () => {}; + + builder.setInlineAction(inlineAction); + + assertThrowsHardhatError( + () => builder.setInlineAction(inlineAction2), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + }); }); }); diff --git a/v-next/hardhat/test/internal/core/tasks/validations.ts b/v-next/hardhat/test/internal/core/tasks/validations.ts new file mode 100644 index 00000000000..c93dd2c7003 --- /dev/null +++ b/v-next/hardhat/test/internal/core/tasks/validations.ts @@ -0,0 +1,49 @@ +import { describe, it } from "node:test"; + +import { HardhatError } from "@nomicfoundation/hardhat-errors"; +import { assertThrowsHardhatError } from "@nomicfoundation/hardhat-test-utils"; + +import { validateAction } from "../../../../src/internal/core/tasks/validations.js"; + +describe("validateAction", () => { + const action = async () => ({ default: () => {} }); + const inlineAction = () => {}; + + it("should not throw when only action is provided", () => { + validateAction(action, undefined, ["task-id"], false); + }); + + it("should not throw when only inlineAction is provided for user tasks", () => { + validateAction(undefined, inlineAction, ["task-id"], false); + }); + + it("should throw when both action and inlineAction are provided", () => { + assertThrowsHardhatError( + () => validateAction(action, inlineAction, ["task-id"], false), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS.ACTION_ALREADY_SET, + { task: "task-id" }, + ); + }); + + it("should throw when inlineAction is provided for plugin tasks", () => { + assertThrowsHardhatError( + () => validateAction(undefined, inlineAction, ["task-id"], true), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS + .INLINE_ACTION_CANNOT_BE_USED_IN_PLUGINS, + { task: "task-id" }, + ); + }); + + it("should allow action for plugin tasks", () => { + validateAction(action, undefined, ["task-id"], true); + }); + + it("should handle subtask ids correctly in error messages", () => { + assertThrowsHardhatError( + () => validateAction(undefined, inlineAction, ["parent", "child"], true), + HardhatError.ERRORS.CORE.TASK_DEFINITIONS + .INLINE_ACTION_CANNOT_BE_USED_IN_PLUGINS, + { task: "parent child" }, + ); + }); +});