cargo-stern4rust 0.10.3

Cargo subcommand that fails the build when a Rust workspace breaks a house coding rule, such as AAA test structure or one struct per file
Documentation
# Implemented Features

What `cargo-stern4rust` does today. Anything not listed here is not built, and
the known gaps in what *is* built are in [OPEN_POINTS.md](OPEN_POINTS.md).

## Version 0.10.3

### Rules

- `ordered-imports` judges and quotes an attributed import by its path rather
  than by the `#[cfg(...)]` above it. An item's span starts at its first
  attribute, which had made the gate the sort key.
  [R019]ADRs/R019-ADR-OrderedImportsRule.md

## Version 0.10.2

### Reporting

- A workspace run counts every member's offences. Deduplication is per package
  rather than per run, so two members sharing a package-relative path -- and in
  a large workspace they nearly all share `src/lib.rs` and `tests/all_tests.rs`
  -- no longer lose the second finding. Found in `etheram-embassy`: 390
  offences reported as 364, across four rules.

## Version 0.10.1

### Rules

- `test-free-source` reads a `cfg` predicate with its polarity. `not(test)`
  gates an item out of the test build, making it production-only, and is no
  longer reported as test code in the source tree. `any(test, ...)` is
  unchanged. [R005]ADRs/R005-ADR-TestFreeSourceRule.md

## Version 0.10.0

### Discoverability

- **`--rules`** lists every rule with a line saying what it wants, a scrap of
  source that breaks it and the same scrap put right, then exits without
  scanning. All twenty-one, in registry order, so the listing reads in the same
  order as the roster a report prints. Honours `--format`, so the same listing
  is available as JSON. Nothing is walked, read or judged, so it works in a
  checkout with no manifest worth reading and always exits clean.
  [ADR-RulesExplainThemselves]ADRs/ADR-RulesExplainThemselves.md
- Every rule the registry can hold is listed, including the two that stay out
  of an ordinary run until configured -- `header` and `spdx-matches-manifest`.
  A listing missing a rule reads as a tool that does not have it.
- `--manifest-path` and `--package` carry help text. They were the only two
  flags printing an empty description.

### Rules

- **`imported-paths` names the primitives instead of guessing at them.**
  `u64::from_le_bytes` was reported with the correction `use
  u64::from_le_bytes;`, which does not compile at all. `u8` through `u128`, the
  signed and pointer-sized forms, `f32`, `f64`, `bool`, `char` and `str`,
  matched whole. [R008]ADRs/R008-ADR-ImportedPathsRule.md
- **`ordered-imports` reads case as a shape rather than as a first letter.**
  `WAL_V2_MAGIC` beside `WalRecord` both open with a capital and the two style
  editions order them oppositely, so the pair stands down like every other
  cross-case pair. [R019]ADRs/R019-ADR-OrderedImportsRule.md

## Version 0.9.1

### Rules

- **`tested-public-api` counts two shapes, not three.** A free `pub fn` and a
  `pub fn` in an inherent impl. Neither half of a trait counts: an implementing
  method is reached through the trait rather than named, and a declared one has
  no behaviour behind it to test. Across the family, 72 offences to 34.

## Version 0.9.0

### Reporting

- A rule that could not run **says what it was waiting for**:
  `header (needs --header-file)`,
  `spdx-matches-manifest (needs a `license` field in Cargo.toml)`.
  `Rule::requirement` answers it, so the printer names no rule in particular.
- `tests-layout` reports a registry it cannot parse as **unchecked** rather than
  saying nothing. One offence, not one per sibling file.

### Baselines

- Keyed on file + rule + **subject**, not the description. A rule may reword an
  offence without every baseline in the family reporting it as new.

### Structure

- `src/finding/` is grouped into `model/` and `parsing/`, mirrored in `tests/`.
- `ItemNaming` holds the identifier an item declares and the source-line
  fallback, where three parsers each carried both.

