]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | //! lint on enum variants that are prefixed or suffixed by the same characters |
2 | ||
cdc7bbd5 XL |
3 | use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; |
4 | use clippy_utils::source::is_present_in_source; | |
a2a8927a XL |
5 | use clippy_utils::str_utils::{camel_case_split, count_match_end, count_match_start}; |
6 | use rustc_hir::{EnumDef, Item, ItemKind, Variant}; | |
17df50a5 | 7 | use rustc_lint::{LateContext, LateLintPass}; |
f20569fa XL |
8 | use rustc_session::{declare_tool_lint, impl_lint_pass}; |
9 | use rustc_span::source_map::Span; | |
10 | use rustc_span::symbol::Symbol; | |
11 | ||
12 | declare_clippy_lint! { | |
94222f64 XL |
13 | /// ### What it does |
14 | /// Detects enumeration variants that are prefixed or suffixed | |
f20569fa XL |
15 | /// by the same characters. |
16 | /// | |
94222f64 XL |
17 | /// ### Why is this bad? |
18 | /// Enumeration variant names should specify their variant, | |
f20569fa XL |
19 | /// not repeat the enumeration name. |
20 | /// | |
a2a8927a XL |
21 | /// ### Limitations |
22 | /// Characters with no casing will be considered when comparing prefixes/suffixes | |
23 | /// This applies to numbers and non-ascii characters without casing | |
24 | /// e.g. `Foo1` and `Foo2` is considered to have different prefixes | |
25 | /// (the prefixes are `Foo1` and `Foo2` respectively), as also `Bar螃`, `Bar蟹` | |
26 | /// | |
94222f64 | 27 | /// ### Example |
f20569fa XL |
28 | /// ```rust |
29 | /// enum Cake { | |
30 | /// BlackForestCake, | |
31 | /// HummingbirdCake, | |
32 | /// BattenbergCake, | |
33 | /// } | |
34 | /// ``` | |
923072b8 | 35 | /// Use instead: |
f20569fa XL |
36 | /// ```rust |
37 | /// enum Cake { | |
38 | /// BlackForest, | |
39 | /// Hummingbird, | |
40 | /// Battenberg, | |
41 | /// } | |
42 | /// ``` | |
a2a8927a | 43 | #[clippy::version = "pre 1.29.0"] |
f20569fa XL |
44 | pub ENUM_VARIANT_NAMES, |
45 | style, | |
46 | "enums where all variants share a prefix/postfix" | |
47 | } | |
48 | ||
f20569fa | 49 | declare_clippy_lint! { |
94222f64 XL |
50 | /// ### What it does |
51 | /// Detects type names that are prefixed or suffixed by the | |
f20569fa XL |
52 | /// containing module's name. |
53 | /// | |
94222f64 XL |
54 | /// ### Why is this bad? |
55 | /// It requires the user to type the module name twice. | |
f20569fa | 56 | /// |
94222f64 | 57 | /// ### Example |
f20569fa XL |
58 | /// ```rust |
59 | /// mod cake { | |
60 | /// struct BlackForestCake; | |
61 | /// } | |
62 | /// ``` | |
923072b8 FG |
63 | /// |
64 | /// Use instead: | |
f20569fa XL |
65 | /// ```rust |
66 | /// mod cake { | |
67 | /// struct BlackForest; | |
68 | /// } | |
69 | /// ``` | |
a2a8927a | 70 | #[clippy::version = "1.33.0"] |
f20569fa XL |
71 | pub MODULE_NAME_REPETITIONS, |
72 | pedantic, | |
73 | "type names prefixed/postfixed with their containing module's name" | |
74 | } | |
75 | ||
76 | declare_clippy_lint! { | |
94222f64 XL |
77 | /// ### What it does |
78 | /// Checks for modules that have the same name as their | |
f20569fa XL |
79 | /// parent module |
80 | /// | |
94222f64 XL |
81 | /// ### Why is this bad? |
82 | /// A typical beginner mistake is to have `mod foo;` and | |
f20569fa XL |
83 | /// again `mod foo { .. |
84 | /// }` in `foo.rs`. | |
85 | /// The expectation is that items inside the inner `mod foo { .. }` are then | |
86 | /// available | |
87 | /// through `foo::x`, but they are only available through | |
88 | /// `foo::foo::x`. | |
89 | /// If this is done on purpose, it would be better to choose a more | |
90 | /// representative module name. | |
91 | /// | |
94222f64 | 92 | /// ### Example |
f20569fa XL |
93 | /// ```ignore |
94 | /// // lib.rs | |
95 | /// mod foo; | |
96 | /// // foo.rs | |
97 | /// mod foo { | |
98 | /// ... | |
99 | /// } | |
100 | /// ``` | |
a2a8927a | 101 | #[clippy::version = "pre 1.29.0"] |
f20569fa XL |
102 | pub MODULE_INCEPTION, |
103 | style, | |
104 | "modules that have the same name as their parent module" | |
105 | } | |
106 | ||
107 | pub struct EnumVariantNames { | |
108 | modules: Vec<(Symbol, String)>, | |
109 | threshold: u64, | |
17df50a5 | 110 | avoid_breaking_exported_api: bool, |
f20569fa XL |
111 | } |
112 | ||
113 | impl EnumVariantNames { | |
114 | #[must_use] | |
17df50a5 | 115 | pub fn new(threshold: u64, avoid_breaking_exported_api: bool) -> Self { |
f20569fa XL |
116 | Self { |
117 | modules: Vec::new(), | |
118 | threshold, | |
17df50a5 | 119 | avoid_breaking_exported_api, |
f20569fa XL |
120 | } |
121 | } | |
122 | } | |
123 | ||
124 | impl_lint_pass!(EnumVariantNames => [ | |
125 | ENUM_VARIANT_NAMES, | |
f20569fa XL |
126 | MODULE_NAME_REPETITIONS, |
127 | MODULE_INCEPTION | |
128 | ]); | |
129 | ||
a2a8927a XL |
130 | fn check_enum_start(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) { |
131 | let name = variant.ident.name.as_str(); | |
132 | let item_name_chars = item_name.chars().count(); | |
133 | ||
134 | if count_match_start(item_name, name).char_count == item_name_chars | |
135 | && name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase()) | |
136 | && name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric()) | |
137 | { | |
138 | span_lint( | |
139 | cx, | |
140 | ENUM_VARIANT_NAMES, | |
141 | variant.span, | |
142 | "variant name starts with the enum's name", | |
143 | ); | |
144 | } | |
145 | } | |
146 | ||
147 | fn check_enum_end(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) { | |
148 | let name = variant.ident.name.as_str(); | |
149 | let item_name_chars = item_name.chars().count(); | |
150 | ||
151 | if count_match_end(item_name, name).char_count == item_name_chars { | |
152 | span_lint( | |
153 | cx, | |
154 | ENUM_VARIANT_NAMES, | |
155 | variant.span, | |
156 | "variant name ends with the enum's name", | |
157 | ); | |
158 | } | |
159 | } | |
160 | ||
161 | fn check_variant(cx: &LateContext<'_>, threshold: u64, def: &EnumDef<'_>, item_name: &str, span: Span) { | |
f20569fa XL |
162 | if (def.variants.len() as u64) < threshold { |
163 | return; | |
164 | } | |
a2a8927a | 165 | |
f20569fa | 166 | let first = &def.variants[0].ident.name.as_str(); |
a2a8927a XL |
167 | let mut pre = camel_case_split(first); |
168 | let mut post = pre.clone(); | |
169 | post.reverse(); | |
17df50a5 | 170 | for var in def.variants { |
a2a8927a XL |
171 | check_enum_start(cx, item_name, var); |
172 | check_enum_end(cx, item_name, var); | |
f20569fa XL |
173 | let name = var.ident.name.as_str(); |
174 | ||
a2a8927a | 175 | let variant_split = camel_case_split(name); |
5099ac24 FG |
176 | if variant_split.len() == 1 { |
177 | return; | |
178 | } | |
f20569fa | 179 | |
a2a8927a XL |
180 | pre = pre |
181 | .iter() | |
182 | .zip(variant_split.iter()) | |
183 | .take_while(|(a, b)| a == b) | |
184 | .map(|e| *e.0) | |
185 | .collect(); | |
186 | post = post | |
187 | .iter() | |
188 | .zip(variant_split.iter().rev()) | |
189 | .take_while(|(a, b)| a == b) | |
190 | .map(|e| *e.0) | |
191 | .collect(); | |
f20569fa | 192 | } |
064997fb | 193 | let (what, value) = match (have_no_extra_prefix(&pre), post.is_empty()) { |
f20569fa | 194 | (true, true) => return, |
a2a8927a XL |
195 | (false, _) => ("pre", pre.join("")), |
196 | (true, false) => { | |
197 | post.reverse(); | |
198 | ("post", post.join("")) | |
199 | }, | |
f20569fa XL |
200 | }; |
201 | span_lint_and_help( | |
202 | cx, | |
17df50a5 | 203 | ENUM_VARIANT_NAMES, |
f20569fa | 204 | span, |
2b03887a | 205 | &format!("all variants have the same {what}fix: `{value}`"), |
f20569fa XL |
206 | None, |
207 | &format!( | |
2b03887a FG |
208 | "remove the {what}fixes and use full paths to \ |
209 | the variants instead of glob imports" | |
f20569fa XL |
210 | ), |
211 | ); | |
212 | } | |
213 | ||
064997fb FG |
214 | #[must_use] |
215 | fn have_no_extra_prefix(prefixes: &[&str]) -> bool { | |
216 | prefixes.iter().all(|p| p == &"" || p == &"_") | |
217 | } | |
218 | ||
f20569fa XL |
219 | #[must_use] |
220 | fn to_camel_case(item_name: &str) -> String { | |
221 | let mut s = String::new(); | |
222 | let mut up = true; | |
223 | for c in item_name.chars() { | |
224 | if c.is_uppercase() { | |
225 | // we only turn snake case text into CamelCase | |
226 | return item_name.to_string(); | |
227 | } | |
228 | if c == '_' { | |
229 | up = true; | |
230 | continue; | |
231 | } | |
232 | if up { | |
233 | up = false; | |
234 | s.extend(c.to_uppercase()); | |
235 | } else { | |
236 | s.push(c); | |
237 | } | |
238 | } | |
239 | s | |
240 | } | |
241 | ||
17df50a5 XL |
242 | impl LateLintPass<'_> for EnumVariantNames { |
243 | fn check_item_post(&mut self, _cx: &LateContext<'_>, _item: &Item<'_>) { | |
f20569fa XL |
244 | let last = self.modules.pop(); |
245 | assert!(last.is_some()); | |
246 | } | |
247 | ||
923072b8 | 248 | #[expect(clippy::similar_names)] |
17df50a5 | 249 | fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { |
f20569fa | 250 | let item_name = item.ident.name.as_str(); |
a2a8927a | 251 | let item_camel = to_camel_case(item_name); |
f20569fa XL |
252 | if !item.span.from_expansion() && is_present_in_source(cx, item.span) { |
253 | if let Some(&(ref mod_name, ref mod_camel)) = self.modules.last() { | |
254 | // constants don't have surrounding modules | |
255 | if !mod_camel.is_empty() { | |
256 | if mod_name == &item.ident.name { | |
257 | if let ItemKind::Mod(..) = item.kind { | |
258 | span_lint( | |
259 | cx, | |
260 | MODULE_INCEPTION, | |
261 | item.span, | |
262 | "module has the same name as its containing module", | |
263 | ); | |
264 | } | |
265 | } | |
3c0e092e XL |
266 | // The `module_name_repetitions` lint should only trigger if the item has the module in its |
267 | // name. Having the same name is accepted. | |
2b03887a | 268 | if cx.tcx.visibility(item.owner_id).is_public() && item_camel.len() > mod_camel.len() { |
3c0e092e XL |
269 | let matching = count_match_start(mod_camel, &item_camel); |
270 | let rmatching = count_match_end(mod_camel, &item_camel); | |
f20569fa XL |
271 | let nchars = mod_camel.chars().count(); |
272 | ||
273 | let is_word_beginning = |c: char| c == '_' || c.is_uppercase() || c.is_numeric(); | |
274 | ||
3c0e092e | 275 | if matching.char_count == nchars { |
f20569fa XL |
276 | match item_camel.chars().nth(nchars) { |
277 | Some(c) if is_word_beginning(c) => span_lint( | |
278 | cx, | |
279 | MODULE_NAME_REPETITIONS, | |
280 | item.span, | |
281 | "item name starts with its containing module's name", | |
282 | ), | |
283 | _ => (), | |
284 | } | |
285 | } | |
3c0e092e | 286 | if rmatching.char_count == nchars { |
f20569fa XL |
287 | span_lint( |
288 | cx, | |
289 | MODULE_NAME_REPETITIONS, | |
290 | item.span, | |
291 | "item name ends with its containing module's name", | |
292 | ); | |
293 | } | |
294 | } | |
295 | } | |
296 | } | |
297 | } | |
298 | if let ItemKind::Enum(ref def, _) = item.kind { | |
2b03887a | 299 | if !(self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(item.owner_id.def_id)) { |
a2a8927a | 300 | check_variant(cx, self.threshold, def, item_name, item.span); |
17df50a5 | 301 | } |
f20569fa XL |
302 | } |
303 | self.modules.push((item.ident.name, item_camel)); | |
304 | } | |
305 | } |