cargo-stern4rust 0.2.0

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
# Rules

Five rules, each independent, each naming itself in the report. This is the
reference; the reasoning behind each one is in its ADR.

Every offence carries a **correction** as well as a description — what to do,
not only what is wrong — and the field is required, so a rule cannot be added
without answering it.

| rule | ADR | needs configuration |
|---|---|---|
| `readable-source` | [R004]ADRs/R004-ADR-ReadableSourceRule.md | no |
| `test-file-structure` | [R002]ADRs/R002-ADR-TestFileStructureRule.md | no |
| `test-free-source` | [R005]ADRs/R005-ADR-TestFreeSourceRule.md | no |
| `tests-layout` | [R003]ADRs/R003-ADR-TestsLayoutRule.md | no |
| `header` | [R001]ADRs/R001-ADR-HeaderRule.md | `--header-file` |

A rule with nothing to work from is left out of the registry rather than
registered and silently passing. Without `--header-file` the header rule does
not run, and the report says which rules were applied — so a clean run cannot be
mistaken for "the header rule passed" when it never ran.

## `readable-source`

Every `.rs` file can be read and parsed.

This one exists because silence is indistinguishable from success. The other
parsing rules give up quietly on source they cannot read, trusting `rustc` to
say so more clearly — right for a file somebody is editing, wrong for a file
nobody is looking at. A corrupted file produces no rows, and a file with no rows
looks exactly like a clean file.

| offence | correction |
|---|---|
| file could not be read | check that the file exists and that its permissions allow reading it |
| file does not parse as Rust | correct the syntax error rustc reports, or restore the file if it is corrupted |

**Does not catch:** anything about validity beyond parsing. A file that parses
but does not compile — unknown type, borrow error, missing import — is this
rule's idea of fine, and rightly so.

## `header`

Every `.rs` file opens with the repository's header, supplied by
`--header-file`.

The expected text is data because it is never the same twice: MIT here,
Apache 2.0 in a sibling repository, a different year again next year. The
comparison is exact after normalisation — a BOM, CRLF line endings and a
trailing newline in the header file are all absorbed, so a wrong year or a
swapped licence line still fails while a Windows checkout does not.

Exactly one offence per file: the first divergence. A file with no header at all
would otherwise emit one row per header line and bury the workspace behind it.
The offence carries the **whole** expected header in `expected`, so the fix is
one pass rather than a loop.

| offence | correction |
|---|---|
| file is empty, so it carries no header | make the first N lines of the file match the expected header |
| expected `X` but found `Y` | *(same)* |
| file has N lines but the header is M | *(same)* |

**Does not catch:** it compares text and nothing else. A well-formed header
naming the wrong copyright holder, or an SPDX identifier that disagrees with
`Cargo.toml`, passes.

## `test-file-structure`

A test file reads top to bottom in one order: header, imports, constants,
helpers, tests. Each group alphabetical, case-insensitively. Imports run
together; everything else is separated by exactly one blank line.

`Helpers` is defined by **exclusion** — whatever is neither an import, nor a
constant, nor a test. That is what keeps the set of item kinds closed: a
`struct`, an `impl`, a type alias and a plain `fn` are all helpers, so a kind
nobody has thought of yet lands where a reader would put it.

Applies to `tests/` only, and skips `all_tests.rs` and `mod.rs` — those are
registries, and demanding a blank line between each `pub mod` would make the one
file whose whole job is to be scannable the hardest to scan. Their shape is
`tests-layout`'s business.

| offence | correction |
|---|---|
| a `constant` follows a `helper` | move \`X\` up above the helpers |
| `X` is out of alphabetic order | move \`X\` above \`Y\` |
| expected N blank line(s) before `X` | leave exactly N blank line(s) between \`Y\` and \`X\` |

**Does not catch:** it judges shape, not content. The AAA convention —
`// Arrange`, `// Act`, `// Assert` inside a body, and the
`<method>_<description>_<outcome>` naming pattern — is not checked. A file that
does not parse reports nothing here; `readable-source` reports it instead.

## `test-free-source`

Tests live in `tests/`, and the production source tree carries none of them.

A `#[cfg(test)] mod tests` inside `src/` is invisible to everything else: it is
not the mirrored test file `twin4rust` looks for, it is not declared from
`all_tests.rs`, it has no required shape, and it is compiled under a
configuration the shipped build never uses — so it can drift out of step with
the code it tests and no build notices.

