hardhat 3.1.3 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/src/internal/builtin-plugins/coverage/coverage-manager.d.ts +14 -14
  3. package/dist/src/internal/builtin-plugins/coverage/coverage-manager.d.ts.map +1 -1
  4. package/dist/src/internal/builtin-plugins/coverage/coverage-manager.js +66 -110
  5. package/dist/src/internal/builtin-plugins/coverage/coverage-manager.js.map +1 -1
  6. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/solidity.d.ts.map +1 -1
  7. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/solidity.js +13 -6
  8. package/dist/src/internal/builtin-plugins/coverage/hook-handlers/solidity.js.map +1 -1
  9. package/dist/src/internal/builtin-plugins/coverage/process-coverage.d.ts +21 -0
  10. package/dist/src/internal/builtin-plugins/coverage/process-coverage.d.ts.map +1 -0
  11. package/dist/src/internal/builtin-plugins/coverage/process-coverage.js +291 -0
  12. package/dist/src/internal/builtin-plugins/coverage/process-coverage.js.map +1 -0
  13. package/dist/src/internal/builtin-plugins/coverage/reports/html.d.ts +3 -0
  14. package/dist/src/internal/builtin-plugins/coverage/reports/html.d.ts.map +1 -0
  15. package/dist/src/internal/builtin-plugins/coverage/reports/html.js +37 -0
  16. package/dist/src/internal/builtin-plugins/coverage/reports/html.js.map +1 -0
  17. package/dist/src/internal/builtin-plugins/coverage/types.d.ts +9 -3
  18. package/dist/src/internal/builtin-plugins/coverage/types.d.ts.map +1 -1
  19. package/package.json +2 -1
  20. package/src/internal/builtin-plugins/coverage/coverage-manager.ts +121 -176
  21. package/src/internal/builtin-plugins/coverage/hook-handlers/solidity.ts +19 -8
  22. package/src/internal/builtin-plugins/coverage/process-coverage.ts +442 -0
  23. package/src/internal/builtin-plugins/coverage/reports/html.ts +56 -0
  24. package/src/internal/builtin-plugins/coverage/types.ts +8 -3
  25. package/templates/hardhat-3/01-node-test-runner-viem/package.json +1 -1
  26. package/templates/hardhat-3/02-mocha-ethers/package.json +1 -1
  27. package/templates/hardhat-3/03-minimal/package.json +1 -1
@@ -3,18 +3,17 @@ import type {
3
3
  CoverageManager,
4
4
  CoverageMetadata,
5
5
  Statement,
6
- Tag,
7
6
  } from "./types.js";
8
7
  import type { TableItem } from "@nomicfoundation/hardhat-utils/format";
9
8
 
10
9
  import path from "node:path";
11
10
 
12
- import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
13
11
  import { divider, formatTable } from "@nomicfoundation/hardhat-utils/format";
