]> git.proxmox.com Git - rustc.git/blame - src/tools/clippy/clippy_lints/src/item_name_repetitions.rs
bump version to 1.80.1+dfsg1-1~bpo12+pve1
[rustc.git] / src / tools / clippy / clippy_lints / src / item_name_repetitions.rs
CommitLineData
f20569fa
XL
1//! lint on enum variants that are prefixed or suffixed by the same characters
2
49aad941 3use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_hir};
c0240ec0 4use clippy_utils::is_bool;
ed00b5ec 5use clippy_utils::macros::span_is_local;
cdc7bbd5 6use clippy_utils::source::is_present_in_source;
ed00b5ec 7use clippy_utils::str_utils::{camel_case_split, count_match_end, count_match_start, to_camel_case, to_snake_case};
e8be2606 8use rustc_data_structures::fx::FxHashSet;
ed00b5ec 9use rustc_hir::{EnumDef, FieldDef, Item, ItemKind, OwnerId, Variant, VariantData};
17df50a5 10use rustc_lint::{LateContext, LateLintPass};
4b012472 11use rustc_session::impl_lint_pass;
f20569fa 12use rustc_span::symbol::Symbol;
4b012472 13use rustc_span::Span;
f20569fa
XL
14
15declare_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 52declare_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
79declare_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
109declare_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 145pub 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 154impl 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 178impl_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]
186fn have_no_extra_prefix(prefixes: &[&str]) -> bool {
187 prefixes.iter().all(|p| p == &"" || p == &"_")
188}
189
190fn 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
258fn 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
298fn 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
316fn 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
331fn 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 390impl 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}