CRAP metrics for Java.
It combines method cyclomatic complexity with JaCoCo method coverage and reports CRAP scores. The toolkit resolves Maven and Gradle modules natively, including standard multi-module layouts, and publishes a standalone CLI plus dedicated Gradle and Maven plugins.
core: analysis engine, build-tool-neutral CLI orchestration, and Maven/Gradle coverage runnercli: executable entrypoint that bundles the core as a runnable jargradle-plugin: self-contained Gradle plugin build exposingmedia.barney.crap-javamaven-plugin: native Maven plugin exposing thecheckgoal
CRAP = CC^2 * (1 - coverage)^3 + CC
CCis cyclomatic complexity.coverageis the lower available method coverage fraction from JaCoCoINSTRUCTIONandBRANCHcounters. JaCoCo omitsBRANCHcounters for branchless methods, so those methods use instruction coverage.
Java method complexity starts at 1 and increments for each AST decision node:
loops, if, conditional expressions, catch, short-circuit boolean operators,
and switch case clauses. Switch handling follows the javac AST: each
case/default clause contributes 1, regardless of how many constants appear
in that clause. For example, case A, B, C -> contributes 1, not 3, and
default also contributes 1.
Lambda bodies are not first-class report rows in the current source-first model.
Their internal decision nodes are excluded from the enclosing source method's
complexity, and javac synthetic JaCoCo methods named like lambda$...$<digits>
are ignored during coverage attribution.
Anonymous-class bodies are also outside the supported source-method scope.
crap-java does not synthesize $N owners from source, does not report methods
or nested declarations inside anonymous classes, and does not fold anonymous-body
complexity into the enclosing source method.
For each resolved module today:
- Detect Maven or Gradle automatically, unless
--build-toolis supplied. - Delete stale JaCoCo artifacts for the detected build tool.
- Run the module-scoped coverage command:
- Maven:
mvnormvnw, using JaCoCo0.8.13 - Gradle:
gradleorgradlew, runningtestandjacocoTestReport
- Maven:
- Read the module report:
- Maven:
target/site/jacoco/jacoco.xml - Gradle:
build/reports/jacoco/test/jacocoTestReport.xml
- Maven:
- Analyze the selected Java files for that module
For Maven CLI coverage generation, crap-java invokes
org.jacoco:jacoco-maven-plugin:0.8.13 explicitly. It does not inspect or
reuse a jacoco-maven-plugin version pinned in the analyzed project's POM. If
your build requires a different JaCoCo version, generate the XML report in your
own Maven build and run the Maven plugin path below, which consumes that report
without starting another coverage run.
Source discovery walks src/main/java roots by default without following
directory symlinks. The CLI can override those roots with repeatable
--source-root <path> options, the Maven plugin uses configured compile source
roots when they differ from Maven defaults, and the Gradle plugin reads the
Java plugin's configured main source set. Symlinked Java files inside a source
root can still be selected and are reported using the symlink path rather than a
canonicalized target path.
mvn -B -pl cli -am packageBuild and test the Gradle plugin module after packaging the core jar:
mvn -B -pl core -am package
cd gradle-plugin
./gradlew testBuild and test the Maven plugin module, including its invoker integration fixtures:
mvn -B -pl maven-plugin -am verifyRepository CI also runs the shared published cognitive-java Maven plugin as a
separate cognitive-java Gate job. The plugin resolves from Maven Central, and
the current version is controlled by the cognitive-java.version property in
pom.xml.
From the repository root, run the same gate locally with:
mvn -B cognitive-java:checkmvn -B verify now also includes the cognitive gate at the reactor root.
Consumer Maven repos standardize on mvn -B -ntp verify, but this repository
keeps dedicated self-hosting gate jobs where needed to preserve full-repo metric
ownership across the embedded gradle-plugin/ source tree.
In CI:
crap-java Gateownscore,cli,maven-plugin, andgradle-plugin/src/main/javacognitive-java Gateowns the same full-repo source scopeGradle Pluginvalidates Gradle plugin build and test behavior only
The standalone Gradle Plugin job is not the owner of metric failures for
gradle-plugin/src/main/java; those failures still belong to the metric gate
jobs.
Build the CLI jar:
mvn -B -pl cli -am -DskipTests packageFrom the project root you want to analyze:
java -jar cli/target/crap-java-cli-0.6.1.jar--help Print usage to stdout
(no args) Analyze all Java files under any nested source root
--changed Analyze changed Java files under any nested source root
--build-tool <tool> Force `auto`, `maven`, or `gradle`
--format <format> Write `toon`, `json`, `text`, `junit`, or `none` output (`toon` by default)
--agent Apply AI-agent defaults: `toon`, failures only, omit redundancy
--failures-only[=true|false] Only include failing methods in the primary report
--omit-redundancy[=true|false] Omit redundant method fields from the primary report
--exclude <glob> Exclude source paths by normalized relative glob; repeatable
--exclude-class <regex> Exclude fully-qualified class names by regex; repeatable
--exclude-annotation <name> Exclude classes by annotation name; repeatable
--use-default-exclusions[=true|false] Enable built-in generated-code exclusions (`true` by default)
--source-root <path> Override production source roots; repeatable
--output <path> Write the selected output format to a file instead of stdout
--junit-report <path> Also write a JUnit XML report for CI test-report UIs
--threshold <number> Override the CRAP threshold (`8.0` by default)
<file ...> Analyze only these files
<directory ...> Analyze all Java files under each directory's nested source roots
Value-taking long options may also be written with inline assignment, such as
--build-tool=maven, --format=json, or --exclude='module-a/**'.
Examples:
java -jar cli/target/crap-java-cli-0.6.1.jar --help
java -jar cli/target/crap-java-cli-0.6.1.jar
java -jar cli/target/crap-java-cli-0.6.1.jar --changed
java -jar cli/target/crap-java-cli-0.6.1.jar --build-tool gradle
java -jar cli/target/crap-java-cli-0.6.1.jar --build-tool=maven
java -jar cli/target/crap-java-cli-0.6.1.jar --format json
java -jar cli/target/crap-java-cli-0.6.1.jar --format none --junit-report target/crap-java/TEST-crap-java.xml
java -jar cli/target/crap-java-cli-0.6.1.jar --format json --output target/crap-java/report.json
java -jar cli/target/crap-java-cli-0.6.1.jar --failures-only=false --format json
java -jar cli/target/crap-java-cli-0.6.1.jar --omit-redundancy=false --format json
java -jar cli/target/crap-java-cli-0.6.1.jar --agent
java -jar cli/target/crap-java-cli-0.6.1.jar --agent --format junit --output target/crap-java/TEST-crap-java-primary.xml
java -jar cli/target/crap-java-cli-0.6.1.jar --junit-report target/crap-java/TEST-crap-java.xml
java -jar cli/target/crap-java-cli-0.6.1.jar --threshold 6
java -jar cli/target/crap-java-cli-0.6.1.jar --threshold=6
java -jar cli/target/crap-java-cli-0.6.1.jar --exclude 'module-a/**' --exclude-class '.*MapperImpl$'
java -jar cli/target/crap-java-cli-0.6.1.jar --exclude='module-a/**' --exclude-class='.*MapperImpl$'
java -jar cli/target/crap-java-cli-0.6.1.jar --source-root src/java --source-root src/main/java17
java -jar cli/target/crap-java-cli-0.6.1.jar --build-tool maven module-a/src/main/java/demo/Sample.java
java -jar cli/target/crap-java-cli-0.6.1.jar src/main/java/demo/Sample.java
java -jar cli/target/crap-java-cli-0.6.1.jar module-a module-bThe CLI writes only the requested primary report format to stdout unless
--output is set. Warnings and threshold errors are written to stderr. Use
--format none when you only want the exit status or a JUnit sidecar.
Report paths passed to --output and --junit-report are filesystem targets:
relative paths resolve against the analyzed project root, absolute paths remain
absolute, and normalized .. segments may target locations outside that root.
Keep those values fixed or otherwise trusted in CI configurations.
Machine-readable primary reports include top-level status (passed or
failed) and threshold values. Method entries use compact fields status,
crap, cc, cov, covKind, method, src, lineStart, and lineEnd.
src is the project-relative source file path. coverageKind identifies the
coverage input used for each CRAP score (instruction, branch, or N/A).
N/A also covers methods whose JaCoCo coverage cannot be attributed
unambiguously to one source method.
Full primary reports also include exclusion audit counts when any source was
considered; optimized primary reports produced through --agent omit that audit
detail by default to stay focused on actionable failures. The JUnit sidecar
keeps the complete exclusion audit.
Built-in exclusions are conservative and generated-code focused. They exclude
source files under any directory segment containing generated, source files
under **/src/main/java-gen/**, and classes annotated with any annotation whose
simple name is Generated regardless of package. Default class-name regexes
are (^|.*\.)generated(\..*)?, (^|.*\.)gen(\..*)?,
(^|.*\.)[^.]*MapperImpl$, (^|.*\.)Dagger[^.]*$,
(^|.*\.)Hilt_[^.]*$, and (^|.*\.)AutoValue_[^.]*$.
The defaults intentionally do not exclude handwritten-looking parser/listener/
visitor classes, Immutable* classes, QueryDSL metamodels, vendor trees,
examples, migrations, bootstrap/configuration classes, or operational scripts.
User exclusions compose with those defaults unless
--use-default-exclusions=false is set.
--agent is a composite shortcut for --format toon --failures-only --omit-redundancy when those settings are not overridden explicitly.
--failures-only and --omit-redundancy affect only the primary report.
Assigned boolean CLI values must be lowercase true or false; bare boolean flags mean true.
--junit-report <path> always writes the complete unfiltered JUnit XML sidecar,
and it can be combined with any primary format, including none.
The default threshold is 8.0. Values below 4.0 print a warning because they
are likely too noisy; values above 8.0 print a warning because they are too
lenient even for hard gates. The warning recommends 8.0 for hard gates,
targeting 6.0 during implementation, and using the 8.0 default when in
doubt.
The JUnit XML format exposes each analyzed method as a testcase and is shaped
for GitLab's Tests tab. Testcases use the project-relative source path for
classname and file, include a concise metric summary in the testcase name
as method:lineStart [CRAP=score, CC=complexity, Cov=percent (kind)], write the
measured analysis duration on the testsuite, and divide that duration across
testcases. Methods with CRAP scores over the configured threshold fail, methods
with unavailable coverage are skipped, and testcase-level system-out plus
failure/skipped element text include CRAP score, threshold, coverage kind,
source path, and line range. Custom properties remain for tools that read them,
but GitLab-visible details do not rely on properties.
The current 0.6.1 release ships through Maven Central, with the Gradle Plugin Portal as the primary Gradle plugin channel:
media.barney:crap-java-core:0.6.1media.barney:crap-java-cli:0.6.1media.barney:crap-java-maven-plugin:0.6.1- Gradle plugin id
media.barney.crap-javaversion0.6.1
Apply the plugin in build.gradle(.kts):
plugins {
id("media.barney.crap-java") version "0.6.1"
}No custom pluginManagement repository configuration is required for published releases.
Run:
./gradlew crap-java-checkBy default the Gradle plugin writes no primary report and writes a JUnit XML
sidecar to build/reports/crap-java/TEST-crap-java.xml.
Configure default report behavior in build.gradle(.kts):
crapJava {
threshold.set(6.0)
format.set("json")
agent.set(false)
failuresOnly.set(false)
omitRedundancy.set(true)
output.set(layout.buildDirectory.file("reports/crap-java/report.json"))
junit.set(true)
junitReport.set(layout.buildDirectory.file("reports/crap-java/custom-junit.xml"))
excludes.set(listOf("module-a/**"))
excludeClasses.set(listOf(".*MapperImpl$"))
excludeAnnotations.set(listOf("Generated"))
useDefaultExclusions.set(true)
}agent switches the default primary report to TOON and defaults
failuresOnly and omitRedundancy to true unless you override them
explicitly. The same properties are available on individual
CrapJavaCheckTask instances for task-specific overrides.
If you want to prefer resolving the Gradle plugin from Maven Central, add Maven Central ahead of the Plugin Portal in settings.gradle(.kts):
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}Then apply the same plugin id in build.gradle(.kts):
plugins {
id("media.barney.crap-java") version "0.6.1"
}The marker publication lives at
media.barney.crap-java:media.barney.crap-java.gradle.plugin:0.6.1 and
resolves to the implementation artifact
media.barney:crap-java-gradle-plugin:0.6.1.
Add the plugin:
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>media.barney</groupId>
<artifactId>crap-java-maven-plugin</artifactId>
<version>0.6.1</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>The Maven plugin consumes the JaCoCo XML files produced by your build. It does not spawn a nested Maven run to generate coverage, so your project's configured JaCoCo version remains in control.
No custom <pluginRepositories> or consumer-side authentication are required for published releases.
Run:
mvn verifyBy default the Maven plugin writes no primary report and writes a JUnit XML
sidecar to target/crap-java/TEST-crap-java.xml.
Control the reports with:
mvn verify -DcrapJava.format=json -DcrapJava.output=target/crap-java/report.json
mvn verify -DcrapJava.agent=true
mvn verify -DcrapJava.failuresOnly=false -DcrapJava.omitRedundancy=true
mvn verify -DcrapJava.junit=false
mvn verify -DcrapJava.junitReport=target/custom-crap-java.xml
mvn verify -DcrapJava.threshold=6.0
mvn verify -DcrapJava.excludes='module-a/**,**/custom-generated/**'
mvn verify -DcrapJava.excludeClasses='.*MapperImpl$' -DcrapJava.excludeAnnotations=Generated
mvn verify -DcrapJava.useDefaultExclusions=falseIn comma-separated Maven properties, escape a literal comma as \,, for
example -DcrapJava.excludeClasses='demo.Name{1\,3}$'.
Defaults: crapJava.format=none, crapJava.agent=false, and
crapJava.junit=true. crapJava.agent=true switches the default primary report
to TOON and defaults crapJava.failuresOnly and crapJava.omitRedundancy to
true unless they are supplied explicitly.
Equivalent XML configuration is available through <excludes>,
<excludeClasses>, <excludeAnnotations>, and
<useDefaultExclusions>.
Override the JUnit XML path with:
mvn verify -DcrapJava.junitReport=target/custom-crap-java.xmlOverride the threshold with:
mvn verify -DcrapJava.threshold=6.0In GitLab CI, upload the generated XML with artifacts:reports:junit. In
GitHub Actions, upload the file as an artifact or feed it into a JUnit
test-report action.
0success, threshold respected1invalid CLI usage2CRAP threshold exceeded (> configured threshold)
See CONTRIBUTING.md for the issue-linked branch, commit, and PR flow used in this repository.