Skip to content

Commit a444c40

Browse files
fix: V8 coverage innermost-wins across FunctionCoverage entries (#116)
Flatten and sort all ranges per script before writing line hits so outer entries cannot overwrite narrower inner ranges when V8 lists the outer function last.
1 parent eb18750 commit a444c40

3 files changed

Lines changed: 76 additions & 14 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stainless-code/codemap": patch
3+
---
4+
5+
Fix V8 coverage ingest so innermost-wins applies across all FunctionCoverage entries in a script, not per-function iteration order.

src/application/coverage-engine.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,65 @@ describe("coverage-engine", () => {
673673
}
674674
});
675675

676+
it("innermost-wins across separate FunctionCoverage entries (inner before outer)", () => {
677+
const source = [
678+
"function outer() {",
679+
" function inner() { return 1; }",
680+
" inner();",
681+
"}",
682+
].join("\n");
683+
const { root, url } = makeTempProject(source);
684+
const innerStart = source.indexOf("function inner");
685+
const innerEnd = source.indexOf("}", source.indexOf("inner"));
686+
687+
const db = setupDb();
688+
try {
689+
insertFile(db, { ...indexedFile("src/a.ts"), language: "ts" });
690+
insertSymbols(db, [
691+
fnSym("src/a.ts", "outer", 1, 4),
692+
fnSym("src/a.ts", "inner", 2, 2),
693+
]);
694+
695+
const scripts: V8ScriptCoverage[] = [
696+
{
697+
scriptId: "1",
698+
url,
699+
functions: [
700+
{
701+
functionName: "inner",
702+
isBlockCoverage: true,
703+
ranges: [
704+
{ startOffset: innerStart, endOffset: innerEnd, count: 0 },
705+
],
706+
},
707+
{
708+
functionName: "outer",
709+
isBlockCoverage: true,
710+
ranges: [
711+
{ startOffset: 0, endOffset: source.length, count: 2 },
712+
],
713+
},
714+
],
715+
},
716+
];
717+
718+
const result = ingestV8({
719+
db,
720+
projectRoot: root,
721+
scripts,
722+
sourcePath: join(root, ".cov"),
723+
});
724+
expect(result.ingested.symbols).toBe(2);
725+
726+
const rows = db
727+
.query("SELECT name, hit_statements FROM coverage ORDER BY name")
728+
.all() as Array<{ name: string; hit_statements: number }>;
729+
expect(rows.find((r) => r.name === "inner")!.hit_statements).toBe(0);
730+
} finally {
731+
closeDb(db);
732+
}
733+
});
734+
676735
it("skips scripts whose url isn't a file:// URL (Node internals, eval)", () => {
677736
const db = setupDb();
678737
try {

src/application/coverage-engine.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -439,20 +439,18 @@ export function ingestV8(opts: V8ParserOpts): IngestResult {
439439
const scriptHits: (number | undefined)[] = new Array(
440440
lineOffsets.length + 1,
441441
);
442-
for (const fn of script.functions ?? []) {
443-
const sorted = (fn.ranges ?? [])
444-
.slice()
445-
.sort(
446-
(a, b) =>
447-
b.endOffset - b.startOffset - (a.endOffset - a.startOffset),
448-
);
449-
for (const range of sorted) {
450-
const startLine = offsetToLine(lineOffsets, range.startOffset);
451-
const endLine = offsetToLine(lineOffsets, range.endOffset);
452-
for (let line = startLine; line <= endLine; line++) {
453-
// Innermost-wins within this script: last write is the smallest range.
454-
scriptHits[line] = range.count;
455-
}
442+
// V8 may emit nested functions as separate entries; innermost-wins must be
443+
// global across all ranges, not per FunctionCoverage iteration order.
444+
const ranges = (script.functions ?? []).flatMap((fn) => fn.ranges ?? []);
445+
ranges.sort(
446+
(a, b) => b.endOffset - b.startOffset - (a.endOffset - a.startOffset),
447+
);
448+
for (const range of ranges) {
449+
const startLine = offsetToLine(lineOffsets, range.startOffset);
450+
const endLine = offsetToLine(lineOffsets, range.endOffset);
451+
for (let line = startLine; line <= endLine; line++) {
452+
// Innermost-wins: last write is the smallest range.
453+
scriptHits[line] = range.count;
456454
}
457455
}
458456
for (let line = 1; line < scriptHits.length; line++) {

0 commit comments

Comments
 (0)