14
12
  import {
15
13
  ensureDir,
16
14
  getAllFilesMatching,
17
15
  readJsonFile,
16
+ readUtf8File,
18
17
  remove,
19
18
  writeJsonFile,
20
19
  writeUtf8File,
@@ -22,36 +21,56 @@ import {
22
21
  import chalk from "chalk";
23
22
  import debug from "debug";
24
23
 
24
+ import { getProcessedCoverageInfo } from "./process-coverage.js";
25
+ import { generateHtmlReport } from "./reports/html.js";
26
+
25
27
  const log = debug("hardhat:core:coverage:coverage-manager");
26
28
 
27
29
  const MAX_COLUMN_WIDTH = 80;
28
30
 
29
31
  type Line = number;
30
- type Branch = [Line, Tag];
31
32
 
32
33
  /**
33
34
  * @private exposed for testing purposes only
34
35
  */
36
+ export interface FileReport {
37
+ // NOTE: currently, the counters for how many times a statement is executed are not implemented in EDR,
38
+ // so the only information available is whether a statement was executed, not how many times it was executed.
39
+ // Also, branch coverage is not available.
40
+ // In addition, partially executed lines (for example, ternary operators) cannot be determined, as this information is missing in EDR,
41
+ // since only whole statements can be registered as executed or not.
42
+
43
+ lineExecutionCounts: Map<Line, number>;
44
+
45
+ executedStatementsCount: number;
46
+ unexecutedStatementsCount: number;
47
+
48
+ executedLinesCount: number;
49
+
50
+ unexecutedLines: Set<Line>;
51
+ }
52
+
35
53
  export interface Report {
36
- [relativePath: string]: {
37
- tagExecutionCounts: Map<Tag, number>;
38
- lineExecutionCounts: Map<Line, number>;
39
- branchExecutionCounts: Map<Branch, number>;
40
-
41
- executedTagsCount: number;
42
- executedLinesCount: number;
43
- executedBranchesCount: number;
44
-
45
- partiallyExecutedLines: Set<Line>;
46
- unexecutedLines: Set<Line>;
47
- };
54
+ [relativePath: string]: FileReport;
48
55
  }
49
56
 
57
+ type FilesMetadata = Map<
58
+ string, // relative path
59
+ Map<
60
+ string, // composite key
61
+ Statement
62
+ >
63
+ >;
64
+
50
65
  export class CoverageManagerImplementation implements CoverageManager {
51
66
  /**
52
67
  * @private exposed for testing purposes only
53
68
  */
54
- public metadata: CoverageMetadata = [];
69
+ public filesMetadata: FilesMetadata = new Map<
70
+ string,
71
+ Map<string, Statement>
72
+ >();
73
+
55
74
  /**
56
75
  * @private exposed for testing purposes only
57
76
  */
@@ -80,13 +99,24 @@ export class CoverageManagerImplementation implements CoverageManager {
80
99
  }
81
100
 
82
101
  public async addMetadata(metadata: CoverageMetadata): Promise<void> {
83
- // NOTE: The received metadata might contain duplicates. We deduplicate it
84
- // when we generate the report.
85
102
  for (const entry of metadata) {
86
- this.metadata.push(entry);
87
- }
103
+ log("Added metadata", JSON.stringify(metadata, null, 2));
104
+
105
+ let fileStatements = this.filesMetadata.get(entry.relativePath);
106
+
107
+ if (fileStatements === undefined) {
108
+ fileStatements = new Map();
109
+ this.filesMetadata.set(entry.relativePath, fileStatements);
110
+ }
88
111
 
89
- log("Added metadata", JSON.stringify(metadata, null, 2));
112
+ const key = `${entry.relativePath}-${entry.tag}-${entry.startUtf16}-${entry.endUtf16}`;
113
+
114
+ const existingData = fileStatements.get(key);
115
+
116
+ if (existingData === undefined) {
117
+ fileStatements.set(key, entry);
118
+ }
119
+ }
90
120
  }
91
121
 
92
122
  public async clearData(id: string): Promise<void> {
@@ -111,7 +141,7 @@ export class CoverageManagerImplementation implements CoverageManager {
111
141
 
112
142
  await this.loadData(...ids);
113
143
 
114
- const report = this.getReport();
144
+ const report = await this.getReport();
115
145
  const lcovReport = this.formatLcovReport(report);
116
146
  const markdownReport = this.formatMarkdownReport(report);
117
147
 
@@ -119,6 +149,10 @@ export class CoverageManagerImplementation implements CoverageManager {
119
149
  await writeUtf8File(lcovReportPath, lcovReport);
120
150
  log(`Saved lcov report to ${lcovReportPath}`);
121
151
 
152
+ const htmlReportPath = path.join(this.#coveragePath, "html");
153
+ await generateHtmlReport(report, htmlReportPath);
154
+ console.log(`Saved html report to ${htmlReportPath}`);
155
+
122
156
  console.log(markdownReport);
123
157
  console.log();
124
158
  log("Printed markdown report");
@@ -153,143 +187,63 @@ export class CoverageManagerImplementation implements CoverageManager {
153
187
  /**
154
188
  * @private exposed for testing purposes only
155
189
  */
156
- public getReport(): Report {
157
- const report: Report = {};
158
-
159
- const relativePaths = this.metadata.map(({ relativePath }) => relativePath);
160
-
161
- const allStatements = this.metadata;
162
-
163
- // NOTE: We preserve only the last statement per tag in the statementsByTag map.
164
- const statementsByTag = new Map<string, Statement>();
165
- for (const statement of allStatements) {
166
- statementsByTag.set(statement.tag, statement);
167
- }
168
-
169
- const allExecutedTags = this.data;
170
-
171
- const allExecutedStatementsByRelativePath = new Map<string, Statement[]>();
172
- for (const tag of allExecutedTags) {
173
- // NOTE: We should not encounter an executed tag we don't have metadata for.
174
- const statement = statementsByTag.get(tag);
175
- assertHardhatInvariant(statement !== undefined, "Expected a statement");
176
-
177
- const relativePath = statement.relativePath;
178
- const allExecutedStatements =
179
- allExecutedStatementsByRelativePath.get(relativePath) ?? [];
180
- allExecutedStatements.push(statement);
181
- allExecutedStatementsByRelativePath.set(
182
- relativePath,
183
- allExecutedStatements,
184
- );
185
- }
186
-
187
- const uniqueExecutedTags = new Set(allExecutedTags);
188
- const uniqueUnexecutedTags = Array.from(statementsByTag.keys()).filter(
189
- (tag) => !uniqueExecutedTags.has(tag),
190
- );
191
-
192
- const uniqueUnexecutedStatementsByRelativePath = new Map<
193
- string,
194
- Statement[]
195
- >();
196
- for (const tag of uniqueUnexecutedTags) {
197
- // NOTE: We cannot encounter an executed tag we don't have metadata for.
198
- const statement = statementsByTag.get(tag);
199
- assertHardhatInvariant(statement !== undefined, "Expected a statement");
200
-
201
- const relativePath = statement.relativePath;
202
- const unexecutedStatements =
203
- uniqueUnexecutedStatementsByRelativePath.get(relativePath) ?? [];
204
- unexecutedStatements.push(statement);
205
- uniqueUnexecutedStatementsByRelativePath.set(
206
- relativePath,
207
- unexecutedStatements,
208
- );
209
- }
190
+ public async getReport(): Promise<Report> {
191
+ const allExecutedTags = new Set(this.data);
210
192
 
211
- for (const relativePath of relativePaths) {
212
- const allExecutedStatements =
213
- allExecutedStatementsByRelativePath.get(relativePath) ?? [];
214
- const uniqueUnexecutedStatements =
215
- uniqueUnexecutedStatementsByRelativePath.get(relativePath) ?? [];
216
-
217
- const tagExecutionCounts = new Map<Tag, number>();
218
-
219
- for (const statement of allExecutedStatements) {
220
- const tagExecutionCount = tagExecutionCounts.get(statement.tag) ?? 0;
221
- tagExecutionCounts.set(statement.tag, tagExecutionCount + 1);
222
- }
223
-
224
- const lineExecutionCounts = new Map<number, number>();
225
- const branchExecutionCounts = new Map<Branch, number>();
226
-
227
- for (const [tag, executionCount] of tagExecutionCounts) {
228
- const statement = statementsByTag.get(tag);
229
- assertHardhatInvariant(statement !== undefined, "Expected a statement");
230
-
231
- for (
232
- let line = statement.startLine;
233
- line <= statement.endLine;
234
- line++
235
- ) {
236
- const lineExecutionCount = lineExecutionCounts.get(line) ?? 0;
237
- lineExecutionCounts.set(line, lineExecutionCount + executionCount);
238
-
239
- const branchExecutionCount =
240
- branchExecutionCounts.get([line, tag]) ?? 0;
241
- branchExecutionCounts.set(
242
- [line, tag],
243
- branchExecutionCount + executionCount,
244
- );
245
- }
246
- }
193
+ const reportPromises = Array.from(this.filesMetadata.entries()).map(
194
+ async ([fileRelativePath, fileStatements]) => {
195
+ const statements = Array.from(fileStatements.values());
247
196
 
248
- const executedTagsCount = tagExecutionCounts.size;
249
- const executedLinesCount = lineExecutionCounts.size;
250
- const executedBranchesCount = branchExecutionCounts.size;
197
+ const fileContent = await readUtf8File(
198
+ path.join(process.cwd(), fileRelativePath),
199
+ );
251
200
 
252
- const partiallyExecutedLines = new Set<number>();
253
- const unexecutedLines = new Set<number>();
201
+ const tags: Set<string> = new Set();
202
+ let executedStatementsCount = 0;
203
+ let unexecutedStatementsCount = 0;
254
204
 
255
- for (const statement of uniqueUnexecutedStatements) {
256
- if (!tagExecutionCounts.has(statement.tag)) {
257
- tagExecutionCounts.set(statement.tag, 0);
258
- }
259
-
260
- for (
261
- let line = statement.startLine;
262
- line <= statement.endLine;
263
- line++
264
- ) {
265
- if (!lineExecutionCounts.has(line)) {
266
- lineExecutionCounts.set(line, 0);
267
- unexecutedLines.add(line);
205
+ for (const { tag } of statements) {
206
+ if (allExecutedTags.has(tag)) {
207
+ tags.add(tag);
208
+ executedStatementsCount++;
268
209
  } else {
269
- partiallyExecutedLines.add(line);
270
- }
271
-
272
- if (!branchExecutionCounts.has([line, statement.tag])) {
273
- branchExecutionCounts.set([line, statement.tag], 0);
210
+ unexecutedStatementsCount++;
274
211
  }
275
212
  }
276
- }
277
-
278
- report[relativePath] = {
279
- tagExecutionCounts,
280
- lineExecutionCounts,
281
- branchExecutionCounts,
282
213
 
283
- executedTagsCount,
284
- executedLinesCount,
285
- executedBranchesCount,
214
+ const coverageInfo = getProcessedCoverageInfo(
215
+ fileContent,
216
+ statements,
217
+ tags,
218
+ );
219
+
220
+ const lineExecutionCounts = new Map<number, number>();
221
+ coverageInfo.lines.executed.forEach((_, line) =>
222
+ lineExecutionCounts.set(line, 1),
223
+ );
224
+ coverageInfo.lines.unexecuted.forEach((_, line) =>
225
+ lineExecutionCounts.set(line, 0),
226
+ );
227
+
228
+ const executedLinesCount = coverageInfo.lines.executed.size;
229
+ const unexecutedLines = new Set(coverageInfo.lines.unexecuted.keys());
230
+
231
+ return {
232
+ path: fileRelativePath,
233
+ data: {
234
+ lineExecutionCounts,
235
+ executedStatementsCount,
236
+ unexecutedStatementsCount,
237
+ executedLinesCount,
238
+ unexecutedLines,
239
+ },
240
+ };
241
+ },
242
+ );
286
243
 
287
- partiallyExecutedLines,
288
- unexecutedLines,
289
- };
290
- }
244
+ const results = await Promise.all(reportPromises);
291
245
 
292
- return report;
246
+ return Object.fromEntries(results.map((r) => [r.path, r.data]));
293
247
  }
294
248
 
295
249
  /**
@@ -315,12 +269,7 @@ export class CoverageManagerImplementation implements CoverageManager {
315
269
 
316
270
  for (const [
317
271
  relativePath,
318
- {
319
- branchExecutionCounts,
320
- executedBranchesCount,
321
- lineExecutionCounts,
322
- executedLinesCount,
323
- },
272
+ { lineExecutionCounts, executedLinesCount },
324
273
  ] of Object.entries(report)) {
325
274
  lcov += `SF:${relativePath}\n`;
326
275
 
@@ -336,11 +285,12 @@ export class CoverageManagerImplementation implements CoverageManager {
336
285
  // BRF:<number of branches found>
337
286
  // BRH:<number of branches hit>
338
287
 
339
- for (const [[line, tag], executionCount] of branchExecutionCounts) {
340
- lcov += `BRDA:${line},0,${tag},${executionCount === 0 ? "-" : executionCount}\n`;
341
- }
342
- lcov += `BRH:${executedBranchesCount}\n`;
343
- lcov += `BRF:${branchExecutionCounts.size}\n`;
288
+ // TODO: currently EDR does not provide branch coverage information.
289
+ // for (const [[line, tag], executionCount] of branchExecutionCounts) {
290
+ // lcov += `BRDA:${line},0,${tag},${executionCount === 0 ? "-" : executionCount}\n`;
291
+ // }
292
+ // lcov += `BRH:${executedBranchesCount}\n`;
293
+ // lcov += `BRF:${branchExecutionCounts.size}\n`;
344
294
 
345
295
  // Then there is a list of execution counts for each instrumented line
346
296
  // (i.e. a line which resulted in executable code):
@@ -487,25 +437,20 @@ export class CoverageManagerImplementation implements CoverageManager {
487
437
  rows.push(divider);
488
438
 
489
439
  rows.push(
490
- [
491
- "File Path",
492
- "Line %",
493
- "Statement %",
494
- "Uncovered Lines",
495
- "Partially Covered Lines",
496
- ].map((s) => chalk.yellow(s)),
440
+ ["File Path", "Line %", "Statement %", "Uncovered Lines"].map((s) =>
441
+ chalk.yellow(s),
442
+ ),
497
443
  );
498
444
 
499
445
  const bodyRows = Object.entries(report).map(
500
446
  ([
501
447
  relativePath,
502
448
  {
503
- tagExecutionCounts,
449
+ executedStatementsCount,
450
+ unexecutedStatementsCount,
504
451
  lineExecutionCounts,
505
- executedTagsCount,
506
452
  executedLinesCount,
507
453
  unexecutedLines,
508
- partiallyExecutedLines,
509
454
  },
510
455
  ]) => {
511
456
  const lineCoverage =
@@ -513,22 +458,23 @@ export class CoverageManagerImplementation implements CoverageManager {
513
458
  ? 0
514
459
  : (executedLinesCount * 100.0) / lineExecutionCounts.size;
515
460
  const statementCoverage =
516
- tagExecutionCounts.size === 0
461
+ executedStatementsCount === 0
517
462
  ? 0
518
- : (executedTagsCount * 100.0) / tagExecutionCounts.size;
463
+ : (executedStatementsCount * 100.0) /
464
+ (executedStatementsCount + unexecutedStatementsCount);
519
465
 
520
466
  totalExecutedLines += executedLinesCount;
521
467
  totalExecutableLines += lineExecutionCounts.size;
522
468
 
523
- totalExecutedStatements += executedTagsCount;
524
- totalExecutableStatements += tagExecutionCounts.size;
469
+ totalExecutedStatements += executedStatementsCount;
470
+ totalExecutableStatements +=
471
+ executedStatementsCount + unexecutedStatementsCount;
525
472
 
526
473
  const row: string[] = [
527
474
  this.formatRelativePath(relativePath),
528
475
  this.formatCoverage(lineCoverage),
529
476
  this.formatCoverage(statementCoverage),
530
477
  this.formatLines(unexecutedLines),
531
- this.formatLines(partiallyExecutedLines),
532
478
  ];
533
479
 
534
480
  return row;
@@ -552,7 +498,6 @@ export class CoverageManagerImplementation implements CoverageManager {
552
498
  this.formatCoverage(totalLineCoverage),
553
499
  this.formatCoverage(totalStatementCoverage),
554
500
  "",
555
- "",
556
501
  ]);
557
502
 
558
503
  return formatTable(rows);
@@ -3,14 +3,19 @@ import type { CoverageMetadata } from "../types.js";
3
3
 
4
4
  import path from "node:path";
5
5
 
6
- import { addStatementCoverageInstrumentation } from "@nomicfoundation/edr";
6
+ import {
7
+ addStatementCoverageInstrumentation,
8
+ latestSupportedSolidityVersion,
9
+ } from "@nomicfoundation/edr";
7
10
  import {
8
11
  assertHardhatInvariant,
9
12
  HardhatError,
10
13
  } from "@nomicfoundation/hardhat-errors";
11
14
  import { ensureError } from "@nomicfoundation/hardhat-utils/error";
12
15
  import { readUtf8File } from "@nomicfoundation/hardhat-utils/fs";
16
+ import { findClosestPackageRoot } from "@nomicfoundation/hardhat-utils/package";
13
17
  import debug from "debug";
18
+ import { satisfies } from "semver";
14
19
 
15
20
  import { CoverageManagerImplementation } from "../coverage-manager.js";
16
21
 
@@ -35,6 +40,13 @@ export default async (): Promise<Partial<SolidityHooks>> => ({
35
40
 
36
41
  if (context.globalOptions.coverage && !isTestSource) {
37
42
  try {
43
+ const latestSupportedVersion = latestSupportedSolidityVersion();
44
+ if (!satisfies(solcVersion, `<=${latestSupportedVersion}`)) {
45
+ console.log(
46
+ `Solidity version ${solcVersion} is not yet supported for coverage instrumentation. Hardhat will try the latest supported version ${latestSupportedVersion} instead.`,
47
+ );
48
+ solcVersion = latestSupportedVersion;
49
+ }
38
50
  const { source, metadata } = addStatementCoverageInstrumentation(
39
51
  fileContent,
40
52
  sourceName,
@@ -63,13 +75,11 @@ export default async (): Promise<Partial<SolidityHooks>> => ({
63
75
  fsPath,
64
76
  );
65
77
  const tag = Buffer.from(m.tag).toString("hex");
66
- const startLine = lineNumbers[m.startUtf16];
67
- const endLine = lineNumbers[m.endUtf16 - 1];
68
78
  coverageMetadata.push({
69
79
  relativePath,
70
80
  tag,
71
- startLine,
72
- endLine,
81
+ startUtf16: m.startUtf16,
82
+ endUtf16: m.endUtf16,
73
83
  });
74
84
  break;
75
85
  default:
@@ -124,9 +134,10 @@ export default async (): Promise<Partial<SolidityHooks>> => ({
124
134
  // NOTE: We add the coverage.sol straight into sources here. The alternative
125
135
  // would be to do it during the resolution phase. However, we decided this
126
136
  // is a simpler solution, at least for now.
127
- const content = await readUtf8File(
128
- path.join(import.meta.dirname, "../../../../../../coverage.sol"),
129
- );
137
+ const packageRoot = await findClosestPackageRoot(import.meta.url);
138
+ const coverageSolPath = path.join(packageRoot, "coverage.sol");
139
+
140
+ const content = await readUtf8File(coverageSolPath);
130
141
  solcInput.sources[COVERAGE_LIBRARY_PATH] = { content };
131
142
  }
132
143