Three shapes, all outside `tests/`:

- a function carrying a test attribute, matched on the **last path segment**, so
  `#[tokio::test]` counts without enumerating harnesses
- `#[cfg(...)]` whose predicate mentions `test`
- `#[cfg_attr(...)]` whose predicate mentions `test`

Both `cfg` forms are recognised through the *predicate*, so `any(test, ...)` and
`not(test)` are caught. The predicate is scanned for an **identifier**, not a
substring, so `#[cfg(feature = "test")]` is a feature named test and not a gate.

The walk descends into inline modules. An item that is itself an offence is not
descended into — a `#[cfg(test)]` module is one decision, not one per test
inside it.

**The line is `test`, not conditional compilation.**
`#[cfg(feature = "...")]` and `#[cfg_attr(feature = "serde", derive(Serialize))]`
are ordinary library work and are left alone: a feature is selectable by the
shipped build, so what is tested is what somebody runs. `test` is the one
predicate no shipped build ever sets.

| offence | correction |
|---|---|
| the `#[cfg(test)]` module `X` | move the tests to `tests/<mirror>_tests.rs` and delete this from the source tree |
| the test function `X` | *(same)* |
| the `#[cfg_attr(test, ...)]` on the struct `X` | apply the attribute unconditionally, or move what it guards into `tests/<mirror>_tests.rs` |

The correction names the **mirrored file** — `src/<path>.rs` maps to
`tests/<path>_tests.rs`, the same pairing `twin4rust` enforces.

**Does not catch:** a test-only helper carrying no test attribute and no gate —
an ordinary `pub fn make_test_widget()` — is invisible, because nothing in the
source distinguishes it from production code.

## `tests-layout`

A tests folder is reached through exactly one door, and every door must exist.

- exactly one `tests/all_tests.rs`; one lower down is a file with a misleading
  name that no `pub mod` will ever reach
- a `mod.rs` in **every** folder on the way down, not only those directly
  holding a file — an intermediate folder is a folder too, and a gap there hides
  everything beneath it
- both registry kinds hold nothing but the header and `pub mod` declarations

A declaration is a declaration whether or not it is `pub`; a private `mod name;`
compiles that file just as well, and being compiled is the whole concern. An
inline `mod name { ... }` is not a declaration — it is code hiding in the one
file a reader scans expecting a list.

The failure this rule exists for is silent by construction: **a test that is
never compiled cannot fail.**

| offence | correction |
|---|---|
| a tests folder has no `all_tests.rs` | create `tests/all_tests.rs` with the header and one `pub mod` line per file in `tests/` |
| a tests subfolder has no `mod.rs` | create `<path>` with the header and one `pub mod` line per file in that folder |
| only `tests/all_tests.rs` is a registry | rename it to `mod.rs`, or delete it and declare its contents from `tests/all_tests.rs` |
| the constant `X` does not belong in a registry | move the constant `X` out of the registry into the file that needs it |

**Does not catch:** it verifies a registry *exists*, not that its declarations
are *complete*. A `mod.rs` that is present, valid, and simply fails to mention a
file leaves that file uncompiled — the same silent failure one level down, and
`rustc` says nothing. Closing that gap means resolving each `pub mod` to a file
and each file to its declaration, in both directions. `#[cfg(...)]`-gated
declarations are treated as ordinary ones.

## What is not walked

- `target/` — generated code nobody wrote
- `.git/`
- **any directory holding its own `Cargo.toml`** — it is a different package,
  its files are that package's to answer for, and cargo would not compile them
  as part of this one either. The shape this matters for is a fixture crate
  under `tests/fixtures/`

A fixture tree *without* a manifest is still walked, and there is currently no
way to exclude it. See [OPEN_POINTS.md](OPEN_POINTS.md).

## Output

The table is the default. `--format json` renders the same run as a document
with a stable shape, for a gate script or an agent. `--offence-threshold N`
caps how many offences are **printed** — never how many are counted, and never
the exit code.

Exit codes: `0` clean, `1` could not run, `2` at least one rule broken. Only `2`
is a finding. See [ADR-ExitCodeContract](ADRs/ADR-ExitCodeContract.md).