Skip to content

Commit

Permalink
Merge branch 'v-next' into galargh/test-reporter-tests-plus
Browse files Browse the repository at this point in the history
  • Loading branch information
galargh authored Aug 30, 2024
2 parents 6194cd5 + 190fc6f commit afe66f4
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 88 deletions.
14 changes: 13 additions & 1 deletion config-v-next/eslint.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ function createConfig(
message:
"Don't use assert.doesNotReject. Just await the async-function-call/promise directly, letting the error bubble up if rejected",
},
{
selector:
"CallExpression[callee.object.name='z'][callee.property.name=union]",
message:
"Use the unionType helper from the zod utils package instead, as it provides better error messages.",
},
],
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/restrict-template-expressions": [
Expand Down Expand Up @@ -388,6 +394,12 @@ function createConfig(
],
message: "Use node:assert/strict instead.",
},
{
name: "zod",
importNames: ["union"],
message:
"Use the unionType helper from the zod utils package instead, as it provides better error messages.",
},
],
},
],
Expand All @@ -414,7 +426,7 @@ function createConfig(
files: ["src/**/*.ts"],
rules: {
"no-restricted-syntax": [
"error",
...config.rules["no-restricted-syntax"],
{
// This is a best effor selector that forbids every throw unless it's a `new HardhatError`,
// or throwing a variable within a catch clause.
Expand Down
39 changes: 29 additions & 10 deletions v-next/hardhat-mocha-test-runner/src/hookHandlers/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { ConfigHooks } from "@ignored/hardhat-vnext/types/hooks";

import { validateUserConfigZodType } from "@ignored/hardhat-vnext-zod-utils";
import {
unionType,
validateUserConfigZodType,
} from "@ignored/hardhat-vnext-zod-utils";
import { z } from "zod";

const mochaConfigType = z.object({
Expand All @@ -27,23 +30,39 @@ const mochaConfigType = z.object({
reporterOptions: z.any().optional(),
retries: z.number().optional(),
slow: z.number().optional(),
timeout: z.union([z.number(), z.string()]).optional(),
ui: z
.union([
timeout: unionType(
[z.number(), z.string()],
"Expected a number or a string",
).optional(),
ui: unionType(
[
z.literal("bdd"),
z.literal("tdd"),
z.literal("qunit"),
z.literal("exports"),
])
.optional(),
],
'Expected "bdd", "tdd", "qunit" or "exports"',
).optional(),
parallel: z.boolean().optional(),
jobs: z.number().optional(),
rootHooks: z
.object({
afterAll: z.union([z.function(), z.array(z.function())]).optional(),
beforeAll: z.union([z.function(), z.array(z.function())]).optional(),
afterEach: z.union([z.function(), z.array(z.function())]).optional(),
beforeEach: z.union([z.function(), z.array(z.function())]).optional(),
afterAll: unionType(
[z.function(), z.array(z.function())],
"Expected a function or an array of functions",
).optional(),
beforeAll: unionType(
[z.function(), z.array(z.function())],
"Expected a function or an array of functions",
).optional(),
afterEach: unionType(
[z.function(), z.array(z.function())],
"Expected a function or an array of functions",
).optional(),
beforeEach: unionType(
[z.function(), z.array(z.function())],
"Expected a function or an array of functions",
).optional(),
})
.optional(),
require: z.array(z.string()).optional(),
Expand Down
104 changes: 33 additions & 71 deletions v-next/hardhat-zod-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,47 @@
import type { HardhatUserConfig } from "@ignored/hardhat-vnext-core/config";
import type { HardhatUserConfigValidationError } from "@ignored/hardhat-vnext-core/types/hooks";
import type { ZodType, ZodTypeDef, ZodIssue } from "zod";
import type { ZodType, ZodTypeDef } from "zod";

import { z } from "zod";

/**
* A Zod untagged union type that returns a custom error message if the value
* is missing or invalid.
*/
export const unionType = (
types: Parameters<typeof z.union>[0],
errorMessage: string,
) =>
// eslint-disable-next-line no-restricted-syntax -- This is the only place we allow z.union
z.union(types, {
errorMap: () => ({
message: errorMessage,
}),
});

/**
* A Zod type to validate Hardhat's ConfigurationVariable objects.
*/
export const configurationVariableType: z.ZodObject<
{
_type: z.ZodLiteral<"ConfigurationVariable">;
name: z.ZodString;
},
"strip",
z.ZodTypeAny,
{
_type: "ConfigurationVariable";
name: string;
},
{
_type: "ConfigurationVariable";
name: string;
}
> = z.object({
export const configurationVariableType = z.object({
_type: z.literal("ConfigurationVariable"),
name: z.string(),
});

/**
* A Zod type to validate Hardhat's SensitiveString values.
*/
export const sensitiveStringType: z.ZodUnion<
[
z.ZodString,
z.ZodObject<
{
_type: z.ZodLiteral<"ConfigurationVariable">;
name: z.ZodString;
},
"strip",
z.ZodTypeAny,
{
_type: "ConfigurationVariable";
name: string;
},
{
_type: "ConfigurationVariable";
name: string;
}
>,
]
> = z.union([z.string(), configurationVariableType]);
export const sensitiveStringType = unionType(
[z.string(), configurationVariableType],
"Expected a string or a Configuration Variable",
);

/**
* A Zod type to validate Hardhat's SensitiveString values that expect a URL.
*/
export const sensitiveUrlType = unionType(
[z.string().url(), configurationVariableType],
"Expected a URL or a Configuration Variable",
);

/**
* A function to validate the user's configuration object against a Zod type.
Expand All @@ -68,38 +59,9 @@ export async function validateUserConfigZodType<
if (result.success) {
return [];
} else {
return result.error.errors.map((issue) =>
zodIssueToValidationError(config, configType, issue),
);
}
}

function zodIssueToValidationError<
Output,
Def extends ZodTypeDef = ZodTypeDef,
Input = Output,
>(
_config: HardhatUserConfig,
_configType: ZodType<Output, Def, Input>,
zodIssue: ZodIssue,
): HardhatUserConfigValidationError {
// TODO: `invalid_union` errors are too ambiguous. How can we improve them?
// This is just a sketch: not perfect nor tested.
if (zodIssue.code === "invalid_union") {
return {
path: zodIssue.path,
message: `Expected ${zodIssue.unionErrors
.flatMap((ue) => ue.errors)
.map((zi) => {
if (zi.code === "invalid_type") {
return zi.expected;
}
return "(please see the docs)";
})
.join(" or ")}`,
};
return result.error.errors.map((issue) => ({
path: issue.path,
message: issue.message,
}));
}

return { path: zodIssue.path, message: zodIssue.message };
}
32 changes: 29 additions & 3 deletions v-next/hardhat-zod-utils/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";

describe("Example tests", () => {
it("foo", function () {
assert.ok(true, "this shouldn't fail");
import { z } from "zod";

import { unionType } from "../src/index.js";

function assertParseResult(
result: z.SafeParseReturnType<any, any>,
expectedMessage: string,
) {
assert.equal(result.error?.errors.length, 1);
assert.equal(result.error?.errors[0].message, expectedMessage);
}

describe("unionType", () => {
it("It should return the expected error message", function () {
const union = unionType(
[z.object({ a: z.string() }), z.object({ b: z.string() })],
"Expected error message",
);

assertParseResult(
union.safeParse({ a: 123, c: 123 }),
"Expected error message",
);

assertParseResult(union.safeParse(123), "Expected error message");

assertParseResult(union.safeParse({}), "Expected error message");

assertParseResult(union.safeParse(undefined), "Expected error message");
});
});
3 changes: 3 additions & 0 deletions v-next/hardhat-zod-utils/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"extends": "../../config-v-next/tsconfig.json",
"compilerOptions": {
"isolatedDeclarations": false
},
"references": [
{
"path": "../core"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import type { ConfigHooks } from "@ignored/hardhat-vnext-core/types/hooks";

import {
sensitiveStringType,
unionType,
validateUserConfigZodType,
} from "@ignored/hardhat-vnext-zod-utils";
import { z } from "zod";

const fooUserConfigType = z.object({
bar: z.optional(z.union([z.number(), z.array(z.number())])),
bar: z.optional(
unionType(
[z.number(), z.array(z.number())],
"Expected a number or an array of numbers",
),
),
});

const hardhatUserConfig = z.object({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-restricted-syntax -- This is the entry point of a
subprocess, so we need to allow of top-level await here */
import { writeJsonFile } from "@ignored/hardhat-vnext-utils/fs";
import { postJsonRequest } from "@ignored/hardhat-vnext-utils/request";

Expand Down
4 changes: 2 additions & 2 deletions v-next/hardhat/src/internal/cli/telemetry/sentry/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class Reporter {
error: Error,
configPath: string = "",
): Promise<boolean> {
if (!(await this.shouldBeReported(error))) {
if (!(await this.#shouldBeReported(error))) {
log("Error not send: this type of error should not be reported");
return false;
}
Expand All @@ -108,7 +108,7 @@ class Reporter {
return true;
}

private async shouldBeReported(error: Error): Promise<boolean> {
async #shouldBeReported(error: Error): Promise<boolean> {
if (!this.#telemetryEnabled) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-restricted-syntax -- This is the entry point of a
subprocess, so we need to allow of top-level await here */
import type { Event } from "@sentry/node";

import { writeJsonFile } from "@ignored/hardhat-vnext-utils/fs";
Expand Down

0 comments on commit afe66f4

Please sign in to comment.