rustqual
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
)
)
)
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
| Dimension | What it checks |
|---|---|
| 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.
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.
[]
= ["cli", "mcp"]
= "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", defaultwarn). - Check D — when two adapters both reach a target, they must reach it with the same handler count. cli accumulating an alias
cmd_grepforcmd_searchwhile mcp has onlyhandle_searchis 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.
Use cases
- AI-assisted Rust development — agent instruction file, pre-commit hook, CI quality gate, baseline tracking. → book/ai-coding-workflow.md
- CI/CD integration — GitHub Actions, SARIF, baseline comparison, coverage. → book/ci-integration.md
- Adopting on a large existing codebase — four staged adoption patterns from "lightest touch" to full enforcement. → book/legacy-adoption.md
- Function-level quality (IOSP, complexity, structural method checks). → book/function-quality.md
- Module-level quality (SRP, LCOM4, file length). → book/module-quality.md
- Coupling quality (instability, SDP, OI/SIT/DEH/IET). → book/coupling-quality.md
- Architecture rules (layers, forbidden edges, symbol patterns, trait contracts). → book/architecture-rules.md
- Adapter parity — call parity, the architecture rule that's unique to rustqual. → book/adapter-parity.md
- Code reuse (DRY, dead code, boilerplate). → book/code-reuse.md
- Test quality (assertions, untested functions, coverage). → book/test-quality.md
What is IOSP?
The Integration Operation Segregation Principle (Ralf Westphal's Flow Design) 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.
Install & first run
Walkthrough with --init, --no-fail, --findings, the common flags, and the first-run output: book/getting-started.md. Full flag reference: book/reference-cli.md.
CI integration
Minimal GitHub Actions step:
- run: cargo install rustqual
- run: rustqual --format github --min-quality-score 90
With coverage and PR annotations:
- 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:
&&
- run: rustqual --compare baseline.json --fail-on-regression
Full patterns: 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:
- ------
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.
Suppression annotations
For genuine exceptions:
// qual:allow(iosp) — match dispatcher; arms intentionally inlined
// qual:api — public re-export, callers live outside this crate
// qual:test_helper — used only from integration tests
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.
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.
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:
Verified by the integration test suite and CI on every push.
Build & test
RUSTFLAGS="-Dwarnings"
In use at
- rlm — Rust local memory manager. The reference adopter codebase that prompted the call-parity rule.
- turboquant — Rust quantitative finance toolkit (in active development).
Known limitations
- Syntactic analysis only. Uses
synfor 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. - 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. - Sequential analysis pass.
proc_macro2::Span(withspan-locationsenabled for line numbers) is notSync. File I/O is parallelised viarayon.
License
MIT. See LICENSE.
Contributing
Bug reports and feature requests: open an issue at github.com/SaschaOnTour/rustqual/issues. For PRs:
cargo nextest run— all tests must stay green.cargo run -- . --fail-on-warnings --coverage coverage.lcov— the source tree must keep its 100% self-compliance score.RUSTFLAGS="-Dwarnings" cargo clippy --all-targets— clippy must stay clean.- Update
CHANGELOG.mdfor any user-visible change; bumpCargo.tomlversion 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.