Skip to main content

stern4rust/
rule_registry.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::rule::Rule;
8use crate::rules::layout::directory_file_count_rule::DirectoryFileCountRule;
9use crate::rules::layout::directory_subfolder_count_rule::DirectorySubfolderCountRule;
10use crate::rules::layout::module_registry_rule::ModuleRegistryRule;
11use crate::rules::layout::paired_test_file_rule::PairedTestFileRule;
12use crate::rules::layout::registry_completeness_rule::RegistryCompletenessRule;
13use crate::rules::layout::test_file_name_postfix_rule::TestFileNamePostfixRule;
14use crate::rules::layout::tests_layout_rule::TestsLayoutRule;
15use crate::rules::manifest::spdx_matches_manifest_rule::SpdxMatchesManifestRule;
16use crate::rules::manifest::workspace_dependencies_rule::WorkspaceDependenciesRule;
17use crate::rules::source::declared_by_name_rule::DeclaredByNameRule;
18use crate::rules::source::header_rule::HeaderRule;
19use crate::rules::source::imported_paths_rule::ImportedPathsRule;
20use crate::rules::source::ordered_imports_rule::OrderedImportsRule;
21use crate::rules::source::pure_traits_rule::PureTraitsRule;
22use crate::rules::source::readable_source_rule::ReadableSourceRule;
23use crate::rules::source::single_implemented_type_rule::SingleImplementedTypeRule;
24use crate::rules::source::test_free_source_rule::TestFreeSourceRule;
25use crate::rules::testing::arrange_act_assert_rule::ArrangeActAssertRule;
26use crate::rules::testing::test_file_structure_rule::TestFileStructureRule;
27use crate::rules::testing::test_naming_rule::TestNamingRule;
28use crate::rules::testing::tested_public_api_rule::TestedPublicApiRule;
29use crate::settings::config::Config;
30use crate::settings::rule_selection::RuleSelection;
31use crate::source_file::SourceFile;
32
33// The one place that knows which rules exist. Adding a rule is a line here and a
34// file under rules/, and nothing else in the tool changes.
35//
36// A rule with nothing to work from is left out rather than registered and
37// silently passing: a run that reports "all rules satisfied" while a rule was
38// never configured is worse than one that says which rules it actually applied.
39pub struct RuleRegistry {
40    rules: Vec<Box<dyn Rule>>,
41}
42
43impl RuleRegistry {
44    // Every rule this tool has, in report order. The single list: `from_config`
45    // narrows it and `known_names` reads its names, so neither can hold an idea
46    // of the rule set that the other does not share.
47    //
48    // readable-source comes first because it is the one rule whose failure
49    // explains every other rule's silence on the same file. The header rule is
50    // built here even without a header, so that it can still name itself -- it
51    // answers `is_configured` with false and `from_config` drops it.
52    fn all(config: &Config) -> Vec<Box<dyn Rule>> {
53        vec![
54            Box::new(ReadableSourceRule::new()),
55            Box::new(ArrangeActAssertRule::new()),
56            Box::new(DeclaredByNameRule::new()),
57            Box::new(DirectoryFileCountRule::new(
58                config
59                    .max_files_per_directory
60                    .unwrap_or(DirectoryFileCountRule::DEFAULT_LIMIT),
61            )),
62            Box::new(DirectorySubfolderCountRule::new(
63                config
64                    .max_subfolders_per_directory
65                    .unwrap_or(DirectorySubfolderCountRule::DEFAULT_LIMIT),
66            )),
67            Box::new(ImportedPathsRule::new()),
68            Box::new(ModuleRegistryRule::new()),
69            Box::new(OrderedImportsRule::new()),
70            Box::new(PairedTestFileRule::new()),
71            Box::new(PureTraitsRule::new()),
72            Box::new(RegistryCompletenessRule::new()),
73            Box::new(SingleImplementedTypeRule::new()),
74            Box::new(SpdxMatchesManifestRule::new(
75                config.manifest_license.clone(),
76            )),
77            Box::new(TestFileNamePostfixRule::new()),
78            Box::new(TestFileStructureRule::new()),
79            Box::new(TestFreeSourceRule::new()),
80            Box::new(TestNamingRule::new()),
81            Box::new(TestedPublicApiRule::new()),
82            Box::new(TestsLayoutRule::new()),
83            Box::new(WorkspaceDependenciesRule::new(
84                config.workspace_dependencies.clone(),
85            )),
86            Box::new(HeaderRule::new(config.expected_header.clone())),
87        ]
88    }
89
90    pub fn from_config(config: &Config) -> Self {
91        let rules = Self::all(config)
92            .into_iter()
93            .filter(|rule| rule.is_configured() && config.selection.includes(rule.name()))
94            .collect();
95        Self { rules }
96    }
97
98    pub fn new(rules: Vec<Box<dyn Rule>>) -> Self {
99        Self { rules }
100    }
101
102    // Every rule this tool has, whether or not this run configured or selected
103    // it. Read off the same list `from_config` narrows, so a rule cannot be
104    // applied by a default run while `--rule <name>` calls it unknown.
105    pub fn known_names() -> Vec<&'static str> {
106        Self::all(&Config::default())
107            .iter()
108            .map(|rule| rule.name())
109            .collect()
110    }
111
112    // What the switches turned off, which is not the same as what went
113    // unregistered: a header rule left out for want of a header file was never
114    // deselected by anybody.
115    pub fn skipped_names(selection: &RuleSelection) -> Vec<&'static str> {
116        Self::known_names()
117            .into_iter()
118            .filter(|name| !selection.includes(name))
119            .collect()
120    }
121
122    // Selected, but not registered, because it had nothing to work from. The
123    // third state: neither applied nor skipped. Reporting it as skipped would
124    // blame the reader for a choice they did not make, and reporting it as
125    // nothing at all would let a run check less than it appears to.
126    pub fn unconfigured_names(&self, config: &Config) -> Vec<&'static str> {
127        self.unconfigured(config)
128            .into_iter()
129            .map(|(name, _)| name)
130            .collect()
131    }
132
133    // Each with what it was waiting for, taken from the rule rather than
134    // guessed at by the printer. A rule that could not run is built by `all`
135    // even though `from_config` dropped it, which is what lets it still answer.
136    pub fn unconfigured(&self, config: &Config) -> Vec<(&'static str, &'static str)> {
137        let applied = self.names();
138        Self::all(config)
139            .iter()
140            .filter(|rule| {
141                config.selection.includes(rule.name()) && !applied.contains(&rule.name())
142            })
143            .map(|rule| (rule.name(), rule.requirement().unwrap_or("not configured")))
144            .collect()
145    }
146
147    pub fn is_empty(&self) -> bool {
148        self.rules.is_empty()
149    }
150
151    pub fn names(&self) -> Vec<&'static str> {
152        self.rules.iter().map(|rule| rule.name()).collect()
153    }
154
155    // In registry order, so `--rules` reads in the same order as the roster the
156    // report prints.
157    pub fn explanations(&self) -> Vec<RuleExplanation> {
158        self.rules.iter().map(|rule| rule.explanation()).collect()
159    }
160
161    pub fn check(&self, file: &SourceFile) -> Vec<Offence> {
162        self.rules
163            .iter()
164            .flat_map(|rule| rule.check(file))
165            .collect()
166    }
167
168    // Asked once, after every file has been read, for the rules whose subject is
169    // the tree rather than a file in it.
170    pub fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
171        self.rules
172            .iter()
173            .flat_map(|rule| rule.check_workspace(files))
174            .collect()
175    }
176}