### Library surface

- **Decided: the library is not a public API.** Depend on the binary. Everything
  is `pub` because this repository forbids unit tests, so the surface is as wide
  as the test suite needs and no wider a promise than that.
  [ADR-LibrarySurfaceIsNotAnApi]ADRs/ADR-LibrarySurfaceIsNotAnApi.md

## Version 0.8.0

### Rules

- **`workspace-dependencies`** -- a workspace declares its dependencies once, in
  the root, and every member takes them with `.workspace`. The three
  requirements asked for are one check, because `cargo` refuses to build a
  `workspace = true` the root does not declare. Read from the TOML, since
  resolution erases how a dependency was written; all three dependency tables
  count; intra-workspace path dependencies are included. 27 offences across
  three workspaces, and silent on the five repositories that are not workspaces.
  [R021]ADRs/R021-ADR-WorkspaceDependenciesRule.md

- **`ordered-imports`** -- imports in `src/` run in alphabetic order, reusing
  the `ImportPath` stand-down so the rule and `cargo fmt` cannot disagree.
  Verified against rustfmt by controlled experiment before it was written. 10
  offences, all in `etheram-ibft`. Records that 56% of `src/` import pairs stand
  down. [R019]ADRs/R019-ADR-OrderedImportsRule.md
- **`spdx-matches-manifest`** -- every file's SPDX identifier says what the
  manifest's `license` says. The first rule configured from the package being
  judged rather than the command line, so it needs no `--header-file`; a
  manifest naming no licence leaves it unconfigured rather than offended.
  [R020]ADRs/R020-ADR-SpdxMatchesManifestRule.md

### Structure

- `src/rules/` is grouped into `source/`, `layout/`, `testing/` and `manifest/`,
  with `tests/rules/` mirroring exactly. `directory-file-count` would not allow a
  twenty-first file in one directory, so the tool's own rule decided the layout.

### Reporting

- Offences are deduplicated by content, per package. The workspace question is
  asked once per package root, so a rule whose subject is the workspace rather
  than the package stated each finding once per member -- 36 where there were 6.
  Per package rather than per run because a path is relative to its package: two
  members' `src/lib.rs` are two files with one name, and collapsing across the
  run discarded the second.
- An unconfigured rule now reads `(not configured)` rather than
  `(needs --header-file)`. Two rules can go unconfigured, and the header rule is
  not what the other is waiting on.

## Version 0.7.0

### Rules

- **`declared-by-name`** -- a module is declared by name; `#[path = "..."]` on a
  `mod` is an offence, package-wide. It exists to hold up the convention
  `registry-completeness` resolves by: without it, a file reached through an
  explicit path is reported as never compiled when it compiles fine. Three
  documents described that standard and no rule enforced it. `cfg_attr`-gated
  paths are left alone as the one honest use. Zero offences across ten
  repositories. [R018]ADRs/R018-ADR-DeclaredByNameRule.md
- **`arrange-act-assert`** -- a test reads `Arrange`, then one or more
  `Act`/`Assert` pairs, sections separated by a blank line. The original
  motivating example for the tool, shipped seventeenth. Markers expand to the
  phases they name and the expansion must match, so the merged forms need no
  special case. The markers are comments, which `syn` discards, so the rule
  reads lines and skips every line a literal occupies -- without which it
  reports this crate's own Rust-in-a-raw-string fixtures. 45 offences across the
  family, 0 here. [R017]ADRs/R017-ADR-ArrangeActAssertRule.md
- **`paired-test-file`** -- a `tests/a/b_tests.rs` names the source file it
  exercises and `src/a/b.rs` exists, matched by path rather than by name alone.
  The other side of the pairing from `twin4rust`, and the direction nothing
  checked: a test file outlives the module it was named for silently, still
  compiling and still passing. Found 40 tests in four such files in
  `etheram-ibft/node`, and independently flagged the same eight `grip4rust`
  files `registry-completeness` found never compiled. `all_tests.rs` and
  `_proptest_tests.rs` are exempt; a harness crate skips the rule.
  [R016]ADRs/R016-ADR-PairedTestFileRule.md
