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
39 changes: 38 additions & 1 deletion packages/istanbul-lib-report/src/reports/cobertura/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ class CoberturaReport extends ReportBase {

this.xml!.openTag("methods");
const fnMap = fileCoverage.fnMap;
const uniqueMethodName = createUniqueNamer();
Object.entries(fnMap).forEach(([k, { name, decl, loc }]) => {
const hits = fileCoverage.f[k];
// Some versions of the instrumenter in the wild populate 'loc'
// but not 'decl':
const start = (decl || loc).start;
this.xml!.openTag("method", {
name: escape(name),
// Jenkins rejects two <method> tags with the same name in one class.
name: escape(uniqueMethodName(name)),
hits,
signature: "()V", //fake out a no-args void return
});
Expand Down Expand Up @@ -167,4 +169,39 @@ function asClassName(node: ReportNode): string {
return node.getRelativeName().replace(/.*[\\/]/, "");
}

const ANONYMOUS_NAME = /^\(anonymous_(\d+)\)$/;

/** Returns a namer that deduplicates method names within one class. */
function createUniqueNamer() {
const used = new Set<string>();
return (name: string | undefined): string => {
const base = name || "(anonymous)";
if (!used.has(base)) {
used.add(base);
return base;
}

const anonMatch = ANONYMOUS_NAME.exec(base);
if (anonMatch) {
let i = Number(anonMatch[1]) + 1;
let next = `(anonymous_${i})`;
while (used.has(next)) {
i += 1;
next = `(anonymous_${i})`;
}
used.add(next);
return next;
}

let i = 2;
let next = `${base}_${i}`;
while (used.has(next)) {
i += 1;
next = `${base}_${i}`;
}
used.add(next);
return next;
};
}

export default CoberturaReport;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import { createRequire } from "node:module";
import path from "node:path";

import * as istanbulLibCoverage from "@vitest/istanbul-lib-coverage";
import { afterAll as after, assert, beforeAll as before, beforeEach, describe, it } from "vitest";
import {
afterAll as after,
assert,
beforeAll as before,
beforeEach,
describe,
expect,
it,
} from "vitest";

import * as istanbulLibReport from "../../../src/index";
import { FileWriter } from "../../../src/index";
Expand Down Expand Up @@ -57,4 +65,85 @@ describe("CoberturaReport", () => {
createTest(file);
}
});

it("emits unique method names when fnMap has collisions", () => {
const context = istanbulLibReport.createContext({
dir: "./",
coverageMap: istanbulLibCoverage.createCoverageMap({
"/tmp/models.js": {
path: "/tmp/models.js",
statementMap: {},
fnMap: {
0: {
name: "constructor",
decl: {
start: { line: 2, column: 4 },
end: { line: 2, column: 20 },
},
loc: {
start: { line: 2, column: 4 },
end: { line: 4, column: 5 },
},
line: 2,
},
1: {
name: "constructor",
decl: {
start: { line: 8, column: 4 },
end: { line: 8, column: 20 },
},
loc: {
start: { line: 8, column: 4 },
end: { line: 10, column: 5 },
},
line: 8,
},
2: {
name: "(anonymous_0)",
decl: {
start: { line: 12, column: 0 },
end: { line: 12, column: 10 },
},
loc: {
start: { line: 12, column: 0 },
end: { line: 14, column: 1 },
},
line: 12,
},
3: {
name: "(anonymous_0)",
decl: {
start: { line: 16, column: 0 },
end: { line: 16, column: 10 },
},
loc: {
start: { line: 16, column: 0 },
end: { line: 18, column: 1 },
},
line: 16,
},
},
branchMap: {},
s: {},
f: { 0: 1, 1: 1, 2: 1, 3: 1 },
b: {},
},
}),
});
const tree = context.getTree("pkg");
const report = new CoberturaReport({
file: "-",
timestamp: "123456789",
projectRoot: "/tmp",
});
tree.visit(report, context);
const output = FileWriter.getOutput();
const names: string[] = [];
const re = /<method name="([^"]+)"/g;
let match: RegExpExecArray | null;
while ((match = re.exec(output))) {
names.push(match[1]);
}
expect(names).toEqual(["constructor", "constructor_2", "(anonymous_0)", "(anonymous_1)"]);
});
});
Loading