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