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) 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" } ] } 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", ); 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 7119543fd09..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 @@ -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, @@ -244,7 +250,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { options, ); - if ("reason" in compilationJobsResult) { + if (!compilationJobsResult.success) { return compilationJobsResult; } @@ -430,7 +436,6 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { const solcConfigSelector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); let subgraphsWithConfig: Array< @@ -446,11 +451,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 @@ -632,7 +637,12 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { } } - return { compilationJobsPerFile, indexedIndividualJobs, cacheHits }; + return { + success: true, + compilationJobsPerFile, + indexedIndividualJobs, + cacheHits, + }; } #getBuildProfile(buildProfileName: string = DEFAULT_BUILD_PROFILE) { 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..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,19 +18,17 @@ 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, buildProfile: SolidityBuildProfileConfig, - _dependencyGraph: DependencyGraph, ) { this.#buildProfileName = buildProfileName; this.#buildProfile = buildProfile; @@ -46,7 +44,7 @@ export class SolcConfigSelector { */ public selectBestSolcConfigForSingleRootGraph( subgraph: DependencyGraph, - ): SolcConfig | CompilationJobCreationError { + ): { success: true; config: SolcConfig } | CompilationJobCreationError { const roots = subgraph.getRoots(); assertHardhatInvariant( @@ -75,7 +73,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 @@ -100,9 +98,21 @@ export class SolcConfigSelector { `Matching config not found for version '${matchingVersion.toString()}'`, ); - return matchingConfig; + return { success: true, config: 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 overridden config. + * @returns The error why we couldn't get a compiler configuration. + */ #getCompilationJobCreationError( root: ResolvedFile, dependencyGraph: DependencyGraph, @@ -110,27 +120,70 @@ 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 { + success: false, + 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 overridden version. + for (const transitiveDependency of this.#getTransitiveDependencies( + root, + dependencyGraph, + )) { + const depOwnRange = + transitiveDependency.dependency.content.versionPragmas.join(" "); + + if (maxSatisfying(compilerVersions, depOwnRange) === null) { + return { + success: false, + 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 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-overridden case: we first check if the root is compatible with any + // configured compiler + if (maxSatisfying(compilerVersions, rootVersionRange) === null) { return { - reason, + success: false, + 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,21 +193,59 @@ 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 { + success: false, + 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 transitive dependency + // 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, 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 transitive dependency + // may not be compatible with any configured compiler. + 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, + 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 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 + // computationally expensive and hard to express to the users. return { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, rootFilePath: root.fsPath, @@ -163,6 +254,12 @@ export class SolcConfigSelector { }; } + /** + * 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, dependencyGraph: DependencyGraph, @@ -179,7 +276,7 @@ export class SolcConfigSelector { continue; } - visited.add(file); + visited = new Set([...visited, file]); yield { fsPath: [file.fsPath], 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 18657d8dd26..c9e8615da31 100644 --- a/v-next/hardhat/src/types/solidity/build-system.ts +++ b/v-next/hardhat/src/types/solidity/build-system.ts @@ -84,26 +84,70 @@ 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 { + success: false; buildProfile: string; rootFilePath: string; 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 +155,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. @@ -174,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. */ @@ -239,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/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/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..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(!("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 +39,10 @@ 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 +80,10 @@ 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 +127,10 @@ 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 +161,10 @@ 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 00c93363a85..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 @@ -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, @@ -60,7 +60,10 @@ 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(); @@ -70,7 +73,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 +127,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 +163,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 +195,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 +243,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..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 @@ -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,10 @@ 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 +101,10 @@ 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 +115,10 @@ 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 +164,10 @@ 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 +183,10 @@ 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 @@ -227,7 +242,7 @@ contract ERC20 {}`, quiet: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert.equal( result.cacheHits.size, 1, @@ -274,7 +289,10 @@ 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(); @@ -284,7 +302,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 +370,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 +427,7 @@ contract ERC20 {}`, force: true, }); - assert(!("reason" in result), "getCompilationJobs should succeed"); + assert(result.success, "getCompilationJobs should succeed"); assert.equal( result.compilationJobsPerFile.size, 1, 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..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 @@ -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,15 +106,35 @@ describe("SolcConfigSelector", () => { settings: {}, }; - const selector = new SolcConfigSelector( - buildProfileName, - buildProfile, - dependencyGraph, - ); + const selector = new SolcConfigSelector(buildProfileName, buildProfile); + const config = + selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); + + 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", () => { + 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); - 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", () => { @@ -135,12 +147,12 @@ describe("SolcConfigSelector", () => { const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.INCOMPATIBLE_OVERRIDDEN_SOLC_VERSION, rootFilePath: root.fsPath, @@ -150,7 +162,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 +176,69 @@ describe("SolcConfigSelector", () => { const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, 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, { + success: false, 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, { + success: true, + 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,15 +266,39 @@ 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]); + assert.deepEqual(config, { + success: true, + 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, { + success: true, + config: buildProfile.compilers[0], + }); }); describe("if it does not satisfy the version range", () => { @@ -263,12 +311,12 @@ describe("SolcConfigSelector", () => { const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_WITH_ROOT, rootFilePath: root.fsPath, @@ -278,11 +326,73 @@ 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, { + success: false, + 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, { + success: false, + 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,18 +402,87 @@ describe("SolcConfigSelector", () => { const selector = new SolcConfigSelector( buildProfileName, buildProfile, - dependencyGraph, ); const config = selector.selectBestSolcConfigForSingleRootGraph(dependencyGraph); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.IMPORT_OF_INCOMPATIBLE_FILE, 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, { + success: false, + 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, { + success: false, + 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 +491,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,18 +506,22 @@ 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); assert.deepEqual(config, { + success: false, reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, rootFilePath: root.fsPath, @@ -344,34 +535,81 @@ 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(!configOrError.success, "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 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"]); + + 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.ok("reason" in configOrError, "Error expected"); + assert.deepEqual(configOrError, { + success: false, + 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.", + }); }); }); });