cargo-grip4rust
How much can tests grab onto your Rust codebase?
cargo-grip4rust is a static analysis tool that measures testability — how many pure functions, public entry points, trait seams, and injected dependencies a codebase exposes for testing. It produces a single grip score (0–100) with a per-module and per-function breakdown.
The problem
Coverage tells you what code was exercised, not how hard it was to write the tests. A codebase can have 95% coverage and be a nightmare to test:
- Functions that construct their own dependencies (
Database::new("prod")instead ofself.db.query(...)) - Concrete types everywhere, no trait seams for test doubles
- Hidden I/O, time queries, randomness smuggled into function bodies
- Side effects mixed with computation — you can't test logic without mocking the world
- Everything private — zero public surface for test entry points
grip measures the root cause, not the symptom.
The formula, briefly
grip = 100 × (0.30 × pure_ratio + 0.20 × public_ratio + 0.25 × trait_ratio + 0.25 × avg_contribution)
Every function also gets its own absolute contribution in [0.0, 1.0]
(grip_absolute/grip_normalized in JSON output), and every module/repo
gets grip_absolute_total — the sum across every function in scope.
Full derivation of every term, every weight, and the structural rules
grip uses to detect hidden dependencies without a denylist:
docs/FORMULA.md.
Documentation
| Doc | What's in it |
|---|---|
docs/ARCHITECTURE.md |
How a grip invocation flows through the code, module by module. |
docs/FORMULA.md |
Every scoring term, in full, kept in sync with src/. |
docs/ADRs/ |
Why the codebase is shaped the way it is. |
ROADMAP.md |
What's shipped, what's next. |
OPEN_POINTS.md |
Known gaps, deliberately deferred. |
CHANGELOG.md |
Release history. |
Installation
Usage
Arguments:
| Argument | Description |
|---|---|
[PATH] |
Path to Rust crate or workspace root (default: .) |
Options:
| Option | Description |
|---|---|
--json |
Emit structured JSON output |
--threshold N |
Exit non-zero if overall grip score < N. Alias: --min-score |
--verbose |
Per-function detail: purity, seam, hidden deps, contribution, labels |
-h, --help |
Print help |
-V, --version |
Print version |
Output
cargo-grip4rust 0.6.0 -- .
══════════════════════════════════════════════════════
Overall grip score: 60 / 100
Absolute grip total: 63.07
Public surface: 28 items
Total functions: 101
Probably pure: 67 / 101 (66.3%)
Trait methods: 18 / 92 impl methods are trait-bound (38.2%)
Hidden deps: avg 37.93 — 4.0% clean (62.4% avg contribution)
Per module:
. grip: 60 pure: 66.3% pub: 24 traits: 38.2% clean: 4.0% ⚠️
traits grip: N/A pure: 0.0% pub: 4 traits: N/A clean: 0.0%
(grip's own source, analyzed by itself — N/A on the traits module is
the zero-function case: grip_score is Option<u32>, None rather than
a misleading default when there's nothing to score.)
Verbose output (--verbose)
grip 0.6.0 -- my-crate — verbose
══════════════════════════════════════════════════════
timer.rs:
schedule_round_timeout pure: no seam: no hidden: 2 contr: 0% [Instant::now, thread::sleep] ❌
compute_timeout_ms pure: yes seam: no hidden: 0 contr: 95% [-] ✅
reset_timer pure: no seam: no hidden: 1 contr: 12% [Instant::now] ⚠️
What the score means
| Range | Meaning |
|---|---|
| 80–100 | High grip. Tests can reach most behavior through pure, seam-bound, injection-friendly code. |
| 50–79 | Moderate grip. Some modules have concrete dependencies or missing seams. |
| 20–49 | Low grip. Most logic mixes side effects with computation, hardcodes dependencies. |
| 0–19 | Minimal grip. The codebase resists testing at every level — every function constructs its own world. |
Hidden dependency detection, without a denylist
grip does not maintain a list of known third-party function/type names.
Instead it uses structural rules — Type::method(...) where Type starts
uppercase and isn't a std allocator, self.concrete_field.method(...)
where the field isn't a trait object, and a handful of known std/core
module calls. This catches StripeGateway, TcpStream, redis::Client,
or any other concrete dependency regardless of crate. Full rule table in
docs/FORMULA.md.
Limitations
- Purity is a heuristic.
gripclassifies functions by signature and body patterns, not by type inference. It makes mistakes at the margin. - No cross-crate analysis. Struct fields from external crates are not resolved —
self.field.method()detection works only when both struct and impl are in the same file. - No inter-procedural tracking. A function that receives a constructed dependency from its caller appears clean.
- No runtime or coverage data.
gripmeasures testability, not testing. Use a coverage tool alongside it. - Single-segment trait ambiguity.
impl Display for Xwithuse std::fmt::Displayis correctly excluded.impl Display for Xwithout the import relies on the known-foreign list.
See docs/ADRs/ADR-AstOnlyNoTypeResolution.md
for why these are accepted rather than fixed outright.
License
MIT — see LICENSE.