Skip to main content

wowlab_tidy/languages/rust/rules/style/
module_prefix_in_name.rs

1#[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}