diff --git a/.changeset/cold-deer-buy.md b/.changeset/cold-deer-buy.md new file mode 100644 index 00000000000..beff40cba0a --- /dev/null +++ b/.changeset/cold-deer-buy.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Introduce multi-compiler abstraction that allows plugins to define new Solidity compiler types ([#8008](https://github.com/NomicFoundation/hardhat/pull/8008)). diff --git a/.changeset/tall-seals-report.md b/.changeset/tall-seals-report.md new file mode 100644 index 00000000000..55a40acd22a --- /dev/null +++ b/.changeset/tall-seals-report.md @@ -0,0 +1,6 @@ +--- +"@nomicfoundation/hardhat-errors": patch +"hardhat": patch +--- + +Introduce the `ConfigHooks#validateResolvedConfig` hook and the `HardhatConfigValidationError` type to be able to run global validations on the resolved config ([#8008](https://github.com/NomicFoundation/hardhat/pull/8008)). diff --git a/cspell.dictionary.txt b/cspell.dictionary.txt index ff7a549e380..b57cd13a827 100644 --- a/cspell.dictionary.txt +++ b/cspell.dictionary.txt @@ -104,6 +104,8 @@ schaable sokol solcjs SOLCJS +solx +Solx sourcify Sourcify SOURCIFY diff --git a/v-next/hardhat-errors/src/descriptors.ts b/v-next/hardhat-errors/src/descriptors.ts index 0d83b7b4f90..2202345b459 100644 --- a/v-next/hardhat-errors/src/descriptors.ts +++ b/v-next/hardhat-errors/src/descriptors.ts @@ -505,6 +505,14 @@ Please install Hardhat locally using pnpm, npm or yarn, and try again.`, websiteTitle: "Global option cannot be hidden", websiteDescription: `A global option was defined as hidden, but global options cannot be hidden.`, }, + INVALID_RESOLVED_CONFIG: { + number: 24, + messageTemplate: `Your configuration is invalid once resolved: +{errors} +`, + websiteTitle: "Invalid resolved config", + websiteDescription: `The configuration you provided is seemingly valid, but once resolved it contains errors. Please check the documentation to learn how to configure Hardhat correctly.`, + }, }, INTERNAL: { ASSERTION_ERROR: { diff --git a/v-next/hardhat/package.json b/v-next/hardhat/package.json index 323b21df626..307acb13171 100644 --- a/v-next/hardhat/package.json +++ b/v-next/hardhat/package.json @@ -39,6 +39,7 @@ "./console.sol": "./console.sol", "./internal/coverage": "./dist/src/internal/builtin-plugins/coverage/exports.js", "./internal/gas-analytics": "./dist/src/internal/builtin-plugins/gas-analytics/exports.js", + "./internal/solidity": "./dist/src/internal/builtin-plugins/solidity/exports.js", "./utils/contract-names": "./dist/src/utils/contract-names.js", "./utils/result": "./dist/src/utils/result.js", "./types/runtime": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js", diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts index 06829590205..0214bce825e 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts @@ -357,6 +357,20 @@ export class NetworkManagerImplementation implements NetworkManager { ); if (!configResolutionResult.success) { + if (configResolutionResult.configValidationErrors !== undefined) { + throw new HardhatError( + HardhatError.ERRORS.CORE.NETWORK.INVALID_CONFIG_OVERRIDE, + { + errors: `\t${configResolutionResult.configValidationErrors + .map( + (error) => + `* Error in resolved config ${error.path.join(".")}: ${error.message}`, + ) + .join("\n\t")}`, + }, + ); + } + throw new HardhatError( HardhatError.ERRORS.CORE.NETWORK.INVALID_CONFIG_OVERRIDE, { diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/edr-artifacts.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/edr-artifacts.ts index d1747a868d2..17cf757b2f9 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/edr-artifacts.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/edr-artifacts.ts @@ -7,8 +7,8 @@ import type { import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; import { readBinaryFile } from "@nomicfoundation/hardhat-utils/fs"; -const BUILD_INFO_FORMAT = - /^solc-(?\d+)_(?\d+)_(?\d+)-[0-9a-fA-F]*$/; +export const BUILD_INFO_FORMAT: RegExp = + /^solc-(?\d+)_(?\d+)_(?\d+)(?:-(?[a-zA-Z][a-zA-Z0-9]*))?-[0-9a-fA-F]*$/; /** * This function returns all the build infos and associated outputs. diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/artifacts.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/artifacts.ts index 4f28348ed07..487b119084d 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/artifacts.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/artifacts.ts @@ -1,4 +1,4 @@ -import type { Artifact, BuildInfo } from "../../../../types/artifacts.js"; +import type { Artifact } from "../../../../types/artifacts.js"; import type { CompilationJob } from "../../../../types/solidity/compilation-job.js"; import type { CompilerOutput, @@ -107,11 +107,16 @@ declare module "hardhat/types/artifacts" { export async function getBuildInfo( compilationJob: CompilationJob, ): Promise { - const buildInfo: Required = { + // Defaulting to "solc" is safe here: if it's already "solc" or undefined, + // this doesn't alter the build info id. + const compilerType = compilationJob.solcConfig.type ?? "solc"; + + const buildInfo: SolidityBuildInfo = { _format: "hh3-sol-build-info-1", id: await compilationJob.getBuildId(), solcVersion: compilationJob.solcConfig.version, solcLongVersion: compilationJob.solcLongVersion, + compilerType, userSourceNameMap: compilationJob.dependencyGraph.getRootsUserSourceNameMap(), input: await compilationJob.getSolcInput(), diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/build-system.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/build-system.ts index 2549fc7f792..0595fec74b0 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/build-system.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/build-system.ts @@ -1,7 +1,11 @@ import type { CompileCache } from "./cache.js"; import type { DependencyGraphImplementation } from "./dependency-graph.js"; import type { Artifact } from "../../../../types/artifacts.js"; -import type { SolcConfig, SolidityConfig } from "../../../../types/config.js"; +import type { + SolidityCompilerConfig, + SolcSolidityCompilerConfig, + SolidityConfig, +} from "../../../../types/config.js"; import type { HookManager } from "../../../../types/hooks.js"; import type { SolidityBuildSystem, @@ -79,14 +83,26 @@ import { shouldSuppressWarning } from "./warning-suppression.js"; const log = debug("hardhat:core:solidity:build-system"); /** - * Resolves the preferWasm setting for a given solc config, falling back + * Returns true if the given compiler config is a SolcSolidityCompilerConfig. + */ +export function isSolcSolidityCompilerConfig( + config: SolidityCompilerConfig, +): config is SolcSolidityCompilerConfig { + return config.type === undefined || config.type === "solc"; +} + +/** + * Resolves the preferWasm setting for a given compiler config, falling back * to the build profile's preferWasm if not set on the compiler. */ function resolvePreferWasm( - solcConfig: SolcConfig, + compilerConfig: SolidityCompilerConfig, buildProfilePreferWasm: boolean, ): boolean { - return solcConfig.preferWasm ?? buildProfilePreferWasm; + if (isSolcSolidityCompilerConfig(compilerConfig)) { + return compilerConfig.preferWasm ?? buildProfilePreferWasm; + } + return false; } // Compiler warnings to suppress from build output. @@ -439,7 +455,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { ); let subgraphsWithConfig: Array< - [SolcConfig, DependencyGraphImplementation] + [SolidityCompilerConfig, DependencyGraphImplementation] > = []; for (const [rootFile, resolvedFile] of dependencyGraph.getRoots()) { log( @@ -459,19 +475,47 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { } // get longVersion and isWasm from the compiler for each version - const solcVersionToLongVersion = new Map(); - const versionIsWasm = new Map(); - for (const [solcConfig] of subgraphsWithConfig) { - let solcLongVersion = solcVersionToLongVersion.get(solcConfig.version); - - if (solcLongVersion === undefined) { - const compiler = await getCompiler(solcConfig.version, { - preferWasm: resolvePreferWasm(solcConfig, buildProfile.preferWasm), - compilerPath: solcConfig.path, + // These maps are keyed by compiler type first, then version, to avoid + // collisions between different compiler types using the same version string. + const solidityVersionToLongVersionPerCompilerType = new Map< + string, + Map + >(); + const versionIsWasmPerCompilerType = new Map< + string, + Map + >(); + for (const [compilerConfig] of subgraphsWithConfig) { + const compilerType = compilerConfig.type ?? "solc"; + let longVersionMap = + solidityVersionToLongVersionPerCompilerType.get(compilerType); + if (longVersionMap === undefined) { + longVersionMap = new Map(); + solidityVersionToLongVersionPerCompilerType.set( + compilerType, + longVersionMap, + ); + } + + let isWasmMap = versionIsWasmPerCompilerType.get(compilerType); + if (isWasmMap === undefined) { + isWasmMap = new Map(); + versionIsWasmPerCompilerType.set(compilerType, isWasmMap); + } + + let longVersion = longVersionMap.get(compilerConfig.version); + + if (longVersion === undefined) { + const compiler = await getCompiler(compilerConfig.version, { + preferWasm: resolvePreferWasm( + compilerConfig, + buildProfile.preferWasm, + ), + compilerPath: compilerConfig.path, }); - solcLongVersion = compiler.longVersion; - solcVersionToLongVersion.set(solcConfig.version, solcLongVersion); - versionIsWasm.set(solcConfig.version, compiler.isSolcJs); + longVersion = compiler.longVersion; + longVersionMap.set(compilerConfig.version, longVersion); + isWasmMap.set(compilerConfig.version, compiler.isSolcJs); } } @@ -480,17 +524,26 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { const sharedContentHashes = new Map(); await Promise.all( subgraphsWithConfig.map(async ([config, subgraph]) => { - const solcLongVersion = solcVersionToLongVersion.get(config.version); + const compilerType = config.type ?? "solc"; + const longVersionMap = + solidityVersionToLongVersionPerCompilerType.get(compilerType); assertHardhatInvariant( - solcLongVersion !== undefined, - "solcLongVersion should not be undefined", + longVersionMap !== undefined, + `No long version map for compiler type ${compilerType}`, + ); + + const longVersion = longVersionMap.get(config.version); + + assertHardhatInvariant( + longVersion !== undefined, + "longVersion should not be undefined", ); const individualJob = new CompilationJobImplementation( subgraph, config, - solcLongVersion, + longVersion, this.#hooks, sharedContentHashes, ); @@ -516,7 +569,15 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { for (const [rootFile, compilationJob] of indexedIndividualJobs.entries()) { const jobHash = await compilationJob.getBuildId(); const cacheResult = this.#compileCache[rootFile]; - const isWasm = versionIsWasm.get(compilationJob.solcConfig.version); + const compilerType = compilationJob.solcConfig.type ?? "solc"; + const isWasmMap = versionIsWasmPerCompilerType.get(compilerType); + + assertHardhatInvariant( + isWasmMap !== undefined, + `No isWasm map for compiler type ${compilerType}`, + ); + + const isWasm = isWasmMap.get(compilationJob.solcConfig.version); assertHardhatInvariant( isWasm !== undefined, @@ -529,6 +590,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { cacheResult === undefined || cacheResult.jobHash !== jobHash || cacheResult.isolated !== isolated || + cacheResult.compilerType !== compilerType || cacheResult.wasm !== isWasm ) { rootFilesToCompile.add(rootFile); @@ -576,13 +638,15 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { log(`Merging compilation jobs`); const mergedSubgraphsByConfig: Map< - SolcConfig, + SolidityCompilerConfig, DependencyGraphImplementation > = new Map(); - // Note: This groups the subgraphs by solc config. It compares the configs - // based on reference, and not by deep equality. It misses some merging - // opportunities, but this is Hardhat v2's behavior and works well enough. + // Note: This groups the subgraphs by compiler config. It compares the + // configs based on reference, and not by deep equality. This is + // inherently type-aware: two configs with different types will always be + // different references. It misses some merging opportunities, but this is + // Hardhat v2's behavior and works well enough. for (const [config, subgraph] of subgraphsWithConfig) { const rootFile = getSingleRootFilePath(subgraph); @@ -613,18 +677,27 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { } const compilationJobsPerFile = new Map(); - for (const [solcConfig, subgraph] of subgraphsWithConfig) { - const solcLongVersion = solcVersionToLongVersion.get(solcConfig.version); + for (const [compilerConfig, subgraph] of subgraphsWithConfig) { + const compilerType = compilerConfig.type ?? "solc"; + const longVersionMap = + solidityVersionToLongVersionPerCompilerType.get(compilerType); + + assertHardhatInvariant( + longVersionMap !== undefined, + `No long version map for compiler type ${compilerType}`, + ); + + const longVersion = longVersionMap.get(compilerConfig.version); assertHardhatInvariant( - solcLongVersion !== undefined, - "solcLongVersion should not be undefined", + longVersion !== undefined, + "longVersion should not be undefined", ); const runnableCompilationJob = new CompilationJobImplementation( subgraph, - solcConfig, - solcLongVersion, + compilerConfig, + longVersion, this.#hooks, sharedContentHashes, ); @@ -1124,6 +1197,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { this.#compileCache[rootFilePath] = { jobHash, isolated, + compilerType: individualJob.solcConfig.type ?? "solc", artifactPaths, buildInfoPath: emitArtifactsResult.buildInfoPath, buildInfoOutputPath: emitArtifactsResult.buildInfoOutputPath, diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/cache.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/cache.ts index c7fe45fc113..2f414241cd0 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/cache.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/cache.ts @@ -17,6 +17,7 @@ export type CompileCache = Record; export interface CompileCacheEntry { jobHash: string; isolated: boolean; + compilerType: string; buildInfoPath: string; buildInfoOutputPath: string; artifactPaths: string[]; diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compilation-job.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compilation-job.ts index 9fe1309d2d6..4666e42982e 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compilation-job.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compilation-job.ts @@ -1,6 +1,6 @@ import type { DependencyGraphImplementation } from "./dependency-graph.js"; import type { BuildInfo } from "../../../../types/artifacts.js"; -import type { SolcConfig } from "../../../../types/config.js"; +import type { SolidityCompilerConfig } from "../../../../types/config.js"; import type { HookManager } from "../../../../types/hooks.js"; import type { CompilationJob } from "../../../../types/solidity/compilation-job.js"; import type { CompilerInput } from "../../../../types/solidity/compiler-io.js"; @@ -21,7 +21,7 @@ import { getEvmVersionFromSolcVersion } from "./solc-info.js"; export class CompilationJobImplementation implements CompilationJob { public readonly dependencyGraph: DependencyGraph; - public readonly solcConfig: SolcConfig; + public readonly solcConfig: SolidityCompilerConfig; public readonly solcLongVersion: string; readonly #hooks: HookManager; @@ -34,7 +34,7 @@ export class CompilationJobImplementation implements CompilationJob { constructor( dependencyGraph: DependencyGraphImplementation, - solcConfig: SolcConfig, + solcConfig: SolidityCompilerConfig, solcLongVersion: string, hooks: HookManager, sharedContentHashes: Map = new Map(), @@ -236,17 +236,44 @@ export class CompilationJobImplementation implements CompilationJob { // Changing this shouldn't be taken lightly, as it makes reproducing // builds pretty difficult when upgrading Hardhat between versions that // change it. - const preimage = JSON.stringify({ + + const compilerType = this.solcConfig.type; + + // We normalize solcConfig.type to `undefined` so that "solc" and undefined + // produce the same hash, for backwards compatibility. + const normalizedSolcConfig = { ...this.solcConfig, type: undefined }; + + const preimageObject: Record = { format, solcLongVersion: this.solcLongVersion, smallerSolcInput, - solcConfig: this.solcConfig, + solcConfig: normalizedSolcConfig, userSourceNameMap: this.dependencyGraph.getRootsUserSourceNameMap(), - }); + }; + + // Include compiler type in the preimage for non-solc types, so that + // different compiler types produce different build IDs. + if (compilerType !== undefined && compilerType !== "solc") { + preimageObject.compilerType = compilerType; + } + + const preimage = JSON.stringify(preimageObject); const jobHash = await createNonCryptographicHashId(preimage); - return `solc-${this.solcConfig.version.replaceAll(".", "_")}-${jobHash}`; + const versionPart = this.solcConfig.version.replaceAll(".", "_"); + + // For non-solc compiler types, include the compiler type in the build ID. + // We keep the `solc-` prefix for all types to avoid breaking codepaths + // that look for it. + if (compilerType !== undefined && compilerType !== "solc") { + /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- + compilerType is `never` in the base type system (only "solc" is registered), + but plugins can extend SolidityCompilerConfigPerType to add new compiler types. */ + return `solc-${versionPart}-${compilerType}-${jobHash}`; + } + + return `solc-${versionPart}-${jobHash}`; } #getSourceContentHash(sourceName: string, text: string): any { diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compiler/compiler.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compiler/compiler.ts index 6d7b90c15e8..f62c528d1dc 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compiler/compiler.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/compiler/compiler.ts @@ -39,7 +39,7 @@ import * as semver from "semver"; * @throws Error if the compilation process exits with a non-zero exit code. * @throws HardhatInvariantError if the any of the io streams are null. */ -async function spawnCompile( +export async function spawnCompile( command: string, args: string[], input: CompilerInput, diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts index 44374553512..be8b8728f64 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts @@ -1,5 +1,5 @@ import type { - SolcConfig, + SolidityCompilerConfig, SolidityBuildProfileConfig, } from "../../../../types/config.js"; import type { CompilationJobCreationError } from "../../../../types/solidity/build-system.js"; @@ -44,7 +44,9 @@ export class SolcConfigSelector { */ public selectBestSolcConfigForSingleRootGraph( subgraph: DependencyGraph, - ): { success: true; config: SolcConfig } | CompilationJobCreationError { + ): + | { success: true; config: SolidityCompilerConfig } + | CompilationJobCreationError { const roots = subgraph.getRoots(); assertHardhatInvariant( diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/config.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/config.ts index 7755712055c..788d1e621bf 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/config.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/config.ts @@ -1,15 +1,21 @@ import type { HardhatUserConfig } from "../../../config.js"; import type { + SolidityCompilerConfig, + SolidityCompilerUserConfig, HardhatConfig, MultiVersionSolidityUserConfig, SingleVersionSolidityUserConfig, - SolcConfig, - SolcUserConfig, SolidityBuildProfileConfig, SolidityConfig, SolidityUserConfig, + CommonSolidityCompilerUserConfig, + SolcSolidityCompilerConfig, + SolcSolidityCompilerUserConfig, } from "../../../types/config.js"; -import type { HardhatUserConfigValidationError } from "../../../types/hooks.js"; +import type { + HardhatConfigValidationError, + HardhatUserConfigValidationError, +} from "../../../types/hooks.js"; import { deepMerge, isObject } from "@nomicfoundation/hardhat-utils/lang"; import { resolveFromRoot } from "@nomicfoundation/hardhat-utils/path"; @@ -27,103 +33,177 @@ import { missesSomeOfficialNativeBuilds, } from "./build-system/solc-info.js"; -const sourcePathsType = conditionalUnionType( - [ - [(data) => typeof data === "string", z.string()], - [(data) => Array.isArray(data), z.array(z.string()).nonempty()], - ], - "Expected a string or an array of strings", -); +/** + * The top-level type SolidityUserConfig is a union type too complex for + * TypeScript to handle properly. It accepts fields of different types of + * configurations. For example, it accepts `compilers` inside of a + * `SingleVersionSolidityUserConfig`. + * + * For this reason, we declare all the fields that shouldn't exist in the + * presence of another one as incompatible. + * + * This object has all the fields that are incompatible with `version`. + */ +const incompatibleVersionFields = { + compilers: incompatibleFieldType("This field is incompatible with `version`"), + overrides: incompatibleFieldType("This field is incompatible with `version`"), + profiles: incompatibleFieldType("This field is incompatible with `version`"), +}; + +/** + * This is the equivalent of `incompatibleVersionFields`, but for the + * `profiles` field. + */ +const incompatibleProfileFields = { + type: incompatibleFieldType("This field is incompatible with `profiles`"), + version: incompatibleFieldType("This field is incompatible with `profiles`"), + compilers: incompatibleFieldType( + "This field is incompatible with `profiles`", + ), + overrides: incompatibleFieldType( + "This field is incompatible with `profiles`", + ), +}; + +/** + * This is the equivalent of `incompatibleVersionFields`, but for the + * `compilers` field. + */ +const incompatibleCompilerFields = { + type: incompatibleFieldType("This field is incompatible with `compilers`"), + version: incompatibleFieldType("This field is incompatible with `compilers`"), + profiles: incompatibleFieldType( + "This field is incompatible with `compilers`", + ), +}; -const commonSolcUserConfigType = z.object({ +const commonSolidityUserConfigFields = { isolated: z.boolean().optional(), -}); + npmFilesToBuild: z.array(z.string()).optional(), +}; -const solcUserConfigType = z.object({ +const commonSolidityCompilerUserConfigFields = { + type: z.string().optional(), version: z.string(), settings: z.any().optional(), path: z.string().optional(), - preferWasm: z.boolean().optional(), - compilers: incompatibleFieldType("This field is incompatible with `version`"), - overrides: incompatibleFieldType("This field is incompatible with `version`"), - profiles: incompatibleFieldType("This field is incompatible with `version`"), -}); +}; -// NOTE: This is only to match the setup present in ./type-extensions.ts -const singleVersionSolcUserConfigType = solcUserConfigType.extend({ - isolated: z.boolean().optional(), +const solcSolidityCompilerUserConfigType = z.object({ + ...commonSolidityCompilerUserConfigFields, + type: z.literal("solc").optional(), preferWasm: z.boolean().optional(), }); -const multiVersionSolcUserConfigType = commonSolcUserConfigType.extend({ - compilers: z.array(solcUserConfigType).nonempty(), - overrides: z.record(z.string(), solcUserConfigType).optional(), - isolated: z.boolean().optional(), - preferWasm: z.boolean().optional(), - version: incompatibleFieldType("This field is incompatible with `compilers`"), - settings: incompatibleFieldType( - "This field is incompatible with `compilers`", - ), -}); +const otherSolidityCompilerUserConfigType = z.object( + commonSolidityCompilerUserConfigFields, +); -const commonSolidityUserConfigType = z.object({ - npmFilesToBuild: z.array(z.string()).optional(), -}); +// Per-compiler config: preferWasm is only allowed for solc (type undefined or "solc") +const solidityCompilerUserConfigType = conditionalUnionType( + [ + [ + (data) => + isObject(data) && + (!("type" in data) || data.type === undefined || data.type === "solc"), + solcSolidityCompilerUserConfigType, + ], + [ + (data) => isObject(data) && "type" in data && data.type !== "solc", + otherSolidityCompilerUserConfigType, + ], + ], + "Expected a valid compiler configuration", +); -const singleVersionSolidityUserConfigType = singleVersionSolcUserConfigType - .merge(commonSolidityUserConfigType) - .extend({ - compilers: incompatibleFieldType( - "This field is incompatible with `version`", - ), - overrides: incompatibleFieldType( - "This field is incompatible with `version`", - ), - profiles: incompatibleFieldType( - "This field is incompatible with `version`", - ), +const solcSingleVersionSolidityUserConfigType = + solcSolidityCompilerUserConfigType.extend({ + ...commonSolidityUserConfigFields, + ...incompatibleVersionFields, }); -const multiVersionSolidityUserConfigType = multiVersionSolcUserConfigType - .merge(commonSolidityUserConfigType) - .extend({ - version: incompatibleFieldType( - "This field is incompatible with `compilers`", - ), - profiles: incompatibleFieldType( - "This field is incompatible with `compilers`", - ), +const otherSingleVersionSolidityUserConfigType = + otherSolidityCompilerUserConfigType.extend({ + ...commonSolidityUserConfigFields, + ...incompatibleVersionFields, }); -const buildProfilesSolidityUserConfigType = commonSolidityUserConfigType.extend( - { - profiles: z.record( - z.string(), - conditionalUnionType( +const singleVersionSolidityUserConfigType = conditionalUnionType( + [ + [ + (data) => + isObject(data) && + (!("type" in data) || data.type === undefined || data.type === "solc"), + solcSingleVersionSolidityUserConfigType, + ], + [ + (data) => isObject(data) && "type" in data && data.type !== "solc", + otherSingleVersionSolidityUserConfigType, + ], + ], + "Expected a valid single-version Solidity configuration", +); + +const multiVersionSolidityUserConfigType = z.object({ + preferWasm: z.boolean().optional(), + compilers: z.array(solidityCompilerUserConfigType).nonempty(), + overrides: z.record(z.string(), solidityCompilerUserConfigType).optional(), + ...commonSolidityUserConfigFields, + ...incompatibleCompilerFields, +}); + +// This definition needs to be aligned with solidityCompilerUserConfigType. +// The reason to duplicate it is that we can't `.extend()` a conditional union +// type. +const singleVersionBuildProfileUserConfigType = conditionalUnionType( + [ + [ + (data) => + isObject(data) && + (!("type" in data) || data.type === undefined || data.type === "solc"), + solcSolidityCompilerUserConfigType.extend({ + isolated: z.boolean().optional(), + ...incompatibleVersionFields, + }), + ], + [ + (data) => isObject(data) && "type" in data && data.type !== "solc", + otherSolidityCompilerUserConfigType.extend({ + isolated: z.boolean().optional(), + ...incompatibleVersionFields, + }), + ], + ], + "Expected a valid compiler configuration", +); + +const multiVersionBuildProfileUserConfigType = z.object({ + preferWasm: z.boolean().optional(), + compilers: z.array(solidityCompilerUserConfigType).nonempty(), + overrides: z.record(z.string(), solidityCompilerUserConfigType).optional(), + isolated: z.boolean().optional(), + ...incompatibleCompilerFields, +}); + +const buildProfilesSolidityUserConfigType = z.object({ + profiles: z.record( + z.string(), + conditionalUnionType( + [ [ - [ - (data) => isObject(data) && "version" in data, - singleVersionSolcUserConfigType, - ], - [ - (data) => isObject(data) && "compilers" in data, - multiVersionSolcUserConfigType, - ], + (data) => isObject(data) && "version" in data, + singleVersionBuildProfileUserConfigType, ], - "Expected an object configuring one or more versions of Solidity", - ), - ), - version: incompatibleFieldType( - "This field is incompatible with `profiles`", - ), - compilers: incompatibleFieldType( - "This field is incompatible with `profiles`", - ), - overrides: incompatibleFieldType( - "This field is incompatible with `profiles`", + [ + (data) => isObject(data) && "compilers" in data, + multiVersionBuildProfileUserConfigType, + ], + ], + "Expected an object configuring one or more versions of Solidity", ), - }, -); + ), + ...incompatibleProfileFields, +}); const solidityUserConfigType = conditionalUnionType( [ @@ -145,6 +225,14 @@ const solidityUserConfigType = conditionalUnionType( "Expected a version string, an array of version strings, or an object configuring one or more versions of Solidity or multiple build profiles", ); +const sourcePathsType = conditionalUnionType( + [ + [(data) => typeof data === "string", z.string()], + [(data) => Array.isArray(data), z.array(z.string()).nonempty()], + ], + "Expected a string or an array of strings", +); + const userConfigType = z.object({ paths: z .object({ @@ -184,6 +272,109 @@ export function validateSolidityUserConfig( return result; } +export function validateSolidityConfig( + resolvedConfig: HardhatConfig, +): HardhatConfigValidationError[] { + const errors: HardhatConfigValidationError[] = []; + + errors.push(...validateRegisteredCompilerTypes(resolvedConfig)); + errors.push(...validatePreferWasmRequiresSolc(resolvedConfig)); + + return errors; +} + +function validateRegisteredCompilerTypes( + resolvedConfig: HardhatConfig, +): HardhatConfigValidationError[] { + const errors: HardhatConfigValidationError[] = []; + const registered = new Set(resolvedConfig.solidity.registeredCompilerTypes); + + for (const [profileName, profile] of Object.entries( + resolvedConfig.solidity.profiles, + )) { + for (const [i, compiler] of profile.compilers.entries()) { + const type = compiler.type ?? "solc"; + if (!registered.has(type)) { + errors.push({ + path: ["solidity", "profiles", profileName, "compilers", i, "type"], + message: `Unknown compiler type "${type}". Registered types: ${[...registered].join(", ")}`, + }); + } + } + for (const [sourceName, override] of Object.entries(profile.overrides)) { + const type = override.type ?? "solc"; + if (!registered.has(type)) { + errors.push({ + path: [ + "solidity", + "profiles", + profileName, + "overrides", + sourceName, + "type", + ], + message: `Unknown compiler type "${type}". Registered types: ${[...registered].join(", ")}`, + }); + } + } + } + + return errors; +} + +function validatePreferWasmRequiresSolc( + resolvedConfig: HardhatConfig, +): HardhatConfigValidationError[] { + const errors: HardhatConfigValidationError[] = []; + + for (const [profileName, profile] of Object.entries( + resolvedConfig.solidity.profiles, + )) { + if (!profile.preferWasm) { + continue; + } + + for (const [i, compiler] of profile.compilers.entries()) { + const type = compiler.type; + if (type !== undefined && type !== "solc") { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- We need to cast because within Hardhat core the type of `type` is + `never`, as you can only get into this if with a plugin. */ + const compilerType: string = (compiler as any).type; + + errors.push({ + path: ["solidity", "profiles", profileName, "compilers", i, "type"], + message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "${compilerType}"`, + }); + } + } + + for (const [sourceName, override] of Object.entries(profile.overrides)) { + const type = override.type; + if (type !== undefined && type !== "solc") { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- We need to cast because within Hardhat core the type of `type` is + `never`, as you can only get into this if with a plugin. */ + const overrideType: string = (override as any).type; + + errors.push({ + path: [ + "solidity", + "profiles", + profileName, + "overrides", + sourceName, + "type", + ], + message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "${overrideType}"`, + }); + } + } + } + + return errors; +} + export async function resolveSolidityUserConfig( userConfig: HardhatUserConfig, resolvedConfig: HardhatConfig, @@ -238,6 +429,7 @@ function resolveSolidityConfig( ), }, npmFilesToBuild: [], + registeredCompilerTypes: ["solc"], }; } @@ -252,6 +444,7 @@ function resolveSolidityConfig( ), }, npmFilesToBuild: solidityConfig.npmFilesToBuild ?? [], + registeredCompilerTypes: ["solc"], }; } @@ -281,6 +474,7 @@ function resolveSolidityConfig( return { profiles, npmFilesToBuild: solidityConfig.npmFilesToBuild ?? [], + registeredCompilerTypes: ["solc"], }; } @@ -292,7 +486,7 @@ function resolveBuildProfileConfig( ): SolidityBuildProfileConfig { if ("version" in solidityConfig) { return { - compilers: [resolveSolcConfig(solidityConfig, production)], + compilers: [resolveSolidityCompilerConfig(solidityConfig, production)], overrides: {}, isolated: solidityConfig.isolated ?? production, preferWasm: solidityConfig.preferWasm ?? false, @@ -301,13 +495,13 @@ function resolveBuildProfileConfig( return { compilers: solidityConfig.compilers.map((compiler) => - resolveSolcConfig(compiler, production), + resolveSolidityCompilerConfig(compiler, production), ), overrides: Object.fromEntries( Object.entries(solidityConfig.overrides ?? {}).map( ([userSourceName, override]) => [ userSourceName, - resolveSolcConfig(override, production), + resolveSolidityCompilerConfig(override, production), ], ), ), @@ -316,11 +510,11 @@ function resolveBuildProfileConfig( }; } -function resolveSolcConfig( - solcConfig: SolcUserConfig, +function resolveSolidityCompilerConfig( + compilerConfig: SolidityCompilerUserConfig, production: boolean = false, -): SolcConfig { - const defaultSolcConfigSettings: SolcConfig["settings"] = { +): SolidityCompilerConfig { + const defaultSettings: SolidityCompilerConfig["settings"] = { outputSelection: { "*": { "": ["ast"], @@ -335,39 +529,68 @@ function resolveSolcConfig( }, }; - if (production) { - defaultSolcConfigSettings.optimizer = { + if (production && isSolcSolidityCompilerUserConfig(compilerConfig)) { + defaultSettings.optimizer = { enabled: true, runs: 200, }; } - // Resolve per-compiler preferWasm: - // If explicitly set, use that value. - // Otherwise, for ARM64 Linux: - // - Versions below the mirror threshold (< 0.5.0) always use WASM, - // since no native ARM64 build exists anywhere. - // - In production, versions without official ARM64 builds - // also default to WASM. - let resolvedPreferWasm: boolean | undefined = solcConfig.preferWasm; - if (resolvedPreferWasm === undefined && missesSomeOfficialNativeBuilds()) { - const version = solcConfig.version; - - if (!hasOfficialArm64Build(version) && !hasArm64MirrorBuild(version)) { - resolvedPreferWasm = true; - } else if (production && !hasOfficialArm64Build(version)) { - resolvedPreferWasm = true; + const resolvedSettings = deepMerge( + defaultSettings, + compilerConfig.settings ?? {}, + ); + + // Resolve solc-specific preferWasm if this is a SolcSolidityCompilerUserConfig + if (isSolcSolidityCompilerUserConfig(compilerConfig)) { + // Resolve per-compiler preferWasm: + // If explicitly set, use that value. + // Otherwise, for ARM64 Linux: + // - Versions below the mirror threshold (< 0.5.0) always use WASM, + // since no native ARM64 build exists anywhere. + // - In production, versions without official ARM64 builds + // also default to WASM. + let resolvedPreferWasm: boolean | undefined = compilerConfig.preferWasm; + if (resolvedPreferWasm === undefined && missesSomeOfficialNativeBuilds()) { + const version = compilerConfig.version; + + if (!hasOfficialArm64Build(version) && !hasArm64MirrorBuild(version)) { + resolvedPreferWasm = true; + } else if (production && !hasOfficialArm64Build(version)) { + resolvedPreferWasm = true; + } } + const solcResolved: SolcSolidityCompilerConfig = { + type: compilerConfig.type, + version: compilerConfig.version, + settings: resolvedSettings, + path: compilerConfig.path, + preferWasm: resolvedPreferWasm, + }; + return solcResolved; } + const unknownCompilerConfig = + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- + We need to cast here because compilerConfig has `never` type here, as this + case is only accessible when there are other types of compilers registered + through plugins. */ + compilerConfig as unknown as CommonSolidityCompilerUserConfig; + return { - version: solcConfig.version, - settings: deepMerge(defaultSolcConfigSettings, solcConfig.settings ?? {}), - path: solcConfig.path, - preferWasm: resolvedPreferWasm, + type: unknownCompilerConfig.type, + version: unknownCompilerConfig.version, + settings: resolvedSettings, + path: unknownCompilerConfig.path, }; } +export function isSolcSolidityCompilerUserConfig( + config: SolidityCompilerUserConfig, +): config is SolcSolidityCompilerUserConfig { + return config.type === undefined || config.type === "solc"; +} + function copyFromDefault( defaultSolidityConfig: | SingleVersionSolidityUserConfig @@ -376,18 +599,20 @@ function copyFromDefault( if ("version" in defaultSolidityConfig) { return { version: defaultSolidityConfig.version, + type: defaultSolidityConfig.type, }; } return { compilers: defaultSolidityConfig.compilers.map((c) => ({ version: c.version, + type: c.type, })), overrides: Object.fromEntries( Object.entries(defaultSolidityConfig.overrides ?? {}).map( ([userSourceName, override]) => [ userSourceName, - { version: override.version }, + { version: override.version, type: override.type }, ], ), ), diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/exports.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/exports.ts new file mode 100644 index 00000000000..32763e81df0 --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/exports.ts @@ -0,0 +1,2 @@ +export { isSolcSolidityCompilerConfig } from "./build-system/build-system.js"; +export { spawnCompile } from "./build-system/compiler/compiler.js"; diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/hook-handlers/config.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/hook-handlers/config.ts index 2e7556e61fe..afd708fba7a 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/hook-handlers/config.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/hook-handlers/config.ts @@ -2,6 +2,7 @@ import type { ConfigHooks } from "../../../../types/hooks.js"; import { resolveSolidityUserConfig, + validateSolidityConfig, validateSolidityUserConfig, } from "../config.js"; @@ -21,6 +22,8 @@ export default async (): Promise> => { return resolveSolidityUserConfig(userConfig, resolvedConfig); }, + validateResolvedConfig: async (resolvedConfig) => + validateSolidityConfig(resolvedConfig), }; return handlers; diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/type-extensions.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/type-extensions.ts index dd60897bf19..f5936aa7f15 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/type-extensions.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/type-extensions.ts @@ -1,4 +1,4 @@ -import type { SolcConfig } from "../../../types/config.js"; +import type { SolidityCompilerConfig } from "../../../types/config.js"; import type { BuildOptions, CompilationJobCreationError, @@ -13,6 +13,26 @@ import type { import "../../../types/config.js"; declare module "../../../types/config.js" { + /** + * An interface with a key per compiler type. + * The types of the values don't matter; we use `true` as a convention. + * + * By default, only "solc" is provided. Plugins can extend this via + * declaration merging to add new compiler types (e.g. "solx"). + */ + export interface SolidityCompilerTypeDefinitions { + solc: true; + } + + /** + * The different Solidity compiler types, derived from + * SolidityCompilerTypeDefinitions. Extensible via declaration merging. + */ + export type SolidityCompilerType = keyof SolidityCompilerTypeDefinitions; + + /** + * The type of `userConfig.solidity`. + */ export type SolidityUserConfig = | string | string[] @@ -20,68 +40,241 @@ declare module "../../../types/config.js" { | MultiVersionSolidityUserConfig | BuildProfilesSolidityUserConfig; - export interface SolcUserConfig { + /** + * Fields that all the object-typed variants of SolidityUserConfig share. + * + * Note: All the variants of SolidityUserConfig except for the string and + * array of strings MUST extend this interface. This is especially relevant + * for plugins creating their own `SingleVersionSolidityUserConfig` variant. + */ + export interface CommonSolidityUserConfig { + isolated?: boolean; + npmFilesToBuild?: string[]; + } + + /** + * Fields that all the SolidityCompilerUserConfig variants share. + * + * Note: All the types of SolidityCompilerUserConfig MUST extend this + * interface. This is especially relevant for plugins creating their own + * `SolidityCompilerUserConfig` variant. + */ + export interface CommonSolidityCompilerUserConfig { + type?: SolidityCompilerType; version: string; settings?: any; path?: string; - preferWasm?: boolean; } - export interface SingleVersionSolcUserConfig extends SolcUserConfig { - isolated?: boolean; + /** + * Deprecated: Use `SolcSolidityCompilerUserConfig` instead. + * @deprecated + */ + export interface SolcUserConfig extends CommonSolidityCompilerUserConfig { + // Note: This field is optional for backwards compatibility. No `type` means + // "solc" across all of Hardhat. + type?: "solc"; preferWasm?: boolean; } - export interface MultiVersionSolcUserConfig { - isolated?: boolean; - preferWasm?: boolean; - compilers: SolcUserConfig[]; - overrides?: Record; - } + /** + * Solc-specific SolidityCompilerUserConfig. + */ + /* eslint-disable-next-line @typescript-eslint/no-empty-interface -- Defined + in SolcUserConfig for backwards compatibility */ + export interface SolcSolidityCompilerUserConfig extends SolcUserConfig {} - export interface CommonSolidityUserConfig { - npmFilesToBuild?: string[]; + /** + * A map from compiler type to its SolidityCompilerUserConfig type. + * + * Note: The types MUST extend `CommonSolidityCompilerUserConfig`. + */ + export interface SolidityCompilerUserConfigPerType { + solc: SolcSolidityCompilerUserConfig; } - export interface SingleVersionSolidityUserConfig - extends SingleVersionSolcUserConfig, + /** + * The type of all the compiler user configs. + */ + export type SolidityCompilerUserConfig = + | { + [type in keyof SolidityCompilerUserConfigPerType]: SolidityCompilerUserConfigPerType[type]; + }[keyof SolidityCompilerUserConfigPerType] + // SolcSolidityCompilerUserConfig when the type isn't present + | (Omit & + Partial>); + + /** + * Deprecated: Use `SolcSingleVersionSolidityUserConfig` instead. + * @deprecated + */ + export interface SingleVersionSolcUserConfig + extends SolcSolidityCompilerUserConfig, CommonSolidityUserConfig {} + /** + * Solc-specific SingleVersionSolidityUserConfig. + */ + /* eslint-disable-next-line @typescript-eslint/no-empty-interface -- Defined + in SingleVersionSolcUserConfig for backwards compatibility */ + export interface SolcSingleVersionSolidityUserConfig + extends SingleVersionSolcUserConfig {} + + /** + * A map from compiler type to its SingleVersionSolidityUserConfig type. + * + * Note: The types MUST extend `CommonSolidityUserConfig`. + */ + export interface SingleVersionSolidityUserConfigPerType { + solc: SolcSingleVersionSolidityUserConfig; + } + + /** + * The type of all the single version user configs. + */ + export type SingleVersionSolidityUserConfig = + | { + [type in keyof SingleVersionSolidityUserConfigPerType]: SingleVersionSolidityUserConfigPerType[type]; + }[keyof SingleVersionSolidityUserConfigPerType] + // SolcSingleVersionSolidityUserConfig when the type isn't present + | (Omit & + Partial>); + + /** + * Deprecated: Use `MultiVersionSolidityUserConfig` or + * `MultiVersionBuildProfileUserConfig` instead. + * @deprecated + */ + export interface MultiVersionSolcUserConfig { + // Note: preferWasm is here for backwards compatibility. It can't be + // defined or not dependent on the type, as there isn't a top-level type. + // Instead, we post-validate the resolved config to make sure that it's + // only `true` if all the `compilers` and `overrides` have type `solc`. + preferWasm?: boolean; + // Note: Duplicated wrt CommonSolidityUserConfig for backwards compatibility + isolated?: boolean; + compilers: SolidityCompilerUserConfig[]; + overrides?: Record; + } + + /** + * The type of a multi-version SolidityUserConfig. + * + * Partially defined in `MultiVersionSolcUserConfig` for backwards + * compatibility. + */ export interface MultiVersionSolidityUserConfig extends MultiVersionSolcUserConfig, CommonSolidityUserConfig {} + /** + * The type of a single-version build profile user config. + */ + export type SingleVersionBuildProfileUserConfig = + SolidityCompilerUserConfig & { + isolated?: boolean; + }; + + /** + * The type of a multi-version build profile user config. + */ + /* eslint-disable-next-line @typescript-eslint/no-empty-interface -- Defined + in `MultiVersionSolcUserConfig` for backwards compatibility. */ + export interface MultiVersionBuildProfileUserConfig + extends MultiVersionSolcUserConfig {} + + /** + * The type of the build profile version of the SolidityUserConfig. + */ export interface BuildProfilesSolidityUserConfig extends CommonSolidityUserConfig { profiles: Record< string, - SingleVersionSolcUserConfig | MultiVersionSolcUserConfig + SingleVersionBuildProfileUserConfig | MultiVersionBuildProfileUserConfig >; } + /** + * Extension of HardhatUserConfig with the `solidity` property. + */ export interface HardhatUserConfig { solidity?: SolidityUserConfig; } - export interface SolcConfig { + /** + * Common fields of a SolidityCompilerConfig. + * + * Note: All the types of SolidityCompiler config MUST extend this interface. + * This is especially relevant for plugins creating their own + * `SolidityCompilerConfig` variant. + */ + export interface CommonSolidityCompilerConfig { + type?: SolidityCompilerType; version: string; settings: any; path?: string; + } + + /** + * Deprecated: Use `SolcSolidityCompilerConfig` instead. + * @deprecated + */ + export interface SolcConfig extends CommonSolidityCompilerConfig { + // Note: This field is optional for backwards compatibility. No `type` means + // "solc" across all of Hardhat. + type?: "solc"; preferWasm?: boolean; } + /** + * The type of a solc-specific SolidityCompilerConfig. + */ + /* eslint-disable-next-line @typescript-eslint/no-empty-interface -- Defined + in SolcConfig for backwards compatibility */ + export interface SolcSolidityCompilerConfig extends SolcConfig {} + + /** + * A map from compiler type to its `SolidityCompilerConfig` type. Note that + * the types MUST extend `CommonSolidityCompilerConfig`. + */ + export interface SolidityCompilerConfigPerType { + solc: SolcSolidityCompilerConfig; + } + + /** + * The type of all the compiler configs. + */ + export type SolidityCompilerConfig = + | { + [type in keyof SolidityCompilerConfigPerType]: SolidityCompilerConfigPerType[type] & { + type: type; + }; + }[keyof SolidityCompilerConfigPerType] + | (Omit & + Partial>); + + /** + * The type of a resolved build profile config. + */ export interface SolidityBuildProfileConfig { isolated: boolean; preferWasm: boolean; - compilers: SolcConfig[]; - overrides: Record; + compilers: SolidityCompilerConfig[]; + overrides: Record; } + /** + * Resolved Solidity config. + */ export interface SolidityConfig { profiles: Record; npmFilesToBuild: string[]; + registeredCompilerTypes: SolidityCompilerType[]; } + /** + * An extension of HardhatConfig with the `solidity` property. + */ export interface HardhatConfig { solidity: SolidityConfig; } @@ -166,7 +359,7 @@ declare module "../../../types/hooks.js" { ): Promise; /** - * Hook triggered within the compilation job when its' solc input is first constructed. + * Hook triggered within the compilation job when its solc input is first constructed. * * @param context The hook context. * @param solcInput The solc input that will be passed to solc. @@ -215,18 +408,18 @@ declare module "../../../types/hooks.js" { ) => Promise>; /** - * Hook triggered to invoke a passed in Solc compiler on the - * Solc input generated for a given compilation job. - * This hook allows for manipulating the Solc input passed into the Solc - * compiler Hardhat has selected for the compilation job, and similarly to - * manipulate the Solc output. + * Hook triggered to invoke a compiler on the standard-json input + * generated for a given compilation job. + * This hook allows for manipulating the input passed into the compiler + * Hardhat has selected for the compilation job, and similarly to + * manipulate the output. * * @param context The hook context. - * @param compile The Solc compiler selected by Hardhat for this compilation - * job. - * @param solcInput The solc input json constructed from the compilation + * @param compiler The compiler selected by Hardhat for this compilation * job. - * @param solcConfig The configuration used to setup solc e.g. version. + * @param solcInput The standard-json input constructed from the + * compilation job. + * @param solcConfig The compiler configuration (version, type, etc.). * @param next A function to call the next handler for this hook, or the * default implementation if no more handlers exist. */ @@ -234,12 +427,12 @@ declare module "../../../types/hooks.js" { context: HookContext, compiler: Compiler, solcInput: CompilerInput, - solcConfig: SolcConfig, + solcConfig: SolidityCompilerConfig, next: ( nextContext: HookContext, nextCompiler: Compiler, nextSolcInput: CompilerInput, - nextSolcConfig: SolcConfig, + nextSolcConfig: SolidityCompilerConfig, ) => Promise, ): Promise; diff --git a/v-next/hardhat/src/internal/core/hre.ts b/v-next/hardhat/src/internal/core/hre.ts index 991ad96fd5a..3cf628029a3 100644 --- a/v-next/hardhat/src/internal/core/hre.ts +++ b/v-next/hardhat/src/internal/core/hre.ts @@ -13,6 +13,7 @@ import type { GlobalOptionDefinitions, } from "../../types/global-options.js"; import type { + HardhatConfigValidationError, HardhatUserConfigValidationError, HookContext, HookManager, @@ -96,6 +97,20 @@ export class HardhatRuntimeEnvironmentImplementation ); if (!configResolutionResult.success) { + if (configResolutionResult.configValidationErrors !== undefined) { + throw new HardhatError( + HardhatError.ERRORS.CORE.GENERAL.INVALID_RESOLVED_CONFIG, + { + errors: `\t${configResolutionResult.configValidationErrors + .map( + (error) => + `* Resolved config error in config.${error.path.join(".")}: ${error.message}`, + ) + .join("\n\t")}`, + }, + ); + } + throw new HardhatError(HardhatError.ERRORS.CORE.GENERAL.INVALID_CONFIG, { errors: `\t${configResolutionResult.userConfigValidationErrors .map( @@ -211,6 +226,7 @@ export async function resolveUserConfigToHardhatConfig( | { success: false; userConfigValidationErrors: HardhatUserConfigValidationError[]; + configValidationErrors?: HardhatConfigValidationError[]; } > { // extend user config: @@ -225,6 +241,7 @@ export async function resolveUserConfigToHardhatConfig( extendedUserConfig, ); + // If user config is structurally invalid, we can't resolve — return early. if (userConfigValidationErrors.length > 0) { return { success: false, @@ -252,6 +269,22 @@ export async function resolveUserConfigToHardhatConfig( plugins: resolvedPlugins, }; + // Validate the resolved config (post-resolution checks). + const resolvedConfigValidationResults = await hooks.runSequentialHandlers( + "config", + "validateResolvedConfig", + [config], + ); + const resolvedConfigValidationErrors = resolvedConfigValidationResults.flat(); + + if (resolvedConfigValidationErrors.length > 0) { + return { + success: false, + userConfigValidationErrors: [], + configValidationErrors: resolvedConfigValidationErrors, + }; + } + return { success: true, config, extendedUserConfig }; } diff --git a/v-next/hardhat/src/types/hooks.ts b/v-next/hardhat/src/types/hooks.ts index 9f3e167236a..3bc36df3df8 100644 --- a/v-next/hardhat/src/types/hooks.ts +++ b/v-next/hardhat/src/types/hooks.ts @@ -93,6 +93,20 @@ export interface ConfigHooks { nextResolveConfigurationVariable: ConfigurationVariableResolver, ) => Promise, ) => Promise; + + /** + * Provide a handler for this hook to validate the resolved config. + * + * This hook runs after all plugins have resolved their config. Use it to + * validate cross-cutting concerns that require the fully resolved config + * (e.g., checking that all compiler types are registered). + * + * @param resolvedConfig The fully resolved config. + * @returns An array of validation errors. + */ + validateResolvedConfig: ( + resolvedConfig: HardhatConfig, + ) => Promise; } /** @@ -114,6 +128,29 @@ export interface HardhatUserConfigValidationError { message: string; } +/** + * A `HardhatConfig` validation error. + * + * This is the equivalent of `HardhatUserConfigValidationError` but for the + * resolved config. + */ +export interface HardhatConfigValidationError { + /** + * The path from the resolved config object to the value that originated this + * validation error. + * + * For example, if `config.solidity.profiles.foo.compilers[0].type` is + * invalid, this array would be + * `["solidity", "profiles", "foo", "compilers", 0, "type"]`. + */ + path: Array; + + /** + * The error message. + */ + message: string; +} + /** * ConfigurationVariable-related hooks. */ diff --git a/v-next/hardhat/src/types/solidity/compilation-job.ts b/v-next/hardhat/src/types/solidity/compilation-job.ts index 12ff4babec6..358489c5777 100644 --- a/v-next/hardhat/src/types/solidity/compilation-job.ts +++ b/v-next/hardhat/src/types/solidity/compilation-job.ts @@ -1,9 +1,9 @@ import type { CompilerInput } from "./compiler-io.js"; import type { DependencyGraph } from "./dependency-graph.js"; -import type { SolcConfig } from "../config.js"; +import type { SolidityCompilerConfig } from "../config.js"; /** - * A compilation job to be run using solc. + * A compilation job to be run. */ export interface CompilationJob { /** @@ -13,12 +13,12 @@ export interface CompilationJob { dependencyGraph: DependencyGraph; /** - * The solc config to use. + * The compiler config to use. */ - solcConfig: SolcConfig; + solcConfig: SolidityCompilerConfig; /** - * The long version of the solc compiler to be used. + * The long version of the compiler to be used. */ solcLongVersion: string; diff --git a/v-next/hardhat/src/types/solidity/solidity-artifacts.ts b/v-next/hardhat/src/types/solidity/solidity-artifacts.ts index 426895e7c56..c40d192738a 100644 --- a/v-next/hardhat/src/types/solidity/solidity-artifacts.ts +++ b/v-next/hardhat/src/types/solidity/solidity-artifacts.ts @@ -14,6 +14,12 @@ export interface SolidityBuildInfo { /** * The id of the build, which is derived from the rest of the data, * guaranteeing that it's unique and deterministic. + * + * When `compilerType` is present and is not "solc", the format is: + * `solc-__--` + * + * Otherwise (i.e. solc or undefined), the format is: + * `solc-__-` */ readonly id: string; @@ -27,6 +33,16 @@ export interface SolidityBuildInfo { */ readonly solcLongVersion: string; + /** + * The compiler type used for this build. If absent or undefined, it means + * "solc" was used. + * + * Note: This is typed as `string` rather than `SolidityCompilerType` because + * the build info may come from a different Hardhat setup where the compiler + * type may not be registered in the current type definitions. + */ + readonly compilerType?: string; + /** * A mapping from user source names to input source names, for the root * files of the build (i.e. the files whose artifacts where being compiled). diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts index 5bfa9ca7c73..7ceaa0d8767 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts @@ -845,6 +845,92 @@ describe("NetworkManagerImplementation", () => { }); }); + describe("connect when resolved config validation fails", () => { + const resolvedConfigValidatorPlugin: HardhatPlugin = { + id: "resolved-config-validator-plugin", + hookHandlers: { + config: async () => ({ + default: async () => { + const handlers: Partial = { + validateResolvedConfig: async (resolvedConfig) => { + // Only return errors when timeout has been overridden to 99999, + // so that HRE creation succeeds but the network-manager override + // path triggers the error. + const localhost = resolvedConfig.networks.localhost; + if ( + localhost !== undefined && + localhost.type === "http" && + localhost.timeout === 99999 + ) { + return [ + { + path: ["networks", "localhost", "custom"], + message: "custom is invalid", + }, + ]; + } + return []; + }, + }; + return handlers; + }, + }), + }, + }; + + beforeEach(async () => { + hre = await createHardhatRuntimeEnvironment({ + plugins: [resolvedConfigValidatorPlugin], + }); + + userNetworks = { + localhost: { + type: "http", + url: "http://localhost:8545", + }, + }; + + networks = { + localhost: resolveHttpNetwork( + { + type: "http", + url: "http://localhost:8545", + }, + (varOrStr) => resolveConfigurationVariable(hre.hooks, varOrStr), + ), + }; + + chainDescriptors = await resolveChainDescriptors(undefined); + + networkManager = new NetworkManagerImplementation( + "localhost", + GENERIC_CHAIN_TYPE, + networks, + hre.hooks, + hre.artifacts, + { networks: userNetworks }, + chainDescriptors, + hre.globalOptions.config, + hre.config.paths.root, + ); + }); + + it("should throw INVALID_CONFIG_OVERRIDE when resolved config validation returns errors", async () => { + await assertRejectsWithHardhatError( + networkManager.connect({ + network: "localhost", + override: { + timeout: 99999, + }, + }), + HardhatError.ERRORS.CORE.NETWORK.INVALID_CONFIG_OVERRIDE, + { + errors: `\t* Error in resolved config networks.localhost.custom: custom is invalid`, + }, + ); + }); + }); + describe("createServer", function () { it("should throw an error if the network type is not edr-simulated", async () => { await assertRejectsWithHardhatError( diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity-test/edr-artifacts.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity-test/edr-artifacts.ts new file mode 100644 index 00000000000..6a22f2439a4 --- /dev/null +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity-test/edr-artifacts.ts @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { BUILD_INFO_FORMAT } from "../../../../src/internal/builtin-plugins/solidity-test/edr-artifacts.js"; + +describe("BUILD_INFO_FORMAT", () => { + it("matches a standard build ID", () => { + const match = BUILD_INFO_FORMAT.exec("solc-0_8_0-abc123"); + + assert.ok( + match !== null && match.groups !== undefined, + "Regexp should match and have groups", + ); + + assert.equal(match.groups.major, "0"); + assert.equal(match.groups.minor, "8"); + assert.equal(match.groups.patch, "0"); + assert.equal(match.groups.compilerType, undefined); + }); + + it("matches a build ID with compiler type", () => { + const match = BUILD_INFO_FORMAT.exec("solc-0_8_0-solx-abc123"); + + assert.ok( + match !== null && match.groups !== undefined, + "Regexp should match and have groups", + ); + + assert.equal(match.groups.compilerType, "solx"); + }); + + it("matches a build ID with empty hash", () => { + // The regex allows zero hex chars in the hash portion + const match = BUILD_INFO_FORMAT.exec("solc-0_8_0-"); + assert.notEqual(match, null); + }); + + it("does not match a build ID without solc prefix", () => { + const match = BUILD_INFO_FORMAT.exec("solx-0_8_0-abc123"); + assert.equal(match, null); + }); + + it("does not match a build ID with dots in version", () => { + const match = BUILD_INFO_FORMAT.exec("solc-0.8.0-abc123"); + assert.equal(match, null); + }); +}); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/build-system.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/build-system.ts index fd2e4690fa0..e1c345fb229 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/build-system.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/build-system.ts @@ -96,6 +96,7 @@ describe( }, }, npmFilesToBuild: [], + registeredCompilerTypes: ["solc"], }; before(async () => { diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/compilation-job.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/compilation-job.ts index e15b98cbbec..b419a4b31e3 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/compilation-job.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/compilation-job.ts @@ -1,4 +1,5 @@ -import type { SolcConfig } from "../../../../../src/types/config.js"; +/* eslint-disable @typescript-eslint/consistent-type-assertions -- Tests use `as any` casts for non-solc compiler types that are not registered in the base type system */ +import type { SolidityCompilerConfig } from "../../../../../src/types/config.js"; import type { HookContext } from "../../../../../src/types/hooks.js"; import assert from "node:assert/strict"; @@ -22,7 +23,7 @@ describe("CompilationJobImplementation", () => { let rootFile: ProjectResolvedFile; let npmDependencyFile: NpmPackageResolvedFile; let projectDependencyFile: ProjectResolvedFile; - let solcConfig: SolcConfig; + let solcConfig: SolidityCompilerConfig; let solcLongVersion: string; let hooks: HookManagerImplementation; let compilationJob: CompilationJobImplementation; @@ -85,7 +86,6 @@ describe("CompilationJobImplementation", () => { solcLongVersion = "0.8.0-c7dfd78"; hooks = new HookManagerImplementation(process.cwd(), []); - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We don't care about hooks in this context hooks.setContext({} as HookContext); compilationJob = new CompilationJobImplementation( dependencyGraph, @@ -369,7 +369,37 @@ describe("CompilationJobImplementation", () => { await compilationJob2.getBuildId(), ); }); + + it("the compiler type changes", async () => { + const newCompilationJob = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: "solx" as any }, + solcLongVersion, + hooks, + ); + assert.notEqual( + await compilationJob.getBuildId(), + await newCompilationJob.getBuildId(), + ); + }); + + it("the compiler type changes between two non-solc types", async () => { + const jobA = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: "solx" as any }, + solcLongVersion, + hooks, + ); + const jobB = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: "foo" as any }, + solcLongVersion, + hooks, + ); + assert.notEqual(await jobA.getBuildId(), await jobB.getBuildId()); + }); }); + describe("should not change when", () => { it("the version of one of the dependencies changes without it being reflected in the input source name", async () => { const newDependencyGraph = new DependencyGraphImplementation(); @@ -410,6 +440,82 @@ describe("CompilationJobImplementation", () => { await newCompilationJob.getBuildId(), ); }); + + it("the compiler type is undefined vs 'solc'", async () => { + const jobUndefined = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: undefined }, + solcLongVersion, + hooks, + ); + const jobSolc = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: "solc" }, + solcLongVersion, + hooks, + ); + assert.equal( + await jobUndefined.getBuildId(), + await jobSolc.getBuildId(), + ); + }); + }); + + describe("build ID format", () => { + it("should use the format solc-- when the compiler type is undefined", async () => { + const buildId = await compilationJob.getBuildId(); + assert.match(buildId, /^solc-\d+_\d+_\d+-[0-9a-f]+$/); + }); + + it("should use the format solc-- when the compiler type is 'solc'", async () => { + const job = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: "solc" }, + solcLongVersion, + hooks, + ); + const buildId = await job.getBuildId(); + assert.match(buildId, /^solc-\d+_\d+_\d+-[0-9a-f]+$/); + }); + + it("should use the format solc--- for non-solc types", async () => { + const job = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: "solx" as any }, + solcLongVersion, + hooks, + ); + const buildId = await job.getBuildId(); + assert.match(buildId, /^solc-\d+_\d+_\d+-solx-[0-9a-f]+$/); + }); + + it("should include the compiler type in the hash preimage for non-solc types", async () => { + // Two different non-solc types should produce different hashes + // even with the same settings, because compilerType is part of the preimage + const jobSolx = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: "solx" as any }, + solcLongVersion, + hooks, + ); + const jobFoo = new CompilationJobImplementation( + dependencyGraph, + { ...solcConfig, type: "foo" as any }, + solcLongVersion, + hooks, + ); + const solxId = await jobSolx.getBuildId(); + const fooId = await jobFoo.getBuildId(); + + // Both should have the format with compiler type + assert.match(solxId, /^solc-\d+_\d+_\d+-solx-[0-9a-f]+$/); + assert.match(fooId, /^solc-\d+_\d+_\d+-foo-[0-9a-f]+$/); + + // The hash portions should differ because compilerType is in the preimage + const solxHash = solxId.split("-").pop(); + const fooHash = fooId.split("-").pop(); + assert.notEqual(solxHash, fooHash); + }); }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/cache-hit-results.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/cache-hit-results.ts index 1670d265f7c..a55124a2028 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/cache-hit-results.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/cache-hit-results.ts @@ -1,8 +1,12 @@ +import type { CompileCache } from "../../../../../../src/internal/builtin-plugins/solidity/build-system/cache.js"; + import assert from "node:assert/strict"; import { writeFile } from "node:fs/promises"; import path from "node:path"; import { describe, it } from "node:test"; +import { readJsonFile, writeJsonFile } from "@nomicfoundation/hardhat-utils/fs"; + import { FileBuildResultType } from "../../../../../../src/types/solidity/build-system.js"; import { useTestProjectTemplate } from "../resolver/helpers.js"; @@ -171,5 +175,83 @@ contract Foo {}`, FileBuildResultType.BUILD_SUCCESS, ); }); + + it("should return BUILD_SUCCESS when compilerType in cache differs", async () => { + await using project = await useTestProjectTemplate({ + name: "test-project", + version: "1.0.0", + files: { + "contracts/Foo.sol": `// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +contract Foo {}`, + }, + }); + + const hre = await getHRE(project); + const filePath = path.join(project.path, "contracts/Foo.sol"); + + // First build + await hre.solidity.build([filePath], { quiet: true }); + + // Tamper with the cache to simulate a different compiler type + const cachePath = path.join(project.path, "cache", "compile-cache.json"); + const cache: CompileCache = await readJsonFile(cachePath); + for (const key of Object.keys(cache)) { + cache[key].compilerType = "different-compiler"; + } + await writeJsonFile(cachePath, cache); + + // Second build should be a cache miss due to compiler type mismatch + const result = await hre.solidity.build([filePath], { quiet: true }); + assert( + hre.solidity.isSuccessfulBuildResult(result), + "Build should succeed", + ); + + assert.equal( + result.get(filePath)?.type, + FileBuildResultType.BUILD_SUCCESS, + ); + }); + + it("should return BUILD_SUCCESS when compilerType is missing from cache (old format)", async () => { + await using project = await useTestProjectTemplate({ + name: "test-project", + version: "1.0.0", + files: { + "contracts/Foo.sol": `// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +contract Foo {}`, + }, + }); + + const hre = await getHRE(project); + const filePath = path.join(project.path, "contracts/Foo.sol"); + + // First build + await hre.solidity.build([filePath], { quiet: true }); + + // Remove compilerType from cache to simulate old cache format + const cachePath = path.join(project.path, "cache", "compile-cache.json"); + const cache: Record> = await readJsonFile( + cachePath, + ); + for (const key of Object.keys(cache)) { + delete cache[key].compilerType; + } + await writeJsonFile(cachePath, cache); + + // Second build should be a cache miss due to missing compiler type + const result = await hre.solidity.build([filePath], { quiet: true }); + assert( + hre.solidity.isSuccessfulBuildResult(result), + "Build should succeed", + ); + + assert.equal( + result.get(filePath)?.type, + FileBuildResultType.BUILD_SUCCESS, + ); + }); }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/config.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/config.ts index cbfa10fe05c..ff1c45ffb2f 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/config.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/config.ts @@ -1,10 +1,17 @@ /* eslint-disable @typescript-eslint/consistent-type-assertions -- test*/ +import type { + HardhatConfig, + SolidityCompilerConfig, +} from "../../../../src/types/config.js"; + import assert from "node:assert/strict"; import { describe, it } from "node:test"; +import { isSolcSolidityCompilerConfig } from "../../../../src/internal/builtin-plugins/solidity/build-system/build-system.js"; import { missesSomeOfficialNativeBuilds } from "../../../../src/internal/builtin-plugins/solidity/build-system/solc-info.js"; import { resolveSolidityUserConfig, + validateSolidityConfig, validateSolidityUserConfig, } from "../../../../src/internal/builtin-plugins/solidity/config.js"; @@ -277,10 +284,6 @@ describe("solidity plugin config validation", () => { }, }), [ - { - message: "Expected boolean, received string", - path: ["solidity", "isolated"], - }, { message: "Expected string, received number", path: ["solidity", "compilers", 0, "version"], @@ -289,6 +292,10 @@ describe("solidity plugin config validation", () => { message: "Expected object, received array", path: ["solidity", "overrides"], }, + { + message: "Expected boolean, received string", + path: ["solidity", "isolated"], + }, ], ); }); @@ -324,6 +331,10 @@ describe("solidity plugin config validation", () => { message: "Expected boolean, received string", path: ["solidity", "profiles", "default", "isolated"], }, + { + message: "Expected boolean, received string", + path: ["solidity", "profiles", "production", "isolated"], + }, { message: "This field is incompatible with `version`", path: ["solidity", "profiles", "production", "compilers"], @@ -332,10 +343,6 @@ describe("solidity plugin config validation", () => { message: "This field is incompatible with `version`", path: ["solidity", "profiles", "production", "overrides"], }, - { - message: "Expected boolean, received string", - path: ["solidity", "profiles", "production", "isolated"], - }, ], ); }); @@ -452,7 +459,7 @@ describe("solidity plugin config validation", () => { }); describe("per-compiler preferWasm validation", () => { - it("Should accept preferWasm in SolcUserConfig", () => { + it("Should accept preferWasm in SolcSolidityCompilerUserConfig", () => { assert.deepEqual( validateSolidityUserConfig({ solidity: { @@ -466,7 +473,7 @@ describe("solidity plugin config validation", () => { ); }); - it("Should reject invalid preferWasm values in SolcUserConfig", () => { + it("Should reject invalid preferWasm values in SolcSolidityCompilerUserConfig", () => { assert.deepEqual( validateSolidityUserConfig({ solidity: { @@ -481,10 +488,70 @@ describe("solidity plugin config validation", () => { ], ); }); + + it("Should accept preferWasm on compiler with type 'solc'", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + compilers: [{ type: "solc", version: "0.8.28", preferWasm: true }], + }, + }), + [], + ); + }); + + it("Should allow extra fields on non-solc compiler types (passthrough)", () => { + // Non-solc types use passthrough validation, allowing plugins to define + // their own fields. preferWasm is meaningless for non-solc but not rejected. + const errors = validateSolidityUserConfig({ + solidity: { + compilers: [ + { type: "solx", version: "0.8.28", preferWasm: true } as any, + ], + }, + }); + assert.deepEqual(errors, []); + }); + }); + + describe("per-compiler type validation", () => { + it("Should accept type field in compiler config", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + compilers: [{ version: "0.8.28", type: "solx" } as any], + }, + }), + [], + ); + }); + + it("Should accept missing type field (backward compat)", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + compilers: [{ version: "0.8.28" }], + }, + }), + [], + ); + }); + + it("Should reject invalid type values", () => { + const errors = validateSolidityUserConfig({ + solidity: { + compilers: [{ version: "0.8.28", type: 123 as any }], + }, + }); + assert.ok( + errors.length > 0, + "Should produce validation error for non-string type", + ); + }); }); describe("per-compiler path validation", () => { - it("Should accept path in SolcUserConfig", () => { + it("Should accept path in SolcSolidityCompilerUserConfig", () => { assert.deepEqual( validateSolidityUserConfig({ solidity: { @@ -495,7 +562,7 @@ describe("solidity plugin config validation", () => { ); }); - it("Should reject invalid path values in SolcUserConfig", () => { + it("Should reject invalid path values in SolcSolidityCompilerUserConfig", () => { assert.deepEqual( validateSolidityUserConfig({ solidity: { @@ -511,6 +578,151 @@ describe("solidity plugin config validation", () => { ); }); }); + + describe("non-solc single-version config validation", () => { + it("Should accept a non-solc single-version config", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + type: "solx", + version: "0.8.28", + } as any, + }), + [], + ); + }); + + it("Should allow extra fields on non-solc single-version configs", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + type: "solx", + version: "0.8.28", + settings: { custom: true }, + path: "/path/to/solx", + } as any, + }), + [], + ); + }); + + it("Should accept non-solc single-version config with isolated and npmFilesToBuild", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + type: "solx", + version: "0.8.28", + isolated: true, + npmFilesToBuild: ["./build.js"], + } as any, + }), + [], + ); + }); + }); + + describe("non-solc build profile validation", () => { + it("Should accept non-solc single-version in build profiles", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + profiles: { + default: { + type: "solx", + version: "0.8.28", + } as any, + }, + }, + }), + [], + ); + }); + + it("Should accept non-solc single-version with isolated in build profiles", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + profiles: { + default: { + type: "solx", + version: "0.8.28", + isolated: true, + } as any, + }, + }, + }), + [], + ); + }); + + it("Should accept non-solc compilers in multi-version build profiles", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + profiles: { + default: { + compilers: [{ type: "solx", version: "0.8.28" } as any], + }, + }, + }, + }), + [], + ); + }); + + it("Should accept mixed solc and non-solc compilers in build profiles", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + profiles: { + default: { + compilers: [ + { version: "0.8.28" }, + { type: "solx", version: "0.8.28" } as any, + ], + }, + }, + }, + }), + [], + ); + }); + + it("Should accept non-solc overrides in build profiles", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + profiles: { + default: { + compilers: [{ version: "0.8.28" }], + overrides: { + "Contract.sol": { type: "solx", version: "0.8.28" } as any, + }, + }, + }, + }, + }), + [], + ); + }); + + it("Should accept non-solc compiler with settings in build profiles", () => { + assert.deepEqual( + validateSolidityUserConfig({ + solidity: { + profiles: { + default: { + type: "solx", + version: "0.8.28", + settings: { optimization: "z" }, + } as any, + }, + }, + }), + [], + ); + }); + }); }); describe("solidity plugin config resolution", () => { @@ -780,4 +992,492 @@ describe("solidity plugin config resolution", () => { }); }, ); + + describe("config resolution with type discriminator", () => { + const otherResolvedConfig = { paths: { root: process.cwd() } } as any; + + it("should resolve compiler entry without type with type undefined (backward compat)", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { + solidity: { + compilers: [{ version: "0.8.28" }], + }, + }, + otherResolvedConfig, + ); + + const compiler = resolvedConfig.solidity.profiles.default.compilers[0]; + assert.equal(compiler.type, undefined); + // Should still be a SolcSolidityCompilerConfig with preferWasm field + assert.ok( + "preferWasm" in compiler, + "Compiler without type should resolve as SolcSolidityCompilerConfig with preferWasm", + ); + }); + + it("should resolve compiler entry with type 'solc' as SolcSolidityCompilerConfig with preferWasm", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { + solidity: { + compilers: [{ type: "solc", version: "0.8.28" }], + }, + }, + otherResolvedConfig, + ); + + const compiler = resolvedConfig.solidity.profiles.default.compilers[0]; + assert.equal(compiler.type, "solc"); + assert.ok( + "preferWasm" in compiler, + "Compiler with type 'solc' should resolve as SolcSolidityCompilerConfig with preferWasm", + ); + }); + + it("should resolve compiler entry with non-solc type without preferWasm", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { + solidity: { + compilers: [{ type: "solx", version: "0.8.28" } as any], + }, + }, + otherResolvedConfig, + ); + + const compiler = resolvedConfig.solidity.profiles.default.compilers[0]; + assert.equal(compiler.type, "solx"); + assert.ok( + !("preferWasm" in compiler), + "Compiler with non-solc type should not have preferWasm field", + ); + }); + }); + + describe("shouldn't enable the optimizer in non-solc production compilers", () => { + const otherResolvedConfig = { paths: { root: process.cwd() } } as any; + + it("should not add optimizer defaults for non-solc compilers in production", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { + solidity: { + profiles: { + default: { + compilers: [{ type: "solx", version: "0.8.28" } as any], + }, + production: { + compilers: [{ type: "solx", version: "0.8.28" } as any], + }, + }, + }, + }, + otherResolvedConfig, + ); + + const prodCompiler = + resolvedConfig.solidity.profiles.production.compilers[0]; + assert.equal( + prodCompiler.settings.optimizer, + undefined, + "Non-solc production compiler should not get optimizer defaults", + ); + }); + }); + + describe("copyFromDefault preserves type field", () => { + const otherResolvedConfig = { paths: { root: process.cwd() } } as any; + + it("should preserve type on auto-generated production profile", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { + solidity: { + compilers: [{ type: "solx", version: "0.8.28" } as any], + }, + }, + otherResolvedConfig, + ); + + const prodCompiler = + resolvedConfig.solidity.profiles.production.compilers[0]; + assert.equal( + prodCompiler.type, + "solx", + "Production profile should inherit type from default", + ); + }); + + it("should preserve type on auto-generated production profile (single version config)", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { + solidity: { + type: "solx", + version: "0.8.28", + } as any, + }, + otherResolvedConfig, + ); + + const prodCompiler = + resolvedConfig.solidity.profiles.production.compilers[0]; + assert.equal( + prodCompiler.type, + "solx", + "Production profile should inherit type from single-version default", + ); + }); + }); + + describe("backward compatibility", () => { + const otherResolvedConfig = { paths: { root: process.cwd() } } as any; + + it("should resolve an existing multi-version config identically (no type field)", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { + solidity: { + compilers: [{ version: "0.8.24" }, { version: "0.8.28" }], + overrides: { + "contracts/Special.sol": { version: "0.8.26" }, + }, + }, + }, + otherResolvedConfig, + ); + + const defaultProfile = resolvedConfig.solidity.profiles.default; + assert.equal(defaultProfile.compilers.length, 2); + assert.equal(defaultProfile.compilers[0].version, "0.8.24"); + assert.equal(defaultProfile.compilers[0].type, undefined); + assert.equal(defaultProfile.compilers[1].version, "0.8.28"); + assert.equal(defaultProfile.compilers[1].type, undefined); + assert.equal( + defaultProfile.overrides["contracts/Special.sol"].version, + "0.8.26", + ); + assert.equal( + defaultProfile.overrides["contracts/Special.sol"].type, + undefined, + ); + // SolcSolidityCompilerConfig fields should be present + assert.ok( + "preferWasm" in defaultProfile.compilers[0], + "Existing configs should still resolve with preferWasm", + ); + // registeredCompilerTypes should be seeded with "solc" + assert.deepEqual(resolvedConfig.solidity.registeredCompilerTypes, [ + "solc", + ]); + }); + + it("should resolve a simple version string config identically", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { solidity: "0.8.28" }, + otherResolvedConfig, + ); + + const defaultProfile = resolvedConfig.solidity.profiles.default; + assert.equal(defaultProfile.compilers.length, 1); + assert.equal(defaultProfile.compilers[0].version, "0.8.28"); + assert.equal(defaultProfile.compilers[0].type, undefined); + assert.ok( + "preferWasm" in defaultProfile.compilers[0], + "Existing configs should still resolve with preferWasm", + ); + // Production profile should also exist + assert.ok( + "production" in resolvedConfig.solidity.profiles, + "Production profile should exist", + ); + assert.deepEqual(resolvedConfig.solidity.registeredCompilerTypes, [ + "solc", + ]); + }); + + it("should resolve a build profiles config identically", async () => { + const resolvedConfig = await resolveSolidityUserConfig( + { + solidity: { + profiles: { + default: { + compilers: [{ version: "0.8.24" }, { version: "0.8.28" }], + }, + production: { + version: "0.8.28", + isolated: true, + }, + }, + }, + }, + otherResolvedConfig, + ); + + const defaultProfile = resolvedConfig.solidity.profiles.default; + const prodProfile = resolvedConfig.solidity.profiles.production; + assert.equal(defaultProfile.compilers.length, 2); + assert.equal(defaultProfile.compilers[0].type, undefined); + assert.equal(prodProfile.compilers[0].version, "0.8.28"); + assert.equal(prodProfile.isolated, true); + assert.deepEqual(resolvedConfig.solidity.registeredCompilerTypes, [ + "solc", + ]); + }); + }); +}); + +describe("isSolcSolidityCompilerConfig type guard", () => { + it("should return true for config with undefined type", () => { + const config: SolidityCompilerConfig = { + type: undefined, + version: "0.8.28", + settings: {}, + }; + assert.equal(isSolcSolidityCompilerConfig(config), true); + }); + + it("should return true for config with type 'solc'", () => { + const config: SolidityCompilerConfig = { + type: "solc", + version: "0.8.28", + settings: {}, + }; + assert.equal(isSolcSolidityCompilerConfig(config), true); + }); + + it("should return false for config with non-solc type", () => { + const config: SolidityCompilerConfig = { + type: "solx" as any, + version: "0.8.28", + settings: {}, + }; + assert.equal(isSolcSolidityCompilerConfig(config), false); + }); +}); + +describe("validateResolvedConfig", () => { + const makeConfig = ( + profiles: HardhatConfig["solidity"]["profiles"], + registeredCompilerTypes: string[], + ) => + ({ + solidity: { + profiles, + npmFilesToBuild: [], + registeredCompilerTypes, + }, + }) as unknown as HardhatConfig; + + const makeProfile = ( + compilers: Array<{ version: string; type?: string }>, + overrides: Record = {}, + preferWasm: boolean = false, + ): HardhatConfig["solidity"]["profiles"][string] => + ({ + compilers: compilers.map((c) => ({ ...c, settings: {} })), + overrides: Object.fromEntries( + Object.entries(overrides).map(([k, v]) => [k, { ...v, settings: {} }]), + ), + isolated: false, + preferWasm, + }) as HardhatConfig["solidity"]["profiles"][string]; + + describe("compiler type registration", () => { + it("should produce no errors when all compiler types are registered", () => { + const config = makeConfig( + { default: makeProfile([{ version: "0.8.28" }]) }, + ["solc"], + ); + const errors = validateSolidityConfig(config); + assert.deepEqual(errors, []); + }); + + it("should produce an error for an unknown compiler type in compilers[]", () => { + const config = makeConfig( + { + default: makeProfile([{ version: "0.8.28", type: "solx" }]), + }, + ["solc"], + ); + assert.deepEqual(validateSolidityConfig(config), [ + { + path: ["solidity", "profiles", "default", "compilers", 0, "type"], + message: `Unknown compiler type "solx". Registered types: solc`, + }, + ]); + }); + + it("should produce no errors when solx is registered", () => { + const config = makeConfig( + { + default: makeProfile([{ version: "0.8.28", type: "solx" }]), + }, + ["solc", "solx"], + ); + const errors = validateSolidityConfig(config); + assert.deepEqual(errors, []); + }); + + it("should detect unknown types in overrides", () => { + const config = makeConfig( + { + default: makeProfile([{ version: "0.8.28" }], { + "Contract.sol": { version: "0.8.28", type: "solx" }, + }), + }, + ["solc"], + ); + assert.deepEqual(validateSolidityConfig(config), [ + { + path: [ + "solidity", + "profiles", + "default", + "overrides", + "Contract.sol", + "type", + ], + message: `Unknown compiler type "solx". Registered types: solc`, + }, + ]); + }); + + it("should collect errors across multiple profiles", () => { + const config = makeConfig( + { + default: makeProfile([{ version: "0.8.28", type: "solx" }]), + test: makeProfile([{ version: "0.8.28", type: "solx" }]), + }, + ["solc"], + ); + assert.deepEqual(validateSolidityConfig(config), [ + { + path: ["solidity", "profiles", "default", "compilers", 0, "type"], + message: `Unknown compiler type "solx". Registered types: solc`, + }, + { + path: ["solidity", "profiles", "test", "compilers", 0, "type"], + message: `Unknown compiler type "solx". Registered types: solc`, + }, + ]); + }); + }); + + describe("top-level preferWasm requires solc compilers", () => { + it("should produce no errors when preferWasm is true and all compilers are solc", () => { + const config = makeConfig( + { + default: makeProfile( + [{ version: "0.8.28" }, { version: "0.8.31", type: "solc" }], + {}, + true, + ), + }, + ["solc", "solx"], + ); + const errors = validateSolidityConfig(config); + assert.deepEqual(errors, []); + }); + + it("should produce an error when preferWasm is true and a compiler has non-solc type", () => { + const config = makeConfig( + { + default: makeProfile([{ version: "0.8.28", type: "solx" }], {}, true), + }, + ["solc", "solx"], + ); + assert.deepEqual(validateSolidityConfig(config), [ + { + path: ["solidity", "profiles", "default", "compilers", 0, "type"], + message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "solx"`, + }, + ]); + }); + + it("should produce an error when preferWasm is true and an override has non-solc type", () => { + const config = makeConfig( + { + default: makeProfile( + [{ version: "0.8.28" }], + { "Contract.sol": { version: "0.8.28", type: "solx" } }, + true, + ), + }, + ["solc", "solx"], + ); + assert.deepEqual(validateSolidityConfig(config), [ + { + path: [ + "solidity", + "profiles", + "default", + "overrides", + "Contract.sol", + "type", + ], + message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "solx"`, + }, + ]); + }); + + it("should produce errors for both non-solc compilers and overrides when preferWasm is true", () => { + const config = makeConfig( + { + default: makeProfile( + [{ version: "0.8.28", type: "solx" }], + { "Contract.sol": { version: "0.8.28", type: "solx" } }, + true, + ), + }, + ["solc", "solx"], + ); + assert.deepEqual(validateSolidityConfig(config), [ + { + path: ["solidity", "profiles", "default", "compilers", 0, "type"], + message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "solx"`, + }, + { + path: [ + "solidity", + "profiles", + "default", + "overrides", + "Contract.sol", + "type", + ], + message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "solx"`, + }, + ]); + }); + + it("should produce no errors when preferWasm is false even with non-solc compilers", () => { + const config = makeConfig( + { + default: makeProfile( + [{ version: "0.8.28", type: "solx" }], + { "Contract.sol": { version: "0.8.28", type: "solx" } }, + false, + ), + }, + ["solc", "solx"], + ); + const errors = validateSolidityConfig(config); + assert.deepEqual(errors, []); + }); + + it("should only produce errors for profiles where preferWasm is true", () => { + const config = makeConfig( + { + default: makeProfile([{ version: "0.8.28", type: "solx" }], {}, true), + production: makeProfile( + [{ version: "0.8.28", type: "solx" }], + {}, + false, + ), + }, + ["solc", "solx"], + ); + assert.deepEqual(validateSolidityConfig(config), [ + { + path: ["solidity", "profiles", "default", "compilers", 0, "type"], + message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "solx"`, + }, + ]); + }); + }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/hooks.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/hooks.ts index 7951781d7ab..8dd98aab257 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/hooks.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/hooks.ts @@ -1,4 +1,4 @@ -import type { SolcConfig } from "../../../../src/types/config.js"; +import type { SolidityCompilerConfig } from "../../../../src/types/config.js"; import type { HookContext, SolidityHooks, @@ -60,12 +60,12 @@ describe("solidity - hooks", () => { context: HookContext, compiler: Compiler, solcInput: CompilerInput, - solcConfig: SolcConfig, + solcConfig: SolidityCompilerConfig, next: ( nextContext: HookContext, nextCompiler: Compiler, nextSolcInput: CompilerInput, - nextSolcConfig: SolcConfig, + nextSolidityCompilerConfig: SolidityCompilerConfig, ) => Promise, ) => { passedCompiler = compiler; diff --git a/v-next/hardhat/test/internal/core/config-validation.ts b/v-next/hardhat/test/internal/core/config-validation.ts index c22f77df22c..6aad0123ff1 100644 --- a/v-next/hardhat/test/internal/core/config-validation.ts +++ b/v-next/hardhat/test/internal/core/config-validation.ts @@ -3,6 +3,7 @@ import type { ProjectPathsUserConfig, TestPathsUserConfig, } from "../../../src/types/config.js"; +import type { HardhatRuntimeEnvironment } from "../../../src/types/hre.js"; import type { HardhatPlugin } from "../../../src/types/plugins.js"; import type { EmptyTaskDefinition, @@ -14,6 +15,9 @@ import type { import assert from "node:assert/strict"; import { describe, it } from "node:test"; +import { HardhatError } from "@nomicfoundation/hardhat-errors"; +import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; + import { validatePositionalArguments, validateOptions, @@ -25,6 +29,9 @@ import { collectValidationErrorsForUserConfig, validatePaths, } from "../../../src/internal/core/config-validation.js"; +import { resolveProjectRoot } from "../../../src/internal/core/hre.js"; +import { resolvePluginList } from "../../../src/internal/core/plugins/resolve-plugin-list.js"; +import { createHardhatRuntimeEnvironment } from "../../../src/internal/hre-initialization.js"; import { type PositionalArgumentDefinition, type OptionDefinition, @@ -1926,3 +1933,126 @@ describe("config validation", function () { }); }); }); + +describe("resolved config validation", function () { + async function createHreWithPlugin( + config: HardhatUserConfig, + plugin: HardhatPlugin, + ): Promise { + const resolvedProjectRoot = await resolveProjectRoot(undefined); + const resolvedPlugins = await resolvePluginList(resolvedProjectRoot, [ + plugin, + ]); + return createHardhatRuntimeEnvironment(config, {}, resolvedProjectRoot, { + resolvedPlugins, + }); + } + + it("should throw INVALID_RESOLVED_CONFIG when validateResolvedConfig returns errors", async function () { + const mockPlugin: HardhatPlugin = { + id: "mock-resolved-config-validator", + hookHandlers: { + config: async () => ({ + default: async () => ({ + validateResolvedConfig: async (_resolvedConfig) => [ + { path: ["foo", "bar"], message: "bar must be positive" }, + ], + }), + }), + }, + }; + + await assertRejectsWithHardhatError( + createHreWithPlugin({}, mockPlugin), + HardhatError.ERRORS.CORE.GENERAL.INVALID_RESOLVED_CONFIG, + { + errors: + "\t* Resolved config error in config.foo.bar: bar must be positive", + }, + ); + }); + + it("should throw INVALID_RESOLVED_CONFIG with multiple errors formatted correctly", async function () { + const mockPlugin: HardhatPlugin = { + id: "mock-resolved-config-validator-multi", + hookHandlers: { + config: async () => ({ + default: async () => ({ + validateResolvedConfig: async (_resolvedConfig) => [ + { + path: [ + "solidity", + "profiles", + "default", + "compilers", + 0, + "type", + ], + message: "unknown compiler type", + }, + { path: ["networks", "localhost"], message: "invalid url" }, + ], + }), + }), + }, + }; + + await assertRejectsWithHardhatError( + createHreWithPlugin({}, mockPlugin), + HardhatError.ERRORS.CORE.GENERAL.INVALID_RESOLVED_CONFIG, + { + errors: + "\t* Resolved config error in config.solidity.profiles.default.compilers.0.type: unknown compiler type\n\t* Resolved config error in config.networks.localhost: invalid url", + }, + ); + }); + + it("should throw INVALID_CONFIG for user config errors even if validateResolvedConfig would also fail", async function () { + const mockPlugin: HardhatPlugin = { + id: "mock-both-validators", + hookHandlers: { + config: async () => ({ + default: async () => ({ + validateUserConfig: async (_userConfig) => [ + { path: ["foo"], message: "foo is required" }, + ], + validateResolvedConfig: async (_resolvedConfig) => [ + { path: ["bar"], message: "bar is invalid" }, + ], + }), + }), + }, + }; + + await assertRejectsWithHardhatError( + createHreWithPlugin({}, mockPlugin), + HardhatError.ERRORS.CORE.GENERAL.INVALID_CONFIG, + { + errors: "\t* Config error in config.foo: foo is required", + }, + ); + }); + + it("should still throw INVALID_CONFIG for user config validation errors", async function () { + const mockPlugin: HardhatPlugin = { + id: "mock-user-config-validator", + hookHandlers: { + config: async () => ({ + default: async () => ({ + validateUserConfig: async (_userConfig) => [ + { path: ["baz"], message: "baz is invalid" }, + ], + }), + }), + }, + }; + + await assertRejectsWithHardhatError( + createHreWithPlugin({}, mockPlugin), + HardhatError.ERRORS.CORE.GENERAL.INVALID_CONFIG, + { + errors: "\t* Config error in config.baz: baz is invalid", + }, + ); + }); +});