Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e5075e3
refactor(shared): consolidate shared analyzer concepts into adapters/…
SaschaOnTour Jun 4, 2026
3571239
refactor(srp)!: collapse file_length baseline/ceiling into one threshold
SaschaOnTour Jun 4, 2026
aa429dd
feat(suppression): parse targeted suppressions allow(dim, target[, =N])
SaschaOnTour Jun 4, 2026
4783ca5
feat(srp): target-aware suppression marking + file_length pin
SaschaOnTour Jun 4, 2026
aaadd74
feat(complexity): per-kind target-aware suppression
SaschaOnTour Jun 4, 2026
f33975a
feat(dry): per-kind target-aware suppression
SaschaOnTour Jun 4, 2026
6fd2827
feat(coupling): per-kind target-aware suppression (module-global)
SaschaOnTour Jun 4, 2026
768e384
feat(architecture): per-rule-family target-aware suppression
SaschaOnTour Jun 4, 2026
13d60d4
feat(test_quality): per-kind target-aware suppression
SaschaOnTour Jun 4, 2026
0c5bfb2
feat(cli): add `rustqual --explain allow` suppression guide
SaschaOnTour Jun 4, 2026
8e0135f
feat(report): fix-first footer pointing at `--explain allow`
SaschaOnTour Jun 4, 2026
c4d927b
refactor(coupling): remove 5 dead blanket allow(coupling) markers
SaschaOnTour Jun 5, 2026
a8a2539
refactor(boilerplate): extract check_trivial_from helpers, drop allow…
SaschaOnTour Jun 5, 2026
249b061
refactor(boilerplate): flatten check_clone_heavy_conversion, drop all…
SaschaOnTour Jun 5, 2026
be047e4
refactor(boilerplate): split check_manual_getter_setter, drop allow(c…
SaschaOnTour Jun 5, 2026
e5457ba
refactor(boilerplate): split check_error_enum_boilerplate, drop allow…
SaschaOnTour Jun 5, 2026
706494f
refactor(boilerplate): split check_builder_boilerplate + share scan_i…
SaschaOnTour Jun 5, 2026
bb7dbd9
refactor(boilerplate): split check_repetitive_match, drop allow(compl…
SaschaOnTour Jun 5, 2026
a491d80
refactor(dry): decompose merge_into_fragments, drop allow(complexity)
SaschaOnTour Jun 5, 2026
29174f7
refactor(coupling): decompose build_module_graph, drop allow(complexity)
SaschaOnTour Jun 5, 2026
aeb9306
refactor(coupling): decompose detect_cycles, drop allow(complexity)
SaschaOnTour Jun 5, 2026
8226c26
refactor(projection): target the two dry markers to boilerplate
SaschaOnTour Jun 5, 2026
be6c0ef
style: rustfmt normalization (toolchain drift)
SaschaOnTour Jun 5, 2026
6b7280c
refactor(iosp): introduce FunctionParts, drop allow(srp) on factory
SaschaOnTour Jun 5, 2026
74ef2a0
refactor(complexity-projection): drop allow(srp) via tuple spec table
SaschaOnTour Jun 5, 2026
a3a2dab
feat(tq): model syn-visitor dispatch as real call-graph edges
SaschaOnTour Jun 5, 2026
6b3549e
feat(srp): trait methods bridge cohesion clusters instead of being ex…
SaschaOnTour Jun 5, 2026
91b18c6
refactor(visitors): drop visit_* from ignore_functions; dogfood compl…
SaschaOnTour Jun 5, 2026
b7ee764
refactor(app): drop main/run from ignore_functions; decompose run()
SaschaOnTour Jun 5, 2026
24e7f34
refactor(config)!: remove the ignore_functions feature entirely
SaschaOnTour Jun 5, 2026
8e7ac32
docs: ignore_functions removal + visitor-analysis fixes (v1.5.0)
SaschaOnTour Jun 5, 2026
58f4829
refactor(normalize): split into module, drop allow(srp) file-length m…
SaschaOnTour Jun 5, 2026
d896ccd
refactor(call-parity): clear CanonicalCallCollector god_struct; pin f…
SaschaOnTour Jun 5, 2026
70dcd24
refactor(call-parity): split calls.rs into a module, drop the file_le…
SaschaOnTour Jun 5, 2026
d3ea256
feat(suppression)!: reject bare allow() for multi-kind dimensions (th…
SaschaOnTour Jun 5, 2026
d7d263e
docs: CHANGELOG — the flip + module splits (v1.5.0)
SaschaOnTour Jun 5, 2026
bf28c3f
feat(suppression): orphan target-awareness + too-loose pin check (v1.…
SaschaOnTour Jun 5, 2026
37eb41a
fix(suppression): address branch review — redesign, 3 orphan bugs, te…
SaschaOnTour Jun 5, 2026
e8a52f1
fix(suppression): second review pass — doc accuracy, +tests, harden S…
SaschaOnTour Jun 5, 2026
f7414a3
fix(structural): targeted markers must not suppress structural findings
SaschaOnTour Jun 5, 2026
b35c5f8
feat(structural): suppressible-by-name structural checks + book doc s…
SaschaOnTour Jun 5, 2026
b5d2ae2
fix(examples): targeted marker in call_parity golden example + guard …
SaschaOnTour Jun 5, 2026
0b53bda
docs: fix stale bare-dim allow() in the ignore_functions migration note
SaschaOnTour Jun 5, 2026
f101358
fix(orphan): carry the target into reports + fix two stale arch rustdocs
SaschaOnTour Jun 5, 2026
074933a
fix(suppression): reject non-finite/negative metric pins + fix coupli…
SaschaOnTour Jun 5, 2026
cad3df9
fix(config): validate [suppression].pin_headroom + fix sdp "metric" w…
SaschaOnTour Jun 5, 2026
1c7f353
fix(report): project orphan kind (stale/too-loose) into JSON, AI, and…
SaschaOnTour Jun 5, 2026
2a50d7f
docs: describe orphans as stale OR too-loose, not stale-only
SaschaOnTour Jun 5, 2026
99ea33d
docs: orphan counter + detector module doc cover too-loose too
SaschaOnTour Jun 5, 2026
0da50c2
docs: detect_orphan_suppressions doc covers stale + too-loose
SaschaOnTour Jun 5, 2026
7c6ccbe
docs: JSON target field doc — reported (stale or too-loose), not stal…
SaschaOnTour Jun 5, 2026
7f53c9a
fix: address Copilot review — no i64 pin saturation, drop bool::then …
SaschaOnTour Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 111 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,113 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.5.0] - 2026-06-05

Minor release with one **breaking** config change. rustqual stops shipping a
blunt name-based ignore for `main`/`run`/`visit_*` and instead makes its own
analyzer pass every dimension on its own merits — which required teaching two
analyzers to see code they were structurally blind to (syn-visitor dispatch and
trait-method cohesion). The `ignore_functions` option is removed. Suppression
markers are also held to a higher bar: targeted pins are orphan-checked against
their own finding-kind, and a metric pin parked too far above the value it
covers is reported.

### Added
- **Structural binary checks are now suppressible by name.** Each structural
check has its own boolean target — `oi`/`sit`/`deh`/`iet` (coupling) and
`btc`/`slm`/`nms` (SRP), the lowercased rule code — so a genuinely-unfixable
finding can be silenced: `// qual:allow(coupling, oi) reason: "orphan rules
force this impl away from the type"`. A targeted marker silences only its own
kind, and marking + orphan-detection share one `structural::target_name_for_code`
mapping. (Before, structural findings could be silenced by *any* targeted
marker of the dimension via a dimension-only `covers()` check — a silent
over-suppression — yet had no way to be named deliberately.)
- **Orphan detection is now target-aware, and reports too-loose metric pins.**
A targeted `// qual:allow(dim, target[=N])` marker is verified against a
finding of *that exact kind* — a `file_length` pin no longer counts a
god-struct finding as a match, so a stale targeted marker surfaces even when
an unrelated finding of the same dimension exists nearby. On top of that, a
metric pin parked more than `pin_headroom` (default **10%**) above the value
it covers is reported as an `ORPHAN_SUPPRESSION` — *"too-loose … tighten to
~value or remove"* — so a pin can no longer silently absorb regressions up to
a far-away ceiling. A pin that re-fires (below its value) is left untouched,
not flagged. New `[suppression].pin_headroom` knob (default `0.10`). Every
reporter projects the orphan's `kind` (stale vs too-loose) and its targeted
finding-kind: text/SARIF/GitHub/AI/HTML lead with the status word, and the
JSON output gains stable `kind` (`"stale"`/`"too_loose"`) and `target`
(`"file_length=400"`) fields so CI consumers branch on the remedy without
parsing the message.

### Removed
- **BREAKING: the `ignore_functions` config option is gone.** It excluded
matching functions from *every* dimension — a far broader effect than its
stated purpose (papering over call-graph blindness for trait-dispatched
methods), and the single largest hidden suppression in a project. A
`rustqual.toml` that still sets `ignore_functions` now fails to parse
(`deny_unknown_fields`). **Migration:** delete the key. For the rare function
that genuinely cannot be analyzed, use a targeted `// qual:allow(<dim>, <target>)`
(or `// qual:allow(iosp)`), `// qual:api`, `// qual:test_helper`, or
`exclude_files` instead.
- **BREAKING: a bare `// qual:allow(<dim>)` is rejected for any dimension with
targets** (srp, complexity, dry, coupling, architecture, test_quality). It
would silence *every* finding of that dimension — too blunt — so it must name
a target: `allow(srp, god_struct)`, `allow(complexity, max_cyclomatic=20)`,
`allow(srp, file_length=400)`, … The error lists the valid targets. `iosp`
(no targets) keeps its bare form; multi-dimension blanket markers
(`allow(a, b)`) are gone — use one marker per dimension. **Migration:** add the
target you mean (`rustqual --explain allow` lists them).
- **BREAKING (config): `[srp].file_length_baseline` / `file_length_ceiling` are
replaced by a single `[srp].file_length`** (default `300`, strict `>`). The
old baseline→ceiling score ramp was cosmetic (the SRP score is count-based);
`SRP-002` now simply fires above `file_length`. The matching `[tests]` knob
is likewise a single `file_length`. **Migration:** a `rustqual.toml` setting
the removed keys fails to parse (`deny_unknown_fields`) — replace them with
`file_length`.

### Changed
- **TQ-003 (untested) now models syn-visitor dispatch as real call-graph
edges.** Previously a visitor's `visit_*` overrides and their helper methods
looked unreachable from tests (the static call graph can't follow syn's
`visit_block → visit_expr` dispatch), and the gap was hidden by seeding every
ignored function as "implicitly tested." That blanket assumption is replaced:
for each visitor type the helper methods its overrides call are recorded, and
at each drive-site (`x.visit_block(..)`, `syn::visit::visit_*(&mut x, ..)`,
`visit_all_files(.., &mut x)`) the driven type is resolved locally and
`driver → helpers` edges are added. Testedness now flows only when a test
actually drives the visitor — an undriven visitor is still flagged.
- **SRP cohesion (LCOM4) is more faithful to the canonical metric.** Two fixes,
both monotonic (they only *merge* components, never create new findings):
- Non-mechanical trait methods (e.g. a `syn::Visit` impl) now *bridge* the
inherent methods they tie together — via their field footprint **and** the
methods they call — without being counted as responsibility nodes. This
stops a struct whose core logic lives in a behavior trait (visitor, walker)
from fragmenting into false god-structs. Mechanical traits
(`Display`/`Debug`/`From`/`Serialize`/`PartialEq`/`Hash`/`Default`/`Clone`/…)
stay excluded so they can't mask genuine god-structs.
- Methods connected by a `self.other()` call are now unioned, matching the
canonical LCOM4 definition (methods cohere when they share a field **or**
call one another).

### Internal
- `main`, `run`, and all `visit_*` methods are no longer name-ignored; every
function in rustqual is analyzed by its own rules. The fat AST visitors
(`Normalizer`, `BodyVisitor`, and several collectors) were refactored to
per-node category dispatch, and the composition root `run()` was decomposed
into phase operations — so rustqual dogfoods its own complexity, IOSP, and
cohesion rules on its own visitor code.
- The two longest files were split into directory modules by concern
(`shared/normalize/` and `call_parity_rule/calls/`), each well under the SRP
file-length baseline — eliminating their blanket `allow(srp)` markers by real
refactoring rather than a pin. rustqual now carries **zero** production `srp`
suppressions.
- `domain::SuppressionTarget` is now a sum type (`Metric { name, pin } |
Boolean { name }`) so the "metric ⇒ has a pin, boolean ⇒ has none" invariant
is unrepresentable otherwise; `suppresses()` matches the variant directly.
The orphan detector was split into `orphan_suppressions/{mod,positions}.rs`
(decision vs. enumeration), made target-aware per finding-kind, and fixed so
a pin on an inactive SRP component, a module-global coupling pin, or a
magic-number marker is judged correctly rather than mis-reported.

## [1.4.2] - 2026-06-04

Patch release: a false-positive fix found while applying 1.4.1. SIT stops
Expand Down Expand Up @@ -140,7 +247,7 @@ never-read `[tests].max_methods` config key.
## [1.4.0] - 2026-06-02

Minor release: **quality checks now run on test code.** DRY (duplicate-function
DRY-001, code-fragment DRY-004, repeated-match DRY-005), function-length
DRY-001, code-fragment DRY-003, repeated-match DRY-005), function-length
(LONG_FN), and SRP file-length (SRP_MODULE) previously skipped
`#[cfg(test)]`/test files. They now analyze test code too, so duplicated test
helpers, copy-pasted arrange/assert blocks, overlong test fns, and oversized
Expand All @@ -151,9 +258,9 @@ production values.
- **BREAKING (config):** the `[duplicates] ignore_tests` field is **removed**.
Because `[duplicates]` uses `deny_unknown_fields`, a `rustqual.toml` that
still sets `ignore_tests` will now fail to parse — delete the line. There is
no replacement: DRY-001/004/005 always run on tests. `detect_repeated_matches`
no replacement: DRY-001/003/005 always run on tests. `detect_repeated_matches`
no longer takes a config argument.
- DRY-003 (wildcard imports) remains test-exempt by its own logic, unchanged.
- DRY-004 (wildcard imports) remains test-exempt by its own logic, unchanged.
- **LONG_FN now applies to test functions** at the new
`[tests].max_function_lines` threshold (an `Option` defaulting to
`[complexity].max_function_lines` = production, 60). Large table-driven tests
Expand Down Expand Up @@ -274,7 +381,7 @@ Failing-first regression tests in `src/adapters/shared/tests/cfg_test.rs`,
`src/` (`src/foo/tests/bar.rs`, a unit-test submodule reached via
`#[cfg(test)] mod`). All four DRY file collectors — `FunctionCollector`
(duplicate hashing, DRY-001), `FragmentCollector` (repeated fragments,
DRY-004), `MatchPatternCollector` (repeated matches, DRY-005), and
DRY-003), `MatchPatternCollector` (repeated matches, DRY-005), and
`WildcardCollector` (wildcard imports) — no longer apply their own
`/tests/` path strings; they consult the shared `cfg_test_files` set
(integration dirs + `#![cfg(test)]` + `#[cfg(test)] mod` chains), so a
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rustqual"
version = "1.4.2"
version = "1.5.0"
edition = "2021"
description = "Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture"
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions book/adapter-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ inherited-default UFCS form. Workarounds are listed inline:
both paths, but the call-graph edge ends up under the short
`<method>:op` form. Workaround: write the impl at the file-level
qualified path (`impl outer::Hidden { … }`) so impl-canonical
and caller-canonical agree, or `qual:allow(architecture)` at the
and caller-canonical agree, or `qual:allow(architecture, call_parity)` at the
call-site.

4. **Public function re-exports.** `mod private { pub fn op() {} }
Expand Down Expand Up @@ -228,7 +228,7 @@ Globs match against the *module path* (with `crate::` stripped), not the layer n
For ad-hoc per-function suppression:

```rust
// qual:allow(architecture) — internal capability, intentionally MCP-only
// qual:allow(architecture, call_parity) reason: "internal capability, intentionally MCP-only"
pub fn admin_purge() { /* … */ }
```

Expand Down
6 changes: 3 additions & 3 deletions book/architecture-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ enabled = true

## Suppression

Architecture is suppression-resistant by design. The `// qual:allow(architecture)` annotation works at the import site or item, but it counts hard against `max_suppression_ratio`, and you should leave a `reason:` rationale in the comment block:
Architecture is suppression-resistant by design. The `// qual:allow(architecture, <family>)` annotation works at the import site or item — name the rule family (`forbidden`, `layer`, `call_parity`, `pattern`, `trait_contract`) — but it counts hard against `max_suppression_ratio`, and a `reason:` is mandatory:

```rust
// qual:allow(architecture) — port adapter must call into the registry directly
// here for serialization round-trip; pure domain accessor would lose ordering.
// qual:allow(architecture, forbidden) reason: "port adapter must call the registry
// directly for serialization round-trip; a pure domain accessor would lose ordering."
use crate::adapters::registry::lookup;
```

Expand Down
6 changes: 3 additions & 3 deletions book/code-reuse.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ By default, the dead-code analysis treats a package's `tests/**` files as call-s
Replace with explicit imports. If a `prelude::*` is unavoidable (some crates require it), suppress narrowly:

```rust
// qual:allow(dry) — diesel requires this prelude for query DSL
// qual:allow(dry, wildcard_imports) reason: "diesel requires this prelude for query DSL"
use diesel::prelude::*;
```

Expand Down Expand Up @@ -142,7 +142,7 @@ Most thresholds are tuned to be opinionated by default. Loosen them via `--init`
For genuine cases where suppression is right:

```rust
// qual:allow(dry) — keeping this duplicate temporarily; consolidating in PR-345
// qual:allow(dry, duplicate) reason: "keeping this duplicate temporarily; consolidating in PR-345"
fn old_path() { /* … */ }

// qual:api — public-API entry, callers outside this crate
Expand All @@ -155,7 +155,7 @@ pub fn build_test_config() -> Config { /* … */ }
fn encode(v: &Value) -> Vec<u8> { /* … */ }
```

`qual:api`, `qual:test_helper`, and `qual:inverse` don't count against `max_suppression_ratio`. `qual:allow(dry)` does.
`qual:api`, `qual:test_helper`, and `qual:inverse` don't count against `max_suppression_ratio`. `qual:allow(dry, …)` does.

Full annotation reference: [reference-suppression.md](./reference-suppression.md).

Expand Down
17 changes: 12 additions & 5 deletions book/coupling-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ check_iet = true

**SDP violation**: invert the dependency — typically by introducing a trait in the stable module that the unstable one implements. The stable module then knows nothing about the unstable one.

**Orphaned impl**: move the `impl` block next to the type definition, or — if it's a trait impl that *can't* live there (orphan rules) — accept it and add `// qual:allow(coupling)`.
**Orphaned impl**: move the `impl` block next to the type definition, or — if it's a trait impl that *can't* live there (orphan rules) — accept it and add `// qual:allow(coupling, oi) reason: "orphan rules force this impl away from the type"`.

**Single-impl trait**: inline the trait. Most "interface for testability" cases can be replaced by direct dependency injection of the concrete type, or by a trait that has more than one real impl somewhere in the codebase.

Expand All @@ -83,16 +83,23 @@ check_iet = true

## Suppression

Coupling warnings are *module-level*, not function-level. The `qual:allow(coupling)` annotation on a single function doesn't silence them — that's intentional. To suppress a coupling finding for a whole module:
Coupling **metric** findings (instability, fan-in, fan-out) are *module-level*, not function-level. Suppress one for a whole module with an inner doc-comment that names the metric — a bare `allow(coupling)` is rejected since coupling has targets:

```rust
// At the top of the module file:
//! qual:allow(coupling) — orchestration layer, intentionally depends on every adapter.
//! qual:allow(coupling, max_instability=0.95) reason: "orchestration layer, intentionally depends on every adapter."
```

Inner doc-comment form (`//!`) attaches to the module, not to a single item.
The inner `//!` form attaches to the module, not a single item; the pin re-fires if the metric climbs past it.

For structural-binary checks (`OI`, `SIT`, `DEH`, `IET`) which target specific items, use `// qual:allow(coupling)` at the impl/trait/use site.
The structural-binary checks each have their own boolean target named by the lowercased code — `oi`, `sit`, `deh`, `iet` (and `btc`, `slm`, `nms` on the SRP side). Prefer fixing the smell (move the impl, inline the single-impl trait, replace the downcast), but for the genuinely-unfixable case — an impl forced away by orphan rules, a single-impl trait that is a real API boundary, a `downcast` plugin seam — name it at the item:

```rust
// qual:allow(coupling, sit) reason: "public extension point; second impl lives downstream"
pub trait Plugin { /* … */ }
```

(`SIT` already tolerates `#[cfg(test)]` test doubles, so test mocks don't need a marker.)

## Related

Expand Down
2 changes: 1 addition & 1 deletion book/function-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ For functions you genuinely cannot refactor right now (legacy entry points, gene
// qual:allow(iosp) — match-dispatcher; arms intentionally inlined for codegen
fn dispatch(cmd: Command) -> Result<()> { /* … */ }

// qual:allow(complexity) — large lookup table; splitting hurts readability
// qual:allow(complexity, max_cyclomatic=20) reason: "large lookup table; splitting hurts readability"
fn rule_table() -> &'static [Rule] { /* … */ }

// qual:allow(unsafe) — FFI boundary, audited 2026-Q1
Expand Down
16 changes: 7 additions & 9 deletions book/module-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ rustqual checks this mechanically through SRP — Single Responsibility Principl
| 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-002` | Module file too long | fires above 300 production lines |
| `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.
Expand All @@ -40,10 +40,9 @@ The verbose output names each cluster so the refactor is mechanical:

## 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:
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 threshold:

- `file_length_baseline = 300` — soft warn
- `file_length_ceiling = 800` — hard finding
- `file_length = 300` — `SRP-002` fires when production lines exceed it (strict `>`)

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.

Expand All @@ -57,8 +56,7 @@ enabled = true
# max_fan_out = 10
# max_parameters = 5
# lcom4_threshold = 2
# file_length_baseline = 300
# file_length_ceiling = 800
# file_length = 300
# max_independent_clusters = 2
# min_cluster_statements = 5
# smell_threshold = 0.6 # composite score for SRP-001
Expand Down Expand Up @@ -112,16 +110,16 @@ fn render(opts: &RenderOptions) { /* … */ }
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.
// qual:allow(srp, file_length=650) reason: "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
// qual:allow(srp, god_struct) reason: "public-API struct, fields are stable and intentional"
pub struct Config { /* 18 fields */ }
```

Expand Down
Loading
Loading