Skip to content

Commit d28c294

Browse files
authored
feat(coverage): Demangling C++ & Rust function names in coverage results (#398)
feat(coverage): Demangling C++ & Rust function names in coverage results Demangle C++ and Rust symbols using `c++filt` and `rustfilt`. We rely on those tools to be installed on the $PATH. I was also considering whether I should compile `c++filt` and / or `rustfilt` to WebAssembly, but decided against it, given that this would make the build much more complicated and it is currently unclear to me whether we will stick with the "traditional" tool chains (npm, webpack, cargo, ...) or if we will pivot to using Bazel.
1 parent 7084368 commit d28c294

File tree

6 files changed

+60
-25
lines changed

6 files changed

+60
-25
lines changed

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ jobs:
4040
- name: Lint
4141
run: npm run check-lint
4242

43+
# Required for the test cases
44+
- name: Install system dependencies
45+
run: sudo apt install -y binutils rustfilt
46+
4347
- run: xvfb-run -a npm test
4448
if: runner.os == 'Linux'
4549

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ In case you are using the code coverage integration with any other language
137137
bazelbuild/vscode-bazel#367. Please share both positive and negative experiences
138138
you might have.
139139

140+
For C++ and Rust, make sure to have `c++filt` / `rustfilt` installed and
141+
available through the `$PATH`. Otherwise, only mangled, hard-to-decipher
142+
function names will be displayed. For Java, no additional steps are required.
143+
140144
## Contributing
141145

142146
If you would like to contribute to the Bazel Visual Studio extension, please

src/bazel/tasks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ async function onTaskProcessEnd(event: vscode.TaskProcessEndEvent) {
184184
);
185185
} else {
186186
// Show the coverage date
187-
showLcovCoverage(
187+
await showLcovCoverage(
188188
description,
189189
workspaceInfo.bazelWorkspacePath,
190190
covFileStr,

src/test-explorer/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function activateTesting(): vscode.Disposable[] {
3131
/**
3232
* Display coverage information from a `.lcov` file.
3333
*/
34-
export function showLcovCoverage(
34+
export async function showLcovCoverage(
3535
description: string,
3636
baseFolder: string,
3737
lcov: string,
@@ -42,7 +42,7 @@ export function showLcovCoverage(
4242
false,
4343
);
4444
run.appendOutput(description.replaceAll("\n", "\r\n"));
45-
for (const c of parseLcov(baseFolder, lcov)) {
45+
for (const c of await parseLcov(baseFolder, lcov)) {
4646
run.addCoverage(c);
4747
}
4848
run.end();

src/test-explorer/lcov_parser.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import * as vscode from "vscode";
22
import * as path from "path";
3+
import * as child_process from "child_process";
4+
import * as which from "which";
5+
import * as util from "util";
36
import { assert } from "../assert";
47

8+
const execFile = util.promisify(child_process.execFile);
9+
510
/**
611
* Demangle JVM method names.
712
*
@@ -109,6 +114,20 @@ function demangleJVMMethodName(mangled: string): string | undefined {
109114
return `${returnType} ${shortClassName}::${functionName}(${argListStr})`;
110115
}
111116

117+
/**
118+
* Demangle a name by calling a filter binary (like c++filt or rustfilt)
119+
*/
120+
async function demangleNameUsingFilter(
121+
execPath: string | null,
122+
mangled: string,
123+
): Promise<string | undefined> {
124+
if (execPath === null) return undefined;
125+
const unmangled = (await execFile(execPath, [mangled])).stdout.trim();
126+
// If unmangling failed, return undefined, so we can fallback to another demangler.
127+
if (!unmangled || unmangled === mangled) return undefined;
128+
return unmangled;
129+
}
130+
112131
/**
113132
* Coverage data from a Bazel run.
114133
*
@@ -136,10 +155,12 @@ export class BazelFileCoverage extends vscode.FileCoverage {
136155
/**
137156
* Parses the LCOV coverage info into VS Code's representation
138157
*/
139-
export function parseLcov(
158+
export async function parseLcov(
140159
baseFolder: string,
141160
lcov: string,
142-
): BazelFileCoverage[] {
161+
): Promise<BazelFileCoverage[]> {
162+
const cxxFiltPath = await which("c++filt", { nothrow: true });
163+
const rustFiltPath = await which("rustfilt", { nothrow: true });
143164
lcov = lcov.replaceAll("\r\n", "\n");
144165

145166
// Documentation of the lcov format:
@@ -218,17 +239,19 @@ export function parseLcov(
218239
location = new vscode.Position(startLine, 0);
219240
}
220241
if (!info.functionsByLine.has(startLine)) {
221-
// TODO: Also add demangling for C++ and Rust.
222-
// https://internals.rust-lang.org/t/symbol-mangling-of-rust-vs-c/7222
223-
// https://github.com/rust-lang/rustc-demangle
224-
//
225-
// Tested with:
226-
// * Go -> no function names, only line coverage
227-
// * C++ -> mangled names
228-
// * Java -> mangled names
229-
// * Rust -> mangled names
230-
// Not tested with Python, Swift, Kotlin etc.
231-
const demangled = demangleJVMMethodName(funcName) ?? funcName;
242+
// Demangle the name.
243+
// We must first try rustfilt before trying c++filt.
244+
// The Rust name mangling scheme is intentionally compatible with
245+
// C++ mangling. Hence, c++filt will be succesful on Rust's mangled
246+
// names. But rustfilt provides more readable demanglings, and hence
247+
// we prefer rustfilt over c++filt. For C++ mangled names, rustfilt
248+
// will fail and we will fallback to c++filt.
249+
// See https://internals.rust-lang.org/t/symbol-mangling-of-rust-vs-c/7222
250+
const demangled =
251+
demangleJVMMethodName(funcName) ??
252+
(await demangleNameUsingFilter(rustFiltPath, funcName)) ??
253+
(await demangleNameUsingFilter(cxxFiltPath, funcName)) ??
254+
funcName;
232255
info.functionsByLine.set(
233256
startLine,
234257
new vscode.DeclarationCoverage(demangled, 0, location),

test/lcov_parser.test.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { DeclarationCoverage, StatementCoverage } from "vscode";
66

77
const testDir = path.join(__dirname, "../..", "test");
88

9-
function parseTestLcov(lcov: string): BazelFileCoverage[] {
9+
function parseTestLcov(lcov: string): Promise<BazelFileCoverage[]> {
1010
return parseLcov("/base", lcov);
1111
}
1212

@@ -50,19 +50,23 @@ function getLineCoverageForLine(
5050
}
5151

5252
describe("The lcov parser", () => {
53-
it("accepts an empty string", () => {
54-
assert.deepEqual(parseTestLcov(""), []);
53+
it("accepts an empty string", async () => {
54+
assert.deepEqual(await parseTestLcov(""), []);
5555
});
5656

57-
it("accepts Linux end-of-lines", () => {
58-
const coveredFiles = parseTestLcov("SF:a.cpp\nFN:1,abc\nend_of_record\n");
57+
it("accepts Linux end-of-lines", async () => {
58+
const coveredFiles = await parseTestLcov(
59+
"SF:a.cpp\nFN:1,abc\nend_of_record\n",
60+
);
5961
assert.equal(coveredFiles.length, 1);
6062
assert.equal(coveredFiles[0].declarationCoverage.total, 1);
6163
});
6264

63-
it("accepts Windows end-of-lines", () => {
65+
it("accepts Windows end-of-lines", async () => {
6466
// \r\n and no final end of line
65-
const coveredFiles = parseTestLcov("SF:a.cpp\r\nFN:1,abc\r\nend_of_record");
67+
const coveredFiles = await parseTestLcov(
68+
"SF:a.cpp\r\nFN:1,abc\r\nend_of_record",
69+
);
6670
assert.equal(coveredFiles.length, 1);
6771
assert.equal(coveredFiles[0].declarationCoverage.total, 1);
6872
});
@@ -142,7 +146,7 @@ describe("The lcov parser", () => {
142146
it("function coverage details", () => {
143147
const initFunc = getFunctionByLine(fileCov, 71);
144148
assert(initFunc !== undefined);
145-
assert.equal(initFunc.name, "_ZN5blaze10RcFileTest5SetUpEv");
149+
assert.equal(initFunc.name, "blaze::RcFileTest::SetUp()");
146150
assert.equal(initFunc.executed, 34);
147151
});
148152
it("line coverage details", () => {
@@ -187,7 +191,7 @@ describe("The lcov parser", () => {
187191
assert(consumeFunc !== undefined);
188192
assert.equal(
189193
consumeFunc.name,
190-
"_RNCNvCscQvVXOS7Ja3_5label20consume_package_name0B3_",
194+
"label::consume_package_name::{closure#0}",
191195
);
192196
assert.equal(consumeFunc.executed, 2);
193197
});

0 commit comments

Comments
 (0)