Skip to main content

stern4rust/rules/layout/
registry_completeness_rule.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use 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
15// A registry declares every file beside it, so nothing in the tree goes
16// uncompiled.
17//
18// This closes the half of the registry question the other rules leave open.
19// `tests-layout` and `module-registry` both check that a registry exists and
20// holds only declarations; neither checks that the declarations are complete. A
21// `mod.rs` that is present, valid, and simply fails to mention `alpha_tests`
22// leaves `alpha_tests.rs` uncompiled -- and a test that is never compiled cannot
23// fail.
24//
25// Only one direction needs a rule, which is worth stating because it is not
26// obvious. `pub mod missing;` with no `missing.rs` is a **compile error**, so
27// rustc already reports it, loudly and better than this could. An orphan
28// `.rs` file that no registry declares produces no error and no warning at all.
29// Silence is the whole failure, so silence is all this rule looks for.
30pub 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, &registries) 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    // None when any registry of the directory cannot be read or parsed. Treating
58    // an unreadable registry as declaring nothing would report every file beside
59    // it as an orphan -- a page of wrong answers caused by one real one, which
60    // readable-source already reports.
61    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    // Reported against the registry rather than the orphan. The orphan is not
78    // wrong -- it is a perfectly good file -- and the edit that fixes it is one
79    // line in the registry.
80    fn offence(&self, registry: &Path, name: &str) -> Offence {
81        let registry = registry.to_string_lossy().replace('\\', "/");
82        Offence::new(
83            &registry,
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    // A fact about a directory rather than about a file: the file that proves
109    // the offence is the one that is missing a line, and the file that suffers
110    // is a different one entirely.
111    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}