Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make options optional #5499

Merged
merged 2 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions v-next/core/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ArgumentTypeToValueType } from "./types/arguments.js";
import type {
ArgumentTypeToValueType,
OptionDefinition,
} from "./types/arguments.js";
import type { ConfigurationVariable } from "./types/config.js";
import type { GlobalOptionDefinition } from "./types/global-options.js";
import type {
EmptyTaskDefinitionBuilder,
NewTaskDefinitionBuilder,
Expand Down Expand Up @@ -63,7 +65,7 @@ export function globalOption<T extends ArgumentType>(options: {
description: string;
type?: T;
defaultValue: ArgumentTypeToValueType<T>;
}): GlobalOptionDefinition {
}): OptionDefinition {
return buildGlobalOptionDefinition(options);
}

Expand All @@ -73,7 +75,7 @@ export function globalOption<T extends ArgumentType>(options: {
export function globalFlag(options: {
name: string;
description: string;
}): GlobalOptionDefinition {
}): OptionDefinition {
return buildGlobalOptionDefinition({
...options,
type: ArgumentType.BOOLEAN,
Expand Down
4 changes: 2 additions & 2 deletions v-next/core/src/internal/global-options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type {
ArgumentTypeToValueType,
ArgumentValue,
OptionDefinition,
} from "../types/arguments.js";
import type {
GlobalOptions,
GlobalOptionDefinition,
GlobalOptionDefinitions,
} from "../types/global-options.js";
import type { HardhatPlugin } from "../types/plugins.js";
Expand Down Expand Up @@ -80,7 +80,7 @@ export function buildGlobalOptionDefinition<T extends ArgumentType>({
description: string;
type?: T;
defaultValue: ArgumentTypeToValueType<T>;
}): GlobalOptionDefinition {
}): OptionDefinition {
const argumentType = type ?? ArgumentType.STRING;

if (!isArgumentNameValid(name)) {
Expand Down
28 changes: 12 additions & 16 deletions v-next/core/src/internal/tasks/builders.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { ArgumentTypeToValueType } from "../../types/arguments.js";
import type {
TaskOptionDefinition,
ArgumentTypeToValueType,
OptionDefinition,
PositionalArgumentDefinition,
} from "../../types/arguments.js";
import type {
NewTaskActionFunction,
NewTaskDefinitionBuilder,
NewTaskDefinition,
TaskPositionalArgumentDefinition,
TaskOverrideActionFunction,
TaskOverrideDefinitionBuilder,
TaskOverrideDefinition,
Expand Down Expand Up @@ -62,8 +64,8 @@ export class NewTaskDefinitionBuilderImplementation
readonly #id: string[];
readonly #usedNames: Set<string> = new Set();

readonly #options: Record<string, TaskOptionDefinition> = {};
readonly #positionalArgs: TaskPositionalArgumentDefinition[] = [];
readonly #options: Record<string, OptionDefinition> = {};
readonly #positionalArgs: PositionalArgumentDefinition[] = [];

#description: string;

Expand Down Expand Up @@ -109,7 +111,7 @@ export class NewTaskDefinitionBuilderImplementation
name: string;
description?: string;
type?: T;
defaultValue?: ArgumentTypeToValueType<T>;
defaultValue: ArgumentTypeToValueType<T>;
}): this {
const argumentType = type ?? ArgumentType.STRING;

Expand All @@ -131,10 +133,7 @@ export class NewTaskDefinitionBuilderImplementation
});
}

if (
defaultValue !== undefined &&
!isArgumentValueValid(argumentType, defaultValue)
) {
if (!isArgumentValueValid(argumentType, defaultValue)) {
throw new HardhatError(
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
{
Expand Down Expand Up @@ -297,7 +296,7 @@ export class TaskOverrideDefinitionBuilderImplementation
{
readonly #id: string[];

readonly #options: Record<string, TaskOptionDefinition> = {};
readonly #options: Record<string, OptionDefinition> = {};

#description?: string;

Expand Down Expand Up @@ -342,7 +341,7 @@ export class TaskOverrideDefinitionBuilderImplementation
name: string;
description?: string;
type?: T;
defaultValue?: ArgumentTypeToValueType<T>;
defaultValue: ArgumentTypeToValueType<T>;
}): this {
const argumentType = type ?? ArgumentType.STRING;

Expand All @@ -364,10 +363,7 @@ export class TaskOverrideDefinitionBuilderImplementation
});
}

if (
defaultValue !== undefined &&
!isArgumentValueValid(argumentType, defaultValue)
) {
if (!isArgumentValueValid(argumentType, defaultValue)) {
throw new HardhatError(
HardhatError.ERRORS.TASK_DEFINITIONS.INVALID_VALUE_FOR_TYPE,
{
Expand Down
55 changes: 30 additions & 25 deletions v-next/core/src/internal/tasks/resolved-task.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { ArgumentValue } from "../../types/arguments.js";
import type {
ArgumentValue,
OptionDefinition,
PositionalArgumentDefinition,
} from "../../types/arguments.js";
import type { HardhatRuntimeEnvironment } from "../../types/hre.js";
import type {
TaskOptionDefinition,
NewTaskActionFunction,
TaskPositionalArgumentDefinition,
Task,
TaskActions,
TaskArguments,
TaskOverrideActionFunction,
TaskArgumentDefinition,
} from "../../types/tasks.js";

import {
Expand Down Expand Up @@ -48,8 +49,8 @@ export class ResolvedTask implements Task {
id: string[],
description: string,
action: NewTaskActionFunction | string,
options: Record<string, TaskOptionDefinition>,
positionalArguments: TaskPositionalArgumentDefinition[],
options: Record<string, OptionDefinition>,
positionalArguments: PositionalArgumentDefinition[],
pluginId?: string,
): ResolvedTask {
return new ResolvedTask(
Expand All @@ -68,8 +69,8 @@ export class ResolvedTask implements Task {
public readonly id: string[],
public readonly description: string,
public readonly actions: TaskActions,
public readonly options: Map<string, TaskOptionDefinition>,
public readonly positionalArguments: TaskPositionalArgumentDefinition[],
public readonly options: Map<string, OptionDefinition>,
public readonly positionalArguments: PositionalArgumentDefinition[],
public readonly pluginId: string | undefined,
public readonly subtasks: Map<string, Task>,
hre: HardhatRuntimeEnvironment,
Expand Down Expand Up @@ -98,25 +99,34 @@ export class ResolvedTask implements Task {
});
}

// Normalize arguments into a single iterable
const allArguments: TaskArgumentDefinition[] = [
...this.options.values(),
...this.positionalArguments,
];

const providedArgumentNames = new Set(Object.keys(taskArguments));
for (const argument of allArguments) {

// 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;
Copy link
Member

@alcuadrado alcuadrado Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't modify this input here, as that can be a user object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, shouldn't we validate the type after resolving the default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be addressed in a follow-up PR.

}

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);
this.#validateArgumentType(argument, value, argument.isVariadic);

// resolve defaults for optional arguments
if (value === undefined && argument.defaultValue !== undefined) {
taskArguments[argument.name] = argument.defaultValue;
}

// Remove processed argument from the set
providedArgumentNames.delete(argument.name);
}

Expand Down Expand Up @@ -166,7 +176,7 @@ export class ResolvedTask implements Task {
* @throws HardhatError if the argument is required and doesn't have a value.
*/
#validateRequiredArgument(
argument: TaskArgumentDefinition,
argument: PositionalArgumentDefinition,
value: ArgumentValue | ArgumentValue[],
) {
if (argument.defaultValue === undefined && value === undefined) {
Expand All @@ -188,20 +198,15 @@ export class ResolvedTask implements Task {
* @throws HardhatError if the argument has an invalid type.
*/
#validateArgumentType(
argument: TaskArgumentDefinition,
argument: OptionDefinition | PositionalArgumentDefinition,
value: ArgumentValue | ArgumentValue[],
isVariadic: boolean = false,
) {
// skip type validation for optional arguments with undefined value
if (value === undefined && argument.defaultValue !== undefined) {
return;
}

// check if the argument is variadic
const isPositionalArgument = (
arg: TaskArgumentDefinition,
): arg is TaskPositionalArgumentDefinition => "isVariadic" in arg;
const isVariadic = isPositionalArgument(argument) && argument.isVariadic;

// check if the value is valid for the argument type
if (!isArgumentValueValid(argument.type, value, isVariadic)) {
throw new HardhatError(
Expand Down
34 changes: 34 additions & 0 deletions v-next/core/src/types/arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,37 @@ export type ArgumentValue = ArgumentValueTypes[keyof ArgumentValueTypes];
*/
export type ArgumentTypeToValueType<T extends ArgumentType> =
ArgumentValueTypes[T];

/**
* Options in CLI are specified as `--<name> value`, where `--<name>` is the
* option's name and `value` is the argument it takes. For example,
* `--network mainnet` sets the network option to `mainnet`.
*
* Options can also be flags, which are boolean options that don't take an
* argument. For example, `--help` is a flag that shows the help message.
*
* Options are always optional and can be provided in any order.
*/
export interface OptionDefinition<T extends ArgumentType = ArgumentType> {
name: string;
description: string;
type: ArgumentType;
defaultValue: ArgumentTypeToValueType<T>;
}

/**
* A positional argument is used as `<value>` in the CLI, where its position
* matters. For example, `mv <from> <to>` has two positional arguments.
*
* If the argument is variadic, it accepts multiple values. A variadic argument
* must be the last positional argument and consumes all remaining values.
*/
export interface PositionalArgumentDefinition<
T extends ArgumentType = ArgumentType,
> {
name: string;
description: string;
type: T;
defaultValue?: ArgumentTypeToValueType<T> | Array<ArgumentTypeToValueType<T>>;
isVariadic: boolean;
}
34 changes: 12 additions & 22 deletions v-next/core/src/types/global-options.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
import type { ArgumentType, ArgumentTypeToValueType } from "./arguments.js";
import type { OptionDefinition } from "./arguments.js";

/**
* A global option with an associated value and a default if not provided by
* the user. They are available in the Hardhat Runtime Environment.
* The values of each global option for a certain instance of the Hardhat
* Runtime Environment are defined here. This interface can be extended through
* module augmentation to include additional global options as needed.
*
* They can be provided in these different ways:
* 1. Through environment variables, with the format
* `HARDHAT_<OPTION_NAME_WITH_THIS_CASING>`.
* 2. Through the CLI with the format `--<option-name-with-this-casing> <value>`.
* 2.1. Through the CLI with the format `--<option-name-with-this-casing>` if
* the option is boolean and its default value is `false`.
* Global options can be provided in several ways and are accessible through
* the Hardhat Runtime Environment:
* 1. Environment variables: `HARDHAT_<OPTION_NAME_WITH_THIS_CASING>`
* 2. CLI arguments:
* - `--<option-name-with-this-casing> <value>` for options with values
* - `--<option-name-with-this-casing>` for boolean options with a false default
*
* If both are present, the second one takes precedence.
*/
export interface GlobalOptionDefinition<T extends ArgumentType = ArgumentType> {
name: string;
description: string;
type: ArgumentType;
defaultValue: ArgumentTypeToValueType<T>;
}

/**
* The values of each global option for a certain instance of the Hardhat
* Runtime Environment.
* CLI arguments take precedence over environment variables.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- To be used through module augmentation
export interface GlobalOptions {}
Expand All @@ -33,7 +23,7 @@ export interface GlobalOptions {}
*/
export interface GlobalOptionDefinitionsEntry {
pluginId: string;
option: GlobalOptionDefinition;
option: OptionDefinition;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions v-next/core/src/types/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { GlobalOptionDefinition } from "./global-options.js";
import type { OptionDefinition } from "./arguments.js";
import type { HardhatHooks } from "./hooks.js";
import type { TaskDefinition } from "./tasks.js";

Expand Down Expand Up @@ -67,7 +67,7 @@ export interface HardhatPlugin {
/**
* An array of the global options that this plugin defines.
*/
globalOptions?: GlobalOptionDefinition[];
globalOptions?: OptionDefinition[];

/**
* An array of type definitions, which should be created using their builders.
Expand Down
Loading