cargo-stern4rust 0.9.1

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
# Rules

Twenty-one 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 |
| `arrange-act-assert` | [R017]ADRs/R017-ADR-ArrangeActAssertRule.md | no |
| `declared-by-name` | [R018]ADRs/R018-ADR-DeclaredByNameRule.md | no |
| `directory-file-count` | [R010]ADRs/R010-ADR-DirectoryFileCountRule.md | optional, default 20 |
| `directory-subfolder-count` | [R011]ADRs/R011-ADR-DirectorySubfolderCountRule.md | optional, default 5 |
| `imported-paths` | [R008]ADRs/R008-ADR-ImportedPathsRule.md | no |
| `registry-completeness` | [R009]ADRs/R009-ADR-RegistryCompletenessRule.md | no |
| `paired-test-file` | [R016]ADRs/R016-ADR-PairedTestFileRule.md | no |
| `test-file-name-postfix` | [R015]ADRs/R015-ADR-TestFileNamePostfixRule.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 |
| `module-registry` | [R006]ADRs/R006-ADR-ModuleRegistryRule.md | no |
| `ordered-imports` | [R019]ADRs/R019-ADR-OrderedImportsRule.md | no |
| `spdx-matches-manifest` | [R020]ADRs/R020-ADR-SpdxMatchesManifestRule.md | `license` in the manifest |
| `workspace-dependencies` | [R021]ADRs/R021-ADR-WorkspaceDependenciesRule.md | no |
| `single-implemented-type` | [R007]ADRs/R007-ADR-SingleImplementedTypeRule.md | no |
| `pure-traits` | [R014]ADRs/R014-ADR-PureTraitsRule.md | no |
| `test-naming` | [R012]ADRs/R012-ADR-TestNamingRule.md | no |
| `tested-public-api` | [R013]ADRs/R013-ADR-TestedPublicApiRule.md | no |
| `header` | [R001]ADRs/R001-ADR-HeaderRule.md | `--header-file` |

`--rule <NAME>` applies only the named rules; `--skip <NAME>` subtracts. Both
repeatable, both default to everything, and skipping wins over selecting. Every
report names the rules it applied; a run that did not apply all of them says
`All applied rules are satisfied` and names each absence with its reason --
`(skipped)` or what the rule was waiting for, such as `(needs --header-file)`. The JSON carries `rules_applied`,
`rules_skipped` and `rules_unconfigured`. An unknown rule
name is an error, as is `--rule header` without `--header-file`. See
[ADR-RuleSelection](ADRs/ADR-RuleSelection.md).

A rule with nothing to work from is left out of the registry rather than
registered and silently passing -- and is then named in the report as not
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 passes. An SPDX identifier disagreeing with
`Cargo.toml` used to pass too; `spdx-matches-manifest` now holds those two
together, and does so without needing `--header-file`.

## `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.

**Imports whose order rustfmt decides are left alone.** rustfmt sorts `self`,
`super` and `crate` ahead of every other path, and treats case as significant in
*opposite* directions at the two levels: an uppercase-initial crate sorts behind
every lowercase one (`Bbb::gamma` after `zzz::last`), while an uppercase-initial
segment later in a path sorts ahead of its lowercase siblings
(`serde_json::Value` before `serde_json::from_str`). None of that matches the
alphabet. Demanding the alphabet there would make the file unsatisfiable rather
than merely wrong, since `cargo fmt` runs first and writes the other order back.

So the check stands down, and the decision is **per pair** rather than per
import: where the two paths first differ, if the segments there are of different
case, rustfmt decides. Keying it on an import's first segment alone was a bug --
`use serde_json::Value;` beside `use serde_json::from_str;` share theirs and part
company at the second, which left a file no edit could make green. Everything
else is still ordered.

Two shapes trigger it: a shared helper inside the tests tree, reached as
`use crate::support::builders::a_widget;`, and a same-crate pair diverging by
case.

| 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.

## `directory-file-count`

A directory holds at most **20** `.rs` files, not counting its own index.
`max-files-per-directory` in `stern4rust.toml` changes the limit.

This is the only rule whose number is taste rather than fact, which is why it is
configuration. It is also the one most in tension with the rest: one struct per
file, one implemented type per file and one test file per source file all
manufacture files by design, so the limit has to be generous enough that the
conventions producing the files are not themselves the offence.

