Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,67 @@ 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.6.0] - 2026-06-13

Release making rustqual **self-explaining for human and agent consumers**
(rule cards, `--explain <RULE-ID>`, a never-dead-end CLI) and making **BP-002
honest**: trivial-`Display` detection is now semantic (what the body means)
instead of syntactic (which macro it uses), with a config-declared house-idiom
policy. Driven by two field reports: an agent flailing through
`--explain BP-009` → `os error 2` with no next step, and a BP-002 finding
"fixed" by mechanically rewriting one `write!` into two `write_char` calls —
syntax-based matching invited evasion instead of a decision.

### Added
- **Rule cards — one registry, every reporter** (`src/domain/rule_cards/`).
One card per catalog rule (id, title, what it detects, why it matters, fix
forms, the copyable suppression marker, the governing config knob). The
SARIF `tool.driver.rules` table renders from it (a sync test pins set
equality), `--explain` renders from it, and the compact findings view takes
its titles from it.
- **`--explain <RULE-ID>`** prints the rule card, case-insensitively
(`rustqual --explain bp-009` works). Dynamic hierarchical ids resolve to
their longest registered prefix card — `architecture/pattern/<name>` and
`architecture/trait_contract/<check>` are exactly what findings print, so
they explain as their family card. The natural first guess of every
consumer now succeeds.
- **`--explain` never dead-ends.** An argument that is no rule id, not
`allow`, and not a readable file prints the three explain modes with
copyable examples instead of stopping at the bare OS error; an argument
matching a `qual:allow` *target* name (`--explain boilerplate`) gets a hint
naming its dimension and the allow guide. The clap help text names all
three modes.
- **`[boilerplate].accepted_display_idioms`** (fixed vocabulary: `write_str`,
`write_char`, `write_macro`, `delegation`; default empty) declares the
project's trivial-`Display` house idiom as policy. Validation is fail-loud
at startup: an unknown entry is a config error naming the offender and the
valid vocabulary — never a silent no-op.

### Changed
- **BP-002 matches semantically.** Any branch-free `fmt` body consisting only
of formatter write ops — `write!(f, …)`/`writeln!(f, …)` (the first macro
argument must be the formatter; writing to any other target is real
logic), `f.write_str`, `f.write_char`, `Display::fmt` delegation — is
trivial, multi-statement bodies included.
The evasion rewrite (two `write_char` statements) is the same finding; the
semantically identical `f.write_str(&self.0)` newtype form no longer slips
through unmatched. With accepted idioms declared, the rule enforces
house-idiom consistency (a stray `write!` still fires) instead of going
silent. *Projects with hand-rolled write-only Displays get new BP-002
findings until they pick a form — declare the idiom, derive, or generate
via a local `macro_rules!`.*
- **BP-002's suggestion names all fix forms** regardless of workspace
dependencies (availability ≠ usability under a dependency policy): derive
(`derive_more::Display` when `suggest_crates`), the accepted-idiom config,
and a local `macro_rules!` as the dependency-free DRY option; it ends with
`rustqual --explain BP-002`.
- **The findings epilogue is tail-proof.** It now names both next steps —
`rustqual --explain <RULE-ID>` (rule details) and `rustqual --explain allow`
(suppression syntax) — and renders last, so it survives an agent's
`| tail` truncation. Boilerplate entries in the compact findings view carry
the registry title (`BP-009 Struct update boilerplate`) instead of the bare
pattern id.

## [1.5.1] - 2026-06-09

