# rustqual
[](https://github.com/SaschaOnTour/rustqual/actions/workflows/ci.yml)
[](https://crates.io/crates/rustqual)
[](https://opensource.org/licenses/MIT)
**A structural quality guardrail for Rust — with AI coding agents specifically in mind.** rustqual scores your code across seven dimensions (IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture) and combines them into one quality number. Equally useful for senior teams enforcing architecture in CI.
What sets it apart from clippy and other Rust linters: rustqual reasons across files and modules, not just within functions. Its architecture rules and call-parity check verify properties that span an entire codebase, which is where most real drift happens.
It catches what AI agents consistently produce and what tired humans consistently miss: god-functions that mix orchestration with logic, copy-paste-with-variation, tests without assertions, architectural drift across adapter layers (CLI, MCP, REST, …), and dead code that piles up after a refactor pivot.
## What it looks like
```bash
$ cargo install rustqual
$ rustqual .
── src/order.rs
✓ INTEGRATION process_order (line 12)
✓ OPERATION calculate_discount (line 28)
✗ VIOLATION process_payment (line 48) [MEDIUM]
═══ Summary ═══
Functions: 24 Quality Score: 82.3%
IOSP: 85.7%
Complexity: 90.0%
DRY: 95.0%
SRP: 100.0%
Test Quality: 100.0%
Coupling: 100.0%
Architecture: 100.0%
4 quality findings. Run with --verbose for details.
```
Exit code `1` on findings — drop it into CI without ceremony.
## Why it exists
If you've used Claude Code, Cursor, GitHub Copilot, or Codex on Rust projects, you've seen the same patterns:
- Functions that mix orchestration ("call helper, if/else, call another helper") with logic — hard to test, hard to refactor.
- Copy-paste with minor variation when asked to "do the same for X" instead of extracting an abstraction.
- Tests that exercise code without checking it (`#[test] fn it_works() { run_thing(); }`) — coverage looks good, real coverage is zero.
- Architectural drift: new functionality lands in one adapter (CLI, MCP, REST) and silently misses the others.
rustqual catches all of these mechanically. Wired into the agent's feedback loop (CI, hooks, instruction file), the agent self-corrects. Reviewer time goes to the actual logic, not to spotting fake tests and inlined god-functions.
The same checks help senior teams enforce architecture decisions in CI — the layer rules and forbidden-edge rules don't care whether the code came from a human or an LLM. They keep the codebase coherent over time.
rustqual addresses each of these patterns through a separate quality dimension. Each is independently tunable; together they produce one aggregated quality score.
## Seven quality dimensions
| **IOSP** | Function separation: every function is either Integration (orchestrates) or Operation (logic), never both. From Ralf Westphal's Flow Design. |
| **Complexity** | Cognitive/cyclomatic complexity, magic numbers, nesting depth, function length, `unsafe`, error-handling style. |
| **DRY** | Duplicate functions, fragments, dead code, boilerplate (10 BP-* rules), repeated match patterns. |
| **SRP** | Struct cohesion (LCOM4), module length, function clusters, structural method-checks (BTC, SLM, NMS). |
| **Coupling** | Module instability, circular deps, Stable Dependencies Principle, structural checks (OI, SIT, DEH, IET). |
| **Test Quality** | Assertion density, no-SUT tests, untested functions, optional LCOV-based coverage gaps. |
| **Architecture** | Layer rules, forbidden edges, symbol patterns, trait contracts, **call parity** across adapters. |
Each dimension contributes to the aggregated quality score with a configurable weight (defaults to a balanced split summing to 1.0). Each dimension can also be tuned or disabled in `rustqual.toml` — full reference: [book/reference-configuration.md](./book/reference-configuration.md).
## What's unusual: call parity
Most architecture linters prove what *can't* be called (containment: "domain doesn't import adapters"). rustqual's `call_parity` rule additionally proves what **must** be called — that several adapter modules *collectively cover every public capability* of a target module.
```toml
[architecture.call_parity]
adapters = ["cli", "mcp"]
target = "application"
```
Four checks under one rule, all anchored at the boundary (the first call from an adapter into the target layer):
- **Check A** — every adapter must delegate. A CLI command that doesn't reach into the application layer is logic in the wrong place.
- **Check B** — every application capability touched by some adapter must be touched by every adapter (or be a genuine orphan).
- **Check C** — each adapter handler should reach exactly one target touchpoint; multi-touchpoint handlers risk silent divergence between adapters. Configurable severity (`single_touchpoint = "off" | "warn" | "error"`, default `warn`).
- **Check D** — when two adapters both reach a target, they must reach it with the same handler count. cli accumulating an alias `cmd_grep` for `cmd_search` while mcp has only `handle_search` is API-surface drift Check D catches.
`#[deprecated]` adapter handlers are excluded from all four checks automatically.
The hard part is making the call graph honest across method chains, field access, trait dispatch, type aliases, framework extractors, and `Self` substitution. rustqual ships a shallow type-inference engine that resolves these cases without fabricating edges. Full write-up: [book/adapter-parity.md](./book/adapter-parity.md).
## Use cases
- **AI-assisted Rust development** — agent instruction file, pre-commit hook, CI quality gate, baseline tracking. → [book/ai-coding-workflow.md](./book/ai-coding-workflow.md)
- **CI/CD integration** — GitHub Actions, SARIF, baseline comparison, coverage. → [book/ci-integration.md](./book/ci-integration.md)
- **Adopting on a large existing codebase** — four staged adoption patterns from "lightest touch" to full enforcement. → [book/legacy-adoption.md](./book/legacy-adoption.md)
- **Function-level quality** (IOSP, complexity, structural method checks). → [book/function-quality.md](./book/function-quality.md)
- **Module-level quality** (SRP, LCOM4, file length). → [book/module-quality.md](./book/module-quality.md)
- **Coupling quality** (instability, SDP, OI/SIT/DEH/IET). → [book/coupling-quality.md](./book/coupling-quality.md)
- **Architecture rules** (layers, forbidden edges, symbol patterns, trait contracts). → [book/architecture-rules.md](./book/architecture-rules.md)
- **Adapter parity** — call parity, the architecture rule that's unique to rustqual. → [book/adapter-parity.md](./book/adapter-parity.md)
- **Code reuse** (DRY, dead code, boilerplate). → [book/code-reuse.md](./book/code-reuse.md)
- **Test quality** (assertions, untested functions, coverage). → [book/test-quality.md](./book/test-quality.md)
## What is IOSP?
The **Integration Operation Segregation Principle** ([Ralf Westphal's Flow Design](https://flow-design.info/)) says every function should be:
- **Integration** — orchestrates other functions. No own logic.
- **Operation** — contains logic. No calls to your own project's functions.
A function that does both is a **Violation** — that's the smell to fix.
```
┌─────────────┐ ┌─────────────┐ ┌────────────────────┐
│ Integration │ │ Operation │ │ ✗ Violation │
│ │ │ │ │ │
│ calls A() │ │ if x > 0 │ │ if x > 0 │
│ calls B() │ │ y = x*2 │ │ r = calc() │ ← mixes both
│ calls C() │ │ return y │ │ return r + 1 │
└─────────────┘ └─────────────┘ └────────────────────┘
```
Out of the box rustqual is forgiving where it matters — closures, iterator chains, match-as-dispatch, and trivial self-getters are all leniency cases. Tighten with `--strict-closures` / `--strict-iterators` if you want them counted as logic. Full breakdown: [book/function-quality.md](./book/function-quality.md).
## Install & first run
```bash
cargo install rustqual
cd your-rust-project
rustqual
```
Walkthrough with `--init`, `--no-fail`, `--findings`, the common flags, and the first-run output: [book/getting-started.md](./book/getting-started.md). Full flag reference: [book/reference-cli.md](./book/reference-cli.md).
## CI integration
Minimal GitHub Actions step:
```yaml
- run: cargo install rustqual
- run: rustqual --format github --min-quality-score 90
```
With coverage and PR annotations:
```yaml
- run: cargo install rustqual cargo-llvm-cov
- run: cargo llvm-cov --lcov --output-path lcov.info
- run: rustqual --diff origin/main --coverage lcov.info --format github
```
For codebases that aren't yet at 100% but want to prevent regression:
```bash
rustqual --save-baseline baseline.json
git add baseline.json && git commit -m "Add quality baseline"
```
```yaml
- run: rustqual --compare baseline.json --fail-on-regression
```
Full patterns: [book/ci-integration.md](./book/ci-integration.md).
## AI coding agent integration
Drop this into `CLAUDE.md`, `.cursorrules`, `.github/copilot-instructions.md`, or whichever instruction file your tool reads:
```markdown
## Code Quality Rules
- Run `rustqual` after making changes. All findings must be resolved before marking a task complete.
- Follow IOSP: every function is either an Integration or an Operation, never both.
- Keep functions under 60 lines and cognitive complexity under 15.
- Don't duplicate logic — extract shared patterns into reusable Operations.
- Don't introduce functions with more than 5 parameters.
- Every test function must contain at least one assertion.
- For public-API functions intentionally untested in this crate, mark with `// qual:api`.
```
The agent gets actionable feedback: rustqual tells it which function violated which principle, so it can self-correct without you having to point each issue out. Full patterns: [book/ai-coding-workflow.md](./book/ai-coding-workflow.md).
## Suppression annotations
For genuine exceptions:
```rust
// qual:allow(iosp) — match dispatcher; arms intentionally inlined
fn dispatch(cmd: Command) -> Result<()> { /* … */ }
// qual:api — public re-export, callers live outside this crate
pub fn parse(input: &str) -> Result<Ast> { /* … */ }
// qual:test_helper — used only from integration tests
pub fn build_test_session() -> Session { /* … */ }
```
`max_suppression_ratio` (default 5%) caps how much code can be under `qual:allow`. Stale suppressions (no matching finding in their window) are flagged as `ORPHAN-001`. Full reference: [book/reference-suppression.md](./book/reference-suppression.md).
## Output formats
`--format <FMT>` — `text` (default), `json`, `github`, `sarif`, `html`, `ai`, `ai-json` all serialise the same findings + summary. `dot` is data-only: it renders the per-function call graph and skips findings / orphan suppressions, so pair it with another format if the run might have diagnostics. Full reference: [book/reference-output-formats.md](./book/reference-output-formats.md).
## Self-compliance
rustqual analyses itself — the full source tree (~2.5k functions across all seven dimensions) reports `Quality Score: 100.0%` with zero findings and zero warnings:
```bash
$ cargo run -- . --fail-on-warnings --coverage coverage.lcov
═══ Summary ═══
Quality Score: 100.0%
IOSP: 100.0%
Complexity: 100.0%
DRY: 100.0%
SRP: 100.0%
Coupling: 100.0%
Test Quality:100.0%
Architecture:100.0%
All quality checks passed! ✓
```
Verified by the integration test suite and CI on every push.
## Build & test
```bash
cargo nextest run # full test suite
cargo run -- . --fail-on-warnings --coverage coverage.lcov # self-analysis
RUSTFLAGS="-Dwarnings" cargo clippy --all-targets # lints (0 warnings)
```
## In use at
- [rlm](https://github.com/SaschaOnTour/rlm) — Rust local memory manager. The reference adopter codebase that prompted the call-parity rule.
- [turboquant](https://github.com/SaschaOnTour/turboquant) — Rust quantitative finance toolkit (in active development).
## Known limitations
1. **Syntactic analysis only.** Uses `syn` for AST parsing. The receiver-type-inference engine (v1.2+) resolves most method-call receivers; what it can't resolve stays unresolved rather than being fabricated.
2. **Macros.** Macro invocations are not expanded. `println!` etc. are special-cased; custom macros producing logic or calls may be misclassified. Configurable via `[architecture.call_parity].transparent_macros`.
3. **Sequential analysis pass.** `proc_macro2::Span` (with `span-locations` enabled for line numbers) is not `Sync`. File I/O is parallelised via `rayon`.
## License
MIT. See [LICENSE](./LICENSE).
## Contributing
Bug reports and feature requests: open an issue at [github.com/SaschaOnTour/rustqual/issues](https://github.com/SaschaOnTour/rustqual/issues). For PRs:
1. `cargo nextest run` — all tests must stay green.
2. `cargo run -- . --fail-on-warnings --coverage coverage.lcov` — the source tree must keep its 100% self-compliance score.
3. `RUSTFLAGS="-Dwarnings" cargo clippy --all-targets` — clippy must stay clean.
4. Update `CHANGELOG.md` for any user-visible change; bump `Cargo.toml` version on release-worthy contributions.
The codebase is its own best reference for IOSP self-compliance and the architecture rules. The CLAUDE.md file documents internal conventions and common pitfalls.