Registries do not count -- a `mod.rs`, `lib.rs` or `all_tests.rs` is an index
*of* the directory rather than something *in* it. `main.rs` does count: it is an
entry point holding real code.

| offence | correction |
|---|---|
| `src` holds 42 files, more than the 20 a directory may hold | group the files of `src` into subfolders of at most 20 each, each with its own `mod.rs` declared by this index |

Reported against the directory's index, because that is where the `pub mod`
lines for the new subfolders have to go.

**Does not catch:** size. Twenty files of two thousand lines each satisfy it --
that is `crap4rust`'s question. Non-`.rs` files are invisible. And it does not
say *where* to split: a cap forces a division and is silent about which one.

**It cannot be autofixed.** The correction is a `git mv`, a new `mod.rs`, a
declaration in the parent, and a matching move under `tests/`.

## `directory-subfolder-count`

A directory holds at most **5** subfolders containing source, checked at every
level so that pushing sprawl one directory down does not escape it.
`max-subfolders-per-directory` changes the limit.

The counterweight to `directory-file-count`: that rule creates folders, and
without this one the cheapest way to satisfy it is a folder per file.

| offence | correction |
|---|---|
| `src` holds 7 subfolders, more than the 5 a directory may hold | group the subfolders of `src` so that no directory holds more than 5 |

**It finds nothing today.** Across eight repositories the deepest tree is two
levels and no directory has more than one subfolder. It is a guard against a
shape the family has not reached, kept because `PackageTree` already models what
it needs and because the folders it counts are about to be created.

**Does not catch:** what is in the folders -- five subfolders of two hundred
files each satisfy it, and `directory-file-count` is what catches that. The two
are only meaningful together.

## `imported-paths`

A function is called through a name this file imported, not through a path.

A file's `use` statements are its list of dependencies. `syn::parse_file(...)`
compiles with nothing in the file mentioning `syn`, so a reader scanning the top
to find out what this file needs is quietly given a wrong answer.
`std::env::args()` is a different cost: it spells out at the call site a route
that belongs at the top, and spells it out again at every other call.

Three shapes are left alone. An **unqualified** call has nothing to import. A
**type qualifier** -- `Widget::new()`, `Self::inner()` -- is not a path standing
in for an import, since the type itself was imported and the qualifier says which
type is being constructed. And **one imported segment** -- `use std::fs;` with
`fs::read_to_string(...)` -- is the point of the rule rather than an exception to
it: it names the route once and still says at the call site which module the
function came from. A bare `read_to_string(...)` would satisfy a stricter rule
while saying strictly less.

Module and type are told apart by **case**, a convention rather than a
resolution, because this tool has no type information.

| offence | correction |
|---|---|
| `syn::parse_file` is reached through a path | add `use syn::parse_file;` and call `parse_file` |
| `std::env::args` is reached through a path | add `use std::env;` and call `env::args` |

The two shapes split differently on purpose. A two-segment path imports whole,
because `use syn;` would be legal and leave the call site unchanged. A longer one
imports all but the last segment, keeping `env` because `env::args()` reads
better than a bare `args()`.

Applies to **both** productive and test files -- the only rule so far with no
`tests/` exemption, because a test file has the same reader and the same list of
dependencies at its top.

**Does not catch:** paths outside call position. A `let x: std::path::PathBuf` or
a `std::fmt::Result` return type passes, since the standard is about function and
method qualifiers. Macros are not checked -- `serde_json::json!(...)` is an
`ExprMacro`, not a call. And a lowercase-named type or an uppercase-named module
is judged by its case rather than by what it is.

## `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.

## `registry-completeness`

A registry declares every module beside it: each sibling `.rs` file, and each
subfolder that has a registry of its own. Nothing in the tree goes uncompiled.

**Only one direction is checked, and it was measured.** `pub mod missing;` with
no `missing.rs` is a compile error -- `rustc` reports `E0583` immediately and
more clearly than this could. An orphan `.rs` file that no registry declares
produces **no error and no warning at all**. Silence is the whole failure, so
silence is all this looks for.

`pub` is not required here: a private `mod name;` compiles the file just as
well, and being compiled is the concern. `module-registry` is the rule that
wants `pub`. An inline `mod name { ... }` declares no file and does not count.
`main.rs` counts as a registry beside `lib.rs`, so a file declared only from the
entry point is not reported.

