cargo-crap: CRAP Scores for Rust Functions, and What They Hide
Change Risk Anti-Patterns (CRAP) metric for Rust projects
At a glance
- What is it?
- cargo-crap scores every Rust function by combining cyclomatic complexity with LCOV coverage. It installs as a cargo plugin, gates CI with --fail-above, and reports a number that is deliberately not a verdict on your tests.
- Who is it for?
- Adopt cargo-crap if your Rust workspace has functions that are both long and lightly tested and you want a single ranked list of them in CI. Do not adopt it if you need a coverage tool that runs without a pre-generated LCOV file, or if you want a score that accounts for how hard a branch is to reach, because the formula only counts whether it was executed.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 10 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The number cargo-crap computes and the question it answers
Coverage tools tell you what ran. Linters tell you what looks wrong. Neither answers the question cargo-crap was built around: which functions are simultaneously hard to follow and barely exercised. The README states the metric comes from Savoia and Evans in 2007 and was originally implemented for Java as Crap4j and for .NET as NDepend. This crate brings the same formula to Rust.
The formula is printed in the README as CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m). Complexity is squared, so it dominates. Coverage enters cubed as a penalty term, which means the curve is steep at low coverage and flat near 100 percent. The README spells out three consequences worth reading before you trust a table. A trivial function with complexity 1 and full coverage scores exactly 1.0, the lower bound. At 100 percent coverage the quadratic term collapses and CRAP equals cyclomatic complexity, so matching values in the CRAP and CC columns are a sign the function is fully covered, not a bug. And above complexity of roughly 30, no coverage value keeps a function under the default threshold of 30.
The intended audience is Rust teams that already measure coverage and want to rank the results by risk rather than by percentage. It fits monorepos and it fits single crates, and the README's framing points at AI-generated code as a motivating case: code that compiles and passes tests but is structurally tangled. That framing is a marketing angle, not a property of the tool. The metric is indifferent to who wrote the function.
How cargo-crap reads LCOV and builds its table
cargo-crap does not run your tests. It consumes an LCOV file produced elsewhere, walks your source tree for .rs files, and joins the two by function location. The README's quick start makes that split explicit: cargo-llvm-cov is described as a separate tool to install once, and the LCOV report is generated before cargo crap is invoked.
The dependency list in Cargo.toml shows how the pieces fit. syn with the full, visit and printing features parses Rust source and walks the AST, and proc-macro2 is pulled in with span-locations so the tool can map syntax nodes to line and column positions. lcov parses the report. globset and ignore handle path selection and .gitignore respect, rayon parallelises the walk, and comfy-table renders the output. There is no test harness in the dependency tree, which confirms the design: cargo-crap is a scorer, not a runner.
The scoring step itself is a join between an AST-derived complexity count per function and the line hit counts in the LCOV records. The README's example output column headers are CRAP, CC, Coverage, Function and Location, with a marker column holding a cross, a triangle or a check. The footer line reads "1/3 function(s) exceed CRAP threshold 30." The coverage column is rendered as a ten-cell bar, so a function at 44.4 percent shows four filled cells. The threshold of 30 is the default, and the README does not document a flag for changing it in the portion reproduced here.
Installing cargo-crap and scoring a first crate
The README lists four install paths: cargo binstall, cargo install from source, the AUR, and a manual pre-built binary download. From source the requirement is Rust stable 1.88 or newer, which matches the rust-version field in Cargo.toml. The simplest route is binstall, which fetches a pre-built binary rather than compiling.
cargo binstall cargo-crapIf you prefer to build it, the README gives this command, and it needs the 1.88 toolchain available.
cargo install cargo-crapScoring requires an LCOV file first. The README installs cargo-llvm-cov as a separate step, then generates the report, then scores it. Run these three commands in order at the root of a crate.
cargo install cargo-llvm-cov
cargo llvm-cov --lcov --output-path lcov.info
cargo crap --lcov lcov.infoThe third command prints the table. Expect one row per function, sorted or not, with a cross for anything above the threshold of 30. A function with complexity 12 and zero coverage scores 156.0 in the README's example, which is the worst-looking row in that sample.
For a workspace, the README adds --workspace to both commands and shows a summary mode that prints an aggregate instead of the table.
cargo llvm-cov --workspace --lcov --output-path lcov.info
cargo crap --workspace --lcov lcov.info --summaryTo restrict the run to specific members, the README uses repeated -p flags, which is the shape you want in a CI job that only touches changed crates.
cargo crap -p backend_core -p backend_identity --lcov lcov.infoFor a gate, add --fail-above. The README shows it as the CI step that turns the threshold into an exit code.
Running without --lcov is not a coverage-free run
The flag table contains the one warning that matters most in practice. Omitting --lcov does not skip the coverage term. Every function is scored as if it had zero coverage, so the formula collapses to CC² + CC and the whole table reads red. The README calls this useful for a first look at the complexity distribution and states plainly that it is not a CRAP run.
That distinction is easy to miss because the output looks identical in shape. A team that runs cargo crap without a report, sees a wall of crosses, and concludes the codebase is in crisis has misread the tool. The right use of the no-LCOV mode is as a complexity census: it tells you where the long functions are before you have any coverage data at all. Once you have an LCOV file, the same functions may drop well below the threshold.
The practical consequence is that cargo-crap inherits every limitation of the coverage report you feed it. If cargo llvm-cov was run without --workspace, functions in other members may be absent or scored at zero. If a function is generic and monomorphised into several instantiations, what the LCOV file records depends on the coverage tool, not on cargo-crap. The README does not document how the join handles a function that appears in the LCOV file but not in the parsed source, or the reverse. Treat mismatches between the row count and your expectation as a reason to inspect the report, not the scorer.
What the CRAP score cannot tell you
The formula counts branches, not the difficulty of reaching them. A match arm guarded by a rare error path and a match arm over a two-variant enum contribute the same amount of complexity if they add the same number of paths, and the coverage term only records whether the branch executed during your test run. A function can therefore score 1.0 while its error handling has never been exercised, provided the tests happened to cover the measured lines. This is a property of the metric as the README describes it, not an implementation defect.
The second boundary is the one the README states directly: above complexity of about 30, no coverage value brings a function under the default threshold. For a large generated function or a hand-written parser state machine, the score is a statement that the function is too big to certify, and no amount of test writing changes that. The only remedy is splitting the function, which is a refactor the tool cannot perform or suggest.
Third, cargo-crap is the wrong tool when you want test execution and scoring in one command, or when your project does not produce LCOV. The README names cargo-llvm-cov and cargo tarpaulin as the sources it expects, and the dependency list contains no coverage instrumentation. If your build cannot emit LCOV, cargo-crap has nothing to score against. It is also the wrong tool for a language other than Rust: the parser is syn, and the file walk targets .rs files.
cargo-crap against coverage thresholds and complexity linters
The nearest alternative in a Rust pipeline is a coverage gate alone, typically cargo llvm-cov --fail-under-lines or the equivalent threshold in cargo tarpaulin. The difference in approach is where the gate sits. A line-coverage threshold is a single aggregate number for the whole crate, so a codebase can pass at 85 percent while one 200-line function sits at zero. cargo-crap inverts that: it produces a per-function score and a per-function threshold, so the failure names a location. The cost of the inversion is that you now need a coverage report as an input file rather than as the final step, which is one more artefact to generate and pass around in CI.
The other comparison is against complexity linters such as Clippy's cognitive_complexity lint. Those measure the source alone and run in the normal cargo clippy invocation, with no coverage data required. They will flag a tangled function that happens to be fully tested, which cargo-crap will score at 1.0 times its complexity and may pass. Conversely, a simple function with no tests scores low on a complexity lint and high on CRAP. Neither tool subsumes the other, and the README's own note that CRAP equals CC at full coverage is the clearest statement of where the two overlap.
Editorial conclusion
Adopt cargo-crap if your Rust workspace has functions that are both long and lightly tested and you want a single ranked list of them in CI. Do not adopt it if you need a coverage tool that runs without a pre-generated LCOV file, or if you want a score that accounts for how hard a branch is to reach, because the formula only counts whether it was executed. Before wiring it into a pipeline, run cargo crap --lcov lcov.info once and check which functions land above 30, then run it again without --lcov to see how many of those are complexity rather than coverage problems.
Frequently asked questions
What does the cargo-crap metric measure?
It combines a function's cyclomatic complexity with its test coverage into one number, using the formula CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m). A function with complexity 1 and full coverage scores exactly 1.0, and above complexity of about 30 no coverage value keeps a function under the default threshold of 30.
Does cargo-crap run my tests to get coverage?
No. The README treats cargo-llvm-cov as a separate tool to install once, and cargo-crap consumes the LCOV file that tool produces. The dependency list contains no test runner or coverage instrumentation, so the report has to exist before you invoke cargo crap.
What happens if I run cargo-crap without a --lcov file?
Every function is scored as if it had 0 percent coverage, so the formula collapses to CC² + CC and the whole table reads red. The README describes that mode as useful for a first look at the complexity distribution and states that it is not a CRAP run.
Community notes