UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

343 lines (291 loc) 10.6 kB
import type { LoDashStatic } from "lodash"; import debug from "debug"; import semver from "semver"; import { SolcConfig, SolidityConfig } from "../../types"; import * as taskTypes from "../../types/builtin-tasks"; import { CompilationJobCreationError, CompilationJobCreationErrorReason, CompilationJobsCreationResult, } from "../../types/builtin-tasks"; import { assertHardhatInvariant } from "../core/errors"; import { ResolvedFile } from "./resolver"; const log = debug("hardhat:core:compilation-job"); // this should have a proper version range when it's fixed const SOLC_BUG_9573_VERSIONS = "<0.8.0"; function isCompilationJobCreationError( x: | taskTypes.CompilationJob | taskTypes.CompilationJobCreationError | SolcConfig ): x is CompilationJobCreationError { return "reason" in x; } export class CompilationJob implements taskTypes.CompilationJob { private _filesToCompile: Map< string, { file: ResolvedFile; emitsArtifacts: boolean } > = new Map(); constructor(public solidityConfig: SolcConfig) {} public addFileToCompile(file: ResolvedFile, emitsArtifacts: boolean) { const fileToCompile = this._filesToCompile.get(file.sourceName); // if the file doesn't exist, we add it // we also add it if emitsArtifacts is true, to override it in case it was // previously added but with a false emitsArtifacts if (fileToCompile === undefined || emitsArtifacts) { this._filesToCompile.set(file.sourceName, { file, emitsArtifacts }); } } public hasSolc9573Bug(): boolean { return ( this.solidityConfig?.settings?.optimizer?.enabled === true && semver.satisfies(this.solidityConfig.version, SOLC_BUG_9573_VERSIONS) ); } public merge(job: taskTypes.CompilationJob): CompilationJob { const isEqual = require("lodash/isEqual") as LoDashStatic["isEqual"]; assertHardhatInvariant( isEqual(this.solidityConfig, job.getSolcConfig()), "Merging jobs with different solidity configurations" ); const mergedJobs = new CompilationJob(job.getSolcConfig()); for (const file of this.getResolvedFiles()) { mergedJobs.addFileToCompile(file, this.emitsArtifacts(file)); } for (const file of job.getResolvedFiles()) { mergedJobs.addFileToCompile(file, job.emitsArtifacts(file)); } return mergedJobs; } public getSolcConfig(): SolcConfig { return this.solidityConfig; } public isEmpty() { return this._filesToCompile.size === 0; } public getResolvedFiles(): ResolvedFile[] { return [...this._filesToCompile.values()].map((x) => x.file); } /** * Check if the given file emits artifacts. * * If no file is given, check if *some* file in the job emits artifacts. */ public emitsArtifacts(file: ResolvedFile): boolean { const fileToCompile = this._filesToCompile.get(file.sourceName); assertHardhatInvariant( fileToCompile !== undefined, `File '${file.sourceName}' does not exist in this compilation job` ); return fileToCompile.emitsArtifacts; } } function mergeCompilationJobs( jobs: taskTypes.CompilationJob[], isMergeable: (job: taskTypes.CompilationJob) => boolean ): taskTypes.CompilationJob[] { const jobsMap: Map<SolcConfig, taskTypes.CompilationJob[]> = new Map(); for (const job of jobs) { const mergedJobs = jobsMap.get(job.getSolcConfig()); if (isMergeable(job)) { if (mergedJobs === undefined) { jobsMap.set(job.getSolcConfig(), [job]); } else if (mergedJobs.length === 1) { const newJob = mergedJobs[0].merge(job); jobsMap.set(job.getSolcConfig(), [newJob]); } else { assertHardhatInvariant( false, "More than one mergeable job was added for the same configuration" ); } } else { if (mergedJobs === undefined) { jobsMap.set(job.getSolcConfig(), [job]); } else { jobsMap.set(job.getSolcConfig(), [...mergedJobs, job]); } } } // Array#flat This method defaults to depth limit 1 return [...jobsMap.values()].flat(1_000_000); } /** * Creates a list of compilation jobs from a dependency graph. *This function * assumes that the given graph is a connected component*. * Returns the list of compilation jobs on success, and a list of * non-compilable files on failure. */ export async function createCompilationJobsFromConnectedComponent( connectedComponent: taskTypes.DependencyGraph, getFromFile: ( file: ResolvedFile ) => Promise<taskTypes.CompilationJob | CompilationJobCreationError> ): Promise<CompilationJobsCreationResult> { const compilationJobs: taskTypes.CompilationJob[] = []; const errors: CompilationJobCreationError[] = []; for (const file of connectedComponent.getResolvedFiles()) { const compilationJobOrError = await getFromFile(file); if (isCompilationJobCreationError(compilationJobOrError)) { log( `'${file.absolutePath}' couldn't be compiled. Reason: '${ compilationJobOrError as any }'` ); errors.push(compilationJobOrError); continue; } compilationJobs.push(compilationJobOrError); } const jobs = mergeCompilationJobsWithBug(compilationJobs); return { jobs, errors }; } export async function createCompilationJobFromFile( dependencyGraph: taskTypes.DependencyGraph, file: ResolvedFile, solidityConfig: SolidityConfig ): Promise<CompilationJob | CompilationJobCreationError> { const directDependencies = dependencyGraph.getDependencies(file); const transitiveDependencies = dependencyGraph.getTransitiveDependencies(file); const compilerConfig = getCompilerConfigForFile( file, directDependencies, transitiveDependencies, solidityConfig ); // if the config cannot be obtained, we just return the failure if (isCompilationJobCreationError(compilerConfig)) { return compilerConfig; } log( `File '${file.absolutePath}' will be compiled with version '${compilerConfig.version}'` ); const compilationJob = new CompilationJob(compilerConfig); compilationJob.addFileToCompile(file, true); for (const { dependency } of transitiveDependencies) { log( `File '${dependency.absolutePath}' added as dependency of '${file.absolutePath}'` ); compilationJob.addFileToCompile(dependency, false); } return compilationJob; } /** * Merge compilation jobs affected by the solc #9573 bug */ export function mergeCompilationJobsWithBug( compilationJobs: taskTypes.CompilationJob[] ): taskTypes.CompilationJob[] { return mergeCompilationJobs(compilationJobs, (job) => job.hasSolc9573Bug()); } /** * Merge compilation jobs not affected by the solc #9573 bug */ export function mergeCompilationJobsWithoutBug( compilationJobs: taskTypes.CompilationJob[] ): taskTypes.CompilationJob[] { return mergeCompilationJobs(compilationJobs, (job) => !job.hasSolc9573Bug()); } /** * Return the compiler config with the newest version that satisfies the given * version ranges, or a value indicating why the compiler couldn't be obtained. */ function getCompilerConfigForFile( file: ResolvedFile, directDependencies: ResolvedFile[], transitiveDependencies: taskTypes.TransitiveDependency[], solidityConfig: SolidityConfig ): SolcConfig | CompilationJobCreationError { const transitiveDependenciesVersionPragmas = transitiveDependencies .map(({ dependency }) => dependency.content.versionPragmas) .flat(); const versionRange = Array.from( new Set([ ...file.content.versionPragmas, ...transitiveDependenciesVersionPragmas, ]) ).join(" "); const overrides = solidityConfig.overrides ?? {}; const overriddenCompiler = overrides[file.sourceName]; // if there's an override, we only check that if (overriddenCompiler !== undefined) { if (!semver.satisfies(overriddenCompiler.version, versionRange)) { return getCompilationJobCreationError( file, directDependencies, transitiveDependencies, [overriddenCompiler.version], true ); } return overriddenCompiler; } // if there's no override, we find a compiler that matches the version range const compilerVersions = solidityConfig.compilers.map((x) => x.version); const matchingVersion = semver.maxSatisfying(compilerVersions, versionRange); if (matchingVersion === null) { return getCompilationJobCreationError( file, directDependencies, transitiveDependencies, compilerVersions, false ); } const matchingConfig = solidityConfig.compilers.find( (x) => x.version === matchingVersion )!; return matchingConfig; } function getCompilationJobCreationError( file: ResolvedFile, directDependencies: ResolvedFile[], transitiveDependencies: taskTypes.TransitiveDependency[], compilerVersions: string[], overriden: boolean ): CompilationJobCreationError { const fileVersionRange = file.content.versionPragmas.join(" "); if (semver.maxSatisfying(compilerVersions, fileVersionRange) === null) { const reason = overriden ? CompilationJobCreationErrorReason.INCOMPATIBLE_OVERRIDEN_SOLC_VERSION : CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND; return { reason, file }; } const incompatibleDirectImports: ResolvedFile[] = []; for (const dependency of directDependencies) { const dependencyVersionRange = dependency.content.versionPragmas.join(" "); if (!semver.intersects(fileVersionRange, dependencyVersionRange)) { incompatibleDirectImports.push(dependency); } } if (incompatibleDirectImports.length > 0) { return { reason: CompilationJobCreationErrorReason.DIRECTLY_IMPORTS_INCOMPATIBLE_FILE, file, extra: { incompatibleDirectImports, }, }; } const incompatibleIndirectImports: taskTypes.TransitiveDependency[] = []; for (const transitiveDependency of transitiveDependencies) { const { dependency } = transitiveDependency; const dependencyVersionRange = dependency.content.versionPragmas.join(" "); if (!semver.intersects(fileVersionRange, dependencyVersionRange)) { incompatibleIndirectImports.push(transitiveDependency); } } if (incompatibleIndirectImports.length > 0) { return { reason: CompilationJobCreationErrorReason.INDIRECTLY_IMPORTS_INCOMPATIBLE_FILE, file, extra: { incompatibleIndirectImports, }, }; } return { reason: CompilationJobCreationErrorReason.OTHER_ERROR, file }; }