rustqual 1.2.4

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
Documentation
# 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

| Rule | Meaning | Default threshold |
|---|---|---|
| `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