| offence | correction |
|---|---|
| `beta_tests` is not declared here, so its file is never compiled | add `pub mod beta_tests;` to `tests/all_tests.rs` |

Reported against the **registry**, not the orphan -- the orphan is a perfectly
good file and the edit that fixes it is one line somewhere else. An unparseable
registry silences the rule for that directory rather than reporting every file
beside it; `readable-source` reports the registry itself.

**Does not catch:** a file declared through `#[path = "..."]`, which reads as
undeclared -- which is why `declared-by-name` forbids that attribute outright, so
this can only be reached by a repository that skipped it. A file the walker never
reached, including anything under
`--exclude`. And `#[cfg(...)]`-gated declarations count as declarations, which
is right for "is it ever compiled" and wrong for "is it compiled in this
configuration".

## `module-registry`

A `lib.rs` or `mod.rs` outside `tests/` is an index of the modules beneath it,
and holds nothing else: the header, the crate's inner attributes,
`extern crate alloc;`, and `pub mod` declarations.

Inner attributes need no exception -- `syn` keeps `#![no_std]` on the file
rather than among its items, so a no_std crate root passes without the rule
knowing which attributes exist. `extern crate alloc;` is the one non-`mod` item
allowed: a no_std crate has to say it somewhere and the crate root is where it
belongs. `pub` is required, because a private `mod` hides part of the crate's
shape from the file whose job is to state it.

The sharpest thing it catches is the **re-export shim** -- `pub use` in a
registry -- which these standards forbid outright and which forms in exactly
this file.

| offence | correction |
|---|---|
| the import `use std::ffi::OsString;` | move the import into a module of its own |
| the function `run` | *(same)* |
| the module `hidden` (private) | *(same)* |
| the inline module `alpha` | *(same)* |

`tests/` is left to `tests-layout`, which asks a different question of the same
filenames and gives a different answer about a private `mod`.

**Does not catch:** whether the declarations are *complete* or *ordered* -- a
`lib.rs` omitting a module that exists on disk passes, the same gap
`tests-layout` has. It says nothing about `main.rs`, which is an entry point
rather than an index and legitimately holds code.

## `single-implemented-type`

A source file outside `tests/` holds at most one type that carries behaviour: at
most one `struct` or `enum` that is both **declared in the file** and has **at
least one `impl` block** in it. Structs and enums without impl blocks are
unlimited -- plain data is not a subject, and a file's payload types belong
beside the subject that uses them.

Both halves do work. *Declared here*, so an `impl Display for SomeoneElsesType`
does not make this file that type's home. *At least one impl block*, inherent or
trait, because both are behaviour -- though `#[derive(...)]` is not an impl block
in the syntax tree and correctly does not count.

The first implemented type is the subject; every later one is reported, so the
offence names the type to move. The walk descends into inline modules, since
wrapping a second subject in `mod detail { ... }` changes nothing a reader cares
about.

| offence | correction |
|---|---|
| `ColumnWidths` is a second type with an impl block; this file's subject is already `ReportPrinter` | move `ColumnWidths` and its impl blocks into `column_widths.rs` |

`tests/` is exempt: a test file legitimately holds several fakes that each carry
an impl block, which is the shape `test-file-structure` asks for.

**Does not catch:** size. One type with forty methods satisfies this completely
-- that is `crap4rust`'s question. It says nothing about free functions, and does
not treat a `trait` with default method bodies as a subject -- `pure-traits`
covers the other half of that gap.

## `pure-traits`

A trait declares; it does not implement. **No method in a `trait` declaration in
`src/` may have a default body.**

A default reads as a convenience and works as a decision nobody made. The
implementor that says nothing about a method is indistinguishable from the one
that considered it and found the default right, and the question of which you are
looking at cannot be answered by reading either file. Make the body a declaration
and every implementor has to answer, in its own file, where the answer is.

The second half of the requirement -- that every implementor implements every
method -- **needs no rule**. With no body to fall back on, `rustc` rejects an
incomplete impl with `E0046`, immediately and more precisely than this tool
could. Only the half the compiler is silent about is checked here, which is the
same split [R009](ADRs/R009-ADR-RegistryCompletenessRule.md) made.

