Skip to content
Open
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
4 changes: 3 additions & 1 deletion @types/lcov-parse/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ declare namespace parse {
block: number,
branch: number,
line: number,
taken: number
taken: number,
condition_coverage?: number, // percentage of condition branches taken (0-100)
missing_branches?: number[] // line numbers of untaken branches
}

interface FunctionDetail {
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ https://github.com/ryanluker/vscode-coverage-gutters/issues
![Coverage Gutters features watch](promo_images/coverage-gutters-features-1.gif)

- Supports any language as long as you can generate a lcov style coverage file
- **[NEW] Support for C/C++/Rust coverage formats**: Cobertura XML (gcovr) and LLVM-cov JSON
- **[NEW] Branch coverage details**: CodeLens display and hover tooltips showing branch execution + missing branches
- **[NEW] LLVM region counts**: Hover shows per-line region execution counts from LLVM JSON exports
- Extensive logging and insight into operations via the output logs
- Multi coverage file support for both xml and lcov
- Coverage caching layer makes for speedy rendering even in large files
Expand Down Expand Up @@ -48,6 +51,9 @@ See [examples directory](example) on how to setup a project.
- [Nodejs](example/node)
- [Ruby](example/ruby)
- [DotNet](example/dotnet)
- [C](example/c) **(NEW)** - Cobertura XML via gcovr
- [C++](example/cpp) **(NEW)** - Cobertura XML (gcovr) & LLVM-cov JSON
- [Rust](example/rust) **(NEW)** - LLVM-cov JSON via cargo-tarpaulin or cargo-llvm-cov

## Tips and Tricks
**Using Breakpoints**: Currently to both use the extension and code debugging breakpoints, you need to disable the gutter coverage and enable the line coverage via the settings ( `coverage-gutters.showGutterCoverage` and `coverage-gutters.showLineCoverage` respectively).
Expand Down
56 changes: 56 additions & 0 deletions esbuild.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import esbuild from 'esbuild';

const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');

async function main() {
const ctx = await esbuild.context({
entryPoints: ['src/extension.ts'],
bundle: true,
format: 'cjs',
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: 'node',
outfile: 'dist/extension.js',
external: ['vscode'],
logLevel: 'silent',
plugins: [
/* add to the end of plugins array */
esbuildProblemMatcherPlugin
]
});
if (watch) {
await ctx.watch();
} else {
await ctx.rebuild();
await ctx.dispose();
}
}

/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: 'esbuild-problem-matcher',

setup(build) {
build.onStart(() => {
console.log('[watch] build started');
});
build.onEnd(result => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`);
console.error(` ${location.file}:${location.line}:${location.column}:`);
});
console.log('[watch] build finished');
});
}
};

main().catch(e => {
console.error(e);
process.exit(1);
});

export {};
68 changes: 68 additions & 0 deletions example/c/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# C Coverage Example (single-file)

This example demonstrates generating Cobertura XML coverage (`coverage.xml`) for a C program using GCC and `gcovr`. The code in `src/main.c` intentionally contains multi-conditional `if` logic to demonstrate partial and full coverage.

## Toolchain

Install GCC and gcovr on your machine:

- Debian/Ubuntu
```bash
sudo apt-get update
sudo apt-get install -y gcc gcovr
```

- macOS (Homebrew)
```bash
brew install gcc gcovr
```

Verify:
```bash
gcovr --version
gcc --version
```

## Generate Coverage (Cobertura XML)

From this folder (example/c):
```bash
# 1) Clean artifacts
rm -rf build && mkdir -p build

# 2) Build with GCC and gcov instrumentation
gcc -O0 -g -std=c11 -fprofile-arcs -ftest-coverage -c src/main.c -o build/main.o
gcc -fprofile-arcs -ftest-coverage -o build/app build/main.o

# 3) Run program to produce .gcda
./build/app

# 4) Generate Cobertura XML using gcovr
gcovr \
--root . \
--object-directory build \
--xml -o build/coverage.xml

# (Optional) HTML report
gcovr --root . --object-directory build --html-details -o build/coverage.html
```

The Coverage Gutters extension will automatically discover `coverage.xml` (Cobertura format).

## View in VS Code

```bash
code ../../example.code-workspace
```

- Open `src/main.c`
- Run command: Coverage Gutters: Watch Coverage and Visible Editors
- Decorations appear (green/yellow/red), hovers show details

## Expected Coverage Display

After generating `build/coverage.xml` and opening in VS Code:

- **Green indicators** (✓ Fully covered): fully covered lines/branches
- **Yellow indicators** (⚠ Partial coverage): lines with partial branch coverage
- **Red indicators** (✗ Not covered): lines or branches not executed
3 changes: 3 additions & 0 deletions example/c/coverage.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE coverage SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-04.dtd'>
<coverage line-rate="0.8636363636363636" branch-rate="0.5268817204301075" lines-covered="76" lines-valid="88" branches-covered="98" branches-valid="186" complexity="0.0" timestamp="1766158321" version="gcovr 4.2"><sources><source>.</source></sources><packages><package name="src" line-rate="0.8636363636363636" branch-rate="0.5268817204301075" complexity="0.0"><classes><class name="main_c" filename="src/main.c" line-rate="0.8636363636363636" branch-rate="0.5268817204301075" complexity="0.0"><methods/><lines><line number="8" hits="3" branch="false"/><line number="9" hits="3" branch="false"/><line number="11" hits="3" branch="true" condition-coverage="50% (3/6)"><conditions><condition number="0" type="jump" coverage="50%"/></conditions></line><line number="12" hits="0" branch="false"/><line number="15" hits="3" branch="true" condition-coverage="50% (4/8)"><conditions><condition number="0" type="jump" coverage="50%"/></conditions></line><line number="16" hits="2" branch="false"/><line number="17" hits="1" branch="true" condition-coverage="12% (1/8)"><conditions><condition number="0" type="jump" coverage="12%"/></conditions></line><line number="18" hits="0" branch="false"/><line number="20" hits="1" branch="false"/><line number="23" hits="3" branch="true" condition-coverage="50% (4/8)"><conditions><condition number="0" type="jump" coverage="50%"/></conditions></line><line number="24" hits="0" branch="false"/><line number="25" hits="3" branch="true" condition-coverage="75% (3/4)"><conditions><condition number="0" type="jump" coverage="75%"/></conditions></line><line number="26" hits="1" branch="false"/><line number="27" hits="2" branch="true" condition-coverage="75% (3/4)"><conditions><condition number="0" type="jump" coverage="75%"/></conditions></line><line number="28" hits="1" branch="false"/><line number="32" hits="3" branch="true" condition-coverage="50% (5/10)"><conditions><condition number="0" type="jump" coverage="50%"/></conditions></line><line number="33" hits="1" branch="false"/><line number="36" hits="3" branch="false"/><line number="40" hits="3" branch="false"/><line number="42" hits="3" branch="true" condition-coverage="50% (3/6)"><conditions><condition number="0" type="jump" coverage="50%"/></conditions></line><line number="43" hits="0" branch="false"/><line number="46" hits="3" branch="true" condition-coverage="62% (5/8)"><conditions><condition number="0" type="jump" coverage="62%"/></conditions></line><line number="47" hits="0" branch="true" condition-coverage="0% (0/4)"><conditions><condition number="0" type="jump" coverage="0%"/></conditions></line><line number="48" hits="1" branch="false"/><line number="51" hits="2" branch="true" condition-coverage="70% (7/10)"><conditions><condition number="0" type="jump" coverage="70%"/></conditions></line><line number="52" hits="1" branch="true" condition-coverage="50% (2/4)"><conditions><condition number="0" type="jump" coverage="50%"/></conditions></line><line number="53" hits="1" branch="false"/><line number="56" hits="1" branch="true" condition-coverage="33% (2/6)"><conditions><condition number="0" type="jump" coverage="33%"/></conditions></line><line number="57" hits="1" branch="false"/><line number="60" hits="0" branch="false"/><line number="65" hits="3" branch="false"/><line number="66" hits="3" branch="false"/><line number="67" hits="3" branch="false"/><line number="68" hits="3" branch="false"/><line number="69" hits="3" branch="false"/><line number="71" hits="3" branch="true" condition-coverage="50% (1/2)"><conditions><condition number="0" type="jump" coverage="50%"/></conditions></line><line number="72" hits="0" branch="false"/><line number="75" hits="3" branch="true" condition-coverage="66% (4/6)"><conditions><condition number="0" type="jump" coverage="66%"/></conditions></line><line number="76" hits="1" branch="false"/><line number="79" hits="2" branch="true" condition-coverage="25% (2/8)"><conditions><condition number="0" type="jump" coverage="25%"/></conditions></line><line number="80" hits="0" branch="false"/><line number="83" hits="2" branch="true" condition-coverage="16% (1/6)"><conditions><condition number="0" type="jump" coverage="16%"/></conditions></line><line number="84" hits="0" branch="false"/><line number="87" hits="2" branch="false"/><line number="91" hits="3" branch="false"/><line number="92" hits="3" branch="true" condition-coverage="50% (2/4)"><conditions><condition number="0" type="jump" coverage="50%"/></conditions></line><line number="94" hits="3" branch="false"/><line number="95" hits="23" branch="true" condition-coverage="100% (2/2)"><conditions><condition number="0" type="jump" coverage="100%"/></conditions></line><line number="96" hits="20" branch="false"/><line number="97" hits="20" branch="true" condition-coverage="100% (4/4)"><conditions><condition number="0" type="jump" coverage="100%"/></conditions></line><line number="98" hits="18" branch="true" condition-coverage="100% (4/4)"><conditions><condition number="0" type="jump" coverage="100%"/></conditions></line><line number="99" hits="18" branch="true" condition-coverage="100% (10/10)"><conditions><condition number="0" type="jump" coverage="100%"/></conditions></line><line number="100" hits="10" branch="true" condition-coverage="75% (3/4)"><conditions><condition number="0" type="jump" coverage="75%"/></conditions></line><line number="101" hits="1" branch="false"/><line number="104" hits="3" branch="false"/><line number="105" hits="3" branch="true" condition-coverage="75% (6/8)"><conditions><condition number="0" type="jump" coverage="75%"/></conditions></line><line number="106" hits="3" branch="true" condition-coverage="33% (2/6)"><conditions><condition number="0" type="jump" coverage="33%"/></conditions></line><line number="107" hits="3" branch="false"/><line number="111" hits="3" branch="false"/><line number="112" hits="3" branch="false"/><line number="113" hits="3" branch="true" condition-coverage="58% (7/12)"><conditions><condition number="0" type="jump" coverage="58%"/></conditions></line><line number="115" hits="2" branch="false"/><line number="116" hits="1" branch="true" condition-coverage="33% (2/6)"><conditions><condition number="0" type="jump" coverage="33%"/></conditions></line><line number="117" hits="1" branch="false"/><line number="118" hits="0" branch="true" condition-coverage="0% (0/10)"><conditions><condition number="0" type="jump" coverage="0%"/></conditions></line><line number="119" hits="0" branch="false"/><line number="121" hits="0" branch="false"/><line number="124" hits="3" branch="true" condition-coverage="75% (6/8)"><conditions><condition number="0" type="jump" coverage="75%"/></conditions></line><line number="125" hits="1" branch="false"/><line number="127" hits="3" branch="false"/><line number="130" hits="1" branch="false"/><line number="131" hits="1" branch="false"/><line number="134" hits="1" branch="false"/><line number="135" hits="1" branch="false"/><line number="136" hits="1" branch="false"/><line number="138" hits="1" branch="false"/><line number="139" hits="1" branch="false"/><line number="140" hits="1" branch="false"/><line number="142" hits="1" branch="false"/><line number="143" hits="1" branch="false"/><line number="144" hits="1" branch="false"/><line number="146" hits="1" branch="false"/><line number="147" hits="1" branch="false"/><line number="148" hits="1" branch="false"/><line number="150" hits="1" branch="false"/><line number="151" hits="1" branch="false"/><line number="152" hits="1" branch="false"/><line number="154" hits="1" branch="false"/></lines></class></classes></package></packages></coverage>
155 changes: 155 additions & 0 deletions example/c/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// C example with multi-conditional branches in a single file
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

// Compute a score with layered conditions and short-circuit logic
int score_user(int age, int yearsActive, int posts, bool verified) {
int score = 0;

if (age < 0 || yearsActive < 0 || posts < 0) {
return -1; // invalid
}

if ((age >= 18 && verified) || (yearsActive > 5 && posts > 100)) {
score += 25;
} else if ((age >= 16 && yearsActive >= 1) && (verified || posts > 10)) {
score += 10;
} else {
score += 1;
}

if ((posts > 500 && yearsActive > 3) || (verified && posts > 250)) {
score += 50;
} else if (posts > 50 && yearsActive > 1) {
score += 15;
} else if (posts == 0 && !verified) {
score -= 5;
}

// tiering bonus
if ((age > 30 && yearsActive > 10 && verified) || (age > 50 && posts > 50)) {
score += 10;
}

return score;
}

// Categorize risk with nested combinations
const char* risk_category(int creditScore, int latePayments, double debtRatio,
bool isStudent, bool hasJob) {
if (creditScore < 0 || creditScore > 850 || debtRatio < 0.0) {
return "invalid";
}

if ((creditScore >= 750 && latePayments == 0 && debtRatio < 0.3) ||
(creditScore >= 700 && latePayments <= 1 && debtRatio < 0.25)) {
return "low";
}

if ((creditScore >= 650 && latePayments <= 2 && debtRatio < 0.4 && hasJob) ||
(isStudent && creditScore >= 620 && debtRatio < 0.35)) {
return "medium";
}

if ((creditScore < 600 && latePayments > 2) || debtRatio > 0.6) {
return "high";
}

return "unknown";
}

// Complex boolean decision utilizing flags (bitmask)
// flags: bit0=require2FA, bit1=admin, bit2=readOnly, bit3=trial
bool allow_action(int hour24, int failedLogins, int flags, bool emailVerified) {
bool require2FA = (flags & 0x1) != 0;
bool isAdmin = (flags & 0x2) != 0;
bool readOnly = (flags & 0x4) != 0;
bool trial = (flags & 0x8) != 0;

if (readOnly) {
return false;
}

if ((hour24 < 6 || hour24 > 22) && !isAdmin) {
return false;
}

if ((failedLogins >= 3 && !isAdmin) || (!emailVerified && require2FA)) {
return false;
}

if (trial && require2FA && !emailVerified) {
return false;
}

return true;
}

// String utility with mixed conditions
int word_score(const char* s) {
if (s == NULL || s[0] == '\0') return 0;

int vowels = 0, consonants = 0, digits = 0, others = 0;
for (size_t i = 0; s[i] != '\0'; ++i) {
char c = s[i];
if ((c >= '0' && c <= '9')) { digits++; continue; }
char lower = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
if (lower=='a'||lower=='e'||lower=='i'||lower=='o'||lower=='u') vowels++;
else if (lower >= 'a' && lower <= 'z') consonants++;
else others++;
}

int score = vowels*2 + consonants - (digits>0 ? 1:0) - others;
if ((vowels >= 3 && consonants >= 3) || (digits >= 2 && others == 0)) score += 5;
if ((vowels == 0 && consonants > 5) || (others > 3)) score -= 3;
return score;
}

// Multi-conditional numerical routine
int bounded_transform(int x, int y, int z) {
int res = 0;
if ((x > 0 && y > 0 && z > 0) && (x + y > z) && (y + z > x) && (x + z > y)) {
// triangle-ish constraints
res = x*y + z;
} else if ((x <= 0 || y <= 0) && z > 100) {
res = z - (x + y);
} else if ((x == 0 && y == 0 && z == 0) || (x == y && y == z)) {
res = x + y + z;
} else {
res = x - y + z;
}

if ((res % 2 == 0 && (x & 1) == 1) || (res % 3 == 0 && (y & 1) == 0)) {
res += 7;
}
return res;
}

int main(void) {
printf("Running C coverage example (single-file, complex branches)\n");

// Exercise multiple code paths but leave some branches uncovered on purpose
printf("score_user A: %d\n", score_user(19, 0, 5, true));
printf("score_user B: %d\n", score_user(35, 12, 60, true));
printf("score_user C: %d\n", score_user(15, 0, 0, false));

printf("risk low: %s\n", risk_category(760, 0, 0.2, false, true));
printf("risk med: %s\n", risk_category(655, 2, 0.33, true, false));
printf("risk high: %s\n", risk_category(580, 3, 0.7, false, false));

printf("allow_action 1: %d\n", allow_action(14, 0, 0x0, true));
printf("allow_action 2: %d\n", allow_action(23, 4, 0x1, false));
printf("allow_action 3: %d\n", allow_action(7, 2, 0x2, true));

printf("word_score(foo42): %d\n", word_score("foo42"));
printf("word_score(STRong!): %d\n", word_score("STRong!"));
printf("word_score(vowels): %d\n", word_score("aeiouxyz"));

printf("bounded_transform A: %d\n", bounded_transform(3,4,5));
printf("bounded_transform B: %d\n", bounded_transform(-1,0,150));
printf("bounded_transform C: %d\n", bounded_transform(2,2,2));

return 0;
}
66 changes: 66 additions & 0 deletions example/cpp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# C++ Coverage Example (LLVM-cov JSON)

This example demonstrates generating LLVM coverage in JSON format for a C++ program using Clang and LLVM tools. The code in `src/main.cpp` intentionally contains multi-conditional `if` logic to demonstrate partial coverage, branch details, and region-wise execution counts.

## Toolchain

Install Clang and LLVM tools:

**Debian/Ubuntu:**
```bash
sudo apt-get update
sudo apt-get install -y clang llvm
```

**macOS (Homebrew):**
```bash
brew install llvm
```

**Verify installation:**
```bash
clang++ --version
llvm-cov --version
llvm-profdata --version
```

## Generate LLVM Coverage JSON

LLVM's native coverage export format includes **region-wise execution counts** and detailed branch information for fine-grained coverage analysis.

From this folder (example/cpp):

```bash
# 1) Clean artifacts
rm -rf build && mkdir -p build

# 2) Build with Clang profile instrumentation
clang++ -fprofile-instr-generate -fcoverage-mapping -O0 -g -std=c++17 \
-c src/main.cpp -o build/main.o
clang++ -fprofile-instr-generate -o build/app build/main.o

# 3) Run with profiling enabled
LLVM_PROFILE_FILE=build/default.profraw ./build/app

# 4) Merge profiling data
llvm-profdata merge -o build/default.profdata build/default.profraw

# 5) Export coverage as JSON
llvm-cov export -format=json -instr-profile=build/default.profdata \
./build/app > llvm-cov.json
```

The extension auto-discovers `llvm-cov.json` and displays:
- **Line coverage** (green/yellow/red decorations)
- **Branch coverage** with true/false edge counts (CodeLens + hover)
- **Region-wise execution counts** on hover (column and count per region entry)

## View in VS Code

```bash
code ../../example.code-workspace
```

- Open `src/main.cpp`
- Run command: Coverage Gutters: Watch Coverage and Visible Editors
- Decorations appear (green/yellow/red), hovers show details
1 change: 1 addition & 0 deletions example/cpp/llvm-cov.json

Large diffs are not rendered by default.

Loading
Loading