[llvm] [llvm-cov] Add inline exclusion marker support for coverage reporting (PR #203724)

Maksim Levental via llvm-commits llvm-commits at lists.llvm.org
Sat Jun 13 13:14:18 PDT 2026


https://github.com/makslevental updated https://github.com/llvm/llvm-project/pull/203724

>From ebb56102f1a3c253410a988e6d148467817a41e9 Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Sat, 13 Jun 2026 12:03:15 -0700
Subject: [PATCH] [llvm-cov] Add inline exclusion marker support for coverage
 reporting

Add --exclude-line-regex, --exclude-region-start-regex, and
--exclude-region-stop-regex options to llvm-cov. These allow excluding
lines from coverage totals based on inline source comments.

Defaults: LCOV_EXCL_LINE (single line), LCOV_EXCL_START/LCOV_EXCL_STOP
(region). This brings parity with lcov/gcov exclusion markers and
kcov --exclude-line regex support.

The implementation scans source files for markers when loaded, builds a
per-file set of excluded line numbers, and subtracts them from the line
coverage totals in prepareFileReports().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 .../report-custom-region.covmapping           | Bin 0 -> 333 bytes
 .../report-custom-region.cpp                  |  18 ++
 .../report-custom-region.profdata             | Bin 0 -> 840 bytes
 .../report-custom.covmapping                  | Bin 0 -> 333 bytes
 .../exclude-markers-custom/report-custom.cpp  |  16 ++
 .../report-custom.profdata                    | Bin 0 -> 840 bytes
 .../report-region.covmapping                  | Bin 0 -> 333 bytes
 .../exclude-markers-region/report-region.cpp  |  18 ++
 .../report-region.profdata                    | Bin 0 -> 840 bytes
 .../Inputs/exclude-markers/report.covmapping  | Bin 0 -> 325 bytes
 .../Inputs/exclude-markers/report.cpp         |  16 ++
 .../Inputs/exclude-markers/report.profdata    | Bin 0 -> 840 bytes
 llvm/test/tools/llvm-cov/exclude-markers.test | 210 ++++++++++++++++++
 llvm/tools/llvm-cov/CodeCoverage.cpp          |  63 +++++-
 llvm/tools/llvm-cov/CoverageReport.cpp        |  16 ++
 llvm/tools/llvm-cov/CoverageSummaryInfo.h     |   9 +
 llvm/tools/llvm-cov/CoverageViewOptions.h     |  58 +++++
 17 files changed, 423 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.covmapping
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.cpp
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.profdata
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.covmapping
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.cpp
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.profdata
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.covmapping
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.cpp
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.profdata
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.covmapping
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.cpp
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.profdata
 create mode 100644 llvm/test/tools/llvm-cov/exclude-markers.test

diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.covmapping b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.covmapping
new file mode 100644
index 0000000000000000000000000000000000000000..a5d3c31255967d69f2a62bba44768879b47fbdac
GIT binary patch
literal 333
zcmd1FDa%dHFUw_QfB at AWtg^}#x4Qd{{7#?uPwD~EUOHOLm(((sR-R1l+1WX>l3^kn
z=feP~dJts~WwQZkCR3Y=TdW5exf&ceSPtZ~>^=C-H}SM!>Zw&1KIf;&ry72EeOvs9
zpIw4iRq#f^hdLL}6$vNZ`zI^4kwNsWh%tlv?%QwQa9y`n1)4Q+3B+LgYfG2D)0$nu
z2n4LGjEthpOpJ`|4BQH=ObkGZm4Sz$x1phdgO!1okp=3O6sKp58bX>MIiY+Ig)oPK
wk&%sukqIc4;xv1!+_j8&oa(rE!Rmfp?R{>RGW9k{odn1*AV4vXkB5Z`08^e=2mk;8

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.cpp b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.cpp
new file mode 100644
index 0000000000000..ee9b0b21b3ef3
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.cpp
@@ -0,0 +1,18 @@
+void foo(bool cond) {
+  if (cond) {
+  }
+}
+
+void bar() {
+}
+
+// BEGIN_NO_COV
+void func() {
+}
+// END_NO_COV
+
+int main() {
+  foo(false);
+  bar();
+  return 0;
+}
diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.profdata b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom-region/report-custom-region.profdata
new file mode 100644
index 0000000000000000000000000000000000000000..a50aa35e8b159980755080f57ff66a6dcedaf177
GIT binary patch
literal 840
zcmeyLQ&5zjmf6V5fE~PGLKU}QM&&y|`D{>O0VoYq#scLtqR~(;15BNOI!pwWUeE^9
zi7M{E3)6`z{$Mi9MpW?zDToVUronV8EMx~OL>E7x&WTk#VI3D%aRyT!tl|rHLqZna
z{10_}Sk)Uy^J5i{u){8X#}T`@nG<$#dmQ4748N}SJ~vC5dK(sg8en%YFytm?=1D-=
zFkirEs4xQuTt3C=8KZ`f=0`}VGBChQiH|Z)N-QdaX at D63qhaC*gWPxDe*1>&x;4xo
t1E><9L23E<NfVbq_^{-OZV<YGFoR+74wHw`9592R0)A+Gn7ZjW!~v(gDV+cS

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.covmapping b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.covmapping
new file mode 100644
index 0000000000000000000000000000000000000000..067571d04a1f4c795738ab590ffb95a59adae379
GIT binary patch
literal 333
zcmd1FDa%dHFUw_QfB at AWtg^}#x4Qd{{7#?uPwD~EUOHOLm(((sR-R1l+1WX>l3^kn
z=R+T;dJts^WwQZkCOxx?Ta&NppVYnJzt-cNrmyGKv%Wf~{B*Uh>7LWq_3{(d at zOn^
zuXWK^Pgn2c`K#V~8Vn2*lmx4xX1MRZ{q_ylb!$~1f8r8|;6}z{J|0^N>llH6m6efE
zl$nW<k)45Cft85?NU<{TF!VMwG;pvo at G`PM%}H^3#;75r`H>UK2T=%f7#JDZco><0
pVku6ux5{11h{vgplNYS+*VW$VW+_u|gVafY3<Clb^LThzm;l-!PU-*v

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.cpp b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.cpp
new file mode 100644
index 0000000000000..c5b6688a3978e
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.cpp
@@ -0,0 +1,16 @@
+void foo(bool cond) {
+  if (cond) {
+  }
+}
+
+void bar() {
+}
+
+void func() { // MY_SKIP
+} // MY_SKIP
+
+int main() {
+  foo(false);
+  bar();
+  return 0;
+}
diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.profdata b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-custom/report-custom.profdata
new file mode 100644
index 0000000000000000000000000000000000000000..a50aa35e8b159980755080f57ff66a6dcedaf177
GIT binary patch
literal 840
zcmeyLQ&5zjmf6V5fE~PGLKU}QM&&y|`D{>O0VoYq#scLtqR~(;15BNOI!pwWUeE^9
zi7M{E3)6`z{$Mi9MpW?zDToVUronV8EMx~OL>E7x&WTk#VI3D%aRyT!tl|rHLqZna
z{10_}Sk)Uy^J5i{u){8X#}T`@nG<$#dmQ4748N}SJ~vC5dK(sg8en%YFytm?=1D-=
zFkirEs4xQuTt3C=8KZ`f=0`}VGBChQiH|Z)N-QdaX at D63qhaC*gWPxDe*1>&x;4xo
t1E><9L23E<NfVbq_^{-OZV<YGFoR+74wHw`9592R0)A+Gn7ZjW!~v(gDV+cS

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.covmapping b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.covmapping
new file mode 100644
index 0000000000000000000000000000000000000000..53f23eb687ebabdbc1b2ed4580d2b2098145cd6a
GIT binary patch
literal 333
zcmd1FDa%dHFUw_QfB at AWtg^}#x4Qd{{7#?uPwD~EUOHOLm(((sR-R1l+1WX>l3^kn
z=R+T;dJts^WwQZkCOxx?Ta&NppVYnJzt-cNrmyGKv%Wf~{B*Uh>7LWq_3{(d at zOn^
zuXRz^%lG`5^OwE#G#D8A8w69JX1MRZ{q_ylb!$~1f8r8|;O#QK!*5Py?`8x7R#rwv
zQD!DaMs@~n1y&{oAjQhS!_eE%(7?gUz{|)2H7CXC8KZ`f=0{E_A4DO{VPIrr<6&e1
qilsQs-YR!3BOa$ZE?%&@Usrpdo25*>4N at loG7Jb%%;V!>VFCbt;Z>~w

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.cpp b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.cpp
new file mode 100644
index 0000000000000..e43570b76c416
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.cpp
@@ -0,0 +1,18 @@
+void foo(bool cond) {
+  if (cond) {
+  }
+}
+
+void bar() {
+}
+
+// LCOV_EXCL_START
+void func() {
+}
+// LCOV_EXCL_STOP
+
+int main() {
+  foo(false);
+  bar();
+  return 0;
+}
diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.profdata b/llvm/test/tools/llvm-cov/Inputs/exclude-markers-region/report-region.profdata
new file mode 100644
index 0000000000000000000000000000000000000000..a50aa35e8b159980755080f57ff66a6dcedaf177
GIT binary patch
literal 840
zcmeyLQ&5zjmf6V5fE~PGLKU}QM&&y|`D{>O0VoYq#scLtqR~(;15BNOI!pwWUeE^9
zi7M{E3)6`z{$Mi9MpW?zDToVUronV8EMx~OL>E7x&WTk#VI3D%aRyT!tl|rHLqZna
z{10_}Sk)Uy^J5i{u){8X#}T`@nG<$#dmQ4748N}SJ~vC5dK(sg8en%YFytm?=1D-=
zFkirEs4xQuTt3C=8KZ`f=0`}VGBChQiH|Z)N-QdaX at D63qhaC*gWPxDe*1>&x;4xo
t1E><9L23E<NfVbq_^{-OZV<YGFoR+74wHw`9592R0)A+Gn7ZjW!~v(gDV+cS

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.covmapping b/llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.covmapping
new file mode 100644
index 0000000000000000000000000000000000000000..8fda4ada796e230ddbef2e7dd91747eccd3ff04c
GIT binary patch
literal 325
zcmd1FDa%dHFUw_QfB at AGtg^}#x4Qd{{7#?uPwD~EUOHOLm(((sR-R1l+1WX>l3^kn
z=R+5$dJts<WwQZkCUyOaTa&NppVYnJzt-cNrmyGKv%Wf~{B*Uh>7LWq_40eF<)wQ<
zU+bc`o(2Ph>^#0T5Y6tpZ at +!Rb=_JO$ep+ZBKECeM=?uf_-jTWU}a at w6lG>&WMpUH
zR$ygf08*?BJPf at J4GkQu47`ji5G at QTPR|%Mgfu^LLir#HVGaW$BO4DR6HqM0Y4%pR
jYZ>u4)p7EI)&08K``j#L>TQrZ36NnxfMOmG4+|3jnA}fG

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.cpp b/llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.cpp
new file mode 100644
index 0000000000000..3c255cdc932f4
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.cpp
@@ -0,0 +1,16 @@
+void foo(bool cond) {
+  if (cond) {
+  }
+}
+
+void bar() {
+}
+
+void func() { // LCOV_EXCL_LINE
+} // LCOV_EXCL_LINE
+
+int main() {
+  foo(false);
+  bar();
+  return 0;
+}
diff --git a/llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.profdata b/llvm/test/tools/llvm-cov/Inputs/exclude-markers/report.profdata
new file mode 100644
index 0000000000000000000000000000000000000000..a50aa35e8b159980755080f57ff66a6dcedaf177
GIT binary patch
literal 840
zcmeyLQ&5zjmf6V5fE~PGLKU}QM&&y|`D{>O0VoYq#scLtqR~(;15BNOI!pwWUeE^9
zi7M{E3)6`z{$Mi9MpW?zDToVUronV8EMx~OL>E7x&WTk#VI3D%aRyT!tl|rHLqZna
z{10_}Sk)Uy^J5i{u){8X#}T`@nG<$#dmQ4748N}SJ~vC5dK(sg8en%YFytm?=1D-=
zFkirEs4xQuTt3C=8KZ`f=0`}VGBChQiH|Z)N-QdaX at D63qhaC*gWPxDe*1>&x;4xo
t1E><9L23E<NfVbq_^{-OZV<YGFoR+74wHw`9592R0)A+Gn7ZjW!~v(gDV+cS

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/exclude-markers.test b/llvm/test/tools/llvm-cov/exclude-markers.test
new file mode 100644
index 0000000000000..6b389baef7be6
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/exclude-markers.test
@@ -0,0 +1,210 @@
+// Tests for --exclude-line-regex and --exclude-region-start/stop-regex.
+
+// ===== LCOV_EXCL_LINE (per-line exclusion) =====
+
+// Baseline: disable all exclusion markers, verify original totals.
+// RUN: llvm-cov report %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   --exclude-line-regex="" --exclude-region-start-regex="" \
+// RUN:   --exclude-region-stop-regex="" --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=BASELINE %s
+
+// Original: 13 lines total, 3 missed, 76.92% covered.
+// BASELINE: TOTAL
+// BASELINE-SAME: 13
+// BASELINE-SAME: 3
+// BASELINE-SAME: 76.92%
+
+// With default LCOV_EXCL_LINE markers: func() lines excluded (2 lines removed).
+// RUN: llvm-cov report %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=EXCL %s
+
+// 13 - 2 excluded = 11 lines, 1 missed, 90.91%.
+// EXCL: TOTAL
+// EXCL-SAME: 11
+// EXCL-SAME: 1
+// EXCL-SAME: 90.91%
+
+// Custom regex that doesn't match: no exclusion.
+// RUN: llvm-cov report %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   --exclude-line-regex="MY_CUSTOM_EXCL" \
+// RUN:   --exclude-region-start-regex="" --exclude-region-stop-regex="" \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=CUSTOM_NOMATCH %s
+
+// CUSTOM_NOMATCH: TOTAL
+// CUSTOM_NOMATCH-SAME: 13
+// CUSTOM_NOMATCH-SAME: 3
+// CUSTOM_NOMATCH-SAME: 76.92%
+
+// Custom regex that matches the markers:
+// RUN: llvm-cov report %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   --exclude-line-regex="LCOV_EXCL" \
+// RUN:   --exclude-region-start-regex="" --exclude-region-stop-regex="" \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=CUSTOM_MATCH %s
+
+// CUSTOM_MATCH: TOTAL
+// CUSTOM_MATCH-SAME: 11
+// CUSTOM_MATCH-SAME: 1
+// CUSTOM_MATCH-SAME: 90.91%
+
+// JSON export with exclusion:
+// RUN: llvm-cov export --summary-only \
+// RUN:   %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   2>&1 | FileCheck -check-prefix=JSON %s
+
+// JSON: "lines":{"count":11,"covered":10,"percent":90
+
+// ===== LCOV_EXCL_START / LCOV_EXCL_STOP (region exclusion) =====
+
+// Region exclusion with defaults:
+// RUN: llvm-cov report %S/Inputs/exclude-markers-region/report-region.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers-region/report-region.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers-region \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=REGION %s
+
+// func() is inside EXCL_START/STOP region (2 mapped lines excluded).
+// 13 - 2 = 11 lines, 1 missed, 90.91%.
+// REGION: TOTAL
+// REGION-SAME: 11
+// REGION-SAME: 1
+// REGION-SAME: 90.91%
+
+// Region exclusion with explicit custom regex (same patterns):
+// RUN: llvm-cov report %S/Inputs/exclude-markers-region/report-region.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers-region/report-region.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers-region \
+// RUN:   --exclude-line-regex="" \
+// RUN:   --exclude-region-start-regex="LCOV_EXCL_START" \
+// RUN:   --exclude-region-stop-regex="LCOV_EXCL_STOP" \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=REGION_CUSTOM %s
+
+// REGION_CUSTOM: TOTAL
+// REGION_CUSTOM-SAME: 11
+// REGION_CUSTOM-SAME: 1
+// REGION_CUSTOM-SAME: 90.91%
+
+// Disabling all exclusion leaves totals unchanged:
+// RUN: llvm-cov report %S/Inputs/exclude-markers-region/report-region.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers-region/report-region.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers-region \
+// RUN:   --exclude-line-regex="" --exclude-region-start-regex="" \
+// RUN:   --exclude-region-stop-regex="" \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=REGION_DISABLED %s
+
+// REGION_DISABLED: TOTAL
+// REGION_DISABLED-SAME: 13
+// REGION_DISABLED-SAME: 3
+// REGION_DISABLED-SAME: 76.92%
+
+// ===== Overriding default markers with custom regex =====
+
+// Custom per-line marker "MY_SKIP" (default LCOV_EXCL_LINE won't match):
+// RUN: llvm-cov report %S/Inputs/exclude-markers-custom/report-custom.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers-custom/report-custom.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers-custom \
+// RUN:   --exclude-line-regex="MY_SKIP" \
+// RUN:   --exclude-region-start-regex="" --exclude-region-stop-regex="" \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=OVERRIDE_LINE %s
+
+// OVERRIDE_LINE: TOTAL
+// OVERRIDE_LINE-SAME: 11
+// OVERRIDE_LINE-SAME: 1
+// OVERRIDE_LINE-SAME: 90.91%
+
+// Default markers don't match "MY_SKIP":
+// RUN: llvm-cov report %S/Inputs/exclude-markers-custom/report-custom.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers-custom/report-custom.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers-custom \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=OVERRIDE_LINE_DEFAULT %s
+
+// OVERRIDE_LINE_DEFAULT: TOTAL
+// OVERRIDE_LINE_DEFAULT-SAME: 13
+// OVERRIDE_LINE_DEFAULT-SAME: 3
+// OVERRIDE_LINE_DEFAULT-SAME: 76.92%
+
+// Custom region markers "BEGIN_NO_COV" / "END_NO_COV":
+// RUN: llvm-cov report %S/Inputs/exclude-markers-custom-region/report-custom-region.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers-custom-region/report-custom-region.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers-custom-region \
+// RUN:   --exclude-line-regex="" \
+// RUN:   --exclude-region-start-regex="BEGIN_NO_COV" \
+// RUN:   --exclude-region-stop-regex="END_NO_COV" \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=OVERRIDE_REGION %s
+
+// OVERRIDE_REGION: TOTAL
+// OVERRIDE_REGION-SAME: 11
+// OVERRIDE_REGION-SAME: 1
+// OVERRIDE_REGION-SAME: 90.91%
+
+// Default region markers don't match "BEGIN_NO_COV" / "END_NO_COV":
+// RUN: llvm-cov report %S/Inputs/exclude-markers-custom-region/report-custom-region.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers-custom-region/report-custom-region.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers-custom-region \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=OVERRIDE_REGION_DEFAULT %s
+
+// OVERRIDE_REGION_DEFAULT: TOTAL
+// OVERRIDE_REGION_DEFAULT-SAME: 13
+// OVERRIDE_REGION_DEFAULT-SAME: 3
+// OVERRIDE_REGION_DEFAULT-SAME: 76.92%
+
+// ===== "show" command with exclusions disabled =====
+// Exercises the scanForExclusionMarkers early-return path when both regexes
+// are empty (show always loads source files regardless of exclusion settings).
+// RUN: llvm-cov show %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   --exclude-line-regex="" --exclude-region-start-regex="" \
+// RUN:   --exclude-region-stop-regex="" \
+// RUN:   2>&1 | FileCheck -check-prefix=SHOW_DISABLED %s
+
+// Source is displayed normally; LCOV_EXCL_LINE markers have no effect.
+// SHOW_DISABLED: func()
+// SHOW_DISABLED: LCOV_EXCL_LINE
+
+// ===== Invalid regex error handling =====
+
+// Invalid --exclude-line-regex:
+// RUN: not llvm-cov report %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   --exclude-line-regex="[unclosed" \
+// RUN:   2>&1 | FileCheck -check-prefix=BAD_LINE_RE %s
+
+// BAD_LINE_RE: error: invalid --exclude-line-regex:
+
+// Invalid --exclude-region-start-regex:
+// RUN: not llvm-cov report %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   --exclude-region-start-regex="(unmatched" \
+// RUN:   2>&1 | FileCheck -check-prefix=BAD_START_RE %s
+
+// BAD_START_RE: error: invalid --exclude-region-start-regex:
+
+// Invalid --exclude-region-stop-regex:
+// RUN: not llvm-cov report %S/Inputs/exclude-markers/report.covmapping \
+// RUN:   -instr-profile %S/Inputs/exclude-markers/report.profdata \
+// RUN:   -path-equivalence=/tmp/exclude-markers-source,%S/Inputs/exclude-markers \
+// RUN:   --exclude-region-stop-regex="*invalid" \
+// RUN:   2>&1 | FileCheck -check-prefix=BAD_STOP_RE %s
+
+// BAD_STOP_RE: error: invalid --exclude-region-stop-regex:
diff --git a/llvm/tools/llvm-cov/CodeCoverage.cpp b/llvm/tools/llvm-cov/CodeCoverage.cpp
index e19101ac76045..14977dc41c8bd 100644
--- a/llvm/tools/llvm-cov/CodeCoverage.cpp
+++ b/llvm/tools/llvm-cov/CodeCoverage.cpp
@@ -119,6 +119,9 @@ class CodeCoverageTool {
   /// Load the coverage mapping data. Return nullptr if an error occurred.
   std::unique_ptr<CoverageMapping> load();
 
+  /// Scan all source files for inline exclusion markers.
+  void scanSourcesForExclusionMarkers(const CoverageMapping &Coverage);
+
   /// Create a mapping from files in the Coverage data to local copies
   /// (path-equivalence).
   void remapPathNames(const CoverageMapping &Coverage);
@@ -284,6 +287,7 @@ ErrorOr<const MemoryBuffer &>
 CodeCoverageTool::getSourceFile(StringRef SourceFile) {
   // If we've remapped filenames, look up the real location for this file.
   std::unique_lock<std::mutex> Guard{LoadedSourceFilesLock};
+  StringRef OriginalFile = SourceFile;
   if (!RemappedFilenames.empty()) {
     auto Loc = RemappedFilenames.find(SourceFile);
     if (Loc != RemappedFilenames.end())
@@ -299,7 +303,17 @@ CodeCoverageTool::getSourceFile(StringRef SourceFile) {
   }
   LoadedSourceFiles.emplace_back(std::string(SourceFile),
                                  std::move(Buffer.get()));
-  return *LoadedSourceFiles.back().second;
+  auto &LoadedBuf = *LoadedSourceFiles.back().second;
+  ViewOpts.scanForExclusionMarkers(OriginalFile, LoadedBuf);
+  return LoadedBuf;
+}
+
+void CodeCoverageTool::scanSourcesForExclusionMarkers(
+    const CoverageMapping &Coverage) {
+  if (!ViewOpts.ExcludeLineRE && !ViewOpts.ExcludeRegionStartRE)
+    return;
+  for (StringRef SF : Coverage.getUniqueSourceFiles())
+    getSourceFile(SF);
 }
 
 void CodeCoverageTool::attachExpansionSubViews(
@@ -740,6 +754,22 @@ int CodeCoverageTool::run(Command Cmd, int argc, const char **argv) {
                "given regular expression"),
       cl::cat(FilteringCategory));
 
+  cl::opt<std::string> ExcludeLineRegex(
+      "exclude-line-regex", cl::Optional,
+      cl::desc("Exclude lines matching this regex from coverage totals"),
+      cl::init("LCOV_EXCL_LINE"), cl::cat(FilteringCategory));
+
+  cl::opt<std::string> ExcludeRegionStartRegex(
+      "exclude-region-start-regex", cl::Optional,
+      cl::desc("Start of an excluded region (lines until stop marker are "
+               "excluded from coverage totals)"),
+      cl::init("LCOV_EXCL_START"), cl::cat(FilteringCategory));
+
+  cl::opt<std::string> ExcludeRegionStopRegex(
+      "exclude-region-stop-regex", cl::Optional,
+      cl::desc("End of an excluded region"), cl::init("LCOV_EXCL_STOP"),
+      cl::cat(FilteringCategory));
+
   cl::opt<double> RegionCoverageLtFilter(
       "region-coverage-lt", cl::Optional,
       cl::desc("Show code coverage only for functions with region coverage "
@@ -947,6 +977,33 @@ int CodeCoverageTool::run(Command Cmd, int argc, const char **argv) {
       FilenameFilters.push_back(std::make_unique<NameRegexCoverageFilter>(
           RE, NameRegexCoverageFilter::FilterType::Include));
 
+    if (!ExcludeLineRegex.empty()) {
+      ViewOpts.ExcludeLineRE = std::make_shared<Regex>(ExcludeLineRegex);
+      std::string RegexError;
+      if (!ViewOpts.ExcludeLineRE->isValid(RegexError)) {
+        error("invalid --exclude-line-regex: " + RegexError);
+        return 1;
+      }
+    }
+    if (!ExcludeRegionStartRegex.empty()) {
+      ViewOpts.ExcludeRegionStartRE =
+          std::make_shared<Regex>(ExcludeRegionStartRegex);
+      std::string RegexError;
+      if (!ViewOpts.ExcludeRegionStartRE->isValid(RegexError)) {
+        error("invalid --exclude-region-start-regex: " + RegexError);
+        return 1;
+      }
+    }
+    if (!ExcludeRegionStopRegex.empty()) {
+      ViewOpts.ExcludeRegionStopRE =
+          std::make_shared<Regex>(ExcludeRegionStopRegex);
+      std::string RegexError;
+      if (!ViewOpts.ExcludeRegionStopRE->isValid(RegexError)) {
+        error("invalid --exclude-region-stop-regex: " + RegexError);
+        return 1;
+      }
+    }
+
     if (!Arches.empty()) {
       for (const std::string &Arch : Arches) {
         if (Triple(Arch).getArch() == llvm::Triple::ArchType::UnknownArch) {
@@ -1288,6 +1345,8 @@ int CodeCoverageTool::doReport(int argc, const char **argv,
   if (!Coverage)
     return 1;
 
+  scanSourcesForExclusionMarkers(*Coverage);
+
   CoverageReport Report(ViewOpts, *Coverage);
   if (!ShowFunctionSummaries) {
     if (SourceFiles.empty())
@@ -1364,6 +1423,8 @@ int CodeCoverageTool::doExport(int argc, const char **argv,
     return 1;
   }
 
+  scanSourcesForExclusionMarkers(*Coverage);
+
   std::unique_ptr<CoverageExporter> Exporter;
 
   switch (ViewOpts.Format) {
diff --git a/llvm/tools/llvm-cov/CoverageReport.cpp b/llvm/tools/llvm-cov/CoverageReport.cpp
index 9b754d613370c..b357af8cfdabb 100644
--- a/llvm/tools/llvm-cov/CoverageReport.cpp
+++ b/llvm/tools/llvm-cov/CoverageReport.cpp
@@ -496,6 +496,22 @@ std::vector<FileCoverageSummary> CoverageReport::prepareFileReports(
   }
   Pool.wait();
 
+  // Subtract excluded lines (from inline markers like LCOV_EXCL_LINE) from
+  // each file's line coverage totals. The ExcludedLines map is populated by
+  // the caller scanning source files before invoking this function.
+  for (unsigned I = 0; I < Files.size(); ++I) {
+    const auto *Excluded = Options.getExcludedLinesForFile(Files[I]);
+    if (!Excluded)
+      continue;
+    auto FileCoverage = Coverage.getCoverageForFile(Files[I]);
+    for (const auto &LCS : getLineCoverageStats(FileCoverage)) {
+      if (!LCS.isMapped())
+        continue;
+      if (Excluded->count(LCS.getLine()))
+        FileReports[I].LineCoverage.subtractLine(LCS.getExecutionCount() > 0);
+    }
+  }
+
   for (const auto &FileReport : FileReports)
     Totals += FileReport;
 
diff --git a/llvm/tools/llvm-cov/CoverageSummaryInfo.h b/llvm/tools/llvm-cov/CoverageSummaryInfo.h
index d9210676c41bf..85e24cda6d735 100644
--- a/llvm/tools/llvm-cov/CoverageSummaryInfo.h
+++ b/llvm/tools/llvm-cov/CoverageSummaryInfo.h
@@ -82,6 +82,15 @@ class LineCoverageInfo {
     return *this;
   }
 
+  void subtractLine(bool WasCovered) {
+    assert(NumLines > 0 && "Cannot subtract from zero lines");
+    --NumLines;
+    if (WasCovered) {
+      assert(Covered > 0 && "Cannot subtract covered from zero");
+      --Covered;
+    }
+  }
+
   void merge(const LineCoverageInfo &RHS) {
     Covered = std::max(Covered, RHS.Covered);
     NumLines = std::max(NumLines, RHS.NumLines);
diff --git a/llvm/tools/llvm-cov/CoverageViewOptions.h b/llvm/tools/llvm-cov/CoverageViewOptions.h
index 4826d4adb53bf..6a41d887c6c2e 100644
--- a/llvm/tools/llvm-cov/CoverageViewOptions.h
+++ b/llvm/tools/llvm-cov/CoverageViewOptions.h
@@ -10,7 +10,12 @@
 #define LLVM_COV_COVERAGEVIEWOPTIONS_H
 
 #include "RenderingSupport.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/StringMap.h"
 #include "llvm/Config/llvm-config.h"
+#include "llvm/Support/LineIterator.h"
+#include "llvm/Support/Regex.h"
+#include <memory>
 #include <vector>
 
 namespace llvm {
@@ -61,6 +66,59 @@ struct CoverageViewOptions {
   float HighCovWatermark;
   float LowCovWatermark;
 
+  /// Per-file sets of line numbers excluded from coverage via inline markers.
+  /// Populated during the pre-scan phase (before multi-threaded reporting).
+  StringMap<DenseSet<unsigned>> ExcludedLines;
+
+  /// Regexes for inline exclusion markers.
+  std::shared_ptr<Regex> ExcludeLineRE;
+  std::shared_ptr<Regex> ExcludeRegionStartRE;
+  std::shared_ptr<Regex> ExcludeRegionStopRE;
+
+  /// Get excluded lines for a file, or nullptr if none.
+  const DenseSet<unsigned> *getExcludedLinesForFile(StringRef Filename) const {
+    auto It = ExcludedLines.find(Filename);
+    if (It == ExcludedLines.end())
+      return nullptr;
+    return &It->second;
+  }
+
+  /// Scan a source buffer for exclusion markers and populate ExcludedLines.
+  /// Must be called before multi-threaded reporting (prepareFileReports).
+  void scanForExclusionMarkers(StringRef Filename, const MemoryBuffer &Buffer) {
+    if (!ExcludeLineRE && !ExcludeRegionStartRE)
+      return;
+
+    DenseSet<unsigned> Excluded;
+    bool InExcludedRegion = false;
+    unsigned LineNo = 0;
+
+    for (line_iterator LI(Buffer, /*SkipBlanks=*/false); !LI.is_at_eof();
+         ++LI) {
+      ++LineNo;
+      StringRef Line = *LI;
+
+      if (InExcludedRegion) {
+        Excluded.insert(LineNo);
+        if (ExcludeRegionStopRE && ExcludeRegionStopRE->match(Line))
+          InExcludedRegion = false;
+        continue;
+      }
+
+      if (ExcludeRegionStartRE && ExcludeRegionStartRE->match(Line)) {
+        InExcludedRegion = true;
+        Excluded.insert(LineNo);
+        continue;
+      }
+
+      if (ExcludeLineRE && ExcludeLineRE->match(Line))
+        Excluded.insert(LineNo);
+    }
+
+    if (!Excluded.empty())
+      ExcludedLines[Filename] = std::move(Excluded);
+  }
+
   /// Change the output's stream color if the colors are enabled.
   ColoredRawOstream colored_ostream(raw_ostream &OS,
                                     raw_ostream::Colors Color) const {



More information about the llvm-commits mailing list