From f5c91481d74fee2c38a57b1f6c3889ae83c87a69 Mon Sep 17 00:00:00 2001 From: ChrisD <18092467+ChristopherDedominici@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:43:16 +0100 Subject: [PATCH 1/3] add logic to validate paths --- .../src/internal/core/config-validation.ts | 73 ++++++++- .../test/internal/core/config-validation.ts | 140 ++++++++++++++++++ 2 files changed, 212 insertions(+), 1 deletion(-) diff --git a/v-next/hardhat/src/internal/core/config-validation.ts b/v-next/hardhat/src/internal/core/config-validation.ts index 989f0d83cc..84dc5aab41 100644 --- a/v-next/hardhat/src/internal/core/config-validation.ts +++ b/v-next/hardhat/src/internal/core/config-validation.ts @@ -1,4 +1,7 @@ -import type { HardhatUserConfig } from "../../types/config.js"; +import type { + HardhatUserConfig, + ProjectPathsUserConfig, +} from "../../types/config.js"; import type { HardhatUserConfigValidationError, HookManager, @@ -85,6 +88,17 @@ export function collectValidationErrorsForUserConfig( ): HardhatUserConfigValidationError[] { const validationErrors: HardhatUserConfigValidationError[] = []; + if (config.paths !== undefined) { + if (isObject(config.paths)) { + validationErrors.push(...validatePaths(config.paths)); + } else { + validationErrors.push({ + path: ["paths"], + message: "paths must be an object", + }); + } + } + if (config.tasks !== undefined) { if (Array.isArray(config.tasks)) { validationErrors.push(...validateTasksConfig(config.tasks)); @@ -110,6 +124,63 @@ export function collectValidationErrorsForUserConfig( return validationErrors; } +export function validatePaths( + paths: ProjectPathsUserConfig, +): HardhatUserConfigValidationError[] { + const validationErrors: HardhatUserConfigValidationError[] = []; + + if (paths.cache !== undefined) { + validationErrors.push(...validatePath(paths.cache, "cache")); + } + + if (paths.artifacts !== undefined) { + validationErrors.push(...validatePath(paths.artifacts, "artifacts")); + } + + if (paths.tests !== undefined) { + // paths.tests of type TestPathsUserConfig is not validated because it is customizable by the user + if (!isObject(paths.tests)) { + validationErrors.push(...validatePath(paths.tests, "tests")); + } + } + + if (paths.sources !== undefined) { + if (Array.isArray(paths.sources)) { + for (const [index, source] of paths.sources.entries()) { + validationErrors.push(...validatePath(source, "sources", index)); + } + // paths.sources of type SourcePathsUserConfig is not validated because it is customizable by the user + } else if (!isObject(paths.sources)) { + validationErrors.push(...validatePath(paths.sources, "sources")); + } + } + + return validationErrors; +} + +function validatePath( + filePath: unknown, + pathName: "cache" | "artifacts" | "tests" | "sources", + index?: number, // If the filePath is part of an array, pass its index to make the error message more specific +): HardhatUserConfigValidationError[] { + const validationErrors: HardhatUserConfigValidationError[] = []; + + if (typeof filePath !== "string") { + const messagePrefix = + index !== undefined + ? `paths.${pathName} at index ${index}` + : `paths.${pathName}`; + + validationErrors.push({ + path: + index !== undefined ? ["paths", pathName, index] : ["paths", pathName], + message: `${messagePrefix} must be a string`, + }); + } + + return validationErrors; +} + export function validateTasksConfig( tasks: TaskDefinition[], path: Array = [], diff --git a/v-next/hardhat/test/internal/core/config-validation.ts b/v-next/hardhat/test/internal/core/config-validation.ts index a9c3ae33f3..720ab57860 100644 --- a/v-next/hardhat/test/internal/core/config-validation.ts +++ b/v-next/hardhat/test/internal/core/config-validation.ts @@ -1,4 +1,5 @@ import type { HardhatUserConfig } from "../../../src/config.js"; +import type { ProjectPathsUserConfig } from "../../../src/types/config.js"; import type { HardhatPlugin } from "../../../src/types/plugins.js"; import type { EmptyTaskDefinition, @@ -19,6 +20,7 @@ import { validateTasksConfig, validatePluginsConfig, collectValidationErrorsForUserConfig, + validatePaths, } from "../../../src/internal/core/config-validation.js"; import { type PositionalArgumentDefinition, @@ -1217,6 +1219,127 @@ describe("config validation", function () { }); }); + describe("validatePaths", function () { + describe("when the paths are valid", function () { + it("should work when all the paths are strings", async function () { + const paths: ProjectPathsUserConfig = { + cache: "./cache", + artifacts: "./artifacts", + tests: "./tests", + sources: "./sources", + }; + + const validationErrors = validatePaths(paths); + + assert.equal(validationErrors.length, 0); + }); + + it("should work when the sources property is an array", async function () { + const paths: ProjectPathsUserConfig = { + sources: ["./sources", "./sources2"], + }; + + const validationErrors = validatePaths(paths); + + assert.equal(validationErrors.length, 0); + }); + + it("should work when the tests and sources properties are both objects", async function () { + // Objects are not validated because they are customizable by the user + const paths: ProjectPathsUserConfig = { + tests: { randomProperty: "randomValue" }, + sources: {}, + }; + + const validationErrors = validatePaths(paths); + + assert.equal(validationErrors.length, 0); + }); + }); + + describe("when the paths are not valid", function () { + it("should return an error when the cache path is not a string", async function () { + const paths: ProjectPathsUserConfig = { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- testing validations for js users who can bypass type checks */ + cache: 123 as any, + }; + + const validationErrors = validatePaths(paths); + + assert.equal(validationErrors.length, 1); + assert.deepEqual(validationErrors[0].path, ["paths", "cache"]); + assert.equal( + validationErrors[0].message, + "paths.cache must be a string", + ); + }); + + it("should return an error when the artifacts path is not a string", async function () { + const paths: ProjectPathsUserConfig = { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- testing validations for js users who can bypass type checks */ + artifacts: 123 as any, + }; + + const validationErrors = validatePaths(paths); + + assert.equal(validationErrors.length, 1); + assert.deepEqual(validationErrors[0].path, ["paths", "artifacts"]); + assert.equal( + validationErrors[0].message, + "paths.artifacts must be a string", + ); + }); + + it("should return an error when the tests path is not a string", async function () { + const paths: ProjectPathsUserConfig = { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- testing validations for js users who can bypass type checks */ + tests: 123 as any, + }; + + const validationErrors = validatePaths(paths); + + assert.equal(validationErrors.length, 1); + assert.deepEqual(validationErrors[0].path, ["paths", "tests"]); + assert.equal( + validationErrors[0].message, + "paths.tests must be a string", + ); + }); + + it("should return an error when the sources path is not a string", async function () { + const paths: ProjectPathsUserConfig = { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- testing validations for js users who can bypass type checks */ + sources: 123 as any, + }; + + const validationErrors = validatePaths(paths); + + assert.equal(validationErrors.length, 1); + assert.deepEqual(validationErrors[0].path, ["paths", "sources"]); + assert.equal( + validationErrors[0].message, + "paths.sources must be a string", + ); + }); + + it("should return an error when one of the source paths in the array are not strings", async function () { + const paths: ProjectPathsUserConfig = { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- testing validations for js users who can bypass type checks */ + sources: ["./sources", 123 as any], + }; + + const validationErrors = validatePaths(paths); + + assert.equal(validationErrors.length, 1); + assert.deepEqual(validationErrors[0].path, ["paths", "sources", 1]); + assert.equal( + validationErrors[0].message, + "paths.sources at index 1 must be a string", + ); + }); + }); + }); + describe("validateTasksConfig", function () { it("should return an error if a task is not a valid task definition", function () { const tasks: TaskDefinition[] = [ @@ -1393,6 +1516,7 @@ describe("config validation", function () { describe("collectValidationErrorsForUserConfig", function () { it("should return an empty array if the config is valid", function () { const config = { + paths: {}, tasks: [], plugins: [], }; @@ -1400,6 +1524,22 @@ describe("config validation", function () { assert.deepEqual(collectValidationErrorsForUserConfig(config), []); }); + it("should return an error if the paths are not an object", function () { + const config: HardhatUserConfig = { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- testing validations for js users who can bypass type checks */ + paths: 1 as any, + tasks: [], + plugins: [], + }; + + assert.deepEqual(collectValidationErrorsForUserConfig(config), [ + { + message: "paths must be an object", + path: ["paths"], + }, + ]); + }); + it("should return an error if the tasks is not an array", function () { const config: HardhatUserConfig = { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- testing validations for js users who can bypass type checks */ From c8319392b0e86b47fe4f4fb8cd8123af10033fe5 Mon Sep 17 00:00:00 2001 From: Christopher Dedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:36:30 +0100 Subject: [PATCH 2/3] Update v-next/hardhat/src/internal/core/config-validation.ts Co-authored-by: John Kane --- v-next/hardhat/src/internal/core/config-validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v-next/hardhat/src/internal/core/config-validation.ts b/v-next/hardhat/src/internal/core/config-validation.ts index 84dc5aab41..0b4dbca7f5 100644 --- a/v-next/hardhat/src/internal/core/config-validation.ts +++ b/v-next/hardhat/src/internal/core/config-validation.ts @@ -161,7 +161,7 @@ export function validatePaths( function validatePath( filePath: unknown, pathName: "cache" | "artifacts" | "tests" | "sources", - index?: number, // If the filePath is part of an array, pass its index to make the error message more specific + index?: number, ): HardhatUserConfigValidationError[] { const validationErrors: HardhatUserConfigValidationError[] = []; From b22a7737b789d388a5547e137508c2fcda4a43d2 Mon Sep 17 00:00:00 2001 From: Christopher Dedominici <18092467+ChristopherDedominici@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:41:32 +0100 Subject: [PATCH 3/3] Update v-next/hardhat/test/internal/core/config-validation.ts Co-authored-by: John Kane --- v-next/hardhat/test/internal/core/config-validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v-next/hardhat/test/internal/core/config-validation.ts b/v-next/hardhat/test/internal/core/config-validation.ts index 720ab57860..27849516bd 100644 --- a/v-next/hardhat/test/internal/core/config-validation.ts +++ b/v-next/hardhat/test/internal/core/config-validation.ts @@ -1222,7 +1222,7 @@ describe("config validation", function () { describe("validatePaths", function () { describe("when the paths are valid", function () { it("should work when all the paths are strings", async function () { - const paths: ProjectPathsUserConfig = { + const paths: Required = { cache: "./cache", artifacts: "./artifacts", tests: "./tests",