- **`test-file-name-postfix`** -- a file under `tests/` holding at least one
  test is named `<X>_tests.rs`. Closes the side of the mirrored-layout pairing
  nothing enforced: `twin4rust` starts from a source file and looks for its
  test, so a file full of tests under any other name was invisible to every tool
  in the family. One direction only. `src/` and registries are exempt, because
  the rule that owns each already reports them and this rule's correction would
  be wrong for both. Zero offences across the family.
  [R015]ADRs/R015-ADR-TestFileNamePostfixRule.md

## Version 0.6.0

### Rules

- **`test-naming`** -- a test's name has at least three underscore-separated
  parts, following `<method>_<conditions>_<result>`. The name and nothing else:
  three earlier versions tried to verify the leading part was the method under
  test and all three accused correct code across 1559 tests, taking 592 offences
  down to 5. [R012]ADRs/R012-ADR-TestNamingRule.md
- **`tested-public-api`** -- every public entry point in `src/` is called by at
  least one test, matched on name and arity. Call sites are gathered from macro
  token streams as well as parsed expressions, since a test's assertion lives in
  `assert!` and never becomes syntax. Found six printer builders shipped
  untested in 0.4.0. Under-reports by design.
  [R013]ADRs/R013-ADR-TestedPublicApiRule.md
- **`pure-traits`** -- a trait declares, it does not implement: no method in a
  `trait` declaration in `src/` may have a default body. The other direction,
  that every implementor implements every method, is `rustc`'s `E0046` and needs
  no rule -- the same split `registry-completeness` made. Four offences across
  the family, three of them in this crate's own `Rule` trait; the other six
  repositories hold 21 trait methods and no defaults at all. Associated types
  and constants may still default; blanket impls are not caught.
  [R014]ADRs/R014-ADR-PureTraitsRule.md

### Trait surface

- `Rule` has no default bodies. Every rule answers all six questions in its own
  file -- `name`, `check`, `check_workspace`, `is_configured`, `requirement`
  and `explanation` -- including the answers that are "nothing", so that a
  rule's file says which question it is about instead of leaving it to be
  inferred from an absence. It began at four methods and 27 such bodies; each
  method added since has been added the same way, breaking every rule at once
  rather than compiling silently.
- `Rule::is_configured` is answered explicitly by every rule. Its old default of
  `true` meant a new rule was configured because nobody said otherwise, which is
  the silent pass this tool exists to catch.

## Version 0.4.0

### Rules

- **`directory-file-count`** -- a directory holds at most 20 `.rs` files, not
  counting its own index. `max-files-per-directory` in `stern4rust.toml` changes
  it; the only rule whose number is taste rather than fact. Ten offences across
  five of eight repositories. [R010]ADRs/R010-ADR-DirectoryFileCountRule.md
- **`directory-subfolder-count`** -- at most 5 subfolders per directory, checked
  at every level. The counterweight, so splitting is not the answer to
  everything. Finds nothing today and says so.
  [R011]ADRs/R011-ADR-DirectorySubfolderCountRule.md

### Autofix

- `--fix` repairs `test-file-structure` offences: item order within a section,
  section order, and blank lines. Everything else is reported untouched.
- Works from `syn` spans, moving whole line ranges without reading them, so a
  string literal holding Rust travels like any other line.
- Only touches files `test-file-structure` governs; never reorders imports,
  which is rustfmt's decision; never loses the preamble or a trailing comment.
- `fixed=N` in the summary and `files_fixed` in the JSON. The checks run against
  the repaired tree, so the report is the truth after fixing.

### Baselines