Release fixing a class of **macro-token blindness** that ran across the
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.5.1"
version = "1.6.0"
edition = "2021"
description = "Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion book/code-reuse.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ rustqual covers four families:
| `DRY-004` | Wildcard import (`use module::*;`) |
| `DRY-005` | Repeated match pattern across functions (≥3 arms identical, ≥3 instances) |
| `BP-001` | Trivial `From` impl (derivable) |
| `BP-002` | Trivial `Display` impl |
| `BP-002` | Trivial `Display` impl — semantic: any branch-free, write-only `fmt` body; house idioms declarable via `[boilerplate].accepted_display_idioms` |
| `BP-003` | Trivial getter/setter (consider field visibility) |
| `BP-004` | Builder pattern — consider `derive_builder` or similar |
| `BP-005` | Manual `Default` impl (derivable) |
Expand Down
19 changes: 19 additions & 0 deletions book/reference-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,29 @@ enabled = true
```toml
[boilerplate]
enabled = true
# patterns = ["BP-002", "BP-007"] # enable-list; empty = all BP-* active
# suggest_crates = true # name crates (derive_more, thiserror) in suggestions
# accepted_display_idioms = ["write_str"] # declare the trivial-Display house idiom
```

The full `BP-*` family. Disable if your project deliberately avoids derive macros.

`accepted_display_idioms` declares which trivial-`Display` forms are your
project's *stated policy* rather than boilerplate — useful under a
dependency policy that rules out `derive_more`. BP-002 matches semantically
(any branch-free, write-only `fmt` body); a body whose every used idiom is
accepted is silent, while any other trivial form keeps firing, so the rule
enforces the declared idiom instead of going quiet. Fixed vocabulary
(validated fail-loud at startup — a typo is a config error, not a silent
no-op):

| Idiom | Covers |
|---|---|
| `write_str` | `f.write_str(…)` on the formatter |
| `write_char` | `f.write_char(…)` on the formatter |
| `write_macro` | `write!(f, …)` / `writeln!(f, …)` |
| `delegation` | `Display::fmt(&self.x, f)` / `self.x.fmt(f)` |

## `[srp]`

| Key | Default | Meaning |
Expand Down
12 changes: 9 additions & 3 deletions book/reference-rules.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# Reference: rule catalog

Every rule rustqual emits, grouped by dimension. Codes are stable
identifiers — but where they actually surface differs by reporter:
Every rule rustqual emits, grouped by dimension. For any code below,
`rustqual --explain <RULE-ID>` (case-insensitive) prints its full rule card:
what it detects, why it matters, the fix forms, the copyable suppression
marker, and the governing config knob — the catalog and the cards render
from the same registry.

Codes are stable identifiers — but where they actually surface differs by
reporter:

- **SARIF**: every result carries the catalog code as `ruleId` and is
registered in `tool.driver.rules` (architecture also registers
Expand Down Expand Up @@ -57,7 +63,7 @@ For dimension intent and refactor patterns, see the use-case guides linked at th
| Code | Meaning |
|---|---|
| `BP-001` | Trivial `From` impl (derivable) |
| `BP-002` | Trivial `Display` impl (derivable) |
| `BP-002` | Trivial `Display` impl — semantic match: any branch-free, write-only `fmt` body (`write!`/`writeln!`, `f.write_str`, `f.write_char`, `Display::fmt` delegation, multi-statement included); gate forms via `[boilerplate].accepted_display_idioms` |
| `BP-003` | Trivial getter/setter (consider field visibility) |
| `BP-004` | Builder pattern (consider derive macro) |
| `BP-005` | Manual `Default` impl (derivable) |
Expand Down
6 changes: 6 additions & 0 deletions rustqual.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ allow_expect = false
enabled = true

# ── Boilerplate Detection ───────────────────────────────────────────────
#
# BP-002 matches semantically (any branch-free, write-only fmt body).
# accepted_display_idioms declares the house idiom as policy; vocabulary:
# write_str, write_char, write_macro, delegation (validated fail-loud).

[boilerplate]
enabled = true
# patterns = [] # enable-list; empty = all BP-* active
# accepted_display_idioms = [] # e.g. ["write_str"]

# ── SRP (Single Responsibility) ─────────────────────────────────────────

Expand Down
Loading
Loading