# 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 of `self.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`](docs/FORMULA.md)**.
---
## Documentation
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | How a `grip` invocation flows through the code, module by module. |
| [`docs/FORMULA.md`](docs/FORMULA.md) | Every scoring term, in full, kept in sync with `src/`. |
| [`docs/ADRs/`](docs/ADRs/) | Why the codebase is shaped the way it is. |
| [`ROADMAP.md`](ROADMAP.md) | What's shipped, what's next. |
| [`OPEN_POINTS.md`](OPEN_POINTS.md) | Known gaps, deliberately deferred. |
| [`CHANGELOG.md`](CHANGELOG.md) | Release history. |
---
## Installation
```sh
cargo install cargo-grip4rust
```
## Usage
```sh
cargo grip4rust [OPTIONS] [PATH]
```
**Arguments:**
| `[PATH]` | Path to Rust crate or workspace root (default: `.`) |
**Options:**
| `--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
| 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`](docs/FORMULA.md#structural-hidden-dependency-detection).
---
## Limitations
- **Purity is a heuristic.** `grip` classifies 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.** `grip` measures *testability*, not *testing*. Use a coverage tool alongside it.
- **Single-segment trait ambiguity.** `impl Display for X` with `use std::fmt::Display` is correctly excluded. `impl Display for X` without the import relies on the known-foreign list.
See [`docs/ADRs/ADR-AstOnlyNoTypeResolution.md`](docs/ADRs/ADR-AstOnlyNoTypeResolution.md)
for why these are accepted rather than fixed outright.
---
## License
MIT — see [`LICENSE`](LICENSE).