# Use case: module-level quality
A module is the unit of cohesion. Functions inside it should belong together — same data, same purpose, same change-axis. When a module grows past a few hundred lines or starts hosting unrelated function clusters, you've usually accumulated two responsibilities pretending to be one.
rustqual checks this mechanically through SRP — Single Responsibility Principle — applied to both modules (file-level) and structs (struct-level).
## What goes wrong
- **Bloated modules** — one file accumulates 800 lines and twelve responsibilities. The new feature lands "wherever there's room" instead of in its own file.
- **God-structs** — a struct with 30 fields and 40 methods, slowly growing because nobody wants to break it apart.
- **Disconnected clusters inside one struct** — methods `a/b/c` only touch fields `x/y`, methods `d/e/f` only touch fields `z/w`. They share a name, not a responsibility. The struct should be two structs.
- **Wide signatures** — functions with 6+ parameters carrying ad-hoc context. The shape of the signature *is* the design: too many parameters means a missing struct.
## What rustqual catches
| `SRP-001` | Struct may violate SRP — too many fields/methods or low cohesion (LCOM4 > 2) | composite score, `max_fields = 12`, `max_methods = 20` |
| `SRP-002` | Module file too long | warn at 300 lines, hard at 800 |
| `SRP-003` | Function has too many parameters | `max_parameters = 5` |
`SRP-001` is a composite score: it weighs field count, method count, fan-out, and LCOM4 cohesion together. A struct that's slightly over on fields but cohesive elsewhere doesn't fire; one with disjoint clusters does.
## LCOM4 and responsibility clusters
LCOM4 (Lack of Cohesion of Methods, version 4) detects when a struct's methods form **disjoint groups** that share no fields. If you have:
- `Cluster A`: `methods = [authenticate, refresh_token]`, `fields = [token, expires_at]`
- `Cluster B`: `methods = [render_avatar, set_theme]`, `fields = [avatar_url, theme]`
…rustqual reports two responsibility clusters and flags the struct. The fix is usually to split into two types — one for auth, one for presentation.
The verbose output names each cluster so the refactor is mechanical:
```
✗ SRP-001 UserSession (line 12) — 2 disjoint clusters
cluster 1: [authenticate, refresh_token] over [token, expires_at]
cluster 2: [render_avatar, set_theme] over [avatar_url, theme]
```
## Module length
Production-line counting excludes blank lines, single-line `//` comments, and `#[cfg(test)]` blocks. So a 1000-line file with 600 lines of tests counts as ~400 production lines. The thresholds:
- `file_length_baseline = 300` — soft warn
- `file_length_ceiling = 800` — hard finding
Tests don't push you over the limit. Comments don't either. The number tracks production code only, which is what actually carries the maintenance cost.
## Configure thresholds
```toml
[srp]
enabled = true
# max_fields = 12
# max_methods = 20
# max_fan_out = 10
# max_parameters = 5
# lcom4_threshold = 2
# file_length_baseline = 300
# file_length_ceiling = 800
# max_independent_clusters = 2
# min_cluster_statements = 5
# smell_threshold = 0.6 # composite score for SRP-001
```
`--init` calibrates these to your current codebase metrics so initial findings are realistic.
## Refactor patterns
**Bloated module** — split by responsibility, not by alphabetical order:
```
# Before
src/order/mod.rs # 850 lines: validation + pricing + persistence + email
# After
src/order/mod.rs # re-exports
src/order/validation.rs # 180 lines
src/order/pricing.rs # 220 lines
src/order/persistence.rs # 190 lines
src/order/notification.rs # 140 lines
```
**God-struct with disjoint clusters** — split into types that match the clusters:
```rust
// Before: SRP-001 with 2 clusters
struct UserSession {
token: Token, expires_at: DateTime,
avatar_url: String, theme: Theme,
}
// After
struct AuthSession { token: Token, expires_at: DateTime }
struct UserPresentation { avatar_url: String, theme: Theme }
```
**Parameter sprawl** — context struct or builder:
```rust
// SRP-003
fn render(width: u32, height: u32, dpi: u32, theme: Theme,
locale: Locale, watermark: Option<&str>) { /* … */ }
// Better
fn render(opts: &RenderOptions) { /* … */ }
```
## Suppression
For modules you genuinely can't split right now (legacy entry points, autogenerated config schemas):
```rust
// qual:allow(srp) — entire module is one well-defined responsibility,
// length comes from a 600-line lookup table that has to live together.
```
The annotation goes on the first item of the file (the topmost `pub use`, `pub fn`, struct, etc.) and applies to the file-level finding. Counts against `max_suppression_ratio`.
For struct-level suppression:
```rust
// qual:allow(srp) — public-API struct, fields are stable and intentional
pub struct Config { /* 18 fields */ }
```
## Self-compliance
rustqual itself runs at 100% SRP and ratchets file lengths down aggressively. The CLAUDE.md note worth knowing if you adopt the same workflow:
> New report code pushes files over 300 production line SRP threshold — extract into submodules proactively.
It's cheaper to split early than to do a 5-file rewrite once the threshold trips on the next feature.
## Related
- [function-quality.md](./function-quality.md) — IOSP, complexity, SRP-003 parameter count
- [coupling-quality.md](./coupling-quality.md) — what happens between modules
- [code-reuse.md](./code-reuse.md) — duplication often signals a missing module
- [reference-rules.md](./reference-rules.md) — every rule code with details
- [reference-configuration.md](./reference-configuration.md) — every config option