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
// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
// Licensed under the MIT License
// SPDX-License-Identifier: MIT
use crate::config::Config;
use crate::offence::Offence;
use crate::rule::Rule;
use crate::rule_selection::RuleSelection;
use crate::rules::header_rule::HeaderRule;
use crate::rules::imported_paths_rule::ImportedPathsRule;
use crate::rules::module_registry_rule::ModuleRegistryRule;
use crate::rules::readable_source_rule::ReadableSourceRule;
use crate::rules::registry_completeness_rule::RegistryCompletenessRule;
use crate::rules::single_implemented_type_rule::SingleImplementedTypeRule;
use crate::rules::test_file_structure_rule::TestFileStructureRule;
use crate::rules::test_free_source_rule::TestFreeSourceRule;
use crate::rules::tests_layout_rule::TestsLayoutRule;
use crate::source_file::SourceFile;
// The one place that knows which rules exist. Adding a rule is a line here and a
// file under rules/, and nothing else in the tool changes.
//
// A rule with nothing to work from is left out rather than registered and
// silently passing: a run that reports "all rules satisfied" while a rule was
// never configured is worse than one that says which rules it actually applied.
pub struct RuleRegistry {
rules: Vec<Box<dyn Rule>>,
}
impl RuleRegistry {
// Every rule this tool has, in report order. The single list: `from_config`
// narrows it and `known_names` reads its names, so neither can hold an idea
// of the rule set that the other does not share.
//
// readable-source comes first because it is the one rule whose failure
// explains every other rule's silence on the same file. The header rule is
// built here even without a header, so that it can still name itself -- it
// answers `is_configured` with false and `from_config` drops it.
fn all(expected_header: Vec<String>) -> Vec<Box<dyn Rule>> {
vec![
Box::new(ReadableSourceRule::new()),
Box::new(ImportedPathsRule::new()),
Box::new(ModuleRegistryRule::new()),
Box::new(RegistryCompletenessRule::new()),
Box::new(SingleImplementedTypeRule::new()),
Box::new(TestFileStructureRule::new()),
Box::new(TestFreeSourceRule::new()),
Box::new(TestsLayoutRule::new()),
Box::new(HeaderRule::new(expected_header)),
]
}
pub fn from_config(config: &Config) -> Self {
let rules = Self::all(config.expected_header.clone())
.into_iter()
.filter(|rule| rule.is_configured() && config.selection.includes(rule.name()))
.collect();
Self { rules }
}
pub fn new(rules: Vec<Box<dyn Rule>>) -> Self {
Self { rules }
}
// Every rule this tool has, whether or not this run configured or selected
// it. Read off the same list `from_config` narrows, so a rule cannot be
// applied by a default run while `--rule <name>` calls it unknown.
pub fn known_names() -> Vec<&'static str> {
Self::all(Vec::new())
.iter()
.map(|rule| rule.name())
.collect()
}
// What the switches turned off, which is not the same as what went
// unregistered: a header rule left out for want of a header file was never
// deselected by anybody.
pub fn skipped_names(selection: &RuleSelection) -> Vec<&'static str> {
Self::known_names()
.into_iter()
.filter(|name| !selection.includes(name))
.collect()
}
// Selected, but not registered, because it had nothing to work from. The
// third state: neither applied nor skipped. Reporting it as skipped would
// blame the reader for a choice they did not make, and reporting it as
// nothing at all would let a run check less than it appears to.
pub fn unconfigured_names(&self, config: &Config) -> Vec<&'static str> {
let applied = self.names();
Self::known_names()
.into_iter()
.filter(|name| config.selection.includes(name) && !applied.contains(name))
.collect()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn names(&self) -> Vec<&'static str> {
self.rules.iter().map(|rule| rule.name()).collect()
}
pub fn check(&self, file: &SourceFile) -> Vec<Offence> {
self.rules
.iter()
.flat_map(|rule| rule.check(file))
.collect()
}
// Asked once, after every file has been read, for the rules whose subject is
// the tree rather than a file in it.
pub fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
self.rules
.iter()
.flat_map(|rule| rule.check_workspace(files))
.collect()
}
}