Only methods are reported. An associated type and an associated constant may
carry a default: neither is behaviour, so neither lets an implementor inherit a
decision while appearing to have made one.

| offence | correction |
|---|---|
| `` `Collection::is_empty` has a default body, so an implementor that says nothing about it cannot be told from one that chose it `` | move the body into each implementor |

The offence names the method rather than the trait, because a trait with three
defaults is three separate edits landing in three different sets of files.

`tests/` is exempt: a test file declares traits to stand in for real ones, and a
stand-in with a body is the shape those fakes are supposed to have.

**Does not catch:** a **blanket impl**, which puts one body behind every
implementor at once and is an `Item::Impl` rather than an `Item::Trait` -- a
trait emptied of defaults can have all of them restored this way and the rule
will say nothing. Nor a default inherited from a supertrait in another crate, nor
whether a removed body was moved into the implementors or simply deleted --
`rustc` guarantees each implementor has *a* body, not the right one. Anything
inside a macro is invisible, as everywhere else in this tool.

## `declared-by-name`

A module is declared by name: `mod alpha;` reaches `alpha.rs` or `alpha/mod.rs`,
and nothing else decides which file that is. `#[path = "..."]` on a `mod` is an
offence, anywhere in the package.

This is not a rule about taste. It is the one attribute that makes another rule
here give a **confident wrong answer**: `registry-completeness` resolves
declarations by convention, so a file reached through an explicit path is
reported as never compiled when it compiles perfectly well. That rule accepted
the gap on the grounds that the house standard forbids `#[path]` -- a convention
nothing enforced until now.

It applies package-wide rather than to registries alone. The standard names
`all_tests.rs` because that is where the temptation is; the harm is the same
wherever the attribute appears.

| offence | correction |
|---|---|
| `` `mod alpha` is reached through `#[path = "elsewhere/other.rs"]`, so the file it declares cannot be found from its name `` | move `elsewhere/other.rs` to `alpha.rs` beside this file and drop the `#[path]` attribute |

`#[cfg_attr(unix, path = "...")]` is deliberately left alone -- a platform-gated
module is the one honest use of the attribute, and reporting it would accuse
correct code.

**Does not catch:** a `cfg_attr`-gated path, by decision, which
`registry-completeness` would still misread on the platform where it applies.
Nor a `#[path]` produced by a macro, nor whether the file it points at exists --
that is `rustc`'s `E0583`.

See [R018](ADRs/R018-ADR-DeclaredByNameRule.md).

## `ordered-imports`

Imports in `src/` run in alphabetic order, on the pairs where the alphabet is
the authority.

`test-file-structure` has asked this of `tests/` since 0.2.0 and nothing asked
it of the source tree -- which is where `imported-paths` routinely *adds* lines,
with nothing saying where a new one lands.

**The stand-downs are the design.** `cargo fmt` runs first in the gate and sorts
`self`, `super`, `crate` and uppercase-initial paths by rules of its own, so
demanding the alphabet there writes a file **no edit can make green**: each run
undoes the last. The rule asks `ImportPath` -- the same seam
`test-file-structure` uses -- rather than deciding again.

A block ends where lines stop being consecutive, so a blank line or a comment
separates two imports and the first of a block is compared with nothing.

| offence | correction |
|---|---|
| `` `use aaa_crate::Alpha;` is out of alphabetic order; it follows `use zzz_crate::Zed;` `` | move `use aaa_crate::Alpha;` above `use zzz_crate::Zed;` |

**Does not catch:** any pair `cargo fmt` decides -- measured on this crate, that
is **56% of adjacent import pairs in `src/`**, because a source file usually
leads with a `crate::` block while a test file never does. So more than half of
what the rule appears to check, it does not. Nor grouping: whether `std`,
external crates and `crate::` are separated at all, or in what order those
blocks appear. Nor imports inside inline modules or macros.

See [R019](ADRs/R019-ADR-OrderedImportsRule.md).

## `workspace-dependencies`

A workspace declares its dependencies once, in the root, and every member takes
them from there with `.workspace` notation.

