From d6d61daf8046f93657eea067287eb1898da0cc4d Mon Sep 17 00:00:00 2001 From: Luis Schaab <schaable@gmail.com> Date: Thu, 11 Jul 2024 19:55:10 -0300 Subject: [PATCH 1/3] Ensure the argument object provided by the user is not modified --- .../core/src/internal/tasks/resolved-task.ts | 45 +++++++++---------- .../core/test/internal/tasks/task-manager.ts | 25 +++++++++-- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/v-next/core/src/internal/tasks/resolved-task.ts b/v-next/core/src/internal/tasks/resolved-task.ts index 61e6c69bd6..351fad9863 100644 --- a/v-next/core/src/internal/tasks/resolved-task.ts +++ b/v-next/core/src/internal/tasks/resolved-task.ts @@ -100,34 +100,29 @@ export class ResolvedTask implements Task { } const providedArgumentNames = new Set(Object.keys(taskArguments)); - - // Validate and resolve the task options - for (const option of this.options.values()) { - const value = taskArguments[option.name]; - - this.#validateArgumentType(option, value); - - // resolve defaults for optional arguments - if (value === undefined) { - taskArguments[option.name] = option.defaultValue; + const argumentDefinitions = [ + ...this.options.values(), + ...this.positionalArguments, + ]; + const validatedTaskArguments: TaskArguments = {}; + for (const argumentDefinition of argumentDefinitions) { + const value = taskArguments[argumentDefinition.name]; + const isPositional = "isVariadic" in argumentDefinition; + + if (isPositional) { + this.#validateRequiredArgument(argumentDefinition, value); } - - providedArgumentNames.delete(option.name); - } - - // Validate and resolve the task positional arguments - for (const argument of this.positionalArguments) { - const value = taskArguments[argument.name]; - - this.#validateRequiredArgument(argument, value); - this.#validateArgumentType(argument, value, argument.isVariadic); + this.#validateArgumentType( + argumentDefinition, + value, + isPositional && argumentDefinition.isVariadic, + ); // resolve defaults for optional arguments - if (value === undefined && argument.defaultValue !== undefined) { - taskArguments[argument.name] = argument.defaultValue; - } + validatedTaskArguments[argumentDefinition.name] = + value ?? argumentDefinition.defaultValue; - providedArgumentNames.delete(argument.name); + providedArgumentNames.delete(argumentDefinition.name); } // At this point, the set should be empty as all the task arguments have @@ -166,7 +161,7 @@ export class ResolvedTask implements Task { ); }; - return next(taskArguments); + return next(validatedTaskArguments); } /** diff --git a/v-next/core/test/internal/tasks/task-manager.ts b/v-next/core/test/internal/tasks/task-manager.ts index 595062b5a3..823e44b1f6 100644 --- a/v-next/core/test/internal/tasks/task-manager.ts +++ b/v-next/core/test/internal/tasks/task-manager.ts @@ -1102,6 +1102,7 @@ describe("TaskManagerImplementation", () => { tasks: [ new NewTaskDefinitionBuilderImplementation("task1") .addOption({ name: "arg1", defaultValue: "default" }) + .addOption({ name: "withDefault", defaultValue: "default" }) .addFlag({ name: "flag1" }) .addPositionalArgument({ name: "posArg" }) .addVariadicArgument({ name: "varArg" }) @@ -1111,6 +1112,7 @@ describe("TaskManagerImplementation", () => { flag1: true, posArg: "posValue", varArg: ["varValue1", "varValue2"], + withDefault: "default", }); }) .build(), @@ -1132,16 +1134,31 @@ describe("TaskManagerImplementation", () => { }, ], }); - - const task1 = hre.tasks.getTask("task1"); - await task1.run({ + // withDefault option is intentionally omitted + const providedArgs = { arg1: "arg1Value", flag1: true, posArg: "posValue", varArg: ["varValue1", "varValue2"], arg2: "arg2Value", flag2: true, - }); + }; + + const task1 = hre.tasks.getTask("task1"); + await task1.run(providedArgs); + // Ensure withDefault is not added to the args + assert.deepEqual( + providedArgs, + { + arg1: "arg1Value", + flag1: true, + posArg: "posValue", + varArg: ["varValue1", "varValue2"], + arg2: "arg2Value", + flag2: true, + }, + "Expected the providedArgs to not change", + ); }); it("should run a task with arguments and resolve their default values", async () => { From 051755d58312b2af0ac3ff7119471aa0f97650ef Mon Sep 17 00:00:00 2001 From: Luis Schaab <schaable@gmail.com> Date: Thu, 11 Jul 2024 19:55:30 -0300 Subject: [PATCH 2/3] Fix type error --- v-next/core/src/types/arguments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v-next/core/src/types/arguments.ts b/v-next/core/src/types/arguments.ts index 231f84b720..b4e69488a2 100644 --- a/v-next/core/src/types/arguments.ts +++ b/v-next/core/src/types/arguments.ts @@ -57,7 +57,7 @@ export type ArgumentTypeToValueType<T extends ArgumentType> = export interface OptionDefinition<T extends ArgumentType = ArgumentType> { name: string; description: string; - type: ArgumentType; + type: T; defaultValue: ArgumentTypeToValueType<T>; } From 76b414e8315d15ec90f781d87d9b46f0c02ad257 Mon Sep 17 00:00:00 2001 From: Luis Schaab <schaable@gmail.com> Date: Thu, 11 Jul 2024 19:56:02 -0300 Subject: [PATCH 3/3] Validate plain task object on the task manager --- v-next/core/src/internal/tasks/builders.ts | 197 ++---- .../core/src/internal/tasks/task-manager.ts | 42 ++ v-next/core/src/internal/tasks/utils.ts | 6 - v-next/core/src/internal/tasks/validations.ts | 151 +++++ .../core/test/internal/tasks/task-manager.ts | 641 +++++++++++++++++- v-next/core/test/internal/tasks/utils.ts | 17 +- 6 files changed, 848 insertions(+), 206 deletions(-) create mode 100644 v-next/core/src/internal/tasks/validations.ts diff --git a/v-next/core/src/internal/tasks/builders.ts b/v-next/core/src/internal/tasks/builders.ts index 62526ac424..f7989acf93 100644 --- a/v-next/core/src/internal/tasks/builders.ts +++ b/v-next/core/src/internal/tasks/builders.ts @@ -18,13 +18,14 @@ import { HardhatError } from "@ignored/hardhat-vnext-errors"; import { ArgumentType } from "../../types/arguments.js"; import { TaskDefinitionType } from "../../types/tasks.js"; -import { - RESERVED_ARGUMENT_NAMES, - isArgumentValueValid, - isArgumentNameValid, -} from "../arguments.js"; -import { formatTaskId, isValidActionUrl } from "./utils.js"; +import { formatTaskId } from "./utils.js"; +import { + validateAction, + validateId, + validateOption, + validatePositionalArgument, +} from "./validations.js"; export class EmptyTaskDefinitionBuilderImplementation implements EmptyTaskDefinitionBuilder @@ -34,11 +35,7 @@ export class EmptyTaskDefinitionBuilderImplementation #description: string; constructor(id: string | string[], description: string = "") { - if (id.length === 0) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID, - ); - } + validateId(id); this.#id = Array.isArray(id) ? id : [id]; this.#description = description; @@ -72,11 +69,7 @@ export class NewTaskDefinitionBuilderImplementation #action?: NewTaskActionFunction | string; constructor(id: string | string[], description: string = "") { - if (id.length === 0) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID, - ); - } + validateId(id); this.#id = Array.isArray(id) ? id : [id]; this.#description = description; @@ -88,14 +81,7 @@ export class NewTaskDefinitionBuilderImplementation } public setAction(action: NewTaskActionFunction | string): this { - if (typeof action === "string" && !isValidActionUrl(action)) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION, - { - action, - }, - ); - } + validateAction(action); this.#action = action; @@ -115,45 +101,17 @@ export class NewTaskDefinitionBuilderImplementation }): this { const argumentType = type ?? ArgumentType.STRING; - if (!isArgumentNameValid(name)) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, { - name, - }); - } - - if (this.#usedNames.has(name)) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, { - name, - }); - } - - if (RESERVED_ARGUMENT_NAMES.has(name)) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, { - name, - }); - } - - if (!isArgumentValueValid(argumentType, defaultValue)) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE, - { - value: defaultValue, - name: "defaultValue", - type: argumentType, - task: formatTaskId(this.#id), - }, - ); - } - - this.#usedNames.add(name); - - this.#options[name] = { + const optionDefinition = { name, description, type: argumentType, defaultValue, }; + validateOption(optionDefinition, this.#usedNames, this.#id); + + this.#options[name] = optionDefinition; + return this; } @@ -223,69 +181,23 @@ export class NewTaskDefinitionBuilderImplementation }): this { const argumentType = type ?? ArgumentType.STRING; - if (!isArgumentNameValid(name)) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, { - name, - }); - } - - if (this.#usedNames.has(name)) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, { - name, - }); - } - - if (RESERVED_ARGUMENT_NAMES.has(name)) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, { - name, - }); - } - - if (defaultValue !== undefined) { - if (!isArgumentValueValid(argumentType, defaultValue, isVariadic)) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE, - { - value: defaultValue, - name: "defaultValue", - type: argumentType, - task: formatTaskId(this.#id), - }, - ); - } - } - - if (this.#positionalArgs.length > 0) { - const lastArg = this.#positionalArgs[this.#positionalArgs.length - 1]; - - if (lastArg.isVariadic) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.POSITIONAL_ARG_AFTER_VARIADIC, - { - name, - }, - ); - } - - if (lastArg.defaultValue !== undefined && defaultValue === undefined) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.REQUIRED_ARG_AFTER_OPTIONAL, - { - name, - }, - ); - } - } - - this.#usedNames.add(name); - - this.#positionalArgs.push({ + const positionalArgDef = { name, description, type: argumentType, defaultValue, isVariadic, - }); + }; + + const lastArg = this.#positionalArgs.at(-1); + validatePositionalArgument( + positionalArgDef, + this.#usedNames, + this.#id, + lastArg, + ); + + this.#positionalArgs.push(positionalArgDef); return this; } @@ -303,11 +215,7 @@ export class TaskOverrideDefinitionBuilderImplementation #action?: TaskOverrideActionFunction | string; constructor(id: string | string[]) { - if (id.length === 0) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID, - ); - } + validateId(id); this.#id = Array.isArray(id) ? id : [id]; } @@ -318,14 +226,7 @@ export class TaskOverrideDefinitionBuilderImplementation } public setAction(action: TaskOverrideActionFunction | string): this { - if (typeof action === "string" && !isValidActionUrl(action)) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION, - { - action, - }, - ); - } + validateAction(action); this.#action = action; @@ -345,43 +246,21 @@ export class TaskOverrideDefinitionBuilderImplementation }): this { const argumentType = type ?? ArgumentType.STRING; - if (!isArgumentNameValid(name)) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, { - name, - }); - } - - if (name in this.#options) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, { - name, - }); - } - - if (RESERVED_ARGUMENT_NAMES.has(name)) { - throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, { - name, - }); - } - - if (!isArgumentValueValid(argumentType, defaultValue)) { - throw new HardhatError( - HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE, - { - value: defaultValue, - name: "defaultValue", - type: argumentType, - task: formatTaskId(this.#id), - }, - ); - } - - this.#options[name] = { + const optionDefinition = { name, description, type: argumentType, defaultValue, }; + validateOption( + optionDefinition, + new Set(Object.keys(this.#options)), + this.#id, + ); + + this.#options[name] = optionDefinition; + return this; } diff --git a/v-next/core/src/internal/tasks/task-manager.ts b/v-next/core/src/internal/tasks/task-manager.ts index 1bace05869..54cd53d29a 100644 --- a/v-next/core/src/internal/tasks/task-manager.ts +++ b/v-next/core/src/internal/tasks/task-manager.ts @@ -1,3 +1,4 @@ +import type { PositionalArgumentDefinition } from "../../types/arguments.js"; import type { GlobalOptionDefinitions } from "../../types/global-options.js"; import type { HardhatRuntimeEnvironment } from "../../types/hre.js"; import type { @@ -17,6 +18,12 @@ import { TaskDefinitionType } from "../../types/tasks.js"; import { ResolvedTask } from "./resolved-task.js"; import { formatTaskId, getActorFragment } from "./utils.js"; +import { + validateAction, + validateId, + validateOption, + validatePositionalArgument, +} from "./validations.js"; export class TaskManagerImplementation implements TaskManager { readonly #hre: HardhatRuntimeEnvironment; @@ -35,6 +42,7 @@ export class TaskManagerImplementation implements TaskManager { } for (const taskDefinition of plugin.tasks) { + this.#validateTaskDefinition(taskDefinition); this.#reduceTaskDefinition( globalOptionDefinitions, taskDefinition, @@ -45,6 +53,7 @@ export class TaskManagerImplementation implements TaskManager { // reduce global user defined tasks for (const taskDefinition of this.#hre.config.tasks) { + this.#validateTaskDefinition(taskDefinition); this.#reduceTaskDefinition(globalOptionDefinitions, taskDefinition); } } @@ -243,4 +252,37 @@ export class TaskManagerImplementation implements TaskManager { 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>(); + + validateAction(taskDefinition.action); + + 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; + }); + } } diff --git a/v-next/core/src/internal/tasks/utils.ts b/v-next/core/src/internal/tasks/utils.ts index 00c67aea0c..3d35304594 100644 --- a/v-next/core/src/internal/tasks/utils.ts +++ b/v-next/core/src/internal/tasks/utils.ts @@ -9,9 +9,3 @@ export function formatTaskId(taskId: string | string[]): string { export function getActorFragment(pluginId: string | undefined): string { return pluginId !== undefined ? `Plugin ${pluginId} is` : "You are"; } - -const FILE_PROTOCOL_PATTERN = /^file:\/\/.+/; - -export function isValidActionUrl(action: string): boolean { - return FILE_PROTOCOL_PATTERN.test(action); -} diff --git a/v-next/core/src/internal/tasks/validations.ts b/v-next/core/src/internal/tasks/validations.ts new file mode 100644 index 0000000000..4973f96c1f --- /dev/null +++ b/v-next/core/src/internal/tasks/validations.ts @@ -0,0 +1,151 @@ +import type { + ArgumentType, + ArgumentValue, + OptionDefinition, + PositionalArgumentDefinition, +} from "../../types/arguments.js"; + +import { HardhatError } from "@ignored/hardhat-vnext-errors"; + +import { + isArgumentNameValid, + isArgumentValueValid, + RESERVED_ARGUMENT_NAMES, +} from "../arguments.js"; + +import { formatTaskId } from "./utils.js"; + +export function validateId(id: string | string[]): void { + if (id.length === 0) { + throw new HardhatError(HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID); + } +} + +export function validateAction(action: unknown): void { + if (typeof action === "string" && !isValidActionUrl(action)) { + throw new HardhatError( + HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION, + { + action, + }, + ); + } +} + +export function validateOption( + optionDefinition: OptionDefinition, + usedNames: Set<string>, + taskId: string | string[], +): void { + validateArgumentName(optionDefinition.name); + + if (usedNames.has(optionDefinition.name)) { + throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, { + name: optionDefinition.name, + }); + } + + validateArgumentValue({ + name: "defaultValue", + value: optionDefinition.defaultValue, + expectedType: optionDefinition.type, + taskId: formatTaskId(taskId), + }); + + usedNames.add(optionDefinition.name); +} + +export function validatePositionalArgument( + positionalArgDef: PositionalArgumentDefinition, + usedNames: Set<string>, + taskId: string | string[], + lastArg?: PositionalArgumentDefinition, +): void { + validateArgumentName(positionalArgDef.name); + + if (usedNames.has(positionalArgDef.name)) { + throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, { + name: positionalArgDef.name, + }); + } + + if (positionalArgDef.defaultValue !== undefined) { + validateArgumentValue({ + name: "defaultValue", + value: positionalArgDef.defaultValue, + isVariadic: positionalArgDef.isVariadic, + expectedType: positionalArgDef.type, + taskId: formatTaskId(taskId), + }); + } + + if (lastArg !== undefined && lastArg.isVariadic) { + throw new HardhatError( + HardhatError.ERRORS.TASK_DEFINITIONS.POSITIONAL_ARG_AFTER_VARIADIC, + { + name: positionalArgDef.name, + }, + ); + } + + if ( + lastArg !== undefined && + lastArg.defaultValue !== undefined && + positionalArgDef.defaultValue === undefined + ) { + throw new HardhatError( + HardhatError.ERRORS.TASK_DEFINITIONS.REQUIRED_ARG_AFTER_OPTIONAL, + { + name: positionalArgDef.name, + }, + ); + } + + usedNames.add(positionalArgDef.name); +} + +const FILE_PROTOCOL_PATTERN = /^file:\/\/.+/; + +function isValidActionUrl(action: string): boolean { + return FILE_PROTOCOL_PATTERN.test(action); +} + +function validateArgumentName(name: string): void { + if (!isArgumentNameValid(name)) { + throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, { + name, + }); + } + + if (RESERVED_ARGUMENT_NAMES.has(name)) { + throw new HardhatError(HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, { + name, + }); + } +} + +function validateArgumentValue({ + name, + expectedType, + isVariadic = false, + value, + taskId, +}: { + name: string; + expectedType: ArgumentType; + isVariadic?: boolean; + value: ArgumentValue | ArgumentValue[]; + taskId: string | string[]; +}): void { + if (!isArgumentValueValid(expectedType, value, isVariadic)) { + throw new HardhatError( + HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE, + { + value, + name, + type: expectedType, + task: formatTaskId(taskId), + }, + ); + } +} diff --git a/v-next/core/test/internal/tasks/task-manager.ts b/v-next/core/test/internal/tasks/task-manager.ts index 823e44b1f6..ffe46625d5 100644 --- a/v-next/core/test/internal/tasks/task-manager.ts +++ b/v-next/core/test/internal/tasks/task-manager.ts @@ -9,6 +9,7 @@ import { import { ArgumentType, globalOption } from "../../../src/config.js"; import { createBaseHardhatRuntimeEnvironment } from "../../../src/index.js"; +import { RESERVED_ARGUMENT_NAMES } from "../../../src/internal/arguments.js"; import { EmptyTaskDefinitionBuilderImplementation, NewTaskDefinitionBuilderImplementation, @@ -538,31 +539,6 @@ describe("TaskManagerImplementation", () => { }); it("should throw if trying to override a task that doesn't exist", async () => { - // Empty id task will not be found as empty ids are not allowed - await assertRejectsWithHardhatError( - createBaseHardhatRuntimeEnvironment({ - plugins: [ - { - id: "plugin1", - tasks: [ - // Manually creating a task as the builder doesn't allow empty ids - { - type: TaskDefinitionType.TASK_OVERRIDE, - id: [], // empty id - description: "", - action: () => {}, - options: {}, - }, - ], - }, - ], - }), - HardhatError.ERRORS.TASK_DEFINITIONS.TASK_NOT_FOUND, - { - task: "", - }, - ); - // task1 will not be found as it's not defined await assertRejectsWithHardhatError( createBaseHardhatRuntimeEnvironment({ @@ -883,6 +859,621 @@ describe("TaskManagerImplementation", () => { }, ); }); + + describe("plain object validations", () => { + it("should throw if the task definition object has an empty id", async () => { + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.EMPTY_TASK, + id: [], + description: "", + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID, + {}, + ); + + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: [], + description: "", + action: () => {}, + options: {}, + positionalArguments: [], + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID, + {}, + ); + + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.TASK_OVERRIDE, + id: [], + description: "", + action: () => {}, + options: {}, + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.EMPTY_TASK_ID, + {}, + ); + }); + + it("should throw if the task definition object has an invalid action file URL", async () => { + const invalidActionFileUrl = "not-a-valid-file-url"; + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: invalidActionFileUrl, + options: {}, + positionalArguments: [], + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION, + { + action: invalidActionFileUrl, + }, + ); + + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "", + action: invalidActionFileUrl, + options: {}, + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_FILE_ACTION, + { + action: invalidActionFileUrl, + }, + ); + }); + + it("should throw if the task definition object has an option with an invalid name", async () => { + const invalidName = "invalid-name"; + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: { + [invalidName]: { + name: invalidName, + description: "A description", + type: ArgumentType.STRING, + defaultValue: "default", + }, + }, + positionalArguments: [], + }, + ], + }, + ], + }), + HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, + { + name: invalidName, + }, + ); + + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "", + action: () => {}, + options: { + [invalidName]: { + name: invalidName, + description: "A description", + type: ArgumentType.STRING, + defaultValue: "default", + }, + }, + }, + ], + }, + ], + }), + HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, + { + name: invalidName, + }, + ); + }); + + it("should throw if the task definition object has an option with an reserved name", async () => { + RESERVED_ARGUMENT_NAMES.forEach(async (reservedName) => { + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: { + [reservedName]: { + name: reservedName, + description: "A description", + type: ArgumentType.STRING, + defaultValue: "default", + }, + }, + positionalArguments: [], + }, + ], + }, + ], + }), + HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, + { + name: reservedName, + }, + ); + + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "", + action: () => {}, + options: { + [reservedName]: { + name: reservedName, + description: "A description", + type: ArgumentType.STRING, + defaultValue: "default", + }, + }, + }, + ], + }, + ], + }), + HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, + { + name: reservedName, + }, + ); + }); + }); + + it("should throw if the task definition object has arguments with an duplicated name", async () => { + const duplicatedName = "duplicatedName"; + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: { + [duplicatedName]: { + name: duplicatedName, + description: "A description", + type: ArgumentType.STRING, + defaultValue: "default", + }, + }, + positionalArguments: [ + { + name: duplicatedName, + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + }, + ], + }, + ], + }, + ], + }), + HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, + { + name: duplicatedName, + }, + ); + + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: {}, + positionalArguments: [ + { + name: duplicatedName, + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + }, + { + name: duplicatedName, + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + }, + ], + }, + ], + }, + ], + }), + HardhatError.ERRORS.ARGUMENTS.DUPLICATED_NAME, + { + name: duplicatedName, + }, + ); + }); + + it("should throw if the task definition object has an option with an invalid type for it's default value", async () => { + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: { + arg: { + name: "optionName", + description: "A description", + type: ArgumentType.STRING, + defaultValue: 1, + }, + }, + positionalArguments: [], + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE, + { + value: 1, + name: "defaultValue", + type: ArgumentType.STRING, + task: "task-id", + }, + ); + + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.TASK_OVERRIDE, + id: ["task-id"], + description: "", + action: () => {}, + options: { + arg: { + name: "optionName", + description: "A description", + type: ArgumentType.STRING, + defaultValue: 1, + }, + }, + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE, + { + value: 1, + name: "defaultValue", + type: ArgumentType.STRING, + task: "task-id", + }, + ); + }); + + it("should throw if the task definition object has a positional argument with an invalid name", async () => { + const invalidName = "invalid-name"; + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: {}, + positionalArguments: [ + { + name: invalidName, + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + }, + ], + }, + ], + }, + ], + }), + HardhatError.ERRORS.ARGUMENTS.INVALID_NAME, + { + name: invalidName, + }, + ); + }); + + it("should throw if the task definition object has a positional argument with an reserved name", async () => { + RESERVED_ARGUMENT_NAMES.forEach(async (reservedName) => { + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: {}, + positionalArguments: [ + { + name: reservedName, + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + }, + ], + }, + ], + }, + ], + }), + HardhatError.ERRORS.ARGUMENTS.RESERVED_NAME, + { + name: reservedName, + }, + ); + }); + }); + + it("should throw if the task definition object has a positional argument with an invalid type for it's default value", async () => { + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: {}, + positionalArguments: [ + { + name: "posArg", + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + defaultValue: 1, + }, + ], + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE, + { + value: 1, + name: "defaultValue", + type: ArgumentType.STRING, + task: "task-id", + }, + ); + }); + + it("should throw if the task definition object has a positional variadic argument with an invalid type for it's default value", async () => { + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: {}, + positionalArguments: [ + { + name: "posArg", + description: "A description", + type: ArgumentType.STRING, + isVariadic: true, + defaultValue: [1], + }, + ], + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE, + { + value: [1], + name: "defaultValue", + type: ArgumentType.STRING, + task: "task-id", + }, + ); + }); + + it("should throw if the task definition object has a positional argument after a variadic argument", async () => { + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: {}, + positionalArguments: [ + { + name: "posArg", + description: "A description", + type: ArgumentType.STRING, + isVariadic: true, + defaultValue: ["default"], + }, + { + name: "posArg2", + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + }, + ], + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.POSITIONAL_ARG_AFTER_VARIADIC, + { + name: "posArg2", + }, + ); + }); + + it("should throw if the task definition object has a required positional argument after an optional argument", async () => { + await assertRejectsWithHardhatError( + createBaseHardhatRuntimeEnvironment({ + plugins: [ + { + id: "plugin1", + tasks: [ + { + type: TaskDefinitionType.NEW_TASK, + id: ["task-id"], + description: "", + action: () => {}, + options: {}, + positionalArguments: [ + { + name: "posArg", + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + defaultValue: "default", + }, + { + name: "posArg2", + description: "A description", + type: ArgumentType.STRING, + isVariadic: false, + }, + ], + }, + ], + }, + ], + }), + HardhatError.ERRORS.TASK_DEFINITIONS.REQUIRED_ARG_AFTER_OPTIONAL, + { + name: "posArg2", + }, + ); + }); + }); }); describe("getTask", () => { diff --git a/v-next/core/test/internal/tasks/utils.ts b/v-next/core/test/internal/tasks/utils.ts index 20ce34fa16..5b1bf7afc5 100644 --- a/v-next/core/test/internal/tasks/utils.ts +++ b/v-next/core/test/internal/tasks/utils.ts @@ -1,10 +1,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import { - formatTaskId, - isValidActionUrl, -} from "../../../src/internal/tasks/utils.js"; +import { formatTaskId } from "../../../src/internal/tasks/utils.js"; describe("Task utils", () => { describe("formatTaskId", () => { @@ -16,16 +13,4 @@ describe("Task utils", () => { assert.equal(formatTaskId(["foo", "bar"]), "foo bar"); }); }); - - describe("isValidActionUrl", () => { - it("should return true if the action is a file URL", () => { - assert.equal(isValidActionUrl("file://foo"), true); - }); - - it("should return false if the action is not a file URL", () => { - assert.equal(isValidActionUrl("http://foo"), false); - assert.equal(isValidActionUrl("file://"), false); - assert.equal(isValidActionUrl("missing-protocol"), false); - }); - }); });