]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/enum_variants.rs
Rollup merge of #85663 - fee1-dead:document-arc-from, r=m-ou-se
[rust.git] / src / tools / clippy / clippy_lints / src / enum_variants.rs
1 //! lint on enum variants that are prefixed or suffixed by the same characters
2
3 use clippy_utils::camel_case;
4 use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
5 use clippy_utils::source::is_present_in_source;
6 use rustc_hir::{EnumDef, Item, ItemKind};
7 use rustc_lint::{LateContext, LateLintPass};
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! {
13     /// **What it does:** Detects enumeration variants that are prefixed or suffixed
14     /// by the same characters.
15     ///
16     /// **Why is this bad?** Enumeration variant names should specify their variant,
17     /// not repeat the enumeration name.
18     ///
19     /// **Known problems:** None.
20     ///
21     /// **Example:**
22     /// ```rust
23     /// enum Cake {
24     ///     BlackForestCake,
25     ///     HummingbirdCake,
26     ///     BattenbergCake,
27     /// }
28     /// ```
29     /// Could be written as:
30     /// ```rust
31     /// enum Cake {
32     ///     BlackForest,
33     ///     Hummingbird,
34     ///     Battenberg,
35     /// }
36     /// ```
37     pub ENUM_VARIANT_NAMES,
38     style,
39     "enums where all variants share a prefix/postfix"
40 }
41
42 declare_clippy_lint! {
43     /// **What it does:** Detects type names that are prefixed or suffixed by the
44     /// containing module's name.
45     ///
46     /// **Why is this bad?** It requires the user to type the module name twice.
47     ///
48     /// **Known problems:** None.
49     ///
50     /// **Example:**
51     /// ```rust
52     /// mod cake {
53     ///     struct BlackForestCake;
54     /// }
55     /// ```
56     /// Could be written as:
57     /// ```rust
58     /// mod cake {
59     ///     struct BlackForest;
60     /// }
61     /// ```
62     pub MODULE_NAME_REPETITIONS,
63     pedantic,
64     "type names prefixed/postfixed with their containing module's name"
65 }
66
67 declare_clippy_lint! {
68     /// **What it does:** Checks for modules that have the same name as their
69     /// parent module
70     ///
71     /// **Why is this bad?** A typical beginner mistake is to have `mod foo;` and
72     /// again `mod foo { ..
73     /// }` in `foo.rs`.
74     /// The expectation is that items inside the inner `mod foo { .. }` are then
75     /// available
76     /// through `foo::x`, but they are only available through
77     /// `foo::foo::x`.
78     /// If this is done on purpose, it would be better to choose a more
79     /// representative module name.
80     ///
81     /// **Known problems:** None.
82     ///
83     /// **Example:**
84     /// ```ignore
85     /// // lib.rs
86     /// mod foo;
87     /// // foo.rs
88     /// mod foo {
89     ///     ...
90     /// }
91     /// ```
92     pub MODULE_INCEPTION,
93     style,
94     "modules that have the same name as their parent module"
95 }
96
97 pub struct EnumVariantNames {
98     modules: Vec<(Symbol, String)>,
99     threshold: u64,
100     avoid_breaking_exported_api: bool,
101 }
102
103 impl EnumVariantNames {
104     #[must_use]
105     pub fn new(threshold: u64, avoid_breaking_exported_api: bool) -> Self {
106         Self {
107             modules: Vec::new(),
108             threshold,
109             avoid_breaking_exported_api,
110         }
111     }
112 }
113
114 impl_lint_pass!(EnumVariantNames => [
115     ENUM_VARIANT_NAMES,
116     MODULE_NAME_REPETITIONS,
117     MODULE_INCEPTION
118 ]);
119
120 /// Returns the number of chars that match from the start
121 #[must_use]
122 fn partial_match(pre: &str, name: &str) -> usize {
123     let mut name_iter = name.chars();
124     let _ = name_iter.next_back(); // make sure the name is never fully matched
125     pre.chars().zip(name_iter).take_while(|&(l, r)| l == r).count()
126 }
127
128 /// Returns the number of chars that match from the end
129 #[must_use]
130 fn partial_rmatch(post: &str, name: &str) -> usize {
131     let mut name_iter = name.chars();
132     let _ = name_iter.next(); // make sure the name is never fully matched
133     post.chars()
134         .rev()
135         .zip(name_iter.rev())
136         .take_while(|&(l, r)| l == r)
137         .count()
138 }
139
140 fn check_variant(
141     cx: &LateContext<'_>,
142     threshold: u64,
143     def: &EnumDef<'_>,
144     item_name: &str,
145     item_name_chars: usize,
146     span: Span,
147 ) {
148     if (def.variants.len() as u64) < threshold {
149         return;
150     }
151     for var in def.variants {
152         let name = var.ident.name.as_str();
153         if partial_match(item_name, &name) == item_name_chars
154             && name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase())
155             && name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric())
156         {
157             span_lint(
158                 cx,
159                 ENUM_VARIANT_NAMES,
160                 var.span,
161                 "variant name starts with the enum's name",
162             );
163         }
164         if partial_rmatch(item_name, &name) == item_name_chars {
165             span_lint(
166                 cx,
167                 ENUM_VARIANT_NAMES,
168                 var.span,
169                 "variant name ends with the enum's name",
170             );
171         }
172     }
173     let first = &def.variants[0].ident.name.as_str();
174     let mut pre = &first[..camel_case::until(&*first)];
175     let mut post = &first[camel_case::from(&*first)..];
176     for var in def.variants {
177         let name = var.ident.name.as_str();
178
179         let pre_match = partial_match(pre, &name);
180         pre = &pre[..pre_match];
181         let pre_camel = camel_case::until(pre);
182         pre = &pre[..pre_camel];
183         while let Some((next, last)) = name[pre.len()..].chars().zip(pre.chars().rev()).next() {
184             if next.is_numeric() {
185                 return;
186             }
187             if next.is_lowercase() {
188                 let last = pre.len() - last.len_utf8();
189                 let last_camel = camel_case::until(&pre[..last]);
190                 pre = &pre[..last_camel];
191             } else {
192                 break;
193             }
194         }
195
196         let post_match = partial_rmatch(post, &name);
197         let post_end = post.len() - post_match;
198         post = &post[post_end..];
199         let post_camel = camel_case::from(post);
200         post = &post[post_camel..];
201     }
202     let (what, value) = match (pre.is_empty(), post.is_empty()) {
203         (true, true) => return,
204         (false, _) => ("pre", pre),
205         (true, false) => ("post", post),
206     };
207     span_lint_and_help(
208         cx,
209         ENUM_VARIANT_NAMES,
210         span,
211         &format!("all variants have the same {}fix: `{}`", what, value),
212         None,
213         &format!(
214             "remove the {}fixes and use full paths to \
215              the variants instead of glob imports",
216             what
217         ),
218     );
219 }
220
221 #[must_use]
222 fn to_camel_case(item_name: &str) -> String {
223     let mut s = String::new();
224     let mut up = true;
225     for c in item_name.chars() {
226         if c.is_uppercase() {
227             // we only turn snake case text into CamelCase
228             return item_name.to_string();
229         }
230         if c == '_' {
231             up = true;
232             continue;
233         }
234         if up {
235             up = false;
236             s.extend(c.to_uppercase());
237         } else {
238             s.push(c);
239         }
240     }
241     s
242 }
243
244 impl LateLintPass<'_> for EnumVariantNames {
245     fn check_item_post(&mut self, _cx: &LateContext<'_>, _item: &Item<'_>) {
246         let last = self.modules.pop();
247         assert!(last.is_some());
248     }
249
250     #[allow(clippy::similar_names)]
251     fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
252         let item_name = item.ident.name.as_str();
253         let item_name_chars = item_name.chars().count();
254         let item_camel = to_camel_case(&item_name);
255         if !item.span.from_expansion() && is_present_in_source(cx, item.span) {
256             if let Some(&(ref mod_name, ref mod_camel)) = self.modules.last() {
257                 // constants don't have surrounding modules
258                 if !mod_camel.is_empty() {
259                     if mod_name == &item.ident.name {
260                         if let ItemKind::Mod(..) = item.kind {
261                             span_lint(
262                                 cx,
263                                 MODULE_INCEPTION,
264                                 item.span,
265                                 "module has the same name as its containing module",
266                             );
267                         }
268                     }
269                     if item.vis.node.is_pub() {
270                         let matching = partial_match(mod_camel, &item_camel);
271                         let rmatching = partial_rmatch(mod_camel, &item_camel);
272                         let nchars = mod_camel.chars().count();
273
274                         let is_word_beginning = |c: char| c == '_' || c.is_uppercase() || c.is_numeric();
275
276                         if matching == nchars {
277                             match item_camel.chars().nth(nchars) {
278                                 Some(c) if is_word_beginning(c) => span_lint(
279                                     cx,
280                                     MODULE_NAME_REPETITIONS,
281                                     item.span,
282                                     "item name starts with its containing module's name",
283                                 ),
284                                 _ => (),
285                             }
286                         }
287                         if rmatching == nchars {
288                             span_lint(
289                                 cx,
290                                 MODULE_NAME_REPETITIONS,
291                                 item.span,
292                                 "item name ends with its containing module's name",
293                             );
294                         }
295                     }
296                 }
297             }
298         }
299         if let ItemKind::Enum(ref def, _) = item.kind {
300             if !(self.avoid_breaking_exported_api && cx.access_levels.is_exported(item.hir_id())) {
301                 check_variant(cx, self.threshold, def, &item_name, item_name_chars, item.span);
302             }
303         }
304         self.modules.push((item.ident.name, item_camel));
305     }
306 }