**Three requirements, one check.** The root holding every reference, each member
using `.workspace`, and no member declaring its own are the same requirement:
`cargo` refuses to build a `foo = { workspace = true }` the root does not
declare, so only the middle one needs code. The same split
[R009](ADRs/R009-ADR-RegistryCompletenessRule.md),
[R014](ADRs/R014-ADR-PureTraitsRule.md) and
[R016](ADRs/R016-ADR-PairedTestFileRule.md) each found before it.

Read from the TOML rather than from `cargo metadata`, because the question is
*how* a dependency was written and resolution erases exactly that. All three
tables count -- `dependencies`, `dev-dependencies`, `build-dependencies` -- since
a member pinning its own `proptest` splits the workspace as surely as a runtime
dependency does. Intra-workspace path dependencies are included.

| offence | correction |
|---|---|
| `` validation/Cargo.toml declares `node` in [dependencies] rather than taking it from the workspace `` | add `node` to [workspace.dependencies] in the root manifest, and write `node = { workspace = true }` here |

A package that is **not** a workspace has no root to centralise into, so the rule
says nothing -- the silence `tests-layout` keeps about a package with no tests
tree, not the `(not configured)` state.

**Does not catch:** whether the root's `[workspace.dependencies]` is tidy -- an
entry nothing references is invisible. Nor version drift within the root, nor
`[patch]`, `[replace]` or target-specific tables, nor whether a member needs the
dependency at all.

See [R021](ADRs/R021-ADR-WorkspaceDependenciesRule.md).

## `spdx-matches-manifest`

Every file's `SPDX-License-Identifier` says what the manifest's `license` says.

`header` compares a header against a text file and nothing else, so an SPDX line
disagreeing with `Cargo.toml` passes it. This rule takes its expected value from
**the package being judged** rather than from a flag -- which is why it needs no
`--header-file` to hold, and why it can catch a file in a repository that has no
header file at all.

The header is the comment block a file opens with: everything before the first
line that is neither blank nor a `//` comment. An SPDX line below that declares
nothing.

| offence | correction |
|---|---|
| `` src/widget.rs carries no `SPDX-License-Identifier:`, so nothing ties it to the `MIT` the manifest declares `` | add `// SPDX-License-Identifier: MIT` to the header, or correct the manifest |
| `` src/widget.rs declares `Apache-2.0` where the manifest declares `MIT` `` | change the header to `// SPDX-License-Identifier: MIT`, or correct the manifest |

Both corrections end **"or correct the manifest"**: the rule knows the two
disagree, not which is right.

A manifest naming no `license` leaves the rule nothing to work from, so it is
**not applied** rather than offended -- named in the report, as the header rule
is.

**Does not catch:** a licence stated only in prose, which reads as a *missing*
identifier rather than a contradicting one. Nor whether the licence is correct,
nor the copyright holder or year. A workspace whose packages declare different
licences leaves the rule unconfigured.

See [R020](ADRs/R020-ADR-SpdxMatchesManifestRule.md).

## `arrange-act-assert`

A test reads `Arrange`, then one or more `Act`/`Assert` pairs, with a blank line
separating the sections.

Every marker expands into the phases it names, and the expanded sequence must be
`Arrange` followed by one or more `Act`, `Assert` pairs. The merged forms expand
identically to the separate ones, so one check covers every legal shape:

| written | expands to |
|---|---|
| `// Arrange` `// Act` `// Assert` | Arrange, Act, Assert |
| `// Arrange & Act` `// Assert` | Arrange, Act, Assert |
| `// Arrange` `// Act & Assert` | Arrange, Act, Assert |
| `// Arrange & Act & Assert` | Arrange, Act, Assert |

and the same check rejects an Act with no Assert, an Assert with no Act, a test
with no markers, and an Arrange **dropped rather than merged** -- a bare
`// Act` first is an offence, because the merged form exists to say so.

A marker may carry **trailing prose** after `--`, `:` or `.`; all three are
established style. A marker ends on a **word boundary**, so `// Actually this
needs explaining` is prose. Comment lines above a marker are **folded into it**,
so a marker documented over two lines is not a spacing offence.

| offence | correction |
|---|---|
| `` `new_empty_collection_is_empty` reads Act, Assert; a test is `Arrange` followed by one or more `Act`/`Assert` pairs `` | label the sections `// Arrange`, `// Act` and `// Assert`, merging adjacent ones as `// Arrange & Act`, `// Act & Assert` or `// Arrange & Act & Assert` |
| `` `// Act & Assert` in `a_method_call_on_bare_self…` is not preceded by a blank line `` | put a blank line before `// Act & Assert` |

