stern4rust/rule.rs
1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use crate::reporting::offence::Offence;
6use crate::reporting::rule_explanation::RuleExplanation;
7use crate::source_file::SourceFile;
8
9// One rule, one file, one implementation. A rule sees a single source file and
10// answers with what is wrong with it -- it does not walk, does not print, and
11// does not know which other rules exist.
12//
13// That is what keeps the set open: adding a rule is a new file implementing this
14// trait plus one line in the registry, and the rule is testable on a string of
15// source without a workspace behind it.
16//
17// Nothing declared here has a body, and that is the point. Every rule answers
18// all six questions in its own file, including the answers that are "nothing".
19//
20// Defaults used to spare a rule from saying so, and what they cost was the
21// ability to read a rule and know what it does. A file with no `check` meant
22// either "this rule's subject is the tree" or "this rule has not been finished",
23// and the two were the same absence. `is_configured` was worse: it made every
24// new rule configured without anybody choosing it, so a rule that could not
25// possibly run would still join the set and report nothing wrong -- the silent
26// pass this tool exists to catch, in the tool itself.
27//
28// The other half of "a trait declares" -- that every implementor implements
29// every method -- costs no code here. With no body to fall back on, `rustc`
30// rejects an incomplete impl outright.
31pub trait Rule {
32 // Appears verbatim in the report's rule column, so it is kebab-case and
33 // reads as the thing being required rather than the thing being forbidden.
34 fn name(&self) -> &'static str;
35
36 // What is wrong with this one file, judged on its own. A rule whose subject
37 // is the tree answers with no offences, deliberately and in its own file.
38 fn check(&self, file: &SourceFile) -> Vec<Offence>;
39
40 // What is wrong with the set of files taken together.
41 //
42 // Some rules are not about a file at all. "There is exactly one all_tests.rs"
43 // and "every subfolder has a mod.rs" are facts about a tree, and the file
44 // that would carry the offence is precisely the one that does not exist --
45 // so there is nothing for check() to be handed. The registry asks both
46 // questions of every rule without caring which one a rule answers.
47 fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence>;
48
49 // Whether this rule has what it needs to say anything.
50 //
51 // Most rules always do. The header rule does not: it has no idea what your
52 // header says until `--header-file` tells it, and registering it anyway
53 // would let a run report "all rules satisfied" for a rule that never looked
54 // at a single file.
55 //
56 // A rule answers this for itself so the registry does not have to name any
57 // rule in particular. The alternative -- an `if` in the registry that knows
58 // about the header rule -- is how the registry ends up with a second,
59 // hand-maintained idea of which rules exist.
60 fn is_configured(&self) -> bool;
61
62 // What this rule needs before it can run, for the report to print when it
63 // could not. `None` where a rule needs nothing and is therefore never
64 // unconfigured.
65 //
66 // The rule answers for itself so the printer names no rule in particular.
67 // The alternative -- a hardcoded reason in the printer -- was there until
68 // a second rule could go unconfigured, at which point it started telling
69 // readers to pass `--header-file` for a rule waiting on a manifest field.
70 // A line saying only that something is unset says what is wrong without
71 // saying what to do, which is the one thing every finding here must do.
72 fn requirement(&self) -> Option<&'static str>;
73
74 // What this rule wants, for `--rules` to print when nobody has run it yet.
75 // A sentence, a scrap of source that breaks the rule, and the same scrap
76 // put right.
77 //
78 // Here rather than in the printer for the reason `requirement` is here: a
79 // rule that changed its mind would otherwise leave a printer describing
80 // what it used to want, and nothing would say so.
81 fn explanation(&self) -> RuleExplanation;
82}