Skip to content

Commit 7084368

Browse files
feat(coverage): Demangling of Java function names in coverage results (#388)
This commit adds demangling support for Java. I couldn't find any good third-party library for this, so I quickly implemented my own, based on https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2 It might still be have a couple of bugs, but at least for my simple smoke tests, demangling seems to work just fine. Co-authored-by: Cameron Martin <cameronmartin123@gmail.com>
1 parent 2614f19 commit 7084368

File tree

2 files changed

+112
-6
lines changed

2 files changed

+112
-6
lines changed

src/test-explorer/lcov_parser.ts

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,113 @@ import * as vscode from "vscode";
22
import * as path from "path";
33
import { assert } from "../assert";
44

5+
/**
6+
* Demangle JVM method names.
7+
*
8+
* See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
9+
*/
10+
function demangleJVMTypeNames(mangled: string): string[] | undefined {
11+
let arrayCnt = 0;
12+
const types = [] as string[];
13+
const flushType = (type: string) => {
14+
for (let i = 0; i < arrayCnt; ++i) {
15+
type += "[]";
16+
}
17+
types.push(type);
18+
arrayCnt = 0;
19+
};
20+
let idx = 0;
21+
while (idx < mangled.length) {
22+
switch (mangled[idx]) {
23+
case "B":
24+
flushType("bool");
25+
break;
26+
case "C":
27+
flushType("char");
28+
break;
29+
case "D":
30+
flushType("double");
31+
break;
32+
case "F":
33+
flushType("float");
34+
break;
35+
case "I":
36+
flushType("int");
37+
break;
38+
case "J":
39+
flushType("long");
40+
break;
41+
case "S":
42+
flushType("short");
43+
break;
44+
case "Z":
45+
flushType("boolean");
46+
break;
47+
case "V":
48+
flushType("void");
49+
break;
50+
case "[":
51+
++arrayCnt;
52+
break;
53+
case "L": {
54+
const startIdx = idx + 1;
55+
while (idx < mangled.length && mangled[idx] !== ";") ++idx;
56+
if (idx === mangled.length) return undefined;
57+
const fullClassName = mangled.substring(startIdx, idx - startIdx + 1);
58+
const shortClassName = fullClassName.split("/").pop();
59+
flushType(shortClassName);
60+
break;
61+
}
62+
default:
63+
return undefined;
64+
}
65+
++idx;
66+
}
67+
if (arrayCnt) return undefined;
68+
return types;
69+
}
70+
71+
function demangleJVMTypeName(mangled: string): string | undefined {
72+
const demangled = demangleJVMTypeNames(mangled) ?? [];
73+
if (demangled.length !== 1) return undefined;
74+
return demangled[0];
75+
}
76+
77+
/**
78+
* Demangle JVM method names.
79+
*
80+
* See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3
81+
*
82+
* Examples:
83+
* ```
84+
* com/example/myproject/Greeter::<clinit> ()V
85+
* com/example/myproject/Greeter::<init> ()V
86+
* com/example/myproject/Greeter::convertStreamToString (Ljava/io/InputStream;)Ljava/lang/String;
87+
* com/example/myproject/Greeter::hello (Ljava/lang/String;)V
88+
* com/example/myproject/Greeter::main ([Ljava/lang/String;)V
89+
* ```
90+
*/
91+
function demangleJVMMethodName(mangled: string): string | undefined {
92+
const match = mangled.match(
93+
// eslint-disable-next-line max-len
94+
/^([\p{XIDS}\p{XIDC}/]+)::([\p{XIDS}\p{XIDC}<>]+) \(([\p{XIDS}\p{XIDC};/[]*)\)([\p{XIDS}\p{XIDC};/[]*)$/u,
95+
);
96+
if (!match) return undefined;
97+
const fullClassName = match[1];
98+
const functionName = match[2];
99+
const mangledArgList = match[3];
100+
const mangledReturnType = match[4];
101+
102+
const shortClassName = fullClassName.split("/").pop();
103+
const argList = demangleJVMTypeNames(mangledArgList);
104+
if (!argList) return undefined;
105+
const argListStr = argList.join(", ");
106+
const returnType = demangleJVMTypeName(mangledReturnType);
107+
if (!returnType) return undefined;
108+
109+
return `${returnType} ${shortClassName}::${functionName}(${argListStr})`;
110+
}
111+
5112
/**
6113
* Coverage data from a Bazel run.
7114
*
@@ -111,20 +218,20 @@ export function parseLcov(
111218
location = new vscode.Position(startLine, 0);
112219
}
113220
if (!info.functionsByLine.has(startLine)) {
114-
// TODO: For Java, C++ and Rust the function names should be demangled.
221+
// TODO: Also add demangling for C++ and Rust.
115222
// https://internals.rust-lang.org/t/symbol-mangling-of-rust-vs-c/7222
116223
// https://github.com/rust-lang/rustc-demangle
117-
// https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3
118224
//
119225
// Tested with:
120226
// * Go -> no function names, only line coverage
121227
// * C++ -> mangled names
122228
// * Java -> mangled names
123229
// * Rust -> mangled names
124230
// Not tested with Python, Swift, Kotlin etc.
231+
const demangled = demangleJVMMethodName(funcName) ?? funcName;
125232
info.functionsByLine.set(
126233
startLine,
127-
new vscode.DeclarationCoverage(funcName, 0, location),
234+
new vscode.DeclarationCoverage(demangled, 0, location),
128235
);
129236
}
130237
functionsByName.set(funcName, info.functionsByLine.get(startLine));

test/lcov_parser.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,12 @@ describe("The lcov parser", () => {
9696
it("function coverage details", () => {
9797
const initFunc = getFunctionByLine(fileCov, 24);
9898
assert(initFunc !== undefined);
99-
assert.equal(initFunc.name, "com/example/myproject/Greeter::<init> ()V");
99+
assert.equal(initFunc.name, "void Greeter::<init>()");
100100
assert.equal(initFunc.executed, 1);
101101
const convertFunc = getFunctionByLine(fileCov, 28);
102102
assert.equal(
103103
convertFunc.name,
104-
"com/example/myproject/Greeter::convertStreamToString " +
105-
"(Ljava/io/InputStream;)Ljava/lang/String;",
104+
"String Greeter::convertStreamToString(InputStream)",
106105
);
107106
assert.equal(convertFunc.executed, 0);
108107
});

0 commit comments

Comments
 (0)