UNPKG

hardhat

Version:

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

270 lines (268 loc) 10.8 kB
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; export class ArtifactManagerImplementation { #artifactsPath; // 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. #readFsData; #fsData; constructor(artifactsPath, readFsData) { this.#artifactsPath = artifactsPath; this.#readFsData = readFsData ?? (() => this.#readFsDataFromFileSystem()); } async readArtifact(contractNameOrFullyQualifiedName) { const artifactPath = await this.getArtifactPath(contractNameOrFullyQualifiedName); return readJsonFile(artifactPath); } async getArtifactPath(contractNameOrFullyQualifiedName) { 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; } async artifactExists(contractNameOrFullyQualifiedName) { 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; } } async getBuildInfoId(contractNameOrFullyQualifiedName) { const artifact = await this.readArtifact(contractNameOrFullyQualifiedName); return artifact.buildInfoId; } async getAllFullyQualifiedNames() { const { allFullyQualifiedNames } = await this.#getFsData(); return allFullyQualifiedNames; } async getAllBuildInfoIds() { 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"))); } async getBuildInfoPath(buildInfoId) { const buildInfoPath = path.join(this.#artifactsPath, BUILD_INFO_DIR_NAME, buildInfoId + ".json"); return (await exists(buildInfoPath)) ? buildInfoPath : undefined; } async getBuildInfoOutputPath(buildInfoId) { const buildInfoOutputPath = path.join(this.#artifactsPath, BUILD_INFO_DIR_NAME, buildInfoId + ".output.json"); return (await exists(buildInfoOutputPath)) ? buildInfoOutputPath : undefined; } async clearCache() { this.#fsData = undefined; } async #getFullyQualifiedName(contractNameOrFullyQualifiedName) { 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, allBareNames, allFullyQualifiedNames) { 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) { return name.includes(":"); } #getFullyQualifiedNameFromArtifactAbsolutePath(artifactPath) { 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, otherStrings, maxEditDistance = EDIT_DISTANCE_THRESHOLD) { return [...otherStrings] .map((s) => [s, editDistance(s, stringToCompare)]) .sort(([_, d1], [__, d2]) => d1 - d2) .filter(([_, d]) => d <= maxEditDistance) .map(([s]) => s); } #formatSimilarNameSuggestions(contractNameOrFullyQualifiedName, similarNames) { 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() { if (this.#fsData === undefined) { this.#fsData = await this.#readFsData(); } return this.#fsData; } async #readFsDataFromFileSystem() { 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); const allFullyQualifiedNames = new Set(); const bareNameToFullyQualifiedNameMap = new Map(); const fullyQualifiedNameToArtifactPath = new Map(); 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, b) { function _min(_d0, _d1, _d2, _bx, _ay) { 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; let d0; let d1; let d2; let d3; let dd = 0; // typescript gets angry if we don't assign here let dy; let ay; let bx0; let bx1; let bx2; let bx3; 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; } //# sourceMappingURL=artifact-manager.js.map