The markers are comments, and `syn` discards comments -- so this rule reads
lines, and **skips every line a literal occupies**, taken from the token stream.
Without that it reports this repository's own Rust-in-a-raw-string fixtures: a
naive scanner finds seven offences here that are all string literals.

**Does not catch:** whether a section does what it says -- an `// Assert` block
that asserts nothing passes. Nor a **stray or duplicated marker**, since several
`Act`/`Assert` pairs are legal and a copy-pasted `// Act` cannot be told from a
second pair. Nor tests generated by a macro, nor anything outside `tests/`.

See [R017](ADRs/R017-ADR-ArrangeActAssertRule.md).

## `paired-test-file`

A `tests/<path>/<X>_tests.rs` names the source file it exercises, and that file
exists. The counterpart of `tests/a/b_tests.rs` is `src/a/b.rs`, matched **by
path rather than by name alone** -- a test file in the wrong directory is as
unpaired as one whose source is gone.

This is the other side of the pairing from `twin4rust`, which starts at a source
file and looks for its test. Nothing asked the reverse, and a test file outlives
the module it was named for **silently**: it still compiles, still runs, still
passes, and its name now points at nothing.

`all_tests.rs` is exempt -- it ends in `_tests.rs` but is a registry, and would
resolve to `src/all.rs`. `_proptest_tests.rs` is exempt because a property-test
suite is a second suite for a module it does not name, so its stem resolves to a
file nobody meant to write.

| offence | correction |
|---|---|
| `` tests/state/etheram_state_ibft_tests.rs is named for src/state/etheram_state_ibft.rs, which does not exist `` | rename it after the source file it exercises, or delete it if that file is gone |

The correction does **not** say "create the missing file". Measured against a
real tree, every unpaired file tested something real under a name that had
drifted, so the file to create is never the answer.

**It assumes the package is mirrored.** A *harness* crate -- one whose `src/` is
apparatus and whose `tests/` are scenarios named after behaviours rather than
files -- is not, and every one of its test files is reported.
`--skip paired-test-file` is the answer there, and a skipped rule is named as
skipped in the report, so it cannot be mistaken for a pass.

**Does not catch:** whether the name is *honest* -- `widget_tests.rs` beside
`widget.rs` containing tests for something else entirely passes. Nor anything in
a `_proptest_tests.rs` file, by decision. Nor a source file with no test at all,
which is `twin4rust`'s direction. Nor a module that was emptied rather than
removed.

See [R016](ADRs/R016-ADR-PairedTestFileRule.md).

## `test-file-name-postfix`

A file under `tests/` holding at least one test is named `<X>_tests.rs`.

The name is what pairs a test file with the source file it exercises, and that
pairing is the basis of the whole mirrored layout. It was enforced from one side
only: `twin4rust` starts at 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 --
`tests-layout` cared only that a registry existed, `registry-completeness` only
that the file was declared, `test-file-structure` only about the order inside
it, `test-naming` only about the function names.

**One direction only.** Holding a test obliges the name; a `_tests.rs` file
holding none is a different failure with a different fix, and is not this rule.

A test is a function whose attribute's last path segment is `test`, so
`#[tokio::test]` counts. The walk descends into inline modules.

| offence | correction |
|---|---|
| `` tests/rules/widget.rs holds 2 test(s) but its name does not end in `_tests.rs`, so nothing pairs it with the source file it exercises `` | rename it `tests/rules/widget_tests.rs` |

The correction names the exact path, and `expected` carries the same string for
a consumer of the JSON report. The offence sits at line 1, because the file's
name is what is wrong rather than any one test in it.

**Two exemptions, both load-bearing.** `src/` is exempt because a `#[test]`
there is already `test-free-source`'s offence and renaming would not fix it --
that file has to *move*. Registries are exempt because a `#[test]` in an
`all_tests.rs` or `mod.rs` is already `tests-layout`'s, and `mod.rs` cannot be
renamed at all. In both cases this rule's correction would be wrong.

