UNPKG

hardhat

Version:

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

439 lines (367 loc) 11.9 kB
import type { ArtifactManager, GetArtifactByName, } from "../../../types/artifacts.js"; import { EOL } from "node:os"; import path from "node:path"; import { assertHardhatInvariant, HardhatError, } from "@nomicfoundation/hardhat-errors"; import { exists, getAllFilesMatching, readJsonFile, } from "@nomicfoundation/hardhat-utils/fs"; export const BUILD_INFO_DIR_NAME = "build-info"; export const EDIT_DISTANCE_THRESHOLD = 3; /** * We cache the info that we read from the file system, otherwise we would * have to traverse the filesystem every time we need to get the an artifact. * * To keep our view of the filesystem consistent, we cache everything at the * same time, using this interface to organize the data. */ interface FsData { allArtifactPaths: ReadonlySet<string>; allFullyQualifiedNames: ReadonlySet<string>; bareNameToFullyQualifiedNameMap: Map<string, ReadonlySet<string>>; fullyQualifiedNameToArtifactPath: Map<string, string>; } export class ArtifactManagerImplementation implements ArtifactManager { readonly #artifactsPath: string; // This function can be overridden in the constructor for testing purposes. // This class will call it whenever the fsData is not already cached, and will // cache the result. readonly #readFsData: () => Promise<FsData>; #fsData?: FsData; constructor(artifactsPath: string, readFsData?: () => Promise<FsData>) { this.#artifactsPath = artifactsPath; this.#readFsData = readFsData ?? (() => this.#readFsDataFromFileSystem()); } public async readArtifact<ContractNameT extends string>( contractNameOrFullyQualifiedName: ContractNameT, ): Promise<GetArtifactByName<ContractNameT>> { const artifactPath = await this.getArtifactPath( contractNameOrFullyQualifiedName, ); return readJsonFile(artifactPath); } public async getArtifactPath( contractNameOrFullyQualifiedName: string, ): Promise<string> { const fqn = await this.#getFullyQualifiedName( contractNameOrFullyQualifiedName, ); const { fullyQualifiedNameToArtifactPath } = await this.#getFsData(); const artifactPath = fullyQualifiedNameToArtifactPath.get(fqn); assertHardhatInvariant( artifactPath !== undefined, "Artifact path should be defined", ); return artifactPath; } public async artifactExists( contractNameOrFullyQualifiedName: string, ): Promise<boolean> { try { // This throw if the artifact doesn't exist await this.getArtifactPath(contractNameOrFullyQualifiedName); return true; } catch (error) { if (HardhatError.isHardhatError(error)) { if ( error.number === HardhatError.ERRORS.CORE.ARTIFACTS.NOT_FOUND.number ) { return false; } } throw error; } } public async getBuildInfoId( contractNameOrFullyQualifiedName: string, ): Promise<string | undefined> { const artifact = await this.readArtifact(contractNameOrFullyQualifiedName); return artifact.buildInfoId; } public async getAllFullyQualifiedNames(): Promise<ReadonlySet<string>> { const { allFullyQualifiedNames } = await this.#getFsData(); return allFullyQualifiedNames; } public async getAllBuildInfoIds(): Promise<ReadonlySet<string>> { const paths = await getAllFilesMatching( path.join(this.#artifactsPath, BUILD_INFO_DIR_NAME), (p) => p.endsWith(".json") && !p.endsWith(".output.json"), ); return new Set(paths.map((p) => path.basename(p, ".json"))); } public async getBuildInfoPath( buildInfoId: string, ): Promise<string | undefined> { const buildInfoPath = path.join( this.#artifactsPath, BUILD_INFO_DIR_NAME, buildInfoId + ".json", ); return (await exists(buildInfoPath)) ? buildInfoPath : undefined; } public async getBuildInfoOutputPath( buildInfoId: string, ): Promise<string | undefined> { const buildInfoOutputPath = path.join( this.#artifactsPath, BUILD_INFO_DIR_NAME, buildInfoId + ".output.json", ); return (await exists(buildInfoOutputPath)) ? buildInfoOutputPath : undefined; } public async clearCache(): Promise<void> { this.#fsData = undefined; } async #getFullyQualifiedName( contractNameOrFullyQualifiedName: string, ): Promise<string> { const { bareNameToFullyQualifiedNameMap, allFullyQualifiedNames } = await this.#getFsData(); if (this.#isFullyQualifiedName(contractNameOrFullyQualifiedName)) { if (allFullyQualifiedNames.has(contractNameOrFullyQualifiedName)) { return contractNameOrFullyQualifiedName; } this.#throwNotFoundError( contractNameOrFullyQualifiedName, bareNameToFullyQualifiedNameMap.keys(), allFullyQualifiedNames, ); } const fqns = bareNameToFullyQualifiedNameMap.get( contractNameOrFullyQualifiedName, ); if (fqns === undefined || fqns.size === 0) { this.#throwNotFoundError( contractNameOrFullyQualifiedName, bareNameToFullyQualifiedNameMap.keys(), allFullyQualifiedNames, ); } if (fqns.size !== 1) { throw new HardhatError( HardhatError.ERRORS.CORE.ARTIFACTS.MULTIPLE_FOUND, { contractName: contractNameOrFullyQualifiedName, candidates: Array.from(fqns).join(EOL), }, ); } const [fqn] = fqns; return fqn; } #throwNotFoundError( contractNameOrFullyQualifiedName: string, allBareNames: Iterable<string>, allFullyQualifiedNames: Iterable<string>, ): never { const names = this.#isFullyQualifiedName(contractNameOrFullyQualifiedName) ? allFullyQualifiedNames : allBareNames; const similarNames = this.#getSimilarStrings( contractNameOrFullyQualifiedName, names, ); const suggestion = this.#formatSimilarNameSuggestions( contractNameOrFullyQualifiedName, similarNames, ); throw new HardhatError(HardhatError.ERRORS.CORE.ARTIFACTS.NOT_FOUND, { contractName: contractNameOrFullyQualifiedName, suggestion, }); } #isFullyQualifiedName(name: string): boolean { return name.includes(":"); } #getFullyQualifiedNameFromArtifactAbsolutePath(artifactPath: string): string { const relativePath = path.relative(this.#artifactsPath, artifactPath); const sourceName = path.dirname(relativePath).split(path.sep).join("/"); const contractName = path.basename(relativePath, ".json"); return `${sourceName}:${contractName}`; } /** * Filters an array of strings to only include the strings that are similar to * the given string. * * @param stringToCompare The string to the other strings with. * @param otherStrings The strings to filter. * @param maxEditDistance The maximum edit distance to consider as a match. * @returns The array of matches, sorted by increasing edit distance. */ #getSimilarStrings( stringToCompare: string, otherStrings: Iterable<string>, maxEditDistance: number = EDIT_DISTANCE_THRESHOLD, ): string[] { return [...otherStrings] .map((s) => [s, editDistance(s, stringToCompare)] as const) .sort(([_, d1], [__, d2]) => d1 - d2) .filter(([_, d]) => d <= maxEditDistance) .map(([s]) => s); } #formatSimilarNameSuggestions( contractNameOrFullyQualifiedName: string, similarNames: string[], ): string { const contractNameType = this.#isFullyQualifiedName( contractNameOrFullyQualifiedName, ) ? "fully qualified contract name" : "contract name"; switch (similarNames.length) { case 0: return ""; case 1: return `Did you mean "${similarNames[0]}"?`; default: return `We found some that were similar: ${similarNames.map((n) => ` * ${n}`).join(EOL)} Please replace "${contractNameOrFullyQualifiedName}" with the correct ${contractNameType} wherever you are trying to read its artifact. `; } } async #getFsData(): Promise<FsData> { if (this.#fsData === undefined) { this.#fsData = await this.#readFsData(); } return this.#fsData; } async #readFsDataFromFileSystem(): Promise<FsData> { const buildInfosDir = path.join(this.#artifactsPath, BUILD_INFO_DIR_NAME); const allArtifactPaths = await getAllFilesMatching( this.#artifactsPath, (p) => p.endsWith(".json") && // Only consider json files // Ignore top level json files p.indexOf(path.sep, this.#artifactsPath.length + path.sep.length) !== -1, (dir) => dir !== buildInfosDir, // Ignore build infos directory ); const allFullyQualifiedNames = new Set<string>(); const bareNameToFullyQualifiedNameMap = new Map<string, Set<string>>(); const fullyQualifiedNameToArtifactPath = new Map<string, string>(); for (const p of allArtifactPaths) { const bareName = path.basename(p, ".json"); const fqn = this.#getFullyQualifiedNameFromArtifactAbsolutePath(p); allFullyQualifiedNames.add(fqn); fullyQualifiedNameToArtifactPath.set(fqn, p); const fqns = bareNameToFullyQualifiedNameMap.get(bareName); if (fqns === undefined) { bareNameToFullyQualifiedNameMap.set(bareName, new Set([fqn])); } else { fqns.add(fqn); } } return { allArtifactPaths: new Set(allArtifactPaths), allFullyQualifiedNames, bareNameToFullyQualifiedNameMap, fullyQualifiedNameToArtifactPath, }; } } /** * Returns the edit-distance between two given strings using Levenshtein distance. * * @param a First string being compared * @param b Second string being compared * @returns distance between the two strings (lower number == more similar) * @see https://github.com/gustf/js-levenshtein * @license MIT - https://github.com/gustf/js-levenshtein/blob/master/LICENSE */ export function editDistance(a: string, b: string): number { function _min( _d0: number, _d1: number, _d2: number, _bx: number, _ay: number, ): number { return _d0 < _d1 || _d2 < _d1 ? _d0 > _d2 ? _d2 + 1 : _d0 + 1 : _bx === _ay ? _d1 : _d1 + 1; } if (a === b) { return 0; } if (a.length > b.length) { [a, b] = [b, a]; } let la = a.length; let lb = b.length; while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) { la--; lb--; } let offset = 0; while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) { offset++; } la -= offset; lb -= offset; if (la === 0 || lb < 3) { return lb; } let x = 0; let y: number; let d0: number; let d1: number; let d2: number; let d3: number; let dd: number = 0; // typescript gets angry if we don't assign here let dy: number; let ay: number; let bx0: number; let bx1: number; let bx2: number; let bx3: number; const vector = []; for (y = 0; y < la; y++) { vector.push(y + 1); vector.push(a.charCodeAt(offset + y)); } const len = vector.length - 1; for (; x < lb - 3; ) { bx0 = b.charCodeAt(offset + (d0 = x)); bx1 = b.charCodeAt(offset + (d1 = x + 1)); bx2 = b.charCodeAt(offset + (d2 = x + 2)); bx3 = b.charCodeAt(offset + (d3 = x + 3)); dd = x += 4; for (y = 0; y < len; y += 2) { dy = vector[y]; ay = vector[y + 1]; d0 = _min(dy, d0, d1, bx0, ay); d1 = _min(d0, d1, d2, bx1, ay); d2 = _min(d1, d2, d3, bx2, ay); dd = _min(d2, d3, dd, bx3, ay); vector[y] = dd; d3 = d2; d2 = d1; d1 = d0; d0 = dy; } } for (; x < lb; ) { bx0 = b.charCodeAt(offset + (d0 = x)); dd = ++x; for (y = 0; y < len; y += 2) { dy = vector[y]; vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1]); d0 = dy; } } return dd; }