- `--write-baseline` records the current offences and exits clean;
  `--baseline <PATH>` judges against one. Discovered as
  `stern4rust-baseline.json` beside the manifest when nobody names one.
- Keyed on file + rule + description, **never the line**, so an offence that
  moved is still the same offence.
- Counts rather than a set: fixing one of two identical offences and adding
  another passes; adding a third does not.
- Every run that used one names it and states how many offences it suppressed;
  `baselined=N` in the summary and the JSON. Entries matching nothing are
  reported as stale.
- A baseline that was asked for and is missing is an error, not an empty one.

### Configuration file

- `stern4rust.toml` beside the manifest, holding `header-file`,
  `offence-threshold`, `rules`, `skip`, `exclude`, `baseline`,
  `max-files-per-directory` and `max-subfolders-per-directory`. Every key
  optional.
- The command line wins per setting; for the list settings that is replacement
  rather than merging.
- An unknown key or an unparseable file is an error, not a silently ignored
  line. A missing file is the ordinary case and is not.
- `header-file` resolves relative to the config, so a checkout works anywhere.
- The report names the config it used, and the JSON carries `config_file`.

### Excludability

- `--exclude <GLOB>`, repeatable, matched as a glob against the package-relative
  path, separators normalised so a checked-in pattern works on every platform.
- Every pattern is named in the report with the files it removed, including
  zero; `files_excluded=N` in the summary and an `exclusions` array in the JSON.
- A pattern that matched nothing is called out by name. An uncompilable pattern
  is an error, exit `1`.
- Exclusion happens after the walk rather than by pruning it, because a tree
  that is never entered cannot be counted.

### Rules

- **`registry-completeness`** -- a registry declares every module beside it, so
  no file goes uncompiled. Only the silent direction is checked: `pub mod x;`
  with no `x.rs` is a compile error rustc already reports, while an orphan
  `x.rs` produces no error and no warning. Found 8 never-compiled test files in
  `grip4rust`. [R009]ADRs/R009-ADR-RegistryCompletenessRule.md
- **`imported-paths`** -- a function is called through a name the file imported,
  not through a path. One imported segment stays legal (`use std::fs;` with
  `fs::read_to_string(...)`); a path no import names does not. Type qualifiers
  are left alone, told apart from modules by case. The first rule to apply to
  test files as well as productive ones.
  [R008]ADRs/R008-ADR-ImportedPathsRule.md
- **`module-registry`** -- a `lib.rs` or `mod.rs` outside `tests/` holds the
  header, inner attributes, `extern crate alloc;` and `pub mod` declarations
  only. Catches the re-export shim where it forms.
  [R006]ADRs/R006-ADR-ModuleRegistryRule.md
- **`single-implemented-type`** -- a source file holds at most one type that is
  both declared there and carries an impl block. Plain data declarations are
  unlimited. [R007]ADRs/R007-ADR-SingleImplementedTypeRule.md
- `RegistryPolicy` -- the shared seam letting one parser answer the registry
  question differently for `src/` and `tests/`.
- `Rule::is_configured` -- a rule answers for itself whether it has what it needs,
  so the registry names no rule in particular and keeps one list of rules rather
  than two.

## Version 0.3.0

### Rule selection

- `--rule <NAME>` and `--skip <NAME>`, both repeatable. `--rule` selects,
  `--skip` subtracts, skipping wins. Default is every rule, nothing excluded.
- Every report names the rules it applied, and names the ones it did not with
  the reason: `(skipped)` or `(needs --header-file)`. Three states, not two.
- `rules_applied`, `rules_skipped`, `rules_unconfigured` in the summary line and
  the JSON.
- An unknown rule name and `--rule header` without `--header-file` are both
  errors (exit `1`) rather than switches that quietly match nothing.

## Version 0.2.0

### Rules

- **`readable-source`** — every `.rs` file can be read and parsed. Registered
  first, because its failure explains every other rule's silence about the same
  file. [R004]ADRs/R004-ADR-ReadableSourceRule.md
