Skip to main content

Crate cargo_crap

Crate cargo_crap 

Source
Expand description

Compute the CRAP (Change Risk Anti-Patterns) metric for Rust projects.

The score combines cyclomatic complexity and test coverage into a single number that is high when code is both hard to understand and poorly tested — the conditions where bugs love to hide.

CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m)

A few properties worth knowing before you use the numbers:

  • A trivial function (CC = 1, 100% covered) always scores 1.0 — the lower bound.
  • At 100% coverage the quadratic term collapses: CRAP equals CC. When both columns match, the function is fully covered. Tests are capping the damage, but the complexity itself remains.
  • Above CC ≈ 30, no amount of coverage keeps the score under the default threshold of 30. The formula refuses to certify a monster method as clean just because it happens to be tested.

§Quick start

The simplest use case is computing the CRAP score for a single function:

use cargo_crap::score::crap;

// Trivial, fully covered → always 1.0 (the lower bound).
assert_eq!(crap(1.0, 100.0), 1.0);

// Moderately complex, half covered: 16 × 0.5³ + 4 = 6.0
assert_eq!(crap(4.0, 50.0), 6.0);

// Savoia & Evans worked example: CC=6, 0% → 6² × 1³ + 6 = 42.0
assert_eq!(crap(6.0, 0.0), 42.0);

// CC=12, untested → 12² + 12 = 156 — well past the threshold of 30.
assert_eq!(crap(12.0, 0.0), 156.0);

§Full pipeline — embedding in a custom tool

The library exposes the same pipeline that the cargo crap CLI uses. You can drive it programmatically to embed CRAP gating into a custom CI tool, an editor plugin, or an automated refactoring advisor.

use cargo_crap::{
    complexity, coverage,
    merge::{MissingCoveragePolicy, merge},
    report::{Format, render},
    score::DEFAULT_THRESHOLD,
};
use std::io;

// 1. Walk the source tree and compute cyclomatic complexity per function.
//    The second argument is a list of glob patterns to exclude.
let fns = complexity::analyze_tree(
    std::path::Path::new("src"),
    &[] as &[&str],
)?;

// 2. Parse the LCOV report produced by `cargo llvm-cov --lcov`.
let cov = coverage::parse_lcov(std::path::Path::new("lcov.info"))?;

// 3. Join complexity with coverage. Functions with no coverage data are
//    treated as 0% covered (the pessimistic default, safest for CI gates).
let entries = merge(fns, cov, MissingCoveragePolicy::Pessimistic).entries;

// 4. Render the human-readable table to stdout.
render(&entries, DEFAULT_THRESHOLD, Format::Human, None, &mut io::stdout())?;

§Threshold gate

To exit non-zero when any function exceeds a threshold — the standard CI gate pattern — check the entries yourself:

use cargo_crap::{
    complexity, coverage,
    merge::{MissingCoveragePolicy, merge},
};

let fns = complexity::analyze_tree(
    std::path::Path::new("src"),
    &[] as &[&str],
)?;
let cov = coverage::parse_lcov(std::path::Path::new("lcov.info"))?;
let entries = merge(fns, cov, MissingCoveragePolicy::Pessimistic).entries;

let threshold = 30.0_f64;
let crappy: Vec<_> = entries.iter().filter(|e| e.crap > threshold).collect();
if !crappy.is_empty() {
    eprintln!("{} function(s) exceed CRAP threshold {threshold}:", crappy.len());
    for e in &crappy {
        eprintln!("  {} — CRAP {:.1} ({}:{})", e.function, e.crap,
                  e.file.display(), e.line);
    }
    std::process::exit(1);
}

§Baseline comparison (delta mode)

To detect regressions relative to a saved baseline — the recommended approach for teams — use delta::compute_delta after loading a previous run’s JSON output:

use cargo_crap::{
    complexity, coverage,
    delta::{DEFAULT_EPSILON, compute_delta, load_baseline},
    merge::{MissingCoveragePolicy, merge},
    report::{Format, render_delta},
};
use std::io;

let fns = complexity::analyze_tree(
    std::path::Path::new("src"),
    &[] as &[&str],
)?;
let cov = coverage::parse_lcov(std::path::Path::new("lcov.info"))?;
let entries = merge(fns, cov, MissingCoveragePolicy::Pessimistic).entries;

// Load baseline saved by a previous `--format json --output baseline.json` run.
let baseline = load_baseline(std::path::Path::new("baseline.json"))?;
let report = compute_delta(&entries, &baseline, DEFAULT_EPSILON);

// Exit non-zero if any function regressed.
if report.regression_count() > 0 {
    render_delta(&report, 30.0, Format::Human, None, &mut io::stdout())?;
    std::process::exit(1);
}

§Modules

ModuleRole
scoreThe CRAP formula and the Clean/Crappy classifier. No I/O.
complexitysyn-based AST walker. Produces (file, function, span, CC) per function.
coverageLCOV parser. Produces (file, line) → hit-count maps.
mergeJoins complexity with coverage. Handles all path-matching cases.
deltaBaseline comparison. Computes per-function deltas and regression counts.
reportRenders Vec<CrapEntry> or DeltaReport as human table, JSON, GitHub annotations, or Markdown.
configLoads .cargo-crap.toml by walking up from CWD.

Modules§

complexity
Extract cyclomatic complexity per function, with source spans.
config
Optional persistent configuration via .cargo-crap.toml.
coverage
Parse LCOV coverage reports into a per-file, per-line hit map.
delta
Delta comparison between two cargo-crap runs.
merge
Join complexity data (per-function) with coverage data (per-file) into CRAP entries.
report
Render CrapEntry lists in any of five output formats.
score
CRAP (Change Risk Anti-Patterns) scoring.