Skip to main content

stern4rust/rules/layout/
module_registry_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::finding::model::registry_policy::RegistryPolicy;
6use crate::finding::parsing::registry_parser::RegistryParser;
7use crate::reporting::offence::Offence;
8use crate::reporting::rule_explanation::RuleExplanation;
9use crate::rule::Rule;
10use crate::source_file::SourceFile;
11
12// A lib.rs or mod.rs outside tests/ is a list of the modules beneath it and
13// nothing else: the header, the crate's inner attributes, `extern crate alloc;`
14// and `pub mod` declarations.
15//
16// The file that names a crate's shape should be readable in one glance. A `use`
17// here is a re-export shim wearing a registry's clothes -- the thing this
18// repository's own standards forbid outright, caught where it most often
19// appears. A `fn` here is code in the one file nobody opens expecting code. An
20// inline `mod name { ... }` is a module that no longer has a file to be found
21// in, hidden inside the index that was supposed to lead to it.
22//
23// Inner attributes never reach the item list: syn keeps `#![no_std]` on the
24// file rather than among its items, so a no_std crate root passes without this
25// rule needing to know which attributes exist.
26//
27// tests/ is left to tests-layout, which asks a different question of the same
28// filenames and gives a different answer about a private `mod`.
29pub struct ModuleRegistryRule;
30
31impl ModuleRegistryRule {
32    pub const TESTS_ROOT: &'static str = "tests/";
33
34    pub fn new() -> Self {
35        Self
36    }
37
38    fn applies_to(file: &SourceFile) -> bool {
39        !file.relative_path().starts_with(Self::TESTS_ROOT) && Self::is_registry(file)
40    }
41
42    fn is_registry(file: &SourceFile) -> bool {
43        matches!(
44            file.relative_path().rsplit('/').next(),
45            Some("lib.rs") | Some("mod.rs")
46        )
47    }
48}
49
50impl Default for ModuleRegistryRule {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl Rule for ModuleRegistryRule {
57    fn name(&self) -> &'static str {
58        "module-registry"
59    }
60
61    fn check(&self, file: &SourceFile) -> Vec<Offence> {
62        if !Self::applies_to(file) {
63            return Vec::new();
64        }
65        RegistryParser::strays(file, RegistryPolicy::source())
66            .unwrap_or_default()
67            .into_iter()
68            .map(|stray| {
69                Offence::new(
70                    file.relative_path(),
71                    stray.line,
72                    self.name(),
73                    format!(
74                        "{} does not belong in a module registry, which holds the \
75                         header, inner attributes, `extern crate alloc;` and pub mod \
76                         declarations only",
77                        stray.label
78                    ),
79                    format!("move {} into a module of its own", stray.label),
80                )
81                .with_subject(&stray.label)
82            })
83            .collect()
84    }
85
86    fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
87        Vec::new()
88    }
89
90    fn requirement(&self) -> Option<&'static str> {
91        None
92    }
93
94    fn is_configured(&self) -> bool {
95        true
96    }
97
98    fn explanation(&self) -> RuleExplanation {
99        RuleExplanation::new(
100            self.name(),
101            "A lib.rs or mod.rs outside tests/ lists the modules beneath it and nothing else.",
102            "pub mod alpha;\n\npub struct Shared;",
103            "pub mod alpha;\npub mod shared;",
104        )
105    }
106}