From 48b70be933096b6aa1025ca4d88dd7884f266f6b Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 20:17:18 +0000 Subject: [PATCH 01/16] Improve the SolcConfigSelector and its types It now has a better algorithm that detects more cases, explaining them to the users. It also uses file-paths everywhere, instead of shoing the internal inputSourceNames. --- .../solidity/build-system/build-system.ts | 1 - .../build-system/solc-config-selection.ts | 128 +++++++++++++++--- .../src/types/solidity/build-system.ts | 70 ++++++++-- 3 files changed, 166 insertions(+), 33 deletions(-) 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 7119543fd09..378aa8bc9f9 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 @@ -430,7 +430,6 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { const solcConfigSelector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); let subgraphsWithConfig: Array< 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 95a2250a887..b3f42f5f141 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 @@ -30,7 +30,6 @@ export class SolcConfigSelector { constructor( buildProfileName: string, buildProfile: SolidityBuildProfileConfig, - _dependencyGraph: DependencyGraph, ) { this.#buildProfileName = buildProfileName; this.#buildProfile = buildProfile; @@ -103,6 +102,18 @@ export class SolcConfigSelector { return matchingConfig; } + /** + * Returns a description of why we couldn't get a compiler configuration for + * the given root file and dependency subgraph. + * + * @param root The root file that created the single-root dependency subgraph + * @param dependencyGraph The dependency subgraph we couldn't get a compiler + * configuration for + * @param compilerVersions The compiler versions that are configured for the + * selected build profile. For overridden roots, it's a single one. + * @param overridden True if the root has an overriden config. + * @returns The error why we couldn't get a compiler configuration. + */ #getCompilationJobCreationError( root: ResolvedFile, dependencyGraph: DependencyGraph, @@ -110,27 +121,67 @@ export class SolcConfigSelector { overridden: boolean, ): CompilationJobCreationError { const rootVersionRange = root.content.versionPragmas.join(" "); - if (maxSatisfying(compilerVersions, rootVersionRange) === null) { - let reason: CompilationJobCreationErrorReason; - let formattedReason: string; - if (overridden) { - reason = - CompilationJobCreationErrorReason.INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION; - formattedReason = `An override with incompatible solc version was found for this file.`; - } else { - reason = - CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_ROOT; - formattedReason = `No solc version enabled in this profile is compatible with this file.`; + + // This logic is pretty different depending if we are dealing with a config + // override or not. If we are, we have a single compiler option, so things + // are simpler. + + if (overridden) { + // The root may not be compatible with the override version + if (maxSatisfying(compilerVersions, rootVersionRange) === null) { + return { + reason: + CompilationJobCreationErrorReason.INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION, + rootFilePath: root.fsPath, + buildProfile: this.#buildProfileName, + formattedReason: `An override with incompatible solc version was found for this file.`, + }; } + // A transitive dependency can have a pragma that's incompatible with + // the overriden version. + for (const transitiveDependency of this.#getTransitiveDependencies( + root, + dependencyGraph, + )) { + const depOwnRange = + transitiveDependency.dependency.content.versionPragmas.join(" "); + + if (maxSatisfying(compilerVersions, depOwnRange) === null) { + return { + reason: + CompilationJobCreationErrorReason.OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY, + rootFilePath: root.fsPath, + buildProfile: this.#buildProfileName, + incompatibleImportPath: transitiveDependency.fsPath, + formattedReason: `The compiler version override is incompatible with a dependency of this file:\n * ${shortenPath(root.fsPath)}\n * ${transitiveDependency.fsPath.map((s) => shortenPath(s)).join("\n * ")}`, + }; + } + } + + // There's no other case. If the root and all the dependencies are + // compatible, and we still can choose a version, we have a bug. + /* c8 ignore next 5 */ + assertHardhatInvariant( + false, + "Trying to get the error for an overriden solidity file that has no compatible config, but failed to detect it, as the root and all the dependencies are compatible with the overriden compiler config.", + ); + } + + // Non-overriden case: we first check if the root is compatible with any + // configured compiler + if (maxSatisfying(compilerVersions, rootVersionRange) === null) { return { - reason, + reason: + CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_ROOT, rootFilePath: root.fsPath, buildProfile: this.#buildProfileName, - formattedReason, + formattedReason: `No solc version enabled in this profile is compatible with this file.`, }; } + // We check all the transitive dependencies of the root to try to return + // the most specific error that we can. for (const transitiveDependency of this.#getTransitiveDependencies( root, dependencyGraph, @@ -140,20 +191,54 @@ export class SolcConfigSelector { .map((pragmas) => pragmas.join(" ")) .join(" "); + const depOwnRange = + transitiveDependency.dependency.content.versionPragmas.join(" "); + + // A transitive dependency can have a pragma that's incompatible with + // all the configured compilers + if (maxSatisfying(compilerVersions, depOwnRange) === null) { + return { + reason: + CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY, + rootFilePath: root.fsPath, + buildProfile: this.#buildProfileName, + incompatibleImportPath: transitiveDependency.fsPath, + formattedReason: `No solc version enabled in this profile is compatible with a dependency of this file:\n * ${shortenPath(root.fsPath)}\n * ${transitiveDependency.fsPath.map((s) => shortenPath(s)).join("\n * ")}`, + }; + } + + // The root and the version ranges to get to this transittive dependency + // may be contradictory, so no version ever can satisfy them. if (!intersects(rootVersionRange, transitiveDependencyVersionRange)) { return { reason: CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE, rootFilePath: root.fsPath, buildProfile: this.#buildProfileName, incompatibleImportPath: transitiveDependency.fsPath, - formattedReason: `Following these imports leads to an incompatible solc version pragma that no version can satisfy: - * ${shortenPath(root.fsPath)} - * ${transitiveDependency.fsPath.map((s) => shortenPath(s)).join("\n * ")} -`, + formattedReason: `Following these imports leads to an incompatible solc version pragma that no version can satisfy:\n * ${shortenPath(root.fsPath)}\n * ${transitiveDependency.fsPath.map((s) => shortenPath(s)).join("\n * ")}`, + }; + } + + // The root and the version ranges to get to this transittive dependency + // may not be compatible with any configured compiler. + const combinedRange = `${rootVersionRange} ${transitiveDependencyVersionRange}`; + if (maxSatisfying(compilerVersions, combinedRange) === null) { + return { + reason: + CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH, + rootFilePath: root.fsPath, + buildProfile: this.#buildProfileName, + incompatibleImportPath: transitiveDependency.fsPath, + formattedReason: `No solc version enabled in this profile is compatible with this file and this import path:\n * ${shortenPath(root.fsPath)}\n * ${transitiveDependency.fsPath.map((s) => shortenPath(s)).join("\n * ")}`, }; } } + // This is a generic case that can happen when the incompatibilities exists + // but we can't detect them with the above algorithm. For example, if a + // root imports two compatible dependencies that are incompatible with each + // other. We could try and improve this error message, but it's + // computationally expensive and hard to express to the users. return { reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, @@ -163,6 +248,11 @@ export class SolcConfigSelector { }; } + /** + * Returns a generator of all the transitive dependencies of a root. Each + * of them has the fsPath from the root file, including the path of version + * pragmas. The paths don't include the root itself. + */ *#getTransitiveDependencies( root: ResolvedFile, dependencyGraph: DependencyGraph, @@ -179,7 +269,7 @@ export class SolcConfigSelector { continue; } - visited.add(file); + visited = new Set([...visited, file]); yield { fsPath: [file.fsPath], diff --git a/v-next/hardhat/src/types/solidity/build-system.ts b/v-next/hardhat/src/types/solidity/build-system.ts index 18657d8dd26..df4993a952f 100644 --- a/v-next/hardhat/src/types/solidity/build-system.ts +++ b/v-next/hardhat/src/types/solidity/build-system.ts @@ -84,10 +84,40 @@ export interface CompileBuildInfoOptions { } export enum CompilationJobCreationErrorReason { - NO_COMPATIBLE_SOLC_VERSION_FOUND = "NO_COMPATIBLE_SOLC_VERSION_FOUND", + /** + * The root file's own pragmas are incompatible with all configured compilers. + */ NO_COMPATIBLE_SOLC_VERSION_WITH_ROOT = "NO_COMPATIBLE_SOLC_VERSION_WITH_ROOT", - INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION = "INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION", + + /** + * A dependency's own pragmas are incompatible with all configured compilers. + */ + NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY = "NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY", + + /** + * Root and a transitive import path have contradictory pragmas (invalid range / empty intersection). + */ IMPORT_OF_INCOMPATIBLE_FILE = "IMPORT_OF_INCOMPATIBLE_FILE", + + /** + * Root and a transitive import path have a valid range but no configured compiler satisfies it. + */ + NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH = "NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH", + + /** + * The override version doesn't satisfy the root file's own pragmas. + */ + INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION = "INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION", + + /** + * A dependency's pragmas are incompatible with the override version. + */ + OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY = "OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY", + + /** + * Generic fallback — no single compiler works for root + all dependencies. + */ + NO_COMPATIBLE_SOLC_VERSION_FOUND = "NO_COMPATIBLE_SOLC_VERSION_FOUND", } export interface BaseCompilationJobCreationError { @@ -96,14 +126,27 @@ export interface BaseCompilationJobCreationError { formattedReason: string; } -export interface CompilationJobCreationErrorNoCompatibleSolcVersionFound +export interface CompilationJobCreationErrorNoCompatibleSolcVersionWithRoot extends BaseCompilationJobCreationError { reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_ROOT; } -export interface CompilationJobCreationErrorIncompatibleOverriddenSolcVersion +export interface CompilationJobCreationErrorNoCompatibleSolcVersionWithDependency extends BaseCompilationJobCreationError { - reason: CompilationJobCreationErrorReason.INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION; + reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY; + incompatibleImportPath: string[]; +} + +export interface CompilationJobCreationErrorImportOfIncompatibleFile + extends BaseCompilationJobCreationError { + reason: CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE; + incompatibleImportPath: string[]; +} + +export interface CompilationJobCreationErrorNoCompatibleSolcVersionForTransitiveImportPath + extends BaseCompilationJobCreationError { + reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH; + incompatibleImportPath: string[]; } export interface CompilationJobCreationErrorIncompatibleOverriddenSolcVersion @@ -111,24 +154,25 @@ export interface CompilationJobCreationErrorIncompatibleOverriddenSolcVersion reason: CompilationJobCreationErrorReason.INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION; } -export interface CompilationJobCreationErrorIportOfIncompatibleFile +export interface CompilationJobCreationErrorOverriddenSolcVersionIncompatibleWithDependency extends BaseCompilationJobCreationError { - reason: CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE; - // The path of absolute files imported, starting from the root, that take you - // to the first file with an incompatible version pragma. + reason: CompilationJobCreationErrorReason.OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY; incompatibleImportPath: string[]; } -export interface NoCompatibleSolcVersionFound +export interface CompilationJobCreationErrorNoCompatibleSolcVersionFound extends BaseCompilationJobCreationError { reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND; } export type CompilationJobCreationError = - | CompilationJobCreationErrorNoCompatibleSolcVersionFound - | CompilationJobCreationErrorIportOfIncompatibleFile + | CompilationJobCreationErrorNoCompatibleSolcVersionWithRoot + | CompilationJobCreationErrorNoCompatibleSolcVersionWithDependency + | CompilationJobCreationErrorImportOfIncompatibleFile + | CompilationJobCreationErrorNoCompatibleSolcVersionForTransitiveImportPath | CompilationJobCreationErrorIncompatibleOverriddenSolcVersion - | NoCompatibleSolcVersionFound; + | CompilationJobCreationErrorOverriddenSolcVersionIncompatibleWithDependency + | CompilationJobCreationErrorNoCompatibleSolcVersionFound; /** * The restult of building a file. From 2c5987df71a6c46747bb5cc5355f3879c7ae7b48 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 21:02:48 +0000 Subject: [PATCH 02/16] Update tests --- .../build-system/solc-config-selection.ts | 328 ++++++++++++++---- 1 file changed, 270 insertions(+), 58 deletions(-) diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts index 0e8245a04c9..d32fbc7d0af 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts @@ -26,13 +26,13 @@ const testHardhatProjectNpmPackage: ResolvedNpmPackage = { }; function createProjectResolvedFile( - inputSourceName: string, + relativePath: string, versionPragmas: string[], ): ProjectResolvedFile { return { type: ResolvedFileType.PROJECT_FILE, - inputSourceName, - fsPath: path.join(process.cwd(), inputSourceName), + inputSourceName: relativePath, + fsPath: path.join(process.cwd(), relativePath), content: { text: "", importPaths: [], @@ -70,11 +70,7 @@ describe("SolcConfigSelector", () => { root.content.versionPragmas, ), ); - const selector = new SolcConfigSelector( - buildProfileName, - buildProfile, - dependencyGraph, - ); + const selector = new SolcConfigSelector(buildProfileName, buildProfile); await assertRejectsWithHardhatError( async () => { @@ -90,11 +86,7 @@ describe("SolcConfigSelector", () => { it("should throw when given a subgraph of size 0", async () => { const emptyDependencyGraph = new DependencyGraphImplementation(); - const selector = new SolcConfigSelector( - buildProfileName, - buildProfile, - emptyDependencyGraph, - ); + const selector = new SolcConfigSelector(buildProfileName, buildProfile); await assertRejectsWithHardhatError( async () => { @@ -114,11 +106,25 @@ describe("SolcConfigSelector", () => { settings: {}, }; - const selector = new SolcConfigSelector( - buildProfileName, - buildProfile, - dependencyGraph, - ); + const selector = new SolcConfigSelector(buildProfileName, buildProfile); + const config = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + assert.deepEqual(config, buildProfile.overrides[root.inputSourceName]); + }); + + it("should return the compiler if it satisfies the version range of root and dependencies", () => { + buildProfile.overrides[root.inputSourceName] = { + version: "0.8.5", + settings: {}, + }; + + const dependency = createProjectResolvedFile("dependency.sol", [ + ">=0.8.0", + ]); + dependencyGraph.addDependency(root, dependency); + + const selector = new SolcConfigSelector(buildProfileName, buildProfile); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); @@ -135,7 +141,6 @@ describe("SolcConfigSelector", () => { const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); @@ -150,7 +155,7 @@ describe("SolcConfigSelector", () => { }); }); - it("should return import of incompatible file error if dependency version range clashes with the root version range", () => { + it("should return overridden version incompatible with dependency error if dependency pragmas are incompatible with the override version", () => { buildProfile.overrides[root.inputSourceName] = { version: "0.8.0", settings: {}, @@ -164,57 +169,64 @@ describe("SolcConfigSelector", () => { const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { reason: - CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE, + CompilationJobCreationErrorReason.OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY, rootFilePath: root.fsPath, buildProfile: buildProfileName, incompatibleImportPath: [dependency.fsPath], - formattedReason: `Following these imports leads to an incompatible solc version pragma that no version can satisfy:\n * .${path.sep}${root.inputSourceName}\n * .${path.sep}${dependency.inputSourceName}\n`, + formattedReason: `The compiler version override is incompatible with a dependency of this file:\n * .${path.sep}${path.relative(process.cwd(), root.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dependency.fsPath)}`, }); }); - it("should return no compatible solc version error otherwise", () => { + it("should return overridden version incompatible with dependency error for a transitive dependency", () => { buildProfile.overrides[root.inputSourceName] = { version: "0.8.0", settings: {}, }; - const dependency1 = createProjectResolvedFile("dependency1.sol", [ - "^0.8.1", - ]); - const dependency2 = createProjectResolvedFile("dependency2.sol", [ - "^0.8.2", - ]); - dependencyGraph.addDependency(root, dependency1); - dependencyGraph.addDependency(root, dependency2); + const dep1 = createProjectResolvedFile("dep1.sol", ["^0.8.0"]); + const dep2 = createProjectResolvedFile("dep2.sol", ["^0.7.0"]); + dependencyGraph.addDependency(root, dep1); + dependencyGraph.addDependency(dep1, dep2); const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { reason: - CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, + CompilationJobCreationErrorReason.OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY, rootFilePath: root.fsPath, buildProfile: buildProfileName, - formattedReason: - "No solc version enabled in this profile is compatible with this file and all of its dependencies.", + incompatibleImportPath: [dep1.fsPath, dep2.fsPath], + formattedReason: `The compiler version override is incompatible with a dependency of this file:\n * .${path.sep}${path.relative(process.cwd(), root.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dep1.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dep2.fsPath)}`, }); }); }); }); describe("without a compiler override", () => { + it("should return the config of the max satisfying compiler for a root with no dependencies", () => { + buildProfile.compilers.push({ + version: "0.8.0", + settings: {}, + }); + + const selector = new SolcConfigSelector(buildProfileName, buildProfile); + const config = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + assert.deepEqual(config, buildProfile.compilers[0]); + }); + it("should return the config of the max satisfying compiler if it exists", () => { buildProfile.compilers.push({ version: "0.8.0", @@ -242,17 +254,35 @@ describe("SolcConfigSelector", () => { dependencyGraph.addDependency(root, dependency1); dependencyGraph.addDependency(root, dependency2); - const selector = new SolcConfigSelector( - buildProfileName, - buildProfile, - dependencyGraph, - ); + const selector = new SolcConfigSelector(buildProfileName, buildProfile); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, buildProfile.compilers[2]); }); + it("should handle multiple version pragmas per file", () => { + root = createProjectResolvedFile("root.sol", [">=0.8.0", "<0.8.5"]); + dependencyGraph = new DependencyGraphImplementation(); + dependencyGraph.addRootFile(root.inputSourceName, root); + + buildProfile.compilers.push({ + version: "0.8.4", + settings: {}, + }); + + const dependency = createProjectResolvedFile("dependency.sol", [ + ">=0.8.3", + ]); + dependencyGraph.addDependency(root, dependency); + + const selector = new SolcConfigSelector(buildProfileName, buildProfile); + const config = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + assert.deepEqual(config, buildProfile.compilers[0]); + }); + describe("if it does not satisfy the version range", () => { it("should return no compatible root solc version error if it does not satisfy the root version range", () => { buildProfile.compilers.push({ @@ -263,7 +293,6 @@ describe("SolcConfigSelector", () => { const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); @@ -278,11 +307,71 @@ describe("SolcConfigSelector", () => { }); }); + it("should return no compatible solc version with dependency error if dependency pragmas are unsatisfiable by any configured compiler", () => { + buildProfile.compilers.push({ + version: "0.8.0", + settings: {}, + }); + + const dependency = createProjectResolvedFile("dependency.sol", [ + "0.8.1", + ]); + dependencyGraph.addDependency(root, dependency); + + const selector = new SolcConfigSelector( + buildProfileName, + buildProfile, + ); + const config = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + assert.deepEqual(config, { + reason: + CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY, + rootFilePath: root.fsPath, + buildProfile: buildProfileName, + incompatibleImportPath: [dependency.fsPath], + formattedReason: `No solc version enabled in this profile is compatible with a dependency of this file:\n * .${path.sep}${path.relative(process.cwd(), root.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dependency.fsPath)}`, + }); + }); + + it("should return no compatible solc version with dependency error for a transitive dependency", () => { + buildProfile.compilers.push({ + version: "0.8.0", + settings: {}, + }); + + const dep1 = createProjectResolvedFile("dep1.sol", ["^0.8.0"]); + const dep2 = createProjectResolvedFile("dep2.sol", ["0.8.99"]); + dependencyGraph.addDependency(root, dep1); + dependencyGraph.addDependency(dep1, dep2); + + const selector = new SolcConfigSelector( + buildProfileName, + buildProfile, + ); + const config = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + assert.deepEqual(config, { + reason: + CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY, + rootFilePath: root.fsPath, + buildProfile: buildProfileName, + incompatibleImportPath: [dep1.fsPath, dep2.fsPath], + formattedReason: `No solc version enabled in this profile is compatible with a dependency of this file:\n * .${path.sep}${path.relative(process.cwd(), root.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dep1.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dep2.fsPath)}`, + }); + }); + it("should return import of incompatible file error if dependency version range clashes with the root version range", () => { buildProfile.compilers.push({ version: "0.8.0", settings: {}, }); + buildProfile.compilers.push({ + version: "0.7.0", + settings: {}, + }); const dependency = createProjectResolvedFile("dependency.sol", [ "^0.7.0", @@ -292,7 +381,6 @@ describe("SolcConfigSelector", () => { const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); @@ -303,7 +391,74 @@ describe("SolcConfigSelector", () => { rootFilePath: root.fsPath, buildProfile: buildProfileName, incompatibleImportPath: [dependency.fsPath], - formattedReason: `Following these imports leads to an incompatible solc version pragma that no version can satisfy:\n * .${path.sep}${root.inputSourceName}\n * .${path.sep}${dependency.inputSourceName}\n`, + formattedReason: `Following these imports leads to an incompatible solc version pragma that no version can satisfy:\n * .${path.sep}${path.relative(process.cwd(), root.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dependency.fsPath)}`, + }); + }); + + it("should return import of incompatible file error for a transitive dependency chain", () => { + buildProfile.compilers.push({ + version: "0.7.0", + settings: {}, + }); + buildProfile.compilers.push({ + version: "0.8.0", + settings: {}, + }); + + const dep1 = createProjectResolvedFile("dep1.sol", [ + ">=0.7.0", + "<=0.8.5", + ]); + const dep2 = createProjectResolvedFile("dep2.sol", ["^0.7.0"]); + dependencyGraph.addDependency(root, dep1); + dependencyGraph.addDependency(dep1, dep2); + + const selector = new SolcConfigSelector( + buildProfileName, + buildProfile, + ); + const config = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + assert.deepEqual(config, { + reason: + CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE, + rootFilePath: root.fsPath, + buildProfile: buildProfileName, + incompatibleImportPath: [dep1.fsPath, dep2.fsPath], + formattedReason: `Following these imports leads to an incompatible solc version pragma that no version can satisfy:\n * .${path.sep}${path.relative(process.cwd(), root.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dep1.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dep2.fsPath)}`, + }); + }); + + it("should return no compatible solc version for transitive import path error if combined range is valid but no compiler satisfies it", () => { + buildProfile.compilers.push({ + version: "0.8.0", + settings: {}, + }); + buildProfile.compilers.push({ + version: "0.8.4", + settings: {}, + }); + + const dep1 = createProjectResolvedFile("dep1.sol", ["<=0.8.3"]); + const dep2 = createProjectResolvedFile("dep2.sol", [">=0.8.2"]); + dependencyGraph.addDependency(root, dep1); + dependencyGraph.addDependency(dep1, dep2); + + const selector = new SolcConfigSelector( + buildProfileName, + buildProfile, + ); + const config = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + assert.deepEqual(config, { + reason: + CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH, + rootFilePath: root.fsPath, + buildProfile: buildProfileName, + incompatibleImportPath: [dep1.fsPath, dep2.fsPath], + formattedReason: `No solc version enabled in this profile is compatible with this file and this import path:\n * .${path.sep}${path.relative(process.cwd(), root.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dep1.fsPath)}\n * .${path.sep}${path.relative(process.cwd(), dep2.fsPath)}`, }); }); @@ -312,6 +467,14 @@ describe("SolcConfigSelector", () => { version: "0.8.0", settings: {}, }); + buildProfile.compilers.push({ + version: "0.8.1", + settings: {}, + }); + buildProfile.compilers.push({ + version: "0.8.2", + settings: {}, + }); const dependency1 = createProjectResolvedFile("dependency1.sol", [ "^0.8.1", @@ -319,13 +482,16 @@ describe("SolcConfigSelector", () => { const dependency2 = createProjectResolvedFile("dependency2.sol", [ "^0.8.2", ]); + const dependency3 = createProjectResolvedFile("dependency3.sol", [ + "0.8.0", + ]); dependencyGraph.addDependency(root, dependency1); dependencyGraph.addDependency(root, dependency2); + dependencyGraph.addDependency(root, dependency3); const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); @@ -344,34 +510,80 @@ describe("SolcConfigSelector", () => { }); describe("Edge cases", () => { - it("Should return an error in the presence of cycles", () => { + it("Should still return an error in the presence of cycles", () => { const dependency1 = createProjectResolvedFile("dependency1.sol", [ - "^0.8.0", + ">0.8.0", ]); const dependency2 = createProjectResolvedFile("dependency2.sol", [ - "0.8.1", + "<0.8.1", ]); dependencyGraph.addDependency(root, dependency1); dependencyGraph.addDependency(dependency1, dependency2); dependencyGraph.addDependency(dependency2, dependency1); - const selector = new SolcConfigSelector( - buildProfileName, - { - compilers: [{ version: "0.8.0", settings: {} }], - overrides: {}, - isolated: false, - preferWasm: false, - }, - dependencyGraph, - ); + const selector = new SolcConfigSelector(buildProfileName, { + compilers: [ + { version: "0.8.1", settings: {} }, + { version: "0.8.0", settings: {} }, + ], + overrides: {}, + isolated: false, + preferWasm: false, + }); const configOrError = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.ok("reason" in configOrError, "Error expected"); + assert.equal( + configOrError.reason, + CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH, + ); + }); + + it("Should skip already-visited nodes when a cycle is fully traversed without early error return", () => { + // Graph: + // root + // / \ + // A C + // / \ + // \ / + // B + // + // The [root, A, B] are compatible with 0.8.1, and [root, C] with 0.8.0, + // so the cycle is fully analyzed, and we only fial for the general case. + const depA = createProjectResolvedFile("depA.sol", ["^0.8.1"]); + const depB = createProjectResolvedFile("depB.sol", ["^0.8.0"]); + const depC = createProjectResolvedFile("depC.sol", ["0.8.0"]); + + dependencyGraph.addDependency(root, depA); + dependencyGraph.addDependency(root, depC); + dependencyGraph.addDependency(depA, depB); + dependencyGraph.addDependency(depB, depA); + + const selector = new SolcConfigSelector(buildProfileName, { + compilers: [ + { version: "0.8.0", settings: {} }, + { version: "0.8.1", settings: {} }, + ], + overrides: {}, + isolated: false, + preferWasm: false, + }); + + const configOrError = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + assert.deepEqual(configOrError, { + reason: + CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, + rootFilePath: root.fsPath, + buildProfile: buildProfileName, + formattedReason: + "No solc version enabled in this profile is compatible with this file and all of its dependencies.", + }); }); }); }); From 7e71393e7e9862cc59ebabd84318fab5a4209f30 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 21:22:08 +0000 Subject: [PATCH 03/16] Add a `success` field to make the SolcConfigSelector easier to work with. --- .../solidity/build-system/build-system.ts | 4 +- .../build-system/solc-config-selection.ts | 13 +++++-- .../src/types/solidity/build-system.ts | 1 + .../build-system/solc-config-selection.ts | 38 ++++++++++++++++--- 4 files changed, 45 insertions(+), 11 deletions(-) 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 378aa8bc9f9..ca66e64be15 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 @@ -445,11 +445,11 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { const configOrError = solcConfigSelector.selectBestSolcConfigForSingleRootGraph(subgraph); - if ("reason" in configOrError) { + if (!configOrError.success) { return configOrError; } - subgraphsWithConfig.push([configOrError, subgraph]); + subgraphsWithConfig.push([configOrError.config, subgraph]); } // get longVersion and isWasm from the compiler for each version 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 b3f42f5f141..4ffefd60b2b 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 @@ -45,7 +45,7 @@ export class SolcConfigSelector { */ public selectBestSolcConfigForSingleRootGraph( subgraph: DependencyGraph, - ): SolcConfig | CompilationJobCreationError { + ): { success: true; config: SolcConfig } | CompilationJobCreationError { const roots = subgraph.getRoots(); assertHardhatInvariant( @@ -74,7 +74,7 @@ export class SolcConfigSelector { ); } - return overriddenCompiler; + return { success: true, config: overriddenCompiler }; } // if there's no override, we find a compiler that matches the version range @@ -99,7 +99,7 @@ export class SolcConfigSelector { `Matching config not found for version '${matchingVersion.toString()}'`, ); - return matchingConfig; + return { success: true, config: matchingConfig }; } /** @@ -130,6 +130,7 @@ export class SolcConfigSelector { // The root may not be compatible with the override version if (maxSatisfying(compilerVersions, rootVersionRange) === null) { return { + success: false, reason: CompilationJobCreationErrorReason.INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION, rootFilePath: root.fsPath, @@ -149,6 +150,7 @@ export class SolcConfigSelector { if (maxSatisfying(compilerVersions, depOwnRange) === null) { return { + success: false, reason: CompilationJobCreationErrorReason.OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY, rootFilePath: root.fsPath, @@ -172,6 +174,7 @@ export class SolcConfigSelector { // configured compiler if (maxSatisfying(compilerVersions, rootVersionRange) === null) { return { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_ROOT, rootFilePath: root.fsPath, @@ -198,6 +201,7 @@ export class SolcConfigSelector { // all the configured compilers if (maxSatisfying(compilerVersions, depOwnRange) === null) { return { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY, rootFilePath: root.fsPath, @@ -211,6 +215,7 @@ export class SolcConfigSelector { // may be contradictory, so no version ever can satisfy them. if (!intersects(rootVersionRange, transitiveDependencyVersionRange)) { return { + success: false, reason: CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE, rootFilePath: root.fsPath, buildProfile: this.#buildProfileName, @@ -224,6 +229,7 @@ export class SolcConfigSelector { const combinedRange = `${rootVersionRange} ${transitiveDependencyVersionRange}`; if (maxSatisfying(compilerVersions, combinedRange) === null) { return { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH, rootFilePath: root.fsPath, @@ -240,6 +246,7 @@ export class SolcConfigSelector { // other. We could try and improve this error message, but it's // computationally expensive and hard to express to the users. return { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, rootFilePath: root.fsPath, diff --git a/v-next/hardhat/src/types/solidity/build-system.ts b/v-next/hardhat/src/types/solidity/build-system.ts index df4993a952f..2d22d8f4d0d 100644 --- a/v-next/hardhat/src/types/solidity/build-system.ts +++ b/v-next/hardhat/src/types/solidity/build-system.ts @@ -121,6 +121,7 @@ export enum CompilationJobCreationErrorReason { } export interface BaseCompilationJobCreationError { + success: false; buildProfile: string; rootFilePath: string; formattedReason: string; diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts index d32fbc7d0af..a66fd36919b 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts @@ -110,7 +110,10 @@ describe("SolcConfigSelector", () => { const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); - assert.deepEqual(config, buildProfile.overrides[root.inputSourceName]); + assert.deepEqual(config, { + success: true, + config: buildProfile.overrides[root.inputSourceName], + }); }); it("should return the compiler if it satisfies the version range of root and dependencies", () => { @@ -128,7 +131,10 @@ describe("SolcConfigSelector", () => { const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); - assert.deepEqual(config, buildProfile.overrides[root.inputSourceName]); + assert.deepEqual(config, { + success: true, + config: buildProfile.overrides[root.inputSourceName], + }); }); describe("if it does not satisfy the version range", () => { @@ -146,6 +152,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION, rootFilePath: root.fsPath, @@ -174,6 +181,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY, rootFilePath: root.fsPath, @@ -202,6 +210,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.OVERRIDDEN_SOLC_VERSION_INCOMPATIBLE_WITH_DEPENDENCY, rootFilePath: root.fsPath, @@ -224,7 +233,10 @@ describe("SolcConfigSelector", () => { const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); - assert.deepEqual(config, buildProfile.compilers[0]); + assert.deepEqual(config, { + success: true, + config: buildProfile.compilers[0], + }); }); it("should return the config of the max satisfying compiler if it exists", () => { @@ -258,7 +270,10 @@ describe("SolcConfigSelector", () => { const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); - assert.deepEqual(config, buildProfile.compilers[2]); + assert.deepEqual(config, { + success: true, + config: buildProfile.compilers[2], + }); }); it("should handle multiple version pragmas per file", () => { @@ -280,7 +295,10 @@ describe("SolcConfigSelector", () => { const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); - assert.deepEqual(config, buildProfile.compilers[0]); + assert.deepEqual(config, { + success: true, + config: buildProfile.compilers[0], + }); }); describe("if it does not satisfy the version range", () => { @@ -298,6 +316,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_ROOT, rootFilePath: root.fsPath, @@ -326,6 +345,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY, rootFilePath: root.fsPath, @@ -354,6 +374,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_DEPENDENCY, rootFilePath: root.fsPath, @@ -386,6 +407,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE, rootFilePath: root.fsPath, @@ -421,6 +443,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE, rootFilePath: root.fsPath, @@ -453,6 +476,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH, rootFilePath: root.fsPath, @@ -497,6 +521,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, rootFilePath: root.fsPath, @@ -536,7 +561,7 @@ describe("SolcConfigSelector", () => { const configOrError = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); - assert.ok("reason" in configOrError, "Error expected"); + assert.ok(!configOrError.success, "Error expected"); assert.equal( configOrError.reason, CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOR_TRANSITIVE_IMPORT_PATH, @@ -577,6 +602,7 @@ describe("SolcConfigSelector", () => { selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(configOrError, { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, rootFilePath: root.fsPath, From 4b77e2808e0703bc636af2aa77c7c4f3d53e7376 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 21:31:20 +0000 Subject: [PATCH 04/16] Add `success: true` to `GetCompilationJobsResult` to make it easier to work with. --- .../solidity/build-system/build-system.ts | 9 +++++++-- v-next/hardhat/src/types/solidity/build-system.ts | 5 +++++ .../solidity/build-system/build-system.ts | 2 +- .../get-compilation-jobs-cache-hits.ts | 12 ++++++------ .../partial-compilation/npm-cache-hits.ts | 8 ++++---- 5 files changed, 23 insertions(+), 13 deletions(-) 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 ca66e64be15..f3973cb29e5 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 @@ -244,7 +244,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { options, ); - if ("reason" in compilationJobsResult) { + if (!compilationJobsResult.success) { return compilationJobsResult; } @@ -631,7 +631,12 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { } } - return { compilationJobsPerFile, indexedIndividualJobs, cacheHits }; + return { + success: true as const, + compilationJobsPerFile, + indexedIndividualJobs, + cacheHits, + }; } #getBuildProfile(buildProfileName: string = DEFAULT_BUILD_PROFILE) { diff --git a/v-next/hardhat/src/types/solidity/build-system.ts b/v-next/hardhat/src/types/solidity/build-system.ts index 2d22d8f4d0d..7508f7a7391 100644 --- a/v-next/hardhat/src/types/solidity/build-system.ts +++ b/v-next/hardhat/src/types/solidity/build-system.ts @@ -219,6 +219,11 @@ export interface CacheHitInfo { * The keys in the maps of this interface are Root File Paths, which means either absolute paths or `npm:/` URIs. */ export interface GetCompilationJobsResult { + /** + * A flag to distinguish between a successful and a failed result. + */ + success: true; + /** * Map from root file path to compilation job for files that need compilation. */ 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 97b149b75d6..fd2e4690fa0 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 @@ -37,7 +37,7 @@ async function emitArtifacts(solidity: SolidityBuildSystem): Promise { ); assert.ok( - !("reason" in compilationJobsResult), + compilationJobsResult.success, "getCompilationJobs should not error", ); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts index 00c93363a85..e10c359b4e6 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts @@ -31,7 +31,7 @@ contract Foo {}`, quiet: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert.equal(result.cacheHits.size, 1, "Should have one cache hit"); assert.equal( result.compilationJobsPerFile.size, @@ -70,7 +70,7 @@ contract Foo {}`, quiet: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); const cacheHitInfo = result.cacheHits.get(filePath); assert(cacheHitInfo !== undefined, "Should have cache hit info"); @@ -124,7 +124,7 @@ contract Foo { uint256 public value; }`, quiet: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert( result.compilationJobsPerFile.has(fooPath), "Modified file should be in compilationJobsPerFile", @@ -160,7 +160,7 @@ contract Foo {}`, force: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert.equal( result.cacheHits.size, 0, @@ -192,7 +192,7 @@ contract Foo {}`, quiet: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert.equal( result.cacheHits.size, 0, @@ -240,7 +240,7 @@ contract Base { uint256 public value; }`, quiet: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert.equal( result.cacheHits.size, 0, diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts index 57c76ff9eb3..d3226813198 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts @@ -227,7 +227,7 @@ contract ERC20 {}`, quiet: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert.equal( result.cacheHits.size, 1, @@ -284,7 +284,7 @@ contract ERC20 {}`, quiet: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); const cacheHitInfo = result.cacheHits.get(npmRootPath); assert(cacheHitInfo !== undefined, "Should have cache hit info"); @@ -352,7 +352,7 @@ contract Foo { uint256 public value; }`, { quiet: true }, ); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); // Local file was modified, should be in compilationJobsPerFile assert( @@ -409,7 +409,7 @@ contract ERC20 {}`, force: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert.equal( result.compilationJobsPerFile.size, 1, From e81097414a13d9c77366bb8e149a6cb9edc7f3b9 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 22:01:20 +0000 Subject: [PATCH 05/16] Add `SolidityBuildSystem#isSuccessfulBuildResult` --- .../builtin-plugins/solidity/build-results.ts | 4 +++- .../solidity/build-system/build-system.ts | 6 ++++++ .../builtin-plugins/solidity/hook-handlers/hre.ts | 8 ++++++++ .../builtin-plugins/solidity/tasks/build.ts | 2 +- v-next/hardhat/src/types/solidity/build-system.ts | 11 +++++++++++ .../build-system/integration/custom-compiler.ts | 14 ++++++++------ .../partial-compilation/cache-hit-results.ts | 10 +++++----- .../get-compilation-jobs-cache-hits.ts | 2 +- .../partial-compilation/npm-cache-hits.ts | 14 +++++++------- 9 files changed, 50 insertions(+), 21 deletions(-) diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-results.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-results.ts index 0d37cab2c88..6279d6f3d1c 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-results.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-results.ts @@ -2,6 +2,7 @@ import type { CompilationJobCreationError, FailedFileBuildResult, FileBuildResult, + SolidityBuildSystem, } from "../../../types/solidity.js"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; @@ -22,9 +23,10 @@ type SuccessfulSolidityBuildResults = Map< * job failed. */ export function throwIfSolidityBuildFailed( + solidity: SolidityBuildSystem, results: SolidityBuildResults, ): asserts results is SuccessfulSolidityBuildResults { - if ("reason" in results) { + if (!solidity.isSuccessfulBuildResult(results)) { throw new HardhatError( HardhatError.ERRORS.CORE.SOLIDITY.COMPILATION_JOB_CREATION_ERROR, { 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 f3973cb29e5..8d8d88a6af2 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 @@ -208,6 +208,12 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { } } + public isSuccessfulBuildResult( + buildResult: CompilationJobCreationError | Map, + ): buildResult is Map { + return buildResult instanceof Map; + } + public async build( rootFilePaths: string[], _options?: BuildOptions, diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/hook-handlers/hre.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/hook-handlers/hre.ts index a57a94b331c..62b918216e0 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/hook-handlers/hre.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/hook-handlers/hre.ts @@ -46,6 +46,14 @@ class LazySolidityBuildSystem implements SolidityBuildSystem { return buildSystem.getScope(fsPath); } + public isSuccessfulBuildResult( + buildResult: CompilationJobCreationError | Map, + ): buildResult is Map { + // Note: This duplicates the logic of the actual implementation because it's + // a synchronous method, so we can't import the implementation. + return buildResult instanceof Map; + } + public async build( rootFiles: string[], options?: BuildOptions, diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/tasks/build.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/tasks/build.ts index b9e63a8a6e6..cb437111b77 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/tasks/build.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/tasks/build.ts @@ -110,7 +110,7 @@ async function buildForScope( scope, }); - throwIfSolidityBuildFailed(results); + throwIfSolidityBuildFailed(solidity, results); // If we recompiled the entire project we cleanup the artifacts if (isFullCompilation) { diff --git a/v-next/hardhat/src/types/solidity/build-system.ts b/v-next/hardhat/src/types/solidity/build-system.ts index 7508f7a7391..c9e8615da31 100644 --- a/v-next/hardhat/src/types/solidity/build-system.ts +++ b/v-next/hardhat/src/types/solidity/build-system.ts @@ -289,12 +289,23 @@ export interface SolidityBuildSystem { * @param options The options to use when building the files. * @returns An `Map` of the files to their build results, or an error if * there was a problem when trying to create the necessary compilation jobs. + * @see `isSuccessfulBuildResult` to check if the build result is successful. */ build( rootFilePaths: string[], options?: BuildOptions, ): Promise>; + /** + * Returns true if the given build result is successful. + * + * @param buildResult Result of the `build` method. + * @returns True if the build result is successful. + */ + isSuccessfulBuildResult( + buildResult: CompilationJobCreationError | Map, + ): buildResult is Map; + /** * Returns the CompilationJobs that would be used to build the provided files. * diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/integration/custom-compiler.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/integration/custom-compiler.ts index d772742affc..60381119822 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/integration/custom-compiler.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/integration/custom-compiler.ts @@ -3,6 +3,7 @@ import type { Compiler, CompilationJobCreationError, FileBuildResult, + SolidityBuildSystem, } from "../../../../../../src/types/solidity.js"; import assert from "node:assert/strict"; @@ -53,10 +54,11 @@ describe( CompilerPlatform.WASM; function assertCompilerSelection( + solidity: SolidityBuildSystem, compiler: Compiler, buildResult: CompilationJobCreationError | Map, ) { - assert(!("reason" in buildResult)); + assert(solidity.isSuccessfulBuildResult(buildResult)); const jobBuildResult = buildResult.values().next().value; assert(jobBuildResult !== undefined); assert(jobBuildResult.type === FileBuildResultType.BUILD_SUCCESS); @@ -144,7 +146,7 @@ describe( { quiet: true }, ); - assertCompilerSelection(compiler, result); + assertCompilerSelection(hre.solidity, compiler, result); }); it("can be specified on multi version config", async function () { @@ -171,7 +173,7 @@ describe( { quiet: true }, ); - assertCompilerSelection(compiler, result); + assertCompilerSelection(hre.solidity, compiler, result); }); it("can be specified on single-version build profile config", async function () { @@ -198,7 +200,7 @@ describe( { quiet: true }, ); - assertCompilerSelection(compiler, result); + assertCompilerSelection(hre.solidity, compiler, result); }); it("can be specified on multi-version build profile config", async function () { @@ -229,7 +231,7 @@ describe( { quiet: true }, ); - assertCompilerSelection(compiler, result); + assertCompilerSelection(hre.solidity, compiler, result); }); it("can be specified on file overrides config", async function () { @@ -262,7 +264,7 @@ describe( { quiet: true }, ); - assertCompilerSelection(compiler, result); + assertCompilerSelection(hre.solidity, compiler, result); }); it("throws a descriptive error if the provided path doesn't exist", async () => { 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 a33fec2aa18..4a660377bb4 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 @@ -26,7 +26,7 @@ contract Foo {}`, // First build const firstResult = await hre.solidity.build([filePath], { quiet: true }); - assert(!("reason" in firstResult), "First build should succeed"); + assert(hre.solidity.isSuccessfulBuildResult(firstResult), "First build should succeed"); const firstBuildResult = firstResult.get(filePath); assert.equal(firstBuildResult?.type, FileBuildResultType.BUILD_SUCCESS); const originalBuildId = @@ -36,7 +36,7 @@ contract Foo {}`, const secondResult = await hre.solidity.build([filePath], { quiet: true, }); - assert(!("reason" in secondResult), "Second build should succeed"); + assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should succeed"); const cacheHitResult = secondResult.get(filePath); assert.equal(cacheHitResult?.type, FileBuildResultType.CACHE_HIT); @@ -74,7 +74,7 @@ contract Bar {}`, const secondResult = await hre.solidity.build([filePath], { quiet: true, }); - assert(!("reason" in secondResult), "Second build should succeed"); + assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should succeed"); const cacheHitResult = secondResult.get(filePath); assert.equal(cacheHitResult?.type, FileBuildResultType.CACHE_HIT); @@ -118,7 +118,7 @@ contract Foo { uint256 public value; }`, const result = await hre.solidity.build([fooPath, barPath], { quiet: true, }); - assert(!("reason" in result), "Build should succeed"); + assert(hre.solidity.isSuccessfulBuildResult(result), "Build should succeed"); assert.equal( result.get(fooPath)?.type, @@ -149,7 +149,7 @@ contract Foo {}`, quiet: true, force: true, }); - assert(!("reason" in result), "Build should succeed"); + assert(hre.solidity.isSuccessfulBuildResult(result), "Build should succeed"); assert.equal( result.get(filePath)?.type, diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts index e10c359b4e6..b4ded7685ce 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts @@ -60,7 +60,7 @@ contract Foo {}`, // First build to get original buildId const buildResult = await hre.solidity.build([filePath], { quiet: true }); - assert(!("reason" in buildResult), "Build should succeed"); + assert(hre.solidity.isSuccessfulBuildResult(buildResult), "Build should succeed"); const fileBuildResult = buildResult.get(filePath); assert.equal(fileBuildResult?.type, FileBuildResultType.BUILD_SUCCESS); const originalBuildId = await fileBuildResult.compilationJob.getBuildId(); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts index d3226813198..6414027fe68 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts @@ -42,7 +42,7 @@ contract ERC20 {}`, }); assert( - !("reason" in firstResult), + hre.solidity.isSuccessfulBuildResult(firstResult), `Build should be successful, got: ${JSON.stringify(firstResult)}`, ); assert.equal(firstResult.size, 1, "Should have one result"); @@ -58,7 +58,7 @@ contract ERC20 {}`, quiet: true, }); - assert(!("reason" in secondResult), "Second build should be successful"); + assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should be successful"); assert.equal(secondResult.size, 1, "Should have one result"); const secondBuildResult = secondResult.get(npmRootPath); assert.equal( @@ -98,7 +98,7 @@ contract ERC20 {}`, const firstResult = await hre.solidity.build([npmRootPath], { quiet: true, }); - assert(!("reason" in firstResult), "First build should be successful"); + assert(hre.solidity.isSuccessfulBuildResult(firstResult), "First build should be successful"); const firstBuildResult = firstResult.get(npmRootPath); assert.equal(firstBuildResult?.type, FileBuildResultType.BUILD_SUCCESS); const originalBuildId = @@ -109,7 +109,7 @@ contract ERC20 {}`, quiet: true, }); - assert(!("reason" in secondResult), "Second build should be successful"); + assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should be successful"); const secondBuildResult = secondResult.get(npmRootPath); assert.equal(secondBuildResult?.type, FileBuildResultType.CACHE_HIT); assert.equal( @@ -155,7 +155,7 @@ contract ERC20 {}`, quiet: true, }); - assert(!("reason" in firstResult), "First build should be successful"); + assert(hre.solidity.isSuccessfulBuildResult(firstResult), "First build should be successful"); assert.equal(firstResult.size, 2, "Should have two results"); // Modify only local file @@ -171,7 +171,7 @@ contract Foo { uint256 public value; }`, quiet: true, }); - assert(!("reason" in secondResult), "Second build should be successful"); + assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should be successful"); assert.equal(secondResult.size, 2, "Should have two results"); // Local file was modified - BUILD_SUCCESS @@ -274,7 +274,7 @@ contract ERC20 {}`, const buildResult = await hre.solidity.build([npmRootPath], { quiet: true, }); - assert(!("reason" in buildResult), "Build should succeed"); + assert(hre.solidity.isSuccessfulBuildResult(buildResult), "Build should succeed"); const fileBuildResult = buildResult.get(npmRootPath); assert.equal(fileBuildResult?.type, FileBuildResultType.BUILD_SUCCESS); const originalBuildId = await fileBuildResult.compilationJob.getBuildId(); From 23eac5bbf17f66988556294cea225c45dfd3422b Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 22:01:52 +0000 Subject: [PATCH 06/16] Update hardhat-verify and hardhat-typechain to use the new ways to identify errors --- v-next/hardhat-typechain/src/internal/hook-handlers/solidity.ts | 2 +- v-next/hardhat-verify/src/internal/artifacts.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/v-next/hardhat-typechain/src/internal/hook-handlers/solidity.ts b/v-next/hardhat-typechain/src/internal/hook-handlers/solidity.ts index 103336e1ff4..86da136dcd9 100644 --- a/v-next/hardhat-typechain/src/internal/hook-handlers/solidity.ts +++ b/v-next/hardhat-typechain/src/internal/hook-handlers/solidity.ts @@ -22,7 +22,7 @@ export default async (): Promise> => { const result = await next(context, rootFilePaths, options); // Skip if build failed (returned an error) - if ("reason" in result) { + if (!context.solidity.isSuccessfulBuildResult(result)) { return result; } diff --git a/v-next/hardhat-verify/src/internal/artifacts.ts b/v-next/hardhat-verify/src/internal/artifacts.ts index 1cbecd38b24..c85cf3277b0 100644 --- a/v-next/hardhat-verify/src/internal/artifacts.ts +++ b/v-next/hardhat-verify/src/internal/artifacts.ts @@ -91,7 +91,7 @@ export async function getCompilerInput( ); assertHardhatInvariant( - !("reason" in getCompilationJobsResult), + getCompilationJobsResult.success, "getCompilationJobs should not error", ); From 18f66d4f593ad204cf14a1bb81e13aed9d8d2f66 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 22:03:29 +0000 Subject: [PATCH 07/16] Fix typo --- .../solidity/build-system/solc-config-selection.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 4ffefd60b2b..fcbab871f86 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 @@ -111,7 +111,7 @@ export class SolcConfigSelector { * configuration for * @param compilerVersions The compiler versions that are configured for the * selected build profile. For overridden roots, it's a single one. - * @param overridden True if the root has an overriden config. + * @param overridden True if the root has an overridden config. * @returns The error why we couldn't get a compiler configuration. */ #getCompilationJobCreationError( @@ -140,7 +140,7 @@ export class SolcConfigSelector { } // A transitive dependency can have a pragma that's incompatible with - // the overriden version. + // the overridden version. for (const transitiveDependency of this.#getTransitiveDependencies( root, dependencyGraph, @@ -166,11 +166,11 @@ export class SolcConfigSelector { /* c8 ignore next 5 */ assertHardhatInvariant( false, - "Trying to get the error for an overriden solidity file that has no compatible config, but failed to detect it, as the root and all the dependencies are compatible with the overriden compiler config.", + "Trying to get the error for an overridden solidity file that has no compatible config, but failed to detect it, as the root and all the dependencies are compatible with the overridden compiler config.", ); } - // Non-overriden case: we first check if the root is compatible with any + // Non-overridden case: we first check if the root is compatible with any // configured compiler if (maxSatisfying(compilerVersions, rootVersionRange) === null) { return { From 2c3aba2ec64392c39323577972fcaae67d53b1b0 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 22:06:07 +0000 Subject: [PATCH 08/16] Linter fixes --- .../partial-compilation/cache-hit-results.ts | 25 ++++++++++++---- .../get-compilation-jobs-cache-hits.ts | 5 +++- .../partial-compilation/npm-cache-hits.ts | 30 +++++++++++++++---- 3 files changed, 48 insertions(+), 12 deletions(-) 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 4a660377bb4..1670d265f7c 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 @@ -26,7 +26,10 @@ contract Foo {}`, // First build const firstResult = await hre.solidity.build([filePath], { quiet: true }); - assert(hre.solidity.isSuccessfulBuildResult(firstResult), "First build should succeed"); + assert( + hre.solidity.isSuccessfulBuildResult(firstResult), + "First build should succeed", + ); const firstBuildResult = firstResult.get(filePath); assert.equal(firstBuildResult?.type, FileBuildResultType.BUILD_SUCCESS); const originalBuildId = @@ -36,7 +39,10 @@ contract Foo {}`, const secondResult = await hre.solidity.build([filePath], { quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should succeed"); + assert( + hre.solidity.isSuccessfulBuildResult(secondResult), + "Second build should succeed", + ); const cacheHitResult = secondResult.get(filePath); assert.equal(cacheHitResult?.type, FileBuildResultType.CACHE_HIT); @@ -74,7 +80,10 @@ contract Bar {}`, const secondResult = await hre.solidity.build([filePath], { quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should succeed"); + assert( + hre.solidity.isSuccessfulBuildResult(secondResult), + "Second build should succeed", + ); const cacheHitResult = secondResult.get(filePath); assert.equal(cacheHitResult?.type, FileBuildResultType.CACHE_HIT); @@ -118,7 +127,10 @@ contract Foo { uint256 public value; }`, const result = await hre.solidity.build([fooPath, barPath], { quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(result), "Build should succeed"); + assert( + hre.solidity.isSuccessfulBuildResult(result), + "Build should succeed", + ); assert.equal( result.get(fooPath)?.type, @@ -149,7 +161,10 @@ contract Foo {}`, quiet: true, force: true, }); - assert(hre.solidity.isSuccessfulBuildResult(result), "Build should succeed"); + assert( + hre.solidity.isSuccessfulBuildResult(result), + "Build should succeed", + ); assert.equal( result.get(filePath)?.type, diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts index b4ded7685ce..b68fb239251 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/get-compilation-jobs-cache-hits.ts @@ -60,7 +60,10 @@ contract Foo {}`, // First build to get original buildId const buildResult = await hre.solidity.build([filePath], { quiet: true }); - assert(hre.solidity.isSuccessfulBuildResult(buildResult), "Build should succeed"); + assert( + hre.solidity.isSuccessfulBuildResult(buildResult), + "Build should succeed", + ); const fileBuildResult = buildResult.get(filePath); assert.equal(fileBuildResult?.type, FileBuildResultType.BUILD_SUCCESS); const originalBuildId = await fileBuildResult.compilationJob.getBuildId(); diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts index 6414027fe68..8716b2e96ff 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/partial-compilation/npm-cache-hits.ts @@ -58,7 +58,10 @@ contract ERC20 {}`, quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should be successful"); + assert( + hre.solidity.isSuccessfulBuildResult(secondResult), + "Second build should be successful", + ); assert.equal(secondResult.size, 1, "Should have one result"); const secondBuildResult = secondResult.get(npmRootPath); assert.equal( @@ -98,7 +101,10 @@ contract ERC20 {}`, const firstResult = await hre.solidity.build([npmRootPath], { quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(firstResult), "First build should be successful"); + assert( + hre.solidity.isSuccessfulBuildResult(firstResult), + "First build should be successful", + ); const firstBuildResult = firstResult.get(npmRootPath); assert.equal(firstBuildResult?.type, FileBuildResultType.BUILD_SUCCESS); const originalBuildId = @@ -109,7 +115,10 @@ contract ERC20 {}`, quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should be successful"); + assert( + hre.solidity.isSuccessfulBuildResult(secondResult), + "Second build should be successful", + ); const secondBuildResult = secondResult.get(npmRootPath); assert.equal(secondBuildResult?.type, FileBuildResultType.CACHE_HIT); assert.equal( @@ -155,7 +164,10 @@ contract ERC20 {}`, quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(firstResult), "First build should be successful"); + assert( + hre.solidity.isSuccessfulBuildResult(firstResult), + "First build should be successful", + ); assert.equal(firstResult.size, 2, "Should have two results"); // Modify only local file @@ -171,7 +183,10 @@ contract Foo { uint256 public value; }`, quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(secondResult), "Second build should be successful"); + assert( + hre.solidity.isSuccessfulBuildResult(secondResult), + "Second build should be successful", + ); assert.equal(secondResult.size, 2, "Should have two results"); // Local file was modified - BUILD_SUCCESS @@ -274,7 +289,10 @@ contract ERC20 {}`, const buildResult = await hre.solidity.build([npmRootPath], { quiet: true, }); - assert(hre.solidity.isSuccessfulBuildResult(buildResult), "Build should succeed"); + assert( + hre.solidity.isSuccessfulBuildResult(buildResult), + "Build should succeed", + ); const fileBuildResult = buildResult.get(npmRootPath); assert.equal(fileBuildResult?.type, FileBuildResultType.BUILD_SUCCESS); const originalBuildId = await fileBuildResult.compilationJob.getBuildId(); From 696b2953f7df1dfc0ea70988ade6d671e93a8b72 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 22:06:14 +0000 Subject: [PATCH 09/16] Add changesets --- .changeset/flat-birds-start.md | 7 +++++++ .changeset/wet-lines-design.md | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/flat-birds-start.md create mode 100644 .changeset/wet-lines-design.md diff --git a/.changeset/flat-birds-start.md b/.changeset/flat-birds-start.md new file mode 100644 index 00000000000..c56a272c012 --- /dev/null +++ b/.changeset/flat-birds-start.md @@ -0,0 +1,7 @@ +--- +"@nomicfoundation/hardhat-typechain": patch +"@nomicfoundation/hardhat-verify": patch +"hardhat": patch +--- + +Make SolidityBuildSystem easier to work with diff --git a/.changeset/wet-lines-design.md b/.changeset/wet-lines-design.md new file mode 100644 index 00000000000..3a6d62dad2b --- /dev/null +++ b/.changeset/wet-lines-design.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Show fs paths and better error messages when a Solidity file can't be compiled with any configured compiler [#7988](https://github.com/NomicFoundation/hardhat/pull/7988) From 53af6ffb1e92e2650baa9b92f78ab114d438d727 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 22 Feb 2026 22:08:30 +0000 Subject: [PATCH 10/16] Add peer bumps --- .peer-bumps.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.peer-bumps.json b/.peer-bumps.json index 6855ae19351..837ee1e19e9 100644 --- a/.peer-bumps.json +++ b/.peer-bumps.json @@ -16,6 +16,16 @@ "package": "@nomicfoundation/hardhat-node-test-runner", "peer": "hardhat", "reason": "It depends on the new TestHooks#onTestRunStart, TestHooks#onTestWorkerDone, and TestHooks#onTestRunDone hooks" + }, + { + "package": "@nomicfoundation/hardhat-verify", + "peer": "hardhat", + "reason": "Uses the new api to detect errors in SolidityBuildSystem#getCompilationJobs" + }, + { + "package": "@nomicfoundation/hardhat-typechain", + "peer": "hardhat", + "reason": "Uses the new api to detect errors in SolidityBuildSystem#build" } ] } From 0db44fb32be9eeaef1c181ede270d531810d91a4 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 23 Feb 2026 19:21:40 -0300 Subject: [PATCH 11/16] Update v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../solidity/build-system/solc-config-selection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts index a66fd36919b..4a3e4179688 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts @@ -578,7 +578,7 @@ describe("SolcConfigSelector", () => { // B // // The [root, A, B] are compatible with 0.8.1, and [root, C] with 0.8.0, - // so the cycle is fully analyzed, and we only fial for the general case. + // so the cycle is fully analyzed, and we only fail for the general case. const depA = createProjectResolvedFile("depA.sol", ["^0.8.1"]); const depB = createProjectResolvedFile("depB.sol", ["^0.8.0"]); const depC = createProjectResolvedFile("depC.sol", ["0.8.0"]); From b60fb4cca99372ec909e4e7b27c1e081464a8141 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 23 Feb 2026 19:21:47 -0300 Subject: [PATCH 12/16] Update v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../solidity/build-system/solc-config-selection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fcbab871f86..4952c80181e 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 @@ -240,7 +240,7 @@ export class SolcConfigSelector { } } - // This is a generic case that can happen when the incompatibilities exists + // This is a generic case that can happen when the incompatibilities exist // but we can't detect them with the above algorithm. For example, if a // root imports two compatible dependencies that are incompatible with each // other. We could try and improve this error message, but it's From db4e9214b5112c8827e7ad3cd875727ed474fa43 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 23 Feb 2026 19:22:28 -0300 Subject: [PATCH 13/16] Update v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../solidity/build-system/solc-config-selection.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 4952c80181e..954da55e8e3 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 @@ -256,9 +256,10 @@ export class SolcConfigSelector { } /** - * Returns a generator of all the transitive dependencies of a root. Each - * of them has the fsPath from the root file, including the path of version - * pragmas. The paths don't include the root itself. + * Returns a generator of all the transitive dependencies of a root file. For each + * dependency, it yields the sequence of fsPaths from the root to that dependency, + * along with the corresponding version pragma paths for each file in the import chain. + * The paths don't include the root itself. */ *#getTransitiveDependencies( root: ResolvedFile, From e8885ebe41ae85e17dd10e79fa10b0db0780c805 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Mon, 23 Feb 2026 22:23:23 +0000 Subject: [PATCH 14/16] Fix typos --- .../solidity/build-system/solc-config-selection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 954da55e8e3..30bf7d9fb4d 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 @@ -211,7 +211,7 @@ export class SolcConfigSelector { }; } - // The root and the version ranges to get to this transittive dependency + // The root and the version ranges to get to this transitive dependency // may be contradictory, so no version ever can satisfy them. if (!intersects(rootVersionRange, transitiveDependencyVersionRange)) { return { @@ -224,7 +224,7 @@ export class SolcConfigSelector { }; } - // The root and the version ranges to get to this transittive dependency + // The root and the version ranges to get to this transitive dependency // may not be compatible with any configured compiler. const combinedRange = `${rootVersionRange} ${transitiveDependencyVersionRange}`; if (maxSatisfying(compilerVersions, combinedRange) === null) { From 7c4b3870c592d044cb8c82002fb43fff559ae857 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Fri, 27 Feb 2026 19:11:03 +0000 Subject: [PATCH 15/16] Remove unnecesary 'as const' --- .../builtin-plugins/solidity/build-system/build-system.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8d8d88a6af2..2549fc7f792 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 @@ -638,7 +638,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { } return { - success: true as const, + success: true, compilationJobsPerFile, indexedIndividualJobs, cacheHits, From df14a312c8c58ea1bf6c5aa9b53b7ac86707ba36 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Fri, 27 Feb 2026 19:13:16 +0000 Subject: [PATCH 16/16] Update jsdocs --- .../solidity/build-system/solc-config-selection.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 30bf7d9fb4d..2f662a06890 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 @@ -18,14 +18,13 @@ export class SolcConfigSelector { /** * Creates a new SolcConfigSelector that can be used to select the best solc - * configuration for subgraphs of the given dependency graph. + * configuration for single-root subgraphs to create their resepective + * individual compilation jobs. * - * All the queries are done in the context of the given dependency graph, and - * using the same build profile. + * All the queries use the same build profile. * * @param buildProfileName The name of the build profile to use. * @param buildProfile The build profile config. - * @param _dependencyGraph The entire dependency graph of the project. */ constructor( buildProfileName: string,