]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | //! lint on enum variants that are prefixed or suffixed by the same characters |
2 | ||
49aad941 | 3 | use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_hir}; |
c0240ec0 | 4 | use clippy_utils::is_bool; |
ed00b5ec | 5 | use clippy_utils::macros::span_is_local; |
cdc7bbd5 | 6 | use clippy_utils::source::is_present_in_source; |
ed00b5ec | 7 | use clippy_utils::str_utils::{camel_case_split, count_match_end, count_match_start, to_camel_case, to_snake_case}; |
e8be2606 | 8 | use rustc_data_structures::fx::FxHashSet; |
ed00b5ec | 9 | use rustc_hir::{EnumDef, FieldDef, Item, ItemKind, OwnerId, Variant, VariantData}; |
17df50a5 | 10 | use rustc_lint::{LateContext, LateLintPass}; |
4b012472 | 11 | use rustc_session::impl_lint_pass; |
f20569fa | 12 | use rustc_span::symbol::Symbol; |
4b012472 | 13 | use rustc_span::Span; |
f20569fa XL |
14 | |
15 | declare_clippy_lint! { | |
94222f64 XL |
16 | /// ### What it does |
17 | /// Detects enumeration variants that are prefixed or suffixed | |
f20569fa XL |
18 | /// by the same characters. |
19 | /// | |
94222f64 XL |
20 | /// ### Why is this bad? |
21 | /// Enumeration variant names should specify their variant, | |
f20569fa XL |
22 | /// not repeat the enumeration name. |
23 | /// | |
a2a8927a XL |
24 | /// ### Limitations |
25 | /// Characters with no casing will be considered when comparing prefixes/suffixes | |
26 | /// This applies to numbers and non-ascii characters without casing | |
27 | /// e.g. `Foo1` and `Foo2` is considered to have different prefixes | |
28 | /// (the prefixes are `Foo1` and `Foo2` respectively), as also `Bar螃`, `Bar蟹` | |
29 | /// | |
94222f64 | 30 | /// ### Example |
ed00b5ec | 31 | /// ```no_run |
f20569fa XL |
32 | /// enum Cake { |
33 | /// BlackForestCake, | |
34 | /// HummingbirdCake, | |
35 | /// BattenbergCake, | |
36 | /// } | |
37 | /// ``` | |
923072b8 | 38 | /// Use instead: |
ed00b5ec | 39 | /// ```no_run |
f20569fa XL |
40 | /// enum Cake { |
41 | /// BlackForest, | |
42 | /// Hummingbird, | |
43 | /// Battenberg, | |
44 | /// } | |
45 | /// ``` | |
a2a8927a | 46 | #[clippy::version = "pre 1.29.0"] |
f20569fa XL |
47 | pub ENUM_VARIANT_NAMES, |
48 | style, | |
49 | "enums where all variants share a prefix/postfix" | |
50 | } | |
51 | ||
f20569fa | 52 | declare_clippy_lint! { |
94222f64 XL |
53 | /// ### What it does |
54 | /// Detects type names that are prefixed or suffixed by the | |
f20569fa XL |
55 | /// containing module's name. |
56 | /// | |
94222f64 XL |
57 | /// ### Why is this bad? |
58 | /// It requires the user to type the module name twice. | |
f20569fa | 59 | /// |
94222f64 | 60 | /// ### Example |
ed00b5ec | 61 | /// ```no_run |
f20569fa XL |
62 | /// mod cake { |
63 | /// struct BlackForestCake; | |
64 | /// } | |
65 | /// ``` | |
923072b8 FG |
66 | /// |
67 | /// Use instead: | |
ed00b5ec | 68 | /// ```no_run |
f20569fa XL |
69 | /// mod cake { |
70 | /// struct BlackForest; | |
71 | /// } | |
72 | /// ``` | |
a2a8927a | 73 | #[clippy::version = "1.33.0"] |
f20569fa XL |
74 | pub MODULE_NAME_REPETITIONS, |
75 | pedantic, | |
76 | "type names prefixed/postfixed with their containing module's name" | |
77 | } | |
78 | ||
79 | declare_clippy_lint! { | |
94222f64 XL |
80 | /// ### What it does |
81 | /// Checks for modules that have the same name as their | |
f20569fa XL |
82 | /// parent module |
83 | /// | |
94222f64 XL |
84 | /// ### Why is this bad? |
85 | /// A typical beginner mistake is to have `mod foo;` and | |
f20569fa XL |
86 | /// again `mod foo { .. |
87 | /// }` in `foo.rs`. | |
88 | /// The expectation is that items inside the inner `mod foo { .. }` are then | |
89 | /// available | |
90 | /// through `foo::x`, but they are only available through | |
91 | /// `foo::foo::x`. | |
92 | /// If this is done on purpose, it would be better to choose a more | |
93 | /// representative module name. | |
94 | /// | |
94222f64 | 95 | /// ### Example |
f20569fa XL |
96 | /// ```ignore |
97 | /// // lib.rs | |
98 | /// mod foo; | |
99 | /// // foo.rs | |
100 | /// mod foo { | |
101 | /// ... | |
102 | /// } | |
103 | /// ``` | |
a2a8927a | 104 | #[clippy::version = "pre 1.29.0"] |
f20569fa XL |
105 | pub MODULE_INCEPTION, |
106 | style, | |
107 | "modules that have the same name as their parent module" | |
108 | } | |
ed00b5ec FG |
109 | declare_clippy_lint! { |
110 | /// ### What it does | |
111 | /// Detects struct fields that are prefixed or suffixed | |
112 | /// by the same characters or the name of the struct itself. | |
113 | /// | |
114 | /// ### Why is this bad? | |
115 | /// Information common to all struct fields is better represented in the struct name. | |
116 | /// | |
117 | /// ### Limitations | |
118 | /// Characters with no casing will be considered when comparing prefixes/suffixes | |
119 | /// This applies to numbers and non-ascii characters without casing | |
120 | /// e.g. `foo1` and `foo2` is considered to have different prefixes | |
121 | /// (the prefixes are `foo1` and `foo2` respectively), as also `bar螃`, `bar蟹` | |
122 | /// | |
123 | /// ### Example | |
124 | /// ```no_run | |
125 | /// struct Cake { | |
126 | /// cake_sugar: u8, | |
127 | /// cake_flour: u8, | |
128 | /// cake_eggs: u8 | |
129 | /// } | |
130 | /// ``` | |
131 | /// Use instead: | |
132 | /// ```no_run | |
133 | /// struct Cake { | |
134 | /// sugar: u8, | |
135 | /// flour: u8, | |
136 | /// eggs: u8 | |
137 | /// } | |
138 | /// ``` | |
139 | #[clippy::version = "1.75.0"] | |
140 | pub STRUCT_FIELD_NAMES, | |
141 | pedantic, | |
142 | "structs where all fields share a prefix/postfix or contain the name of the struct" | |
143 | } | |
f20569fa | 144 | |
ed00b5ec | 145 | pub struct ItemNameRepetitions { |
fe692bf9 | 146 | modules: Vec<(Symbol, String, OwnerId)>, |
ed00b5ec FG |
147 | enum_threshold: u64, |
148 | struct_threshold: u64, | |
17df50a5 | 149 | avoid_breaking_exported_api: bool, |
fe692bf9 | 150 | allow_private_module_inception: bool, |
e8be2606 | 151 | allowed_prefixes: FxHashSet<String>, |
f20569fa XL |
152 | } |
153 | ||
ed00b5ec | 154 | impl ItemNameRepetitions { |
f20569fa | 155 | #[must_use] |
ed00b5ec FG |
156 | pub fn new( |
157 | enum_threshold: u64, | |
158 | struct_threshold: u64, | |
159 | avoid_breaking_exported_api: bool, | |
160 | allow_private_module_inception: bool, | |
e8be2606 | 161 | allowed_prefixes: &[String], |
ed00b5ec | 162 | ) -> Self { |
f20569fa XL |
163 | Self { |
164 | modules: Vec::new(), | |
ed00b5ec FG |
165 | enum_threshold, |
166 | struct_threshold, | |
17df50a5 | 167 | avoid_breaking_exported_api, |
fe692bf9 | 168 | allow_private_module_inception, |
e8be2606 | 169 | allowed_prefixes: allowed_prefixes.iter().map(|s| to_camel_case(s)).collect(), |
f20569fa XL |
170 | } |
171 | } | |
e8be2606 FG |
172 | |
173 | fn is_allowed_prefix(&self, prefix: &str) -> bool { | |
174 | self.allowed_prefixes.contains(prefix) | |
175 | } | |
f20569fa XL |
176 | } |
177 | ||
ed00b5ec | 178 | impl_lint_pass!(ItemNameRepetitions => [ |
f20569fa | 179 | ENUM_VARIANT_NAMES, |
ed00b5ec | 180 | STRUCT_FIELD_NAMES, |
f20569fa XL |
181 | MODULE_NAME_REPETITIONS, |
182 | MODULE_INCEPTION | |
183 | ]); | |
184 | ||
ed00b5ec FG |
185 | #[must_use] |
186 | fn have_no_extra_prefix(prefixes: &[&str]) -> bool { | |
187 | prefixes.iter().all(|p| p == &"" || p == &"_") | |
188 | } | |
189 | ||
190 | fn check_fields(cx: &LateContext<'_>, threshold: u64, item: &Item<'_>, fields: &[FieldDef<'_>]) { | |
191 | if (fields.len() as u64) < threshold { | |
192 | return; | |
193 | } | |
194 | ||
195 | check_struct_name_repetition(cx, item, fields); | |
196 | ||
197 | // if the SyntaxContext of the identifiers of the fields and struct differ dont lint them. | |
198 | // this prevents linting in macros in which the location of the field identifier names differ | |
199 | if !fields.iter().all(|field| item.ident.span.eq_ctxt(field.ident.span)) { | |
200 | return; | |
201 | } | |
202 | ||
203 | let mut pre: Vec<&str> = match fields.first() { | |
204 | Some(first_field) => first_field.ident.name.as_str().split('_').collect(), | |
205 | None => return, | |
206 | }; | |
207 | let mut post = pre.clone(); | |
208 | post.reverse(); | |
209 | for field in fields { | |
210 | let field_split: Vec<&str> = field.ident.name.as_str().split('_').collect(); | |
211 | if field_split.len() == 1 { | |
212 | return; | |
213 | } | |
214 | ||
215 | pre = pre | |
216 | .into_iter() | |
217 | .zip(field_split.iter()) | |
218 | .take_while(|(a, b)| &a == b) | |
219 | .map(|e| e.0) | |
220 | .collect(); | |
221 | post = post | |
222 | .into_iter() | |
223 | .zip(field_split.iter().rev()) | |
224 | .take_while(|(a, b)| &a == b) | |
225 | .map(|e| e.0) | |
226 | .collect(); | |
227 | } | |
228 | let prefix = pre.join("_"); | |
229 | post.reverse(); | |
230 | let postfix = match post.last() { | |
231 | Some(&"") => post.join("_") + "_", | |
232 | Some(_) | None => post.join("_"), | |
233 | }; | |
234 | if fields.len() > 1 { | |
235 | let (what, value) = match ( | |
236 | prefix.is_empty() || prefix.chars().all(|c| c == '_'), | |
237 | postfix.is_empty(), | |
238 | ) { | |
239 | (true, true) => return, | |
240 | (false, _) => ("pre", prefix), | |
241 | (true, false) => ("post", postfix), | |
242 | }; | |
c0240ec0 FG |
243 | if fields.iter().all(|field| is_bool(field.ty)) { |
244 | // If all fields are booleans, we don't want to emit this lint. | |
245 | return; | |
246 | } | |
ed00b5ec FG |
247 | span_lint_and_help( |
248 | cx, | |
249 | STRUCT_FIELD_NAMES, | |
250 | item.span, | |
e8be2606 | 251 | format!("all fields have the same {what}fix: `{value}`"), |
ed00b5ec | 252 | None, |
e8be2606 | 253 | format!("remove the {what}fixes"), |
ed00b5ec FG |
254 | ); |
255 | } | |
256 | } | |
257 | ||
258 | fn check_struct_name_repetition(cx: &LateContext<'_>, item: &Item<'_>, fields: &[FieldDef<'_>]) { | |
259 | let snake_name = to_snake_case(item.ident.name.as_str()); | |
260 | let item_name_words: Vec<&str> = snake_name.split('_').collect(); | |
261 | for field in fields { | |
262 | if field.ident.span.eq_ctxt(item.ident.span) { | |
263 | //consider linting only if the field identifier has the same SyntaxContext as the item(struct) | |
264 | let field_words: Vec<&str> = field.ident.name.as_str().split('_').collect(); | |
265 | if field_words.len() >= item_name_words.len() { | |
266 | // if the field name is shorter than the struct name it cannot contain it | |
267 | if field_words.iter().zip(item_name_words.iter()).all(|(a, b)| a == b) { | |
268 | span_lint_hir( | |
269 | cx, | |
270 | STRUCT_FIELD_NAMES, | |
271 | field.hir_id, | |
272 | field.span, | |
273 | "field name starts with the struct's name", | |
274 | ); | |
275 | } | |
276 | if field_words.len() > item_name_words.len() { | |
277 | // lint only if the end is not covered by the start | |
278 | if field_words | |
279 | .iter() | |
280 | .rev() | |
281 | .zip(item_name_words.iter().rev()) | |
282 | .all(|(a, b)| a == b) | |
283 | { | |
284 | span_lint_hir( | |
285 | cx, | |
286 | STRUCT_FIELD_NAMES, | |
287 | field.hir_id, | |
288 | field.span, | |
289 | "field name ends with the struct's name", | |
290 | ); | |
291 | } | |
292 | } | |
293 | } | |
294 | } | |
295 | } | |
296 | } | |
297 | ||
a2a8927a XL |
298 | fn check_enum_start(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) { |
299 | let name = variant.ident.name.as_str(); | |
300 | let item_name_chars = item_name.chars().count(); | |
301 | ||
302 | if count_match_start(item_name, name).char_count == item_name_chars | |
303 | && name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase()) | |
304 | && name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric()) | |
305 | { | |
49aad941 | 306 | span_lint_hir( |
a2a8927a XL |
307 | cx, |
308 | ENUM_VARIANT_NAMES, | |
49aad941 | 309 | variant.hir_id, |
a2a8927a XL |
310 | variant.span, |
311 | "variant name starts with the enum's name", | |
312 | ); | |
313 | } | |
314 | } | |
315 | ||
316 | fn check_enum_end(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) { | |
317 | let name = variant.ident.name.as_str(); | |
318 | let item_name_chars = item_name.chars().count(); | |
319 | ||
320 | if count_match_end(item_name, name).char_count == item_name_chars { | |
49aad941 | 321 | span_lint_hir( |
a2a8927a XL |
322 | cx, |
323 | ENUM_VARIANT_NAMES, | |
49aad941 | 324 | variant.hir_id, |
a2a8927a XL |
325 | variant.span, |
326 | "variant name ends with the enum's name", | |
327 | ); | |
328 | } | |
329 | } | |
330 | ||
331 | fn check_variant(cx: &LateContext<'_>, threshold: u64, def: &EnumDef<'_>, item_name: &str, span: Span) { | |
f20569fa XL |
332 | if (def.variants.len() as u64) < threshold { |
333 | return; | |
334 | } | |
a2a8927a | 335 | |
ed00b5ec FG |
336 | for var in def.variants { |
337 | check_enum_start(cx, item_name, var); | |
338 | check_enum_end(cx, item_name, var); | |
339 | } | |
340 | ||
781aab86 FG |
341 | let first = match def.variants.first() { |
342 | Some(variant) => variant.ident.name.as_str(), | |
343 | None => return, | |
344 | }; | |
a2a8927a XL |
345 | let mut pre = camel_case_split(first); |
346 | let mut post = pre.clone(); | |
347 | post.reverse(); | |
17df50a5 | 348 | for var in def.variants { |
f20569fa XL |
349 | let name = var.ident.name.as_str(); |
350 | ||
a2a8927a | 351 | let variant_split = camel_case_split(name); |
5099ac24 FG |
352 | if variant_split.len() == 1 { |
353 | return; | |
354 | } | |
f20569fa | 355 | |
a2a8927a XL |
356 | pre = pre |
357 | .iter() | |
358 | .zip(variant_split.iter()) | |
359 | .take_while(|(a, b)| a == b) | |
360 | .map(|e| *e.0) | |
361 | .collect(); | |
362 | post = post | |
363 | .iter() | |
364 | .zip(variant_split.iter().rev()) | |
365 | .take_while(|(a, b)| a == b) | |
366 | .map(|e| *e.0) | |
367 | .collect(); | |
f20569fa | 368 | } |
064997fb | 369 | let (what, value) = match (have_no_extra_prefix(&pre), post.is_empty()) { |
f20569fa | 370 | (true, true) => return, |
a2a8927a XL |
371 | (false, _) => ("pre", pre.join("")), |
372 | (true, false) => { | |
373 | post.reverse(); | |
374 | ("post", post.join("")) | |
375 | }, | |
f20569fa XL |
376 | }; |
377 | span_lint_and_help( | |
378 | cx, | |
17df50a5 | 379 | ENUM_VARIANT_NAMES, |
f20569fa | 380 | span, |
e8be2606 | 381 | format!("all variants have the same {what}fix: `{value}`"), |
f20569fa | 382 | None, |
e8be2606 | 383 | format!( |
2b03887a FG |
384 | "remove the {what}fixes and use full paths to \ |
385 | the variants instead of glob imports" | |
f20569fa XL |
386 | ), |
387 | ); | |
388 | } | |
389 | ||
ed00b5ec | 390 | impl LateLintPass<'_> for ItemNameRepetitions { |
17df50a5 | 391 | fn check_item_post(&mut self, _cx: &LateContext<'_>, _item: &Item<'_>) { |
f20569fa XL |
392 | let last = self.modules.pop(); |
393 | assert!(last.is_some()); | |
394 | } | |
395 | ||
17df50a5 | 396 | fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { |
f20569fa | 397 | let item_name = item.ident.name.as_str(); |
a2a8927a | 398 | let item_camel = to_camel_case(item_name); |
f20569fa | 399 | if !item.span.from_expansion() && is_present_in_source(cx, item.span) { |
fe692bf9 | 400 | if let [.., (mod_name, mod_camel, owner_id)] = &*self.modules { |
f20569fa XL |
401 | // constants don't have surrounding modules |
402 | if !mod_camel.is_empty() { | |
fe692bf9 FG |
403 | if mod_name == &item.ident.name |
404 | && let ItemKind::Mod(..) = item.kind | |
405 | && (!self.allow_private_module_inception || cx.tcx.visibility(owner_id.def_id).is_public()) | |
406 | { | |
407 | span_lint( | |
408 | cx, | |
409 | MODULE_INCEPTION, | |
410 | item.span, | |
411 | "module has the same name as its containing module", | |
412 | ); | |
f20569fa | 413 | } |
3c0e092e XL |
414 | // The `module_name_repetitions` lint should only trigger if the item has the module in its |
415 | // name. Having the same name is accepted. | |
2b03887a | 416 | if cx.tcx.visibility(item.owner_id).is_public() && item_camel.len() > mod_camel.len() { |
3c0e092e XL |
417 | let matching = count_match_start(mod_camel, &item_camel); |
418 | let rmatching = count_match_end(mod_camel, &item_camel); | |
f20569fa XL |
419 | let nchars = mod_camel.chars().count(); |
420 | ||
421 | let is_word_beginning = |c: char| c == '_' || c.is_uppercase() || c.is_numeric(); | |
422 | ||
3c0e092e | 423 | if matching.char_count == nchars { |
f20569fa XL |
424 | match item_camel.chars().nth(nchars) { |
425 | Some(c) if is_word_beginning(c) => span_lint( | |
426 | cx, | |
427 | MODULE_NAME_REPETITIONS, | |
9ffffee4 | 428 | item.ident.span, |
f20569fa XL |
429 | "item name starts with its containing module's name", |
430 | ), | |
431 | _ => (), | |
432 | } | |
433 | } | |
e8be2606 FG |
434 | if rmatching.char_count == nchars |
435 | && !self.is_allowed_prefix(&item_camel[..item_camel.len() - rmatching.byte_count]) | |
436 | { | |
f20569fa XL |
437 | span_lint( |
438 | cx, | |
439 | MODULE_NAME_REPETITIONS, | |
9ffffee4 | 440 | item.ident.span, |
f20569fa XL |
441 | "item name ends with its containing module's name", |
442 | ); | |
443 | } | |
444 | } | |
445 | } | |
446 | } | |
447 | } | |
ed00b5ec FG |
448 | if !(self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(item.owner_id.def_id)) |
449 | && span_is_local(item.span) | |
450 | { | |
451 | match item.kind { | |
452 | ItemKind::Enum(def, _) => check_variant(cx, self.enum_threshold, &def, item_name, item.span), | |
4b012472 | 453 | ItemKind::Struct(VariantData::Struct { fields, .. }, _) => { |
ed00b5ec FG |
454 | check_fields(cx, self.struct_threshold, item, fields); |
455 | }, | |
456 | _ => (), | |
17df50a5 | 457 | } |
f20569fa | 458 | } |
fe692bf9 | 459 | self.modules.push((item.ident.name, item_camel, item.owner_id)); |
f20569fa XL |
460 | } |
461 | } |