stern4rust/rules/source/
declared_by_name_rule.rs1use std::iter::once;
6
7use syn::Attribute;
8use syn::Expr;
9use syn::Item;
10use syn::ItemMod;
11use syn::Lit;
12use syn::Meta;
13use syn::parse_file;
14
15use crate::reporting::offence::Offence;
16use crate::reporting::rule_explanation::RuleExplanation;
17use crate::rule::Rule;
18use crate::source_file::SourceFile;
19
20pub struct DeclaredByNameRule;
41
42impl DeclaredByNameRule {
43 pub const ATTRIBUTE: &'static str = "path";
44
45 pub fn new() -> Self {
46 Self
47 }
48
49 fn declarations(items: &[Item]) -> Vec<&ItemMod> {
52 items
53 .iter()
54 .flat_map(|item| match item {
55 Item::Mod(module) => once(module)
56 .chain(
57 Self::inside(module)
58 .map(Self::declarations)
59 .unwrap_or_default(),
60 )
61 .collect::<Vec<&ItemMod>>(),
62 _ => Vec::new(),
63 })
64 .collect()
65 }
66
67 fn inside(module: &ItemMod) -> Option<&[Item]> {
68 module.content.as_ref().map(|(_, items)| items.as_slice())
69 }
70
71 fn target_of(attrs: &[Attribute]) -> Option<String> {
74 attrs.iter().find_map(|attr| match &attr.meta {
75 Meta::NameValue(pair) if pair.path.is_ident(Self::ATTRIBUTE) => match &pair.value {
76 Expr::Lit(literal) => match &literal.lit {
77 Lit::Str(text) => Some(text.value()),
78 _ => None,
79 },
80 _ => None,
81 },
82 _ => None,
83 })
84 }
85
86 fn offence(&self, file: &SourceFile, module: &ItemMod, target: &str) -> Offence {
89 let name = module.ident.to_string();
90 let expected = format!("{name}.rs");
91 Offence::new(
92 file.relative_path(),
93 module.ident.span().start().line,
94 self.name(),
95 format!(
96 "`mod {name}` is reached through `#[path = \"{target}\"]`, so the file it \
97 declares cannot be found from its name"
98 ),
99 format!(
100 "move `{target}` to `{expected}` beside this file and drop the `#[path]` attribute"
101 ),
102 )
103 .with_subject(&name)
104 .with_expected(&expected)
105 }
106}
107
108impl Default for DeclaredByNameRule {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl Rule for DeclaredByNameRule {
115 fn name(&self) -> &'static str {
116 "declared-by-name"
117 }
118
119 fn check(&self, file: &SourceFile) -> Vec<Offence> {
120 let Ok(syntax) = parse_file(&file.contents()) else {
121 return Vec::new();
122 };
123 Self::declarations(&syntax.items)
124 .into_iter()
125 .filter_map(|module| {
126 Self::target_of(&module.attrs).map(|target| self.offence(file, module, &target))
127 })
128 .collect()
129 }
130
131 fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
132 Vec::new()
133 }
134
135 fn requirement(&self) -> Option<&'static str> {
136 None
137 }
138
139 fn is_configured(&self) -> bool {
140 true
141 }
142
143 fn explanation(&self) -> RuleExplanation {
144 RuleExplanation::new(
145 self.name(),
146 "A module is declared by name: `mod alpha;` reaches alpha.rs or alpha/mod.rs.",
147 "#[path = \"other.rs\"]\nmod alpha;",
148 "mod alpha;",
149 )
150 }
151}