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