**Does not catch:** an **orphan** -- `banana_tests.rs` with no `banana.rs`
behind it passes, because `<X>` is never resolved against the source tree. That
gap stays open on `twin4rust`'s blind side. Nor a `_tests.rs` file holding no
tests, by decision; nor tests generated by a macro, which never reach the syntax
tree; nor whether the name matches the *right* source file.

See [R015](ADRs/R015-ADR-TestFileNamePostfixRule.md).

## `test-naming`

A test's name has at least three underscore-separated parts, following
`<method>_<conditions>_<result>`.

A test's name is the only part of it anybody reads at the moment it matters:
`cargo test` prints names, not bodies, and `scores_good` says nothing about what
broke. A name with fewer than three parts cannot carry a method, a condition and
a result, whatever its words are.

The rule reads the name and **nothing else**, which is a deliberate retreat.
Three earlier versions tried to verify the leading part was the method actually
under test -- by looking in the body, then through the test file's helpers
transitively, then against the mirrored source file. Measured across 1559 tests
in eight repositories, all three accused correct code, and counting underscores
instead took 592 offences down to 5. See
[R012](ADRs/R012-ADR-TestNamingRule.md).

| offence | correction |
|---|---|
| `` `scores_good` has fewer than 3 parts, so it cannot say what it calls, under what conditions, and with what result `` | rename it `<method>_<conditions>_<result>`, starting with the method `scores_good` calls |

Only `#[test]` functions under `tests/` are judged. A helper beside them is not a
test; `all_tests.rs` and `mod.rs` are registries.

**Does not catch:** whether the first part is the method under test -- the whole
point of the retreat, and answered from the other end by `tested-public-api`. Nor
whether the name is *true*, nor whether the body asserts anything: a three-part
name on an empty test passes. `foo_bar_baz` passes. Tests generated by a macro
are invisible.

## `tested-public-api`

Every public entry point declared in `src/` is called by at least one test.

Two shapes count: a free `pub fn` and a `pub fn` in an inherent impl.

**Neither half of a trait counts.** A method implementing a trait has no
visibility of its own and is reached through the trait rather than named; a
method a trait *declares* is not an implementation at all, so there is no
behaviour behind it to test. Counting declarations while excusing implementations
demanded a fake per trait whose only purpose was to be asserted against.

Matched on **name and arity**. Types and parameter order are not checked and
cannot be without type inference, which this tool does not do. Call sites are
gathered from macro token streams as well as parsed expressions, and that is
load-bearing: a Rust test puts its assertion in `assert!` or `assert_eq!`, whose
contents never become syntax, so skipping them would report the best-tested code
as untested. See [R013](ADRs/R013-ADR-TestedPublicApiRule.md).

| offence | correction |
|---|---|
| `` `with_fixed` is public but no test calls it with 1 argument(s) `` | call `with_fixed` from a test, or stop exposing it if nothing outside needs it |

The correction offers two answers deliberately: an uncalled `pub fn` is as often
over-exposure as it is missing coverage.

**Does not catch:** whether the call *tests* anything -- a bare
`let _ = thing.method();` satisfies it. Matching by name and arity across the
whole test tree means the rule **under-reports**: two entry points sharing both
are indistinguishable, so a test calling one marks both. A call in a test helper
no test ever invokes still counts. Macro-generated entry points, derived methods
and trait impls are all outside its idea of an entry point.

## `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* — that is `registry-completeness`. `#[cfg(...)]`-gated
declarations are treated as ordinary ones.

## What is not walked

- `target/` — generated code nobody wrote
- `.git/`

`--exclude <GLOB>` removes further paths, repeatable and matched against the
package-relative path. It is not a silent skip: every pattern is named in the
report with the number of files it removed, `files_excluded=N` sits in the
summary, and a pattern that matched **nothing** is called out by name so a dead
exclusion can be deleted rather than trusted. An uncompilable pattern is an
error. See [ADR-ExclusionsAreCounted](ADRs/ADR-ExclusionsAreCounted.md).

Nothing else is skipped by default. A nested package with its own `Cargo.toml`
is walked like any other directory: a manifest is a fact about cargo, not about whose conventions
apply, and skipping on sight let a whole tree go unreported with nothing in the
report saying so. Sample code a tool analyses belongs beside the package rather
than inside it — see
[ADR-WalkEveryFileInThePackage](ADRs/ADR-WalkEveryFileInThePackage.md) and
[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).