hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
207 lines (179 loc) • 6.71 kB
text/typescript
import type {
SolcConfig,
SolidityBuildProfileConfig,
} from "../../../../types/config.js";
import type { CompilationJobCreationError } from "../../../../types/solidity/build-system.js";
import type { ResolvedFile } from "../../../../types/solidity/resolved-file.js";
import type { DependencyGraph } from "../../../../types/solidity.js";
import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
import { shortenPath } from "@nomicfoundation/hardhat-utils/path";
import { intersects, maxSatisfying, satisfies } from "semver";
import { CompilationJobCreationErrorReason } from "../../../../types/solidity/build-system.js";
export class SolcConfigSelector {
readonly #buildProfileName: string;
readonly #buildProfile: SolidityBuildProfileConfig;
/**
* Creates a new SolcConfigSelector that can be used to select the best solc
* configuration for subgraphs of the given dependency graph.
*
* All the queries are done in the context of the given dependency graph, and
* using 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;
}
/**
* Selects the best solc configuration for a subgraph of the dependency graph
* with which this selector was created.
*
* @param subgraph A single-root subgraph of the dependency graph.
* @returns The best solc configuration for the subgraph, or a
* CompilationJobCreationError if no compatible solc version could be found.
*/
public selectBestSolcConfigForSingleRootGraph(
subgraph: DependencyGraph,
): SolcConfig | CompilationJobCreationError {
const roots = subgraph.getRoots();
assertHardhatInvariant(
roots.size === 1,
"This method only works for single root graphs",
);
const [userSourceName, root] = [...roots.entries()][0];
const allVersionPragamas = [...subgraph.getAllFiles()]
.map(({ content }) => content.versionPragmas)
.flat(1);
const versionRange = Array.from(new Set(allVersionPragamas)).join(" ");
const overriddenCompiler = this.#buildProfile.overrides[userSourceName];
// if there's an override, we only check that
if (overriddenCompiler !== undefined) {
if (!satisfies(overriddenCompiler.version, versionRange)) {
return this.#getCompilationJobCreationError(
root,
subgraph,
[overriddenCompiler.version],
true,
);
}
return overriddenCompiler;
}
// if there's no override, we find a compiler that matches the version range
const compilerVersions = this.#buildProfile.compilers.map((x) => x.version);
const matchingVersion = maxSatisfying(compilerVersions, versionRange);
if (matchingVersion === null) {
return this.#getCompilationJobCreationError(
root,
subgraph,
compilerVersions,
false,
);
}
const matchingConfig = this.#buildProfile.compilers.find(
(x) => x.version === matchingVersion,
);
assertHardhatInvariant(
matchingConfig !== undefined,
`Matching config not found for version '${matchingVersion.toString()}'`,
);
return matchingConfig;
}
#getCompilationJobCreationError(
root: ResolvedFile,
dependencyGraph: DependencyGraph,
compilerVersions: string[],
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.`;
}
return {
reason,
rootFilePath: root.fsPath,
buildProfile: this.#buildProfileName,
formattedReason,
};
}
for (const transitiveDependency of this.#getTransitiveDependencies(
root,
dependencyGraph,
)) {
const transitiveDependencyVersionRange =
transitiveDependency.versionPragmasPath
.map((pragmas) => pragmas.join(" "))
.join(" ");
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 * ")}
`,
};
}
}
return {
reason:
CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND,
rootFilePath: root.fsPath,
buildProfile: this.#buildProfileName,
formattedReason: `No solc version enabled in this profile is compatible with this file and all of its dependencies.`,
};
}
*#getTransitiveDependencies(
root: ResolvedFile,
dependencyGraph: DependencyGraph,
visited = new Set<ResolvedFile>([root]),
): Generator<{
fsPath: string[];
versionPragmasPath: string[][];
dependency: ResolvedFile;
}> {
for (const dependency of dependencyGraph.getDependencies(root)) {
const file = dependency.file;
if (visited.has(file)) {
continue;
}
visited.add(file);
yield {
fsPath: [file.fsPath],
versionPragmasPath: [file.content.versionPragmas],
dependency: file,
};
for (const transitive of this.#getTransitiveDependencies(
file,
dependencyGraph,
visited,
)) {
yield {
fsPath: [file.fsPath, ...transitive.fsPath],
versionPragmasPath: [
file.content.versionPragmas,
...transitive.versionPragmasPath,
],
dependency: transitive.dependency,
};
}
}
}
}