hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
136 lines (118 loc) • 4.67 kB
text/typescript
import type { SolidityHooks } from "../../../../types/hooks.js";
import type { CoverageMetadata } from "../types.js";
import path from "node:path";
import { addStatementCoverageInstrumentation } from "@nomicfoundation/edr";
import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import { readUtf8File } from "@nomicfoundation/hardhat-utils/fs";
import debug from "debug";
import { CoverageManagerImplementation } from "../coverage-manager.js";
const log = debug("hardhat:core:coverage:hook-handlers:solidity");
const COVERAGE_LIBRARY_PATH =
"__hardhat_coverage_library_a3e9cfe2-41b4-4a1f-ad9e-ac62dd82979e.sol";
export default async (): Promise<Partial<SolidityHooks>> => ({
preprocessProjectFileBeforeBuilding: async (
context,
sourceName,
fsPath,
fileContent,
solcVersion,
next,
) => {
// NOTE: We do not want to instrument the test project files as we don't
// want to report coverage for them.
const isTestSource = (await context.solidity.getScope(fsPath)) === "tests";
if (context.globalOptions.coverage && !isTestSource) {
try {
const { source, metadata } = addStatementCoverageInstrumentation(
fileContent,
sourceName,
solcVersion,
COVERAGE_LIBRARY_PATH,
);
// TODO: Remove this once EDR starts returning line information as part
// of the metadata.
let lineNumber = 1;
const lineNumbers = [];
for (const character of fileContent) {
lineNumbers.push(lineNumber);
if (character === "\n") {
lineNumber++;
}
}
const coverageMetadata: CoverageMetadata = [];
for (const m of metadata) {
switch (m.kind) {
case "statement":
const relativePath = path.relative(
context.config.paths.root,
fsPath,
);
const tag = Buffer.from(m.tag).toString("hex");
const startLine = lineNumbers[m.startUtf16];
const endLine = lineNumbers[m.endUtf16 - 1];
coverageMetadata.push({
relativePath,
tag,
startLine,
endLine,
});
break;
default:
// NOTE: We don't support other kinds of metadata yet; however,
// we don't want to start throwing errors if/when EDR adds support
// for new kinds of coverage metadata.
log("Unsupported coverage metadata kind", m.kind);
break;
}
}
assertHardhatInvariant(
"_coverage" in context &&
context._coverage instanceof CoverageManagerImplementation,
"Expected _coverage to be defined in the HookContext, as it's should be defined in the HRE",
);
await context._coverage.addMetadata(coverageMetadata);
return await next(context, sourceName, fsPath, source, solcVersion);
} catch (e) {
ensureError(e);
// NOTE: These could be raised if the source content we pass to EDR
// cannot be parsed with a version of the parser we provided, for example.
throw new HardhatError(
HardhatError.ERRORS.CORE.COVERAGE.SOURCE_NOT_INSTRUMENTED,
{
sourceName,
},
e,
);
}
} else {
return next(context, sourceName, fsPath, fileContent, solcVersion);
}
},
preprocessSolcInputBeforeBuilding: async (context, solcInput, next) => {
if (context.globalOptions.coverage) {
// NOTE: We check for a source name clash here. It could happen if the user
// wanted to compile a source with our highly unlikely name by chance or
// if we accidentally tried to preprocess the same solc input twice.
if (solcInput.sources[COVERAGE_LIBRARY_PATH] !== undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.COVERAGE.IMPORT_PATH_ALREADY_DEFINED,
{
importPath: COVERAGE_LIBRARY_PATH,
},
);
}
// NOTE: We add the coverage.sol straight into sources here. The alternative
// would be to do it during the resolution phase. However, we decided this
// is a simpler solution, at least for now.
const content = await readUtf8File(
path.join(import.meta.dirname, "../../../../../../coverage.sol"),
);
solcInput.sources[COVERAGE_LIBRARY_PATH] = { content };
}
return next(context, solcInput);
},
});