1
0

coverage-uncovered-locations.cjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. 'use strict';
  2. /**
  3. * Istanbul coverage reporter printing one clickable `path:line:col` record per
  4. * uncovered statement, branch path, and function. Vitest's per-file threshold
  5. * failures name only the file; this reporter supplies the exact locations,
  6. * printed just above those ERROR lines (reports run before threshold checks).
  7. * Files at 100% print nothing, so a green run stays silent.
  8. *
  9. * CommonJS by requirement: istanbul-reports loads custom reporters with a bare
  10. * require() outside the tsx/ESM pipeline (istanbul-reports index.js create()),
  11. * so this file can be neither TypeScript nor ESM. Wired into vitest.config.ts
  12. * by absolute path — require() would resolve a relative specifier against
  13. * istanbul-reports' own directory.
  14. */
  15. const path = require('node:path');
  16. const { ReportBase } = require('istanbul-lib-report');
  17. /**
  18. * End column of a location that spans to the end of its start line. Whole-line
  19. * statements carry `Infinity` until a partition blob serializes it, which the
  20. * partition reporter replaces with this finite column
  21. * (scripts/coverage-canonical-locations.ts); both spellings read as line ends.
  22. */
  23. const END_OF_LINE_COLUMN = Number.MAX_SAFE_INTEGER;
  24. /**
  25. * Editor-convention `line:column` of an istanbul location start (istanbul
  26. * columns are 0-based; editors and terminal link handlers expect 1-based).
  27. */
  28. function pos(loc) {
  29. return `${loc.start.line}:${loc.start.column + 1}`;
  30. }
  31. /** Whether a location carries a usable 1-based start line. */
  32. function usable(loc) {
  33. return Boolean(loc && loc.start && Number.isFinite(loc.start.line) && loc.start.line >= 1);
  34. }
  35. /** Whether an end column marks its line's end instead of a measured position. */
  36. function atLineEnd(column) {
  37. return !Number.isFinite(column) || column >= END_OF_LINE_COLUMN;
  38. }
  39. /**
  40. * ` (to line:col)` suffix when the range end adds information beyond the
  41. * start. v8-remapped whole-line statements end at their line end; those
  42. * degrade to a line-only suffix, or to nothing on a single line.
  43. */
  44. function endSuffix(loc) {
  45. const end = loc.end;
  46. if (!end || !Number.isFinite(end.line) || end.line < 1) return '';
  47. if (atLineEnd(end.column)) {
  48. return end.line === loc.start.line ? '' : ` (to ${end.line})`;
  49. }
  50. if (end.line === loc.start.line && end.column === loc.start.column) return '';
  51. return ` (to ${end.line}:${end.column + 1})`;
  52. }
  53. class UncoveredLocationsReport extends ReportBase {
  54. constructor(opts = {}) {
  55. super(opts);
  56. // Vitest passes the resolved config root alongside reporter options.
  57. this.projectRoot = opts.projectRoot || process.cwd();
  58. this.records = [];
  59. }
  60. onStart() {
  61. this.records = [];
  62. }
  63. onDetail(node) {
  64. const fc = node.getFileCoverage();
  65. const rel = path.relative(this.projectRoot, fc.path).split(path.sep).join('/');
  66. const items = [];
  67. const add = (loc, text) => items.push({ line: loc.start.line, column: loc.start.column, text });
  68. for (const id of Object.keys(fc.statementMap)) {
  69. if (fc.s[id] !== 0) continue;
  70. const loc = fc.statementMap[id];
  71. if (!usable(loc)) continue;
  72. add(loc, `${rel}:${pos(loc)} uncovered statement${endSuffix(loc)}`);
  73. }
  74. for (const id of Object.keys(fc.fnMap)) {
  75. if (fc.f[id] !== 0) continue;
  76. const fn = fc.fnMap[id];
  77. const loc = usable(fn.decl) ? fn.decl : fn.loc;
  78. if (!usable(loc)) continue;
  79. const name = fn.name ? ` ${fn.name}` : '';
  80. add(loc, `${rel}:${pos(loc)} uncovered function${name}`);
  81. }
  82. for (const id of Object.keys(fc.branchMap)) {
  83. const counts = fc.b[id];
  84. const branch = fc.branchMap[id];
  85. for (let i = 0; i < counts.length; i += 1) {
  86. if (counts[i] !== 0) continue;
  87. // Implicit arms (e.g. a missing else) may carry an empty location;
  88. // fall back to the branch's own span so the record stays clickable.
  89. const loc = usable(branch.locations && branch.locations[i]) ? branch.locations[i] : branch.loc;
  90. if (!usable(loc)) continue;
  91. add(loc, `${rel}:${pos(loc)} uncovered branch (${branch.type}, path ${i + 1}/${counts.length})`);
  92. }
  93. }
  94. if (items.length === 0) return;
  95. items.sort((a, b) => a.line - b.line || a.column - b.column);
  96. for (const item of items) this.records.push(item.text);
  97. }
  98. onEnd() {
  99. if (this.records.length === 0) return;
  100. console.log(`\nUncovered locations (per-file 100% gate): ${this.records.length}`);
  101. for (const record of this.records) console.log(record);
  102. console.log('');
  103. }
  104. }
  105. module.exports = UncoveredLocationsReport;