Skip to main content

cargo_crap/
lib.rs

1//! Compute the **CRAP** (Change Risk Anti-Patterns) metric for Rust projects.
2//!
3//! The score combines cyclomatic complexity and test coverage into a single
4//! number that is high when code is both hard to understand *and* poorly
5//! tested — the conditions where bugs love to hide.
6//!
7//! ```text
8//! CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m)
9//! ```
10//!
11//! A few properties worth knowing before you use the numbers:
12//!
13//! - A trivial function (CC = 1, 100% covered) always scores **1.0** — the
14//!   lower bound.
15//! - At 100% coverage the quadratic term collapses: **CRAP equals CC**. When
16//!   both columns match, the function is fully covered. Tests are capping the
17//!   damage, but the complexity itself remains.
18//! - Above CC ≈ 30, no amount of coverage keeps the score under the default
19//!   threshold of 30. The formula refuses to certify a monster method as
20//!   clean just because it happens to be tested.
21//!
22//! # Quick start
23//!
24//! The simplest use case is computing the CRAP score for a single function:
25//!
26//! ```rust
27//! use cargo_crap::score::crap;
28//!
29//! // Trivial, fully covered → always 1.0 (the lower bound).
30//! assert_eq!(crap(1.0, 100.0), 1.0);
31//!
32//! // Moderately complex, half covered: 16 × 0.5³ + 4 = 6.0
33//! assert_eq!(crap(4.0, 50.0), 6.0);
34//!
35//! // Savoia & Evans worked example: CC=6, 0% → 6² × 1³ + 6 = 42.0
36//! assert_eq!(crap(6.0, 0.0), 42.0);
37//!
38//! // CC=12, untested → 12² + 12 = 156 — well past the threshold of 30.
39//! assert_eq!(crap(12.0, 0.0), 156.0);
40//! ```
41//!
42//! # Full pipeline — embedding in a custom tool
43//!
44//! The library exposes the same pipeline that the `cargo crap` CLI uses.
45//! You can drive it programmatically to embed CRAP gating into a custom CI
46//! tool, an editor plugin, or an automated refactoring advisor.
47//!
48//! ```no_run
49//! use cargo_crap::{
50//!     complexity, coverage,
51//!     merge::{MissingCoveragePolicy, merge},
52//!     report::{Format, render},
53//!     score::DEFAULT_THRESHOLD,
54//! };
55//! use std::io;
56//!
57//! // 1. Walk the source tree and compute cyclomatic complexity per function.
58//! //    The second argument is a list of glob patterns to exclude.
59//! let fns = complexity::analyze_tree(
60//!     std::path::Path::new("src"),
61//!     &[] as &[&str],
62//! )?;
63//!
64//! // 2. Parse the LCOV report produced by `cargo llvm-cov --lcov`.
65//! let cov = coverage::parse_lcov(std::path::Path::new("lcov.info"))?;
66//!
67//! // 3. Join complexity with coverage. Functions with no coverage data are
68//! //    treated as 0% covered (the pessimistic default, safest for CI gates).
69//! let entries = merge(fns, cov, MissingCoveragePolicy::Pessimistic).entries;
70//!
71//! // 4. Render the human-readable table to stdout.
72//! render(&entries, DEFAULT_THRESHOLD, Format::Human, None, &mut io::stdout())?;
73//!
74//! # Ok::<(), anyhow::Error>(())
75//! ```
76//!
77//! # Threshold gate
78//!
79//! To exit non-zero when any function exceeds a threshold — the standard CI
80//! gate pattern — check the entries yourself:
81//!
82//! ```no_run
83//! use cargo_crap::{
84//!     complexity, coverage,
85//!     merge::{MissingCoveragePolicy, merge},
86//! };
87//!
88//! let fns = complexity::analyze_tree(
89//!     std::path::Path::new("src"),
90//!     &[] as &[&str],
91//! )?;
92//! let cov = coverage::parse_lcov(std::path::Path::new("lcov.info"))?;
93//! let entries = merge(fns, cov, MissingCoveragePolicy::Pessimistic).entries;
94//!
95//! let threshold = 30.0_f64;
96//! let crappy: Vec<_> = entries.iter().filter(|e| e.crap > threshold).collect();
97//! if !crappy.is_empty() {
98//!     eprintln!("{} function(s) exceed CRAP threshold {threshold}:", crappy.len());
99//!     for e in &crappy {
100//!         eprintln!("  {} — CRAP {:.1} ({}:{})", e.function, e.crap,
101//!                   e.file.display(), e.line);
102//!     }
103//!     std::process::exit(1);
104//! }
105//! # Ok::<(), anyhow::Error>(())
106//! ```
107//!
108//! # Baseline comparison (delta mode)
109//!
110//! To detect regressions relative to a saved baseline — the recommended
111//! approach for teams — use [`delta::compute_delta`] after loading a previous
112//! run's JSON output:
113//!
114//! ```no_run
115//! use cargo_crap::{
116//!     complexity, coverage,
117//!     delta::{DEFAULT_EPSILON, compute_delta, load_baseline},
118//!     merge::{MissingCoveragePolicy, merge},
119//!     report::{Format, render_delta},
120//! };
121//! use std::io;
122//!
123//! let fns = complexity::analyze_tree(
124//!     std::path::Path::new("src"),
125//!     &[] as &[&str],
126//! )?;
127//! let cov = coverage::parse_lcov(std::path::Path::new("lcov.info"))?;
128//! let entries = merge(fns, cov, MissingCoveragePolicy::Pessimistic).entries;
129//!
130//! // Load baseline saved by a previous `--format json --output baseline.json` run.
131//! let baseline = load_baseline(std::path::Path::new("baseline.json"))?;
132//! let report = compute_delta(&entries, &baseline, DEFAULT_EPSILON);
133//!
134//! // Exit non-zero if any function regressed.
135//! if report.regression_count() > 0 {
136//!     render_delta(&report, 30.0, Format::Human, None, &mut io::stdout())?;
137//!     std::process::exit(1);
138//! }
139//! # Ok::<(), anyhow::Error>(())
140//! ```
141//!
142//! # Modules
143//!
144//! | Module | Role |
145//! |---|---|
146//! | [`score`] | The CRAP formula and the `Clean`/`Crappy` classifier. No I/O. |
147//! | [`complexity`] | `syn`-based AST walker. Produces `(file, function, span, CC)` per function. |
148//! | [`coverage`] | LCOV parser. Produces `(file, line) → hit-count` maps. |
149//! | [`merge`] | Joins complexity with coverage. Handles all path-matching cases. |
150//! | [`delta`] | Baseline comparison. Computes per-function deltas and regression counts. |
151//! | [`report`] | Renders `Vec<CrapEntry>` or `DeltaReport` as human table, JSON, GitHub annotations, or Markdown. |
152//! | [`config`] | Loads `.cargo-crap.toml` by walking up from CWD. |
153
154pub mod complexity;
155pub mod config;
156pub mod coverage;
157pub mod delta;
158pub mod merge;
159pub mod report;
160pub mod score;