Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-v8-coverage-innermost-wins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/codemap": patch
---

Fix V8 coverage ingest so innermost-wins applies across all FunctionCoverage entries in a script, not per-function iteration order.
59 changes: 59 additions & 0 deletions src/application/coverage-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,65 @@ describe("coverage-engine", () => {
}
});

it("innermost-wins across separate FunctionCoverage entries (inner before outer)", () => {
const source = [
"function outer() {",
" function inner() { return 1; }",
" inner();",
"}",
].join("\n");
const { root, url } = makeTempProject(source);
const innerStart = source.indexOf("function inner");
const innerEnd = source.indexOf("}", source.indexOf("inner"));

const db = setupDb();
try {
insertFile(db, { ...indexedFile("src/a.ts"), language: "ts" });
insertSymbols(db, [
fnSym("src/a.ts", "outer", 1, 4),
fnSym("src/a.ts", "inner", 2, 2),
]);

const scripts: V8ScriptCoverage[] = [
{
scriptId: "1",
url,
functions: [
{
functionName: "inner",
isBlockCoverage: true,
ranges: [
{ startOffset: innerStart, endOffset: innerEnd, count: 0 },
],
},
{
functionName: "outer",
isBlockCoverage: true,
ranges: [
{ startOffset: 0, endOffset: source.length, count: 2 },
],
},
],
},
];

const result = ingestV8({
db,
projectRoot: root,
scripts,
sourcePath: join(root, ".cov"),
});
expect(result.ingested.symbols).toBe(2);

const rows = db
.query("SELECT name, hit_statements FROM coverage ORDER BY name")
.all() as Array<{ name: string; hit_statements: number }>;
expect(rows.find((r) => r.name === "inner")!.hit_statements).toBe(0);
} finally {
closeDb(db);
}
});

it("skips scripts whose url isn't a file:// URL (Node internals, eval)", () => {
const db = setupDb();
try {
Expand Down
26 changes: 12 additions & 14 deletions src/application/coverage-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,20 +439,18 @@ export function ingestV8(opts: V8ParserOpts): IngestResult {
const scriptHits: (number | undefined)[] = new Array(
lineOffsets.length + 1,
);
for (const fn of script.functions ?? []) {
const sorted = (fn.ranges ?? [])
.slice()
.sort(
(a, b) =>
b.endOffset - b.startOffset - (a.endOffset - a.startOffset),
);
for (const range of sorted) {
const startLine = offsetToLine(lineOffsets, range.startOffset);
const endLine = offsetToLine(lineOffsets, range.endOffset);
for (let line = startLine; line <= endLine; line++) {
// Innermost-wins within this script: last write is the smallest range.
scriptHits[line] = range.count;
}
// V8 may emit nested functions as separate entries; innermost-wins must be
// global across all ranges, not per FunctionCoverage iteration order.
const ranges = (script.functions ?? []).flatMap((fn) => fn.ranges ?? []);
ranges.sort(
(a, b) => b.endOffset - b.startOffset - (a.endOffset - a.startOffset),
);
for (const range of ranges) {
const startLine = offsetToLine(lineOffsets, range.startOffset);
const endLine = offsetToLine(lineOffsets, range.endOffset);
for (let line = startLine; line <= endLine; line++) {
// Innermost-wins: last write is the smallest range.
scriptHits[line] = range.count;
}
}
for (let line = 1; line < scriptHits.length; line++) {
Expand Down
Loading