From 00b6553b9cc631699d377c08a969125d19f75778 Mon Sep 17 00:00:00 2001 From: Travis Zhang Date: Mon, 31 Aug 2026 13:48:17 +0800 Subject: [PATCH 1/2] fix(cobertura): deduplicate method names within a class --- .../src/reports/cobertura/index.ts | 20 +++- .../test/reports/cobertura/index.test.ts | 91 ++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/istanbul-lib-report/src/reports/cobertura/index.ts b/packages/istanbul-lib-report/src/reports/cobertura/index.ts index ac569d7..4e8aaeb 100644 --- a/packages/istanbul-lib-report/src/reports/cobertura/index.ts +++ b/packages/istanbul-lib-report/src/reports/cobertura/index.ts @@ -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 tags with the same name in one class. + name: escape(uniqueMethodName(name)), hits, signature: "()V", //fake out a no-args void return }); @@ -167,4 +169,20 @@ function asClassName(node: ReportNode): string { return node.getRelativeName().replace(/.*[\\/]/, ""); } +/** Returns a namer that deduplicates method names within one class. */ +function createUniqueNamer() { + const used = new Set(); + return (name: string | undefined): string => { + const base = name || "(anonymous)"; + let next = base; + let i = 2; + while (used.has(next)) { + next = `${base}_${i}`; + i += 1; + } + used.add(next); + return next; + }; +} + export default CoberturaReport; diff --git a/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts b/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts index 7077033..91db839 100644 --- a/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts +++ b/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts @@ -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"; @@ -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 = / Date: Mon, 31 Aug 2026 17:32:16 +0800 Subject: [PATCH 2/2] fix(cobertura): use (anonymous_N+1) for duplicate anonymous names --- .../src/reports/cobertura/index.ts | 23 +++++++++++++++++-- .../test/reports/cobertura/index.test.ts | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/istanbul-lib-report/src/reports/cobertura/index.ts b/packages/istanbul-lib-report/src/reports/cobertura/index.ts index 4e8aaeb..1ff490b 100644 --- a/packages/istanbul-lib-report/src/reports/cobertura/index.ts +++ b/packages/istanbul-lib-report/src/reports/cobertura/index.ts @@ -169,16 +169,35 @@ 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(); return (name: string | undefined): string => { const base = name || "(anonymous)"; - let next = base; + 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)) { - next = `${base}_${i}`; i += 1; + next = `${base}_${i}`; } used.add(next); return next; diff --git a/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts b/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts index 91db839..5b25328 100644 --- a/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts +++ b/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts @@ -144,6 +144,6 @@ describe("CoberturaReport", () => { while ((match = re.exec(output))) { names.push(match[1]); } - expect(names).toEqual(["constructor", "constructor_2", "(anonymous_0)", "(anonymous_0)_2"]); + expect(names).toEqual(["constructor", "constructor_2", "(anonymous_0)", "(anonymous_1)"]); }); });