stern4rust/rules/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::registry_parser::RegistryParser;
6use crate::finding::registry_policy::RegistryPolicy;
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}