Skip to content

Commit f5a835d

Browse files
authored
Merge pull request #6942 from NomicFoundation/level-flag
feat: added FLAG and LEVEL arguments types
2 parents 330acbb + 5b4d4a0 commit f5a835d

File tree

20 files changed

+503
-100
lines changed

20 files changed

+503
-100
lines changed

.changeset/long-bats-nail.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"hardhat": patch
3+
"@nomicfoundation/hardhat-typechain": patch
4+
---
5+
6+
Added FLAG and LEVEL arguments types

v-next/hardhat-typechain/src/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { HardhatPlugin } from "hardhat/types/plugins";
22

33
import "./type-extensions.js";
4-
import { globalOption } from "hardhat/config";
5-
import { ArgumentType } from "hardhat/types/arguments";
4+
import { globalFlag } from "hardhat/config";
65

76
const hardhatTypechain: HardhatPlugin = {
87
id: "hardhat-typechain",
@@ -15,11 +14,9 @@ const hardhatTypechain: HardhatPlugin = {
1514
async () => (await import("@nomicfoundation/hardhat-ethers")).default,
1615
],
1716
globalOptions: [
18-
globalOption({
17+
globalFlag({
1918
name: "noTypechain",
2019
description: "Disables the typechain type generation",
21-
defaultValue: false,
22-
type: ArgumentType.BOOLEAN,
2320
}),
2421
],
2522
};

v-next/hardhat/src/internal/builtin-global-options.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { GlobalOptionDefinitions } from "../types/global-options.js";
22

3-
import { globalOption } from "../config.js";
3+
import { globalFlag, globalOption } from "../config.js";
44
import { ArgumentType } from "../types/arguments.js";
55

66
export const BUILTIN_GLOBAL_OPTIONS_DEFINITIONS: GlobalOptionDefinitions =
@@ -21,60 +21,50 @@ export const BUILTIN_GLOBAL_OPTIONS_DEFINITIONS: GlobalOptionDefinitions =
2121
"help",
2222
{
2323
pluginId: "builtin",
24-
option: globalOption({
24+
option: globalFlag({
2525
name: "help",
2626
description:
2727
"Shows this message, or a task's help if its name is provided.",
28-
type: ArgumentType.BOOLEAN,
29-
defaultValue: false,
3028
}),
3129
},
3230
],
3331
[
3432
"init",
3533
{
3634
pluginId: "builtin",
37-
option: globalOption({
35+
option: globalFlag({
3836
name: "init",
3937
description: "Initializes a Hardhat project.",
40-
type: ArgumentType.BOOLEAN,
41-
defaultValue: false,
4238
}),
4339
},
4440
],
4541
[
4642
"showStackTraces",
4743
{
4844
pluginId: "builtin",
49-
option: globalOption({
45+
option: globalFlag({
5046
name: "showStackTraces",
5147
description: "Show stack traces (always enabled on CI servers).",
52-
type: ArgumentType.BOOLEAN,
53-
defaultValue: false,
5448
}),
5549
},
5650
],
5751
[
5852
"verbose",
5953
{
6054
pluginId: "builtin",
61-
option: globalOption({
55+
option: globalFlag({
6256
name: "verbose",
6357
description: "Enables Hardhat verbose logging.",
64-
type: ArgumentType.BOOLEAN,
65-
defaultValue: false,
6658
}),
6759
},
6860
],
6961
[
7062
"version",
7163
{
7264
pluginId: "builtin",
73-
option: globalOption({
65+
option: globalFlag({
7466
name: "version",
7567
description: "Shows hardhat's version.",
76-
type: ArgumentType.BOOLEAN,
77-
defaultValue: false,
7868
}),
7969
},
8070
],

v-next/hardhat/src/internal/builtin-plugins/coverage/index.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import type { HardhatPlugin } from "../../../types/plugins.js";
22

3-
import { ArgumentType } from "../../../types/arguments.js";
4-
import { globalOption } from "../../core/config.js";
3+
import { globalFlag } from "../../core/config.js";
54

65
import "./type-extensions.js";
76

87
const hardhatPlugin: HardhatPlugin = {
98
id: "builtin:coverage",
109
tasks: [],
1110
globalOptions: [
12-
globalOption({
11+
globalFlag({
1312
name: "coverage",
1413
description: "Enables code coverage",
15-
type: ArgumentType.BOOLEAN,
16-
defaultValue: false,
1714
}),
1815
],
1916
hookHandlers: {

v-next/hardhat/src/internal/cli/help/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,15 @@ export function getUsageString(
163163
if (options.length > 0) {
164164
output += ` ${options
165165
.sort((a, b) => a.name.localeCompare(b.name))
166-
.map((o) => `[${o.name}${o.type === "BOOLEAN" ? "" : ` <${o.type}>`}]`)
166+
.map((o) => {
167+
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- We want to explicitly handle all the other types via the default case
168+
switch (o.type) {
169+
case "FLAG":
170+
return `[${o.name}]`;
171+
default:
172+
return `[${o.name} <${o.type}>]`;
173+
}
174+
})
167175
.join(" ")}`;
168176
}
169177

v-next/hardhat/src/internal/cli/main.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -543,9 +543,14 @@ function parseOptions(
543543
const optionName = optionDefinition.name;
544544

545545
// Check if the short name is valid again now that we know its type
546+
// E.g. --flag --flag
547+
const optionAlreadyProvided = providedArguments[optionName] !== undefined;
548+
// E.g. -ff
549+
const shortOptionGroupedAndRepeated = providedByShortName && arg.length > 2;
550+
const isLevelOption = optionDefinition.type === ArgumentType.LEVEL;
546551
if (
547-
providedArguments[optionName] !== undefined ||
548-
(providedByShortName && arg.length > 2)
552+
optionAlreadyProvided ||
553+
(shortOptionGroupedAndRepeated && !isLevelOption)
549554
) {
550555
throw new HardhatError(
551556
HardhatError.ERRORS.CORE.ARGUMENTS.CANNOT_REPEAT_OPTIONS,
@@ -558,30 +563,15 @@ function parseOptions(
558563

559564
usedCliArguments[i] = true;
560565

561-
if (optionDefinition.type === ArgumentType.BOOLEAN) {
562-
if (
563-
usedCliArguments[i + 1] !== undefined &&
564-
usedCliArguments[i + 1] === false &&
565-
(cliArguments[i + 1] === "true" || cliArguments[i + 1] === "false")
566-
) {
567-
// The argument could be followed by a boolean value if it does not
568-
// behaves like a flag
569-
providedArguments[optionName] = parseArgumentValue(
570-
cliArguments[i + 1],
571-
ArgumentType.BOOLEAN,
572-
optionName,
573-
);
574-
575-
usedCliArguments[i + 1] = true;
576-
continue;
577-
}
578-
579-
if (optionDefinition.defaultValue === false) {
580-
// If the default value for the argument is false, the argument behaves
581-
// like a flag, so there is no need to specify the value
582-
providedArguments[optionName] = true;
583-
continue;
584-
}
566+
if (optionDefinition.type === ArgumentType.FLAG) {
567+
providedArguments[optionName] = true;
568+
continue;
569+
} else if (
570+
optionDefinition.type === ArgumentType.LEVEL &&
571+
providedByShortName
572+
) {
573+
providedArguments[optionName] = arg.length - 1;
574+
continue;
585575
} else if (
586576
usedCliArguments[i + 1] !== undefined &&
587577
usedCliArguments[i + 1] === false

v-next/hardhat/src/internal/core/arguments.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ const argumentTypeValidators: Record<
143143
[ArgumentType.STRING]: (value): value is string => typeof value === "string",
144144
[ArgumentType.BOOLEAN]: (value): value is boolean =>
145145
typeof value === "boolean",
146+
[ArgumentType.FLAG]: (value): value is boolean => typeof value === "boolean",
146147
[ArgumentType.INT]: (value): value is number => Number.isInteger(value),
148+
[ArgumentType.LEVEL]: (value): value is number =>
149+
Number.isInteger(value) && Number(value) >= 0,
147150
[ArgumentType.BIGINT]: (value): value is bigint => typeof value === "bigint",
148151
[ArgumentType.FLOAT]: (value): value is number => typeof value === "number",
149152
[ArgumentType.FILE]: (value): value is string => typeof value === "string",
@@ -173,12 +176,21 @@ export function parseArgumentValue(
173176
return value;
174177
case ArgumentType.INT:
175178
return validateAndParseInt(name, value);
179+
case ArgumentType.LEVEL:
180+
return validateAndParseLevel(name, value);
176181
case ArgumentType.FLOAT:
177182
return validateAndParseFloat(name, value);
178183
case ArgumentType.BIGINT:
179184
return validateAndParseBigInt(name, value);
180185
case ArgumentType.BOOLEAN:
181186
return validateAndParseBoolean(name, value);
187+
case ArgumentType.FLAG:
188+
throw new HardhatError(
189+
HardhatError.ERRORS.CORE.INTERNAL.ASSERTION_ERROR,
190+
{
191+
message: "Flags should never accept values",
192+
},
193+
);
182194
}
183195
}
184196

@@ -200,6 +212,23 @@ function validateAndParseInt(name: string, value: string): number {
200212
return Number(value);
201213
}
202214

215+
function validateAndParseLevel(name: string, value: string): number {
216+
const decimalPattern = /^\d+$/;
217+
218+
if (!decimalPattern.test(value) || Number(value) < 0) {
219+
throw new HardhatError(
220+
HardhatError.ERRORS.CORE.ARGUMENTS.INVALID_VALUE_FOR_TYPE,
221+
{
222+
value,
223+
name,
224+
type: ArgumentType.LEVEL,
225+
},
226+
);
227+
}
228+
229+
return Number(value);
230+
}
231+
203232
function validateAndParseFloat(name: string, value: string): number {
204233
const decimalPattern = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE]\d+)?$/;
205234
const hexPattern = /^0[xX][\dABCDEabcde]+$/;

v-next/hardhat/src/internal/core/config-validation.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
} from "../../types/hooks.js";
99
import type { HardhatPlugin } from "../../types/plugins.js";
1010

11+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
1112
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
1213

1314
import {
@@ -409,6 +410,7 @@ export function validateOptions(
409410
}
410411
break;
411412
}
413+
case ArgumentType.FLAG:
412414
case ArgumentType.BOOLEAN: {
413415
if (typeof option.defaultValue !== "boolean") {
414416
validationErrors.push({
@@ -428,6 +430,17 @@ export function validateOptions(
428430
}
429431
break;
430432
}
433+
case ArgumentType.LEVEL:
434+
if (
435+
typeof option.defaultValue !== "number" ||
436+
option.defaultValue < 0
437+
) {
438+
validationErrors.push({
439+
path: [...path, name, "defaultValue"],
440+
message: "option defaultValue must be a non-negative number",
441+
});
442+
}
443+
break;
431444
case ArgumentType.BIGINT: {
432445
if (typeof option.defaultValue !== "bigint") {
433446
validationErrors.push({
@@ -538,6 +551,14 @@ export function validatePositionalArguments(
538551

539552
break;
540553
}
554+
case ArgumentType.FLAG:
555+
case ArgumentType.LEVEL:
556+
throw new HardhatError(
557+
HardhatError.ERRORS.CORE.INTERNAL.ASSERTION_ERROR,
558+
{
559+
message: `Argument type ${arg.type} cannot be used as a positional argument`,
560+
},
561+
);
541562
}
542563
}
543564

v-next/hardhat/src/internal/core/config.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,22 @@ export function globalFlag(options: {
8989
}): OptionDefinition {
9090
return buildGlobalOptionDefinition({
9191
...options,
92-
type: ArgumentType.BOOLEAN,
92+
type: ArgumentType.FLAG,
9393
defaultValue: false,
9494
});
9595
}
96+
97+
/**
98+
* Defines a global level.
99+
*/
100+
export function globalLevel(options: {
101+
name: string;
102+
shortName?: string;
103+
description: string;
104+
}): OptionDefinition {
105+
return buildGlobalOptionDefinition({
106+
...options,
107+
type: ArgumentType.LEVEL,
108+
defaultValue: 0,
109+
});
110+
}

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,29 @@ export class NewTaskDefinitionBuilderImplementation<
128128
shortName?: string;
129129
description?: string;
130130
}): NewTaskDefinitionBuilder<
131-
ExtendTaskArguments<NameT, ArgumentType.BOOLEAN, TaskArgumentsT>
131+
ExtendTaskArguments<NameT, ArgumentType.FLAG, TaskArgumentsT>
132132
> {
133133
return this.addOption({
134134
...flagConfig,
135-
type: ArgumentType.BOOLEAN,
135+
type: ArgumentType.FLAG,
136136
defaultValue: false,
137137
});
138138
}
139139

140+
public addLevel<NameT extends string>(levelConfig: {
141+
name: NameT;
142+
shortName?: string;
143+
description?: string;
144+
}): NewTaskDefinitionBuilder<
145+
ExtendTaskArguments<NameT, ArgumentType.LEVEL, TaskArgumentsT>
146+
> {
147+
return this.addOption({
148+
...levelConfig,
149+
type: ArgumentType.LEVEL,
150+
defaultValue: 0,
151+
});
152+
}
153+
140154
public addPositionalArgument<
141155
NameT extends string,
142156
TypeT extends ArgumentType = ArgumentType.STRING,
@@ -320,15 +334,29 @@ export class TaskOverrideDefinitionBuilderImplementation<
320334
shortName?: string;
321335
description?: string;
322336
}): TaskOverrideDefinitionBuilder<
323-
ExtendTaskArguments<NameT, ArgumentType.BOOLEAN, TaskArgumentsT>
337+
ExtendTaskArguments<NameT, ArgumentType.FLAG, TaskArgumentsT>
324338
> {
325339
return this.addOption({
326340
...flagConfig,
327-
type: ArgumentType.BOOLEAN,
341+
type: ArgumentType.FLAG,
328342
defaultValue: false,
329343
});
330344
}
331345

346+
public addLevel<NameT extends string>(levelConfig: {
347+
name: string;
348+
shortName?: string;
349+
description?: string;
350+
}): TaskOverrideDefinitionBuilder<
351+
ExtendTaskArguments<NameT, ArgumentType.LEVEL, TaskArgumentsT>
352+
> {
353+
return this.addOption({
354+
...levelConfig,
355+
type: ArgumentType.LEVEL,
356+
defaultValue: 0,
357+
});
358+
}
359+
332360
public build(): TaskOverrideDefinition {
333361
if (this.#action === undefined) {
334362
throw new HardhatError(

0 commit comments

Comments
 (0)