- **`header`** — every `.rs` file opens with the repository's header, supplied
  by `--header-file`. Exact after normalisation; one offence per file, carrying
  the whole expected header. [R001]ADRs/R001-ADR-HeaderRule.md
- **`test-file-structure`** — header, imports, constants, helpers, tests; each
  group alphabetical; spacing part of the shape. `Helpers` defined by exclusion.
  [R002]ADRs/R002-ADR-TestFileStructureRule.md
- **`test-free-source`** — no `#[test]`, `#[cfg(test)]` or `#[cfg_attr(test, …)]`
  outside `tests/`. Feature gates untouched.
  [R005]ADRs/R005-ADR-TestFreeSourceRule.md
- **`tests-layout`** — one `tests/all_tests.rs`, a `mod.rs` in every subfolder
  on the way down, both holding only the header and `pub mod` declarations.
  [R003]ADRs/R003-ADR-TestsLayoutRule.md

### Analysis

- AST-structural via `syn`, with `span-locations` for item positions. No type
  resolution, no build required — the tool runs on any syntactically valid
  source.
- Comment folding: `//` lines above an item count as part of that item, so a
  documented test is not a spacing offence.
- Predicate scanning for `cfg`/`cfg_attr`, by identifier rather than substring,
  so `any(test, …)` and `not(test)` are caught while `feature = "test"` is not.
- Normalisation once, in `SourceFile`: BOM stripped, `\r` stripped, paths
  forward-slashed. No rule can forget it.
- Whole-package read before judging, which the tree-wide rules require and which
  removes any dependence on walker order.

### Resolution

- `--manifest-path` and `--package`, resolved through `cargo_metadata`.
- `target/`, `.git/` and **any directory holding its own `Cargo.toml`** are not
  walked. A nested package is a different package.

### Reporting

- Text table, columns sized to contents, each offence followed by an indented
  `fix:` line.
- `--format json` — the same run as a document: `files_scanned`,
  `offences_found`, `offences_reported`, `offences_omitted`,
  `offence_threshold`, `rules_broken`, and the offence array. Every key always
  present, so the shape never varies.
- Every offence carries a **required** `correction`, plus optional `subject` and
  `expected`.
- `--offence-threshold N` (default 100, `0` for all) caps what is **printed**.
  The summary, the omitted count and the exit code all see every offence.
- Offences sorted by file, then line, then rule.
- Exit codes `0` / `1` / `2`, with `2` distinct from `1` so a wrapper can tell a
  finding from a broken CI step.

### Packaging

- Publishes as `cargo-stern4rust` with a `stern4rust` library, so cargo resolves
  `cargo stern4rust`.
- `Args::without_cargo_subcommand` — the argv fixup every cargo subcommand
  needs, conditional and positional, so a package named `stern4rust` survives.
- `[package.<name>]` sections — one `stern4rust.toml` at a workspace root
  configures every member and one call judges them all. Precedence runs command
  line, package, root, default, replacing rather than merging; `baseline` and
  `offence-threshold` are root-only; a section naming no scanned package is an
  error.
- Manifest data resolved per package — the licence and the dependency list come
  from the package being walked rather than once for the run, which is what lets
  `spdx-matches-manifest` apply to a workspace at all.
- A roster per package in the report, stated once where they agree so an
  ordinary run is unchanged, and a block each where they differ.

### Project

- 725 tests, one test file per source file, `autotests = false` with a single
  `[[test]] all_tests`.
- Two gates: stage 1 (fmt, clippy `-D warnings`, tests), stage 2 (`crap4rust`,
  `twin4rust`, `iceberg4rust`, and stern4rust against its own tree, built from
  source rather than from whatever is installed).
- Self-gating: a rule that would fail this repository cannot be merged into it.
- Thirty-three ADRs — twenty-one `R` rule ADRs, twelve unnumbered.