hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
273 lines (233 loc) • 8.73 kB
text/typescript
import type { SolidityBuildInfoOutput } from "../../../../types/solidity/solidity-artifacts.js";
import type {
BuildInfoAndOutput,
EdrArtifactWithMetadata,
} from "../edr-artifacts.js";
import type { RawInlineOverride } from "./types.js";
import type { ArtifactId, TestFunctionOverride } from "@nomicfoundation/edr";
import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import { bytesToUtf8String } from "@nomicfoundation/hardhat-utils/bytes";
import { getFullyQualifiedName } from "../../../../utils/contract-names.js";
import {
buildInfoContainsInlineConfig,
resolveFunctionSelector,
buildConfigOverride,
getFunctionFqn,
} from "./helpers.js";
import { extractInlineConfigFromAst } from "./parsing.js";
import { validateInlineOverrides } from "./validation.js";
export type { RawInlineOverride } from "./types.js";
export {
buildInfoContainsInlineConfig,
resolveFunctionSelector,
buildConfigOverride,
getFunctionFqn,
} from "./helpers.js";
export {
extractInlineConfigFromAst,
extractDocText,
parseInlineConfigLine,
} from "./parsing.js";
export { validateInlineOverrides } from "./validation.js";
interface CollectedOverrides {
overrides: RawInlineOverride[];
artifactIdsByFqn: Map<string, ArtifactId>;
methodIdentifiersByContract: Map<string, Record<string, string>>;
}
/**
* Extracts per-test inline configuration overrides from the NatSpec comments
* in the solc AST. It only extracts them from the build info where each
* test artifact's file was compiled as a root file.
*/
export function getTestFunctionOverrides(
testSuiteArtifacts: EdrArtifactWithMetadata[],
buildInfosAndOutputs: BuildInfoAndOutput[],
): TestFunctionOverride[] {
const allRawOverrides = collectRawOverrides(
testSuiteArtifacts,
buildInfosAndOutputs,
);
validateInlineOverrides(allRawOverrides.overrides);
return buildTestFunctionOverrides(allRawOverrides);
}
function collectRawOverrides(
testSuiteArtifacts: EdrArtifactWithMetadata[],
buildInfosAndOutputs: BuildInfoAndOutput[],
): CollectedOverrides {
const overrides: RawInlineOverride[] = [];
const methodIdentifiersByContract = new Map<string, Record<string, string>>();
// Note: We group the artifacts by their build info, so that we only process
// the relevant build infos, and only the root files of each of them.
//
// The last part is important, as a test file can be present in multiple build
// infos in the presence of partial recompilations
//
// At the same time, the same root file could have produced multiple test
// artifacts, so we need a two-level map to avoid processing the root files
// multiple times.
// Build lookup structures for fast access
const artifactsGroupedByBuildInfo = new Map<
/* buildInfoId */ string,
Map</* inputSourceName */ string, EdrArtifactWithMetadata[]>
>();
const artifactIdsByFqn = new Map<string, ArtifactId>();
const buildInfoAndOutputById: Map<string, BuildInfoAndOutput> = new Map(
buildInfosAndOutputs.map((bio) => [bio.buildInfoId, bio]),
);
for (const edrArtifactWithMetadata of testSuiteArtifacts) {
const fqn = getFullyQualifiedName(
edrArtifactWithMetadata.edrArtifact.id.source,
edrArtifactWithMetadata.edrArtifact.id.name,
);
const buildInfoId = edrArtifactWithMetadata.buildInfoId;
let artifactsBySource = artifactsGroupedByBuildInfo.get(buildInfoId);
if (artifactsBySource === undefined) {
artifactsBySource = new Map();
artifactsGroupedByBuildInfo.set(buildInfoId, artifactsBySource);
}
let artifacts = artifactsBySource.get(
edrArtifactWithMetadata.edrArtifact.id.source,
);
if (artifacts === undefined) {
artifacts = [];
artifactsBySource.set(
edrArtifactWithMetadata.edrArtifact.id.source,
artifacts,
);
}
artifacts.push(edrArtifactWithMetadata);
artifactIdsByFqn.set(fqn, edrArtifactWithMetadata.edrArtifact.id);
}
for (const [
buildInfoId,
artifactsBySource,
] of artifactsGroupedByBuildInfo.entries()) {
const buildInfoAndOutput = buildInfoAndOutputById.get(buildInfoId);
if (buildInfoAndOutput === undefined) {
// We can throw for this error for the first artifact with this build info
// as all of them have the same problem.
const artifacts = artifactsBySource.values().next().value;
assertHardhatInvariant(
artifacts !== undefined && artifacts.length > 0,
"An artifact must be present for the build info",
);
const anyArtifact = artifacts[0];
const fqn = getFullyQualifiedName(
anyArtifact.userSourceName,
anyArtifact.edrArtifact.id.name,
);
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.BUILD_INFO_NOT_FOUND_FOR_CONTRACT,
{
fqn,
},
);
}
if (!buildInfoContainsInlineConfig(buildInfoAndOutput.buildInfo)) {
continue;
}
const buildInfoOutput: SolidityBuildInfoOutput = JSON.parse(
bytesToUtf8String(buildInfoAndOutput.output),
);
for (const [
inputSourceName,
sourceArtifacts,
] of artifactsBySource.entries()) {
const contractNames = new Set(
sourceArtifacts.map((a) => a.edrArtifact.id.name),
);
const source = buildInfoOutput.output.sources[inputSourceName];
const extracted = extractInlineConfigFromAst(
source.ast,
inputSourceName,
contractNames,
);
overrides.push(...extracted);
for (const artifact of sourceArtifacts) {
const contractName = artifact.edrArtifact.id.name;
const fqn = getFullyQualifiedName(inputSourceName, contractName);
const methodIdentifiers =
buildInfoOutput.output.contracts?.[inputSourceName][contractName]?.evm
?.methodIdentifiers;
methodIdentifiersByContract.set(fqn, methodIdentifiers ?? {});
}
}
}
return { overrides, artifactIdsByFqn, methodIdentifiersByContract };
}
function buildTestFunctionOverrides(
collected: CollectedOverrides,
): TestFunctionOverride[] {
const { overrides, artifactIdsByFqn, methodIdentifiersByContract } =
collected;
// Group overrides by function. When the AST provides a functionSelector
// (public/external functions in solc >= 0.6.0), use it to distinguish
// overloaded functions. Otherwise fall back to function name only.
const overridesByFunction = new Map<string, RawInlineOverride[]>();
for (const override of overrides) {
const functionFqn = getFunctionFqn(
override.inputSourceName,
override.contractName,
override.functionName,
);
const groupKey =
override.functionSelector !== undefined
? `${functionFqn}#${override.functionSelector}`
: functionFqn;
const existing = overridesByFunction.get(groupKey);
if (existing === undefined) {
overridesByFunction.set(groupKey, [override]);
} else {
existing.push(override);
}
}
// Build TestFunctionOverride objects
const testFunctionOverrides: TestFunctionOverride[] = [];
for (const [_groupKey, groupOverrides] of overridesByFunction.entries()) {
const firstOverride = groupOverrides[0];
const functionFqn = getFunctionFqn(
firstOverride.inputSourceName,
firstOverride.contractName,
firstOverride.functionName,
);
const contractFqn = `${firstOverride.inputSourceName}:${firstOverride.contractName}`;
const artifactId = artifactIdsByFqn.get(contractFqn);
assertHardhatInvariant(
artifactId !== undefined,
`Missing artifact id for "${contractFqn}"`,
);
// Use the AST-provided selector when available, otherwise fall back to
// resolving via methodIdentifiers.
let selector: string | undefined;
if (firstOverride.functionSelector !== undefined) {
selector = `0x${firstOverride.functionSelector}`;
} else {
const methodIdentifiers = methodIdentifiersByContract.get(contractFqn);
assertHardhatInvariant(
methodIdentifiers !== undefined,
`Missing method identifiers for "${contractFqn}"`,
);
selector = resolveFunctionSelector(
methodIdentifiers,
firstOverride.functionName,
);
}
if (selector === undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY_TESTS.INLINE_CONFIG_UNRESOLVED_SELECTOR,
{ functionFqn },
);
}
testFunctionOverrides.push({
identifier: {
contractArtifact: artifactId,
functionSelector: selector,
},
config: buildConfigOverride(groupOverrides),
});
}
return testFunctionOverrides;
}