Full syntax reference, authoring guide, and diagnostic workflows for jacoco-method-filter rules files.
- How to Use
- Rule Anatomy
- JVM Descriptor Type Mapping
- Descriptor Normalization
- Common Pitfalls
- Scala Name Mangling
- Examples
- Exclude and Include Rules
- Global and Local Rules
- How Rules Are Merged
- Verify: Preview What Gets Filtered
- Diagnostic Workflow
- Verification Workflow
- Global Rule Safety Warning
- CLI Reference
- Ready-to-Use Rules Template
- Review the GLOBAL RULES in the template — they cover compiler-generated boilerplate (case class methods, lambdas, value class extensions, default parameters).
- Add project-specific patterns in the PROJECT RULES section.
- Keep rules narrow (by package), prefer flags (
synthetic/bridge) for compiler artifacts, and addid:labels so logs are readable. - Use
--verifymode to confirm rules match what you expect before committing. - Add
--strictto your CI build command (CLI) to fail fast when rules are missingid:labels. (The sbt and Maven plugins do not yet expose--strictas a configuration key.) - Prefer include rules over commenting out globals. When a broad global accidentally matches
a method with real logic, keep the global active and add a
+include rule to rescue that specific method. Commenting out the global loses filtering for all other matching methods. See Exclude and Include Rules.
<FQCN_glob>#<method_glob>(<descriptor_glob>) [FLAGS] [PREDICATES] id:<label>
FQCN_glob — Class name in dot-form with globs.
Examples: *MyClass, *.model.*, com.example.*
Use $ for inner classes (Foo$Bar) and companions (Foo$).
method_glob — Method name with globs.
Examples: copy, get*, $anonfun$*, *_$eq
descriptor — JVM descriptor in (args)ret format with globs.
Examples: (I)V, (Ljava/lang/String;)*, (*)*
Omitting the descriptor entirely is equivalent to (*)*.
FLAGS — Space-separated. Optional.
public | protected | private | synthetic | bridge | static | abstract
IMPORTANT: flags must be space-separated from the descriptor.
WRONG: *#*(*):synthetic (colon makes it part of the descriptor)
RIGHT: *#*(*) synthetic (space separates flag from descriptor)
PREDICATES — Space-separated key:value pairs. Optional.
ret:<glob>— Match return type only (e.g.,ret:V,ret:Lcom/example/*;)id:<string>— Identifier shown in logs/reports (required for traceability; empty value treated as absent)name-contains:<s>— Method name must contain<s>name-starts:<s>— Method name must start with<s>name-ends:<s>— Method name must end with<s>forward-compat— Exempt from the unmatched rules warning (see Verify: Unmatched Rules)
IMPORTANT: predicates must be space-separated from the descriptor.
WRONG: *#*(*):ret:V (colon makes it part of the descriptor)
RIGHT: *#*(*) ret:V (space separates predicate from descriptor)
Notes
- Regex selectors (
re:) are not supported — globs only.- Always use dot-form (
com.example.Foo) for class names.- Comments (
# …) and blank lines are ignored.#is both the comment marker (start of line) and FQCN/method separator. Inline comments after rules are harmless but best avoided — use dedicated comment lines.
JMF matches against raw JVM bytecode descriptors, NOT source-level types.
Writing human-readable types (e.g., "int", "java.lang.String") produces rules that load
successfully but silently never match.
| Source type | JVM descriptor |
|---|---|
int |
I |
boolean |
Z |
long |
J |
double |
D |
float |
F |
byte |
B |
char |
C |
short |
S |
void / Unit |
V |
String |
Ljava/lang/String; |
Option[A] |
Lscala/Option; |
Object |
Ljava/lang/Object; |
Array[Int] |
[I |
Array[String] |
[Ljava/lang/String; |
Use javap -p -verbose <ClassFile.class> to see actual descriptors.
Short/empty descriptor forms are all equivalent wildcards:
| What you write | What JMF uses | Matches |
|---|---|---|
| (omitted) | (*)* |
any args, any return |
() |
(*)* |
any args, any return |
(*) |
(*)* |
any args, any return |
(I)V |
(I)V |
exactly int→void |
WARNING: *#productElement() LOOKS like "no-arg method" but actually matches ALL overloads
(including productElement(I)Ljava/lang/Object;) because () normalizes to (*)*.
To target a specific overload, use an explicit descriptor: *#myMethod()V (actually no-arg, returns void).
1. FQCN wildcard prefix required for qualified class names:
WRONG: QueryResult#noMore() id:qr-nomore
(matches only unqualified "QueryResult", not "com.example...QueryResult")
RIGHT: *QueryResult#noMore() id:qr-nomore
(* prefix matches any package prefix)
2. Use JVM descriptors, not source types:
WRONG: *#apply(int)* (human-readable — silently never matches)
RIGHT: *#apply(I)* (JVM format)
3. Flags and predicates must be SPACE-separated from descriptor:
WRONG: *#*(*):synthetic (colon makes ":synthetic" part of descriptor)
RIGHT: *#*(*) synthetic (space separates — flag parsed correctly)
WRONG: *#*(*):ret:V (colon makes ":ret:V" part of descriptor)
RIGHT: *#*(*) ret:V (space separates — predicate parsed correctly)
4. Object return types in ret: globs need trailing semicolon:
WRONG: *#make(*) ret:Lcom/example/model/Id (missing semicolon)
RIGHT: *#make(*) ret:Lcom/example/model/Id; (semicolon required)
5. Every rule should include id:<label> for traceability in logs. Rules without id: emit
a [warn] message at load time (showing source file and line number) regardless of whether
--strict is set. To enforce id: as a hard CI requirement, pass --strict to the CLI —
any unlabelled rule causes a non-zero exit:
java -cp ... io.moranaapps.jacocomethodfilter.CoverageRewriter \
--verify \
--in target/classes \
--local-rules jmf-rules.txt \
--strict6. # is both the comment marker (start of line) and FQCN/method separator. Inline comments
after rules are harmless but can be confusing. Best practice: use dedicated comment lines above the rule.
7. Audit your rules for human-readable descriptors:
grep -n '[a-z]\.[A-Z]' jmf-rules.txt | grep -v '^#'Any matches likely contain human-readable class names in descriptors.
8. Use --verify --error-on-unmatched in CI to catch rules that silently stopped matching
(e.g. after a rename or refactor). Rules that intentionally target classes absent from the current
build should be marked forward-compat to suppress the warning:
*com.example.ProdOnlyService#copy(*) forward-compat id:prod-only-copy
JMF operates on bytecodes — globs must use the compiled method name, not the source name. Common Scala name-mangling patterns:
| Source name | Bytecode name |
|---|---|
name_= (setter) |
name_$eq |
a_+_b (operator) |
a_$plus_b |
? |
$qmark |
! |
$bang |
++ |
$plus$plus |
:: |
$colon$colon |
Inner class Foo.Bar |
Foo$Bar |
Companion object Foo |
Foo$ |
Lambda from foo |
$anonfun$foo$1 |
Use javap -p classfile to confirm the exact bytecode name before writing a glob.
*#*(*)
Match EVERY method in EVERY class (any package). DO NOT commit this rule — it suppresses all JaCoCo coverage and produces artificially inflated (up to 100%) coverage numbers, silently masking regressions. Use only for one-off diagnostics and remove immediately.
*.dto.*#*(*)
Match every method on any class under any package segment named dto. Good when you treat DTOs as
generated/boilerplate.
*.model.*#copy(*) # case-class copy, any parameter list
*.model.*#productArity() # NOTE: () normalizes to (*)* — matches ALL overloads, not just zero-arg
*.model.*#productElement(*) # JVM signature: (I)Ljava/lang/Object;
*.model.*#productPrefix() # returns the case class name as a String
*.model.*$*#apply(*) # companion apply factories — BE CAREFUL: can hide real factory logic
*.model.*$*#unapply(*) # extractor unapply methods in companions
*#*$default$*(*) # Scala default-argument helpers — compiler-synthesized, safe
*#$anonfun$* # any method whose name contains $anonfun$ (Scala lambdas)
*#*(*) synthetic id:any-synthetic # any ACC_SYNTHETIC method; scope by package!
*#*(*) bridge id:any-bridge # Java generic bridge methods (usually safe globally)
NOTE: flags are space-separated, NOT colon-prefixed.
*.dto.*#*_$eq(*) # Scala var setters (source: name_= → bytecode: name_$eq)
*.builder.*#with*(*) # builder-style fluent setters
*.client.*#with*(*) ret:Lcom/api/client/*; # builder setters returning a specific type
NOTE: Source-level
name_=is compiled toname_$eqin bytecode. Using the source form*_=(*)would silently never match.
*.jobs.*#*(*) ret:V id:jobs-void # void-returning, often orchestration
*.math.*#*(*) ret:I id:math-int # int-returning math methods
*.model.*#*(*) ret:Lcom/example/model/*; id:model-ret # return type in model package
NOTE: trailing semicolon is required for object type globs in
ret:.
- Always use dot-form (
com.example.Foo) for PACKAGE separators (not slash-formcom/example/Foo). - The
$character IS required for inner classes (Foo$Bar) and companion objects (Foo$).
Scala compiles lazy val foo = expr into two methods:
foo()— the accessor (checks bitmap flag, calls$lzycomputeif unset)foo$lzycompute()— the actual initializer body (containsexpr)
The $lzycompute method body IS the lazy initializer, so filtering it hides whatever expr does.
Only filter when the initializer is a trivial constant, a boundary call not unit-testable (e.g.,
DriverManager.getConnection), or an already-covered computation. Rescue with + include rules
for any lazy val that contains real logic.
# CAUTION: only enable after verifying each lazy val's body is trivial or boundary-only
*#*$lzycompute(*) id:scala-lzycompute
# Rescue a lazy val whose initializer contains real logic
+*MyService#cache$lzycompute(*) id:keep-cache-lzycompute
The template ships this rule disabled (commented out). Enable it per class or enable globally with include-rule rescues.
When Scala compiles a companion object, the compiler emits static forwarder methods on the main
class (Foo) that delegate to Foo$.MODULE$.method(). Each forwarder is a single-call delegate
with no logic of its own. There is no general glob to isolate them (they look like any public
static method), so rules must be class-specific:
# Static forwarder on Foo delegates to Foo$.MODULE$.apply(...)
*Foo#apply(*) id:foo-apply-fwd
*Foo#fromString(*) id:foo-fromstring-fwd
Verify with javap -p Foo.class — forwarders appear as public static methods with a body of
return Foo$.MODULE$.methodName(args).
When a class extends Iterator (or another large trait), the Scala compiler generates ~80+
forwarding methods on the implementing class, each delegating to the trait default implementation.
None contain project logic. Since the class name is project-specific, rules must be scoped to it:
# QueryResult extends Iterator[Row] — filter all trait-generated forwarders
*QueryResult#to*(*) id:qr-iter-to
*QueryResult#mk*(*) id:qr-iter-mk
*QueryResult#map(*) id:qr-iter-map
*QueryResult#flatMap(*) id:qr-iter-flatmap
*QueryResult#filter*(*) id:qr-iter-filter
*QueryResult#fold*(*) id:qr-iter-fold
*QueryResult#reduce*(*) id:qr-iter-reduce
# ... add remaining Iterator methods as needed
Use javap -p ClassName.class | grep 'public' to enumerate all forwarders, then write
scoped rules for the ones you want to filter.
By default, all rules are exclusion rules — they mark methods to be filtered from coverage.
Include rules (whitelist) can override exclusions for specific methods.
Prefix a rule with + to mark it as an inclusion:
# Exclude all companion object apply methods
*$#apply(*) id:comp-apply
# But keep this one — it has custom business logic
+com.example.Config$#apply(*) id:keep-config-apply
Resolution logic:
- A method is excluded if any exclusion rule matches AND no inclusion rule matches.
- A method is rescued (kept in coverage) if both exclusion and inclusion rules match — include always wins.
- A method is unaffected if no exclusion rule matches.
When a broad global rule accidentally matches a method with real logic, the preferred response is
to keep the global active and add a + include rule for the exception — not to comment out
the global.
Commenting out a global loses the filtering benefit for every other class it matched. A +
include rule is surgical: it rescues exactly the method that needs coverage while leaving all
other matches filtered.
# BAD: commenting out the global loses filtering for ALL copy methods everywhere
#*#copy(*) id:case-copy
# GOOD: keep the global, rescue the one method that has real logic
*#copy(*) id:case-copy
+com.example.MutableRecord#copy(*) id:keep-mutablerecord-copy
The + rule also serves as explicit documentation: a future reader sees that this method was
reviewed and confirmed to contain real logic.
When commenting out is appropriate:
- The rule causes widespread false positives across many unrelated classes (writing dozens of include rules would be noisier than disabling the global).
- The rule pattern is fundamentally wrong for this project (e.g., a naming collision on a domain term used everywhere).
- You are still evaluating a candidate global and have not yet committed to enabling it.
Method filtering is opt-in. With no rules configured at all, every class passes through
unchanged and you get a normal, unfiltered JaCoCo report — so the plugin is safe to wire into a
shared parent POM before every project has authored its own jmf-rules.txt. Enforce that a rules
source is present with --require-rules (CLI) / jmf.requireRules=true (Maven) /
jmfRequireRules := true (sbt).
Most users can start with a single local rules file.
- Simple (single file): use
--local-rules jmf-rules.txt(CLI) or the plugin defaultjmf-rules.txt - Advanced (two-layer): use global rules (shared defaults) + local rules (project overrides)
- Global rules can be a local path or an HTTP/HTTPS URL
- Local rules are a local file path
| Type | Purpose | Source |
|---|---|---|
| Global | Org-wide defaults (e.g., always ignore Scala boilerplate) | Path or URL |
| Local | Project-specific overrides and additions | Local file |
sbt:
jmfGlobalRules := Some("https://myorg.com/scala-defaults.txt")
jmfLocalRules := Some(baseDirectory.value / "jmf-local-rules.txt")Maven:
<configuration>
<globalRules>https://myorg.com/scala-defaults.txt</globalRules>
<localRules>${project.basedir}/jmf-local-rules.txt</localRules>
</configuration>CLI:
java -cp ... io.moranaapps.jacocomethodfilter.CoverageRewriter \
--in target/classes \
--out target/classes-filtered \
--global-rules https://myorg.com/scala-defaults.txt \
--local-rules jmf-local-rules.txtWhen using global and local rules:
- Global rules are loaded first (from URL or path).
- Local rules are appended.
- During evaluation, any include rule overrides any exclude rule for the same method.
This lets you:
- Define broad exclusions globally (e.g.,
*#copy(*)) - Override selectively in local rules (e.g.,
+com.example.Config$#copy(*))
verify runs against the compiled class files (bytecode) in the given --in directory
(e.g. target/classes), not against raw source code — so it only reports exclusions/rescues for
methods that actually exist after compilation.
Important: Because
verifyscans bytecode, it sees all methods the JVM compiler generated (synthetic bridges, anonymous function bodies, default parameter accessors, etc.) alongside your hand-written code. Some broad exclusion rules may accidentally match methods you wrote yourself (e.g.,apply). Use include rules (+...) to rescue those methods.
sbt:
sbt jmfVerifyMaven:
mvn jacoco-method-filter:verifyCLI:
java -cp ... io.moranaapps.jacocomethodfilter.CoverageRewriter \
--verify \
--in target/classes \
--local-rules jmf-rules.txtExample output:
[verify] EXCLUDED (15 methods):
[verify] com.example.User
[verify] #copy(I)Lcom/example/User; rule-id:case-copy
[verify] #apply(...)... rule-id:comp-apply
[verify] RESCUED by include rules (1 method):
[verify] com.example.Config$
[verify] #apply(Lcom/example/Config;)Lcom/...; excl:comp-apply → incl:keep-config-apply
[verify] Summary: 42 classes scanned, 15 methods excluded, 1 method rescued
- Excluded — matched by an exclusion rule; will be filtered from coverage.
- Rescued — matched by an exclusion rule and an include rule (
+…). Include always wins. Theexcl:… → incl:…trace shows which rules were involved.
After the EXCLUDED / RESCUED sections, --verify prints an UNMATCHED RULES section listing
every rule that matched zero methods in the scanned class directory:
[verify] UNMATCHED RULES (2 rules matched zero methods):
[verify] *com.example.DoesNotExist#copy(*) id:ghost-copy [local: jmf-rules.txt]
[verify] *QueryResult#noMore() (no id) [local: jmf-rules.txt]
An unmatched rule almost always indicates a misconfiguration:
- Wrong FQCN prefix (missing
*, wrong package, stale class name). - Human-readable descriptor instead of JVM format (
int→ should beI). - Method was removed but the rule was never cleaned up.
To treat unmatched rules as a hard CI failure, add --error-on-unmatched:
java -cp ... io.moranaapps.jacocomethodfilter.CoverageRewriter \
--verify \
--in target/classes \
--local-rules jmf-rules.txt \
--error-on-unmatchedExit code is 1 when any unmatched rules exist; 0 when all rules matched.
To enforce that every rule has an id: label, add --strict:
java -cp ... io.moranaapps.jacocomethodfilter.CoverageRewriter \
--verify \
--in target/classes \
--local-rules jmf-rules.txt \
--strictExit code is 1 when any rules lack an id: label; 0 when all rules are labelled.
Both --error-on-unmatched and --strict can be combined. When combined, the --strict
check runs first; if any rules lack id:, the process aborts before scanning for unmatched
rules. Rules without id: also emit a [warn] message at load time (regardless of --strict),
making them visible in non-strict runs.
Some rules are intentionally written to target classes present in a production build but absent from a given module's test classpath. These rules would always appear in UNMATCHED RULES without any real misconfiguration.
Mark such rules with the forward-compat token to exempt them from the unmatched check:
# This service exists in production but is not compiled in the payment module.
*com.example.OrderService#copy(*) forward-compat id:order-copy
Forward-compat rules are still active — if the class later appears in the build, they filter normally. They are only excluded from the "zero-match" warning.
In the --verify active-rules listing, forward-compat rules are labelled (forward-compat)
so you can identify them at a glance:
[verify] Active rules from local: jmf-rules.txt:
[verify] 1. [-] id:order-copy (forward-compat) [local: jmf-rules.txt]
[verify] 2. [-] id:prod-copy [local: jmf-rules.txt]
When JaCoCo reports missed instructions after you believe you have a rule:
-
Find the method in jacoco.xml:
grep -A2 'name="myMethod"' target/scala-2.12/jacoco/jacoco.xml -
Get the actual bytecode descriptor:
javap -p -verbose target/scala-2.12/classes/com/.../MyClass.class | grep -A4 "myMethod"
-
Compare the descriptor in your rule against the bytecode output. Common mistakes:
(int)vs(I), missing;after object types,*vs explicit return.
Before committing any new JMF rule:
-
Baseline: note "Marked N methods" in build output.
# sbt sbt '++2.12.18; jacoco' # Maven mvn clean verify -Pcode-coverage
-
Add the new rule to your rules file.
-
Re-run: "Marked N+k methods" confirms the rule matched.
-
Use
--verifymode for a detailed matching report:java -jar jmf.jar --in classes/ --local-rules rules.txt --verify
Global rules match ALL classes in ALL packages. Some rules in the template are intentionally
broad. Before committing, use --verify to check what they match, then add + include rules to
rescue any methods with real logic — rather than commenting the global out entirely.
See Exclude and Include Rules for the recommended strategy.
*#apply(*) id:case-apply
Companion apply methods often contain validation or factory logic. Shipped commented-out as a
conservative default — but the recommended approach is to enable it and rescue false positives
with + include rules rather than leaving it off entirely.
Enable the rule, run --verify, then rescue false positives:
*#apply(*) id:case-apply
+*Config$#apply(*) id:keep-config-apply # has validation logic
+*Factory$#apply(*) id:keep-factory-apply # has branching
*#writeReplace(*) id:case-writereplace
Emitted by the Scala compiler on case classes to support Java serialization. The body is a
single-expression proxy construction — no project logic. Safe to filter globally.
If a class overrides writeReplace with real logic, rescue it:
+com.example.CustomSerializable#writeReplace(*) id:keep-writereplace
*#name(), *#groups(), *#optionalAttributes()
Added for compiler-generated Scala patterns (e.g., Regex group names, case class fields) but will
also match domain methods with those names. Run --verify to check collisions; rescue with +
include rules for any that contain real logic.
*#*$lzycompute(*) (disabled in template)
The $lzycompute method body IS the lazy val initializer — filtering it hides whatever the lazy
val computes. Shipped disabled. Recommended approach: enable selectively per class or enable
globally and rescue lazy vals with real logic:
*#*$lzycompute(*) id:scala-lzycompute
+*MyService#cache$lzycompute(*) id:keep-cache-lzycompute # complex initializer
| Flag | Required | Description |
|---|---|---|
--in <dir> |
Yes | Input classes directory (must exist, must contain .class files) |
--out <dir> |
Unless --verify |
Output classes directory |
--global-rules <path|url> |
Optional | Global rules file path or URL |
--local-rules <path> |
Optional | Local rules file path. A path that does not exist is treated as empty (a [warn] is printed) |
--require-rules |
No | Exit non-zero if no rules source is configured; off by default |
--dry-run |
No | Only print matches; do not modify classes |
--verify |
No | Read-only scan: list all methods that would be excluded by rules |
--error-on-unmatched |
No | Exit non-zero if any rules matched zero methods (requires --verify) |
--strict |
No | Exit non-zero if any rules have no id: label |
--report-file <path> |
No | Write the filtered-methods report to this file |
--report-format <fmt> |
No | Report format: txt (default), json, or csv (requires --report-file) |
In rewrite mode, --out is required (omit only when using --verify).
No rules configured is not an error. With neither --global-rules nor --local-rules, every
class passes through unchanged (Loaded 0 rule(s), marked 0 method(s)) and a later JaCoCo report
step produces a normal, unfiltered report. Pass --require-rules to make the absence of a rules
source a hard failure instead.
- Scala (sbt) project:
jmf-rules.template.txt - Maven project:
maven-plugin/src/main/resources/jmf-rules.template.txt
Copy the template to your project root and customize the PROJECT RULES section.