stern4rust/rules/layout/
registry_completeness_rule.rs1use std::collections::BTreeSet;
6use std::path::Path;
7
8use crate::finding::model::package_tree::PackageTree;
9use crate::finding::parsing::module_declaration_finder::ModuleDeclarationFinder;
10use crate::reporting::offence::Offence;
11use crate::reporting::rule_explanation::RuleExplanation;
12use crate::rule::Rule;
13use crate::source_file::SourceFile;
14
15pub struct RegistryCompletenessRule;
31
32impl RegistryCompletenessRule {
33 pub fn new() -> Self {
34 Self
35 }
36
37 fn undeclared_in(
38 &self,
39 tree: &PackageTree,
40 files: &[SourceFile],
41 directory: &Path,
42 ) -> Vec<Offence> {
43 let registries = tree.registries_in(directory);
44 let Some(primary) = registries.first() else {
45 return Vec::new();
46 };
47 let Some(declared) = Self::declared_by(files, ®istries) else {
48 return Vec::new();
49 };
50 tree.expected_modules_in(directory)
51 .into_iter()
52 .filter(|name| !declared.contains(name))
53 .map(|name| self.offence(primary, &name))
54 .collect()
55 }
56
57 fn declared_by(files: &[SourceFile], registries: &[&Path]) -> Option<BTreeSet<String>> {
62 let mut declared = BTreeSet::new();
63 for path in registries {
64 let file = Self::file_at(files, path)?;
65 declared.extend(ModuleDeclarationFinder::find(file)?);
66 }
67 Some(declared)
68 }
69
70 fn file_at<'a>(files: &'a [SourceFile], path: &Path) -> Option<&'a SourceFile> {
71 let wanted = path.to_string_lossy().replace('\\', "/");
72 files
73 .iter()
74 .find(|file| file.relative_path().replace('\\', "/") == wanted)
75 }
76
77 fn offence(&self, registry: &Path, name: &str) -> Offence {
81 let registry = registry.to_string_lossy().replace('\\', "/");
82 Offence::new(
83 ®istry,
84 1,
85 self.name(),
86 format!("`{name}` is not declared here, so its file is never compiled"),
87 format!("add `pub mod {name};` to {registry}"),
88 )
89 .with_subject(name)
90 }
91}
92
93impl Default for RegistryCompletenessRule {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl Rule for RegistryCompletenessRule {
100 fn name(&self) -> &'static str {
101 "registry-completeness"
102 }
103
104 fn check(&self, _file: &SourceFile) -> Vec<Offence> {
105 Vec::new()
106 }
107
108 fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
112 let tree = PackageTree::of(files);
113 tree.directories()
114 .iter()
115 .flat_map(|directory| self.undeclared_in(&tree, files, directory))
116 .collect()
117 }
118
119 fn requirement(&self) -> Option<&'static str> {
120 None
121 }
122
123 fn is_configured(&self) -> bool {
124 true
125 }
126
127 fn explanation(&self) -> RuleExplanation {
128 RuleExplanation::new(
129 self.name(),
130 "A registry declares every file beside it, so nothing in the tree goes uncompiled.",
131 "src/alpha.rs and src/beta.rs, with mod.rs naming only alpha",
132 "pub mod alpha;\npub mod beta;",
133 )
134 }
135}