wowlab_tidy/languages/rust/rules/style/
module_prefix_in_name.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::ast::{self, HasVisibility, VisibilityKind};
4use wowlab_fs::path::Path;
5
6use super::naming;
7use crate::{AstCtx, Example, Violation};
8
9#[rustfmt::skip]
10const EXAMPLES: &[Example] = &[
11 Example {
12 label: "pub struct repeats module name",
13 code: "pub struct FooId;",
14 pass: false,
15 },
16 Example {
17 label: "pub enum repeats module name",
18 code: "pub enum FooKind { A }",
19 pass: false,
20 },
21 Example {
22 label: "pub trait repeats module name",
23 code: "pub trait FooLike {}",
24 pass: false,
25 },
26 Example {
27 label: "pub type alias repeats module name",
28 code: "pub type FooResult = ();",
29 pass: false,
30 },
31 Example {
32 label: "name equal to module name",
33 code: "pub struct Foo;",
34 pass: true,
35 },
36 Example {
37 label: "prefix is not a whole segment",
38 code: "pub struct Food;",
39 pass: true,
40 },
41 Example {
42 label: "unrelated name",
43 code: "pub struct Bar;",
44 pass: true,
45 },
46 Example {
47 label: "private item exempt",
48 code: "struct FooId;",
49 pass: true,
50 },
51 Example {
52 label: "prefixed type in test module",
53 code: "#[cfg(test)]\nmod tests {\n pub struct FooFixture;\n}",
54 pass: true,
55 },
56];
57
58crate::ast_rule!(
59 module_prefix_in_name,
60 "Flag pub type definitions whose name repeats the module name as a prefix (`FooId` in `foo.rs`).",
61 "Module information baked into type names is redundant: users can write `foo::Id` and disambiguate locally (M-SHORT-NAMES).",
62 Low,
63);
64
65fn check_module_prefix_in_name(ctx: &AstCtx<'_>) -> Vec<Violation> {
66 let Some(stem_parts) = stem_parts(ctx.file.rel) else {
67 return Vec::new();
68 };
69
70 ctx.nodes::<ast::Item>()
71 .filter(|item| !ctx.is_in_test(item) && is_pub(item))
72 .filter_map(|item| {
73 let name = naming::type_def_name(&item)?;
74 let name_text = name.text();
75 let segments = naming::segments(&name_text);
76 let is_prefixed = segments.len() > stem_parts.len()
77 && stem_parts
78 .iter()
79 .zip(&segments)
80 .all(|(part, segment)| part.eq_ignore_ascii_case(segment));
81
82 is_prefixed.then(|| {
83 let short: String = segments
84 .iter()
85 .skip(stem_parts.len())
86 .map(String::as_str)
87 .collect();
88
89 ctx.violation(
90 &name,
91 format!(
92 "type name `{name_text}` repeats its module name — prefer `{short}` (used as `module::{short}`)"
93 ),
94 )
95 })
96 })
97 .collect()
98}
99
100fn stem_parts(rel: &str) -> Option<Vec<String>> {
101 let path = Path::new(rel);
102 let mut stem = path.file_stem()?.to_str()?;
103
104 if stem == "mod" {
105 stem = path.parent()?.file_name()?.to_str()?;
106 }
107
108 if stem == "lib" || stem == "main" {
109 return None;
110 }
111
112 Some(
113 stem.split('_')
114 .filter(|part| !part.is_empty())
115 .map(str::to_string)
116 .collect(),
117 )
118}
119
120fn is_pub(item: &ast::Item) -> bool {
121 let visibility = match item {
122 ast::Item::Struct(item) => item.visibility(),
123 ast::Item::Enum(item) => item.visibility(),
124 ast::Item::Trait(item) => item.visibility(),
125 ast::Item::TypeAlias(item) => item.visibility(),
126 _ => return false,
127 };
128
129 visibility.is_some_and(|visibility| matches!(visibility.kind(), VisibilityKind::Pub))
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn run_at(rel: &str, source: &str) -> Vec<Violation> {
137 crate::test_support::check_source_ast_at(rel, source, check_module_prefix_in_name)
138 }
139
140 #[gtest]
141 fn examples() -> Result<()> {
142 for ex in EXAMPLES {
143 let violations = run_at("src/foo.rs", ex.code);
144
145 verify_eq!(violations.is_empty(), ex.pass)?;
146 }
147
148 Ok(())
149 }
150
151 #[gtest]
152 fn mod_rs_uses_parent_directory_name() -> Result<()> {
153 verify_false!(run_at("src/widgets/mod.rs", "pub struct WidgetsId;").is_empty())?;
154 verify_true!(run_at("src/widgets/mod.rs", "pub struct Widgets;").is_empty())?;
155
156 Ok(())
157 }
158
159 #[gtest]
160 fn snake_case_module_names_match_segment_wise() -> Result<()> {
161 verify_false!(run_at("src/spell_id.rs", "pub struct SpellIdMap;").is_empty())?;
162 verify_true!(run_at("src/spell_id.rs", "pub struct SpellId;").is_empty())?;
163
164 Ok(())
165 }
166
167 #[gtest]
168 fn lib_and_main_are_exempt() -> Result<()> {
169 verify_true!(run_at("src/lib.rs", "pub struct LibId;").is_empty())?;
170 verify_true!(run_at("src/main.rs", "pub struct MainLoop;").is_empty())?;
171
172 Ok(())
173 }
174}