]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/non_expressive_names.rs
f39cae46de03e5212f009ff3f84df4fdb69b4f29
[rust.git] / clippy_lints / src / non_expressive_names.rs
1 use crate::utils::{span_lint, span_lint_and_then};
2 use rustc::lint::{EarlyContext, EarlyLintPass, LintArray, LintPass};
3 use rustc::{declare_tool_lint, lint_array};
4 use syntax::ast::*;
5 use syntax::attr;
6 use syntax::source_map::Span;
7 use syntax::symbol::LocalInternedString;
8 use syntax::visit::{walk_block, walk_expr, walk_pat, Visitor};
9
10 /// **What it does:** Checks for names that are very similar and thus confusing.
11 ///
12 /// **Why is this bad?** It's hard to distinguish between names that differ only
13 /// by a single character.
14 ///
15 /// **Known problems:** None?
16 ///
17 /// **Example:**
18 /// ```rust
19 /// let checked_exp = something;
20 /// let checked_expr = something_else;
21 /// ```
22 declare_clippy_lint! {
23     pub SIMILAR_NAMES,
24     pedantic,
25     "similarly named items and bindings"
26 }
27
28 /// **What it does:** Checks for too many variables whose name consists of a
29 /// single character.
30 ///
31 /// **Why is this bad?** It's hard to memorize what a variable means without a
32 /// descriptive name.
33 ///
34 /// **Known problems:** None?
35 ///
36 /// **Example:**
37 /// ```rust
38 /// let (a, b, c, d, e, f, g) = (...);
39 /// ```
40 declare_clippy_lint! {
41     pub MANY_SINGLE_CHAR_NAMES,
42     style,
43     "too many single character bindings"
44 }
45
46 /// **What it does:** Checks if you have variables whose name consists of just
47 /// underscores and digits.
48 ///
49 /// **Why is this bad?** It's hard to memorize what a variable means without a
50 /// descriptive name.
51 ///
52 /// **Known problems:** None?
53 ///
54 /// **Example:**
55 /// ```rust
56 /// let _1 = 1;
57 /// let ___1 = 1;
58 /// let __1___2 = 11;
59 /// ```
60 declare_clippy_lint! {
61     pub JUST_UNDERSCORES_AND_DIGITS,
62     style,
63     "unclear name"
64 }
65
66 pub struct NonExpressiveNames {
67     pub single_char_binding_names_threshold: u64,
68 }
69
70 impl LintPass for NonExpressiveNames {
71     fn get_lints(&self) -> LintArray {
72         lint_array!(SIMILAR_NAMES, MANY_SINGLE_CHAR_NAMES, JUST_UNDERSCORES_AND_DIGITS)
73     }
74 }
75
76 struct ExistingName {
77     interned: LocalInternedString,
78     span: Span,
79     len: usize,
80     whitelist: &'static [&'static str],
81 }
82
83 struct SimilarNamesLocalVisitor<'a, 'tcx: 'a> {
84     names: Vec<ExistingName>,
85     cx: &'a EarlyContext<'tcx>,
86     lint: &'a NonExpressiveNames,
87     single_char_names: Vec<char>,
88 }
89
90 // this list contains lists of names that are allowed to be similar
91 // the assumption is that no name is ever contained in multiple lists.
92 #[rustfmt::skip]
93 const WHITELIST: &[&[&str]] = &[
94     &["parsed", "parser"],
95     &["lhs", "rhs"],
96     &["tx", "rx"],
97     &["set", "get"],
98     &["args", "arms"],
99     &["qpath", "path"],
100     &["lit", "lint"],
101 ];
102
103 struct SimilarNamesNameVisitor<'a: 'b, 'tcx: 'a, 'b>(&'b mut SimilarNamesLocalVisitor<'a, 'tcx>);
104
105 impl<'a, 'tcx: 'a, 'b> Visitor<'tcx> for SimilarNamesNameVisitor<'a, 'tcx, 'b> {
106     fn visit_pat(&mut self, pat: &'tcx Pat) {
107         match pat.node {
108             PatKind::Ident(_, ident, _) => self.check_name(ident.span, ident.name),
109             PatKind::Struct(_, ref fields, _) => {
110                 for field in fields {
111                     if !field.node.is_shorthand {
112                         self.visit_pat(&field.node.pat);
113                     }
114                 }
115             },
116             _ => walk_pat(self, pat),
117         }
118     }
119     fn visit_mac(&mut self, _mac: &Mac) {
120         // do not check macs
121     }
122 }
123
124 fn get_whitelist(interned_name: &str) -> Option<&'static [&'static str]> {
125     for &allow in WHITELIST {
126         if whitelisted(interned_name, allow) {
127             return Some(allow);
128         }
129     }
130     None
131 }
132
133 fn whitelisted(interned_name: &str, list: &[&str]) -> bool {
134     list.iter()
135         .any(|&name| interned_name.starts_with(name) || interned_name.ends_with(name))
136 }
137
138 impl<'a, 'tcx, 'b> SimilarNamesNameVisitor<'a, 'tcx, 'b> {
139     fn check_short_name(&mut self, c: char, span: Span) {
140         // make sure we ignore shadowing
141         if self.0.single_char_names.contains(&c) {
142             return;
143         }
144         self.0.single_char_names.push(c);
145         if self.0.single_char_names.len() as u64 >= self.0.lint.single_char_binding_names_threshold {
146             span_lint(
147                 self.0.cx,
148                 MANY_SINGLE_CHAR_NAMES,
149                 span,
150                 &format!(
151                     "{}th binding whose name is just one char",
152                     self.0.single_char_names.len()
153                 ),
154             );
155         }
156     }
157     fn check_name(&mut self, span: Span, name: Name) {
158         let interned_name = name.as_str();
159         if interned_name.chars().any(char::is_uppercase) {
160             return;
161         }
162         if interned_name.chars().all(|c| c.is_digit(10) || c == '_') {
163             span_lint(
164                 self.0.cx,
165                 JUST_UNDERSCORES_AND_DIGITS,
166                 span,
167                 "consider choosing a more descriptive name",
168             );
169             return;
170         }
171         let count = interned_name.chars().count();
172         if count < 3 {
173             if count == 1 {
174                 let c = interned_name.chars().next().expect("already checked");
175                 self.check_short_name(c, span);
176             }
177             return;
178         }
179         for existing_name in &self.0.names {
180             if whitelisted(&interned_name, existing_name.whitelist) {
181                 continue;
182             }
183             let mut split_at = None;
184             if existing_name.len > count {
185                 if existing_name.len - count != 1 || levenstein_not_1(&interned_name, &existing_name.interned) {
186                     continue;
187                 }
188             } else if existing_name.len < count {
189                 if count - existing_name.len != 1 || levenstein_not_1(&existing_name.interned, &interned_name) {
190                     continue;
191                 }
192             } else {
193                 let mut interned_chars = interned_name.chars();
194                 let mut existing_chars = existing_name.interned.chars();
195                 let first_i = interned_chars.next().expect("we know we have at least one char");
196                 let first_e = existing_chars.next().expect("we know we have at least one char");
197                 let eq_or_numeric = |(a, b): (char, char)| a == b || a.is_numeric() && b.is_numeric();
198
199                 if eq_or_numeric((first_i, first_e)) {
200                     let last_i = interned_chars.next_back().expect("we know we have at least two chars");
201                     let last_e = existing_chars.next_back().expect("we know we have at least two chars");
202                     if eq_or_numeric((last_i, last_e)) {
203                         if interned_chars
204                             .zip(existing_chars)
205                             .filter(|&ie| !eq_or_numeric(ie))
206                             .count()
207                             != 1
208                         {
209                             continue;
210                         }
211                     } else {
212                         let second_last_i = interned_chars
213                             .next_back()
214                             .expect("we know we have at least three chars");
215                         let second_last_e = existing_chars
216                             .next_back()
217                             .expect("we know we have at least three chars");
218                         if !eq_or_numeric((second_last_i, second_last_e))
219                             || second_last_i == '_'
220                             || !interned_chars.zip(existing_chars).all(eq_or_numeric)
221                         {
222                             // allowed similarity foo_x, foo_y
223                             // or too many chars differ (foo_x, boo_y) or (foox, booy)
224                             continue;
225                         }
226                         split_at = interned_name.char_indices().rev().next().map(|(i, _)| i);
227                     }
228                 } else {
229                     let second_i = interned_chars.next().expect("we know we have at least two chars");
230                     let second_e = existing_chars.next().expect("we know we have at least two chars");
231                     if !eq_or_numeric((second_i, second_e))
232                         || second_i == '_'
233                         || !interned_chars.zip(existing_chars).all(eq_or_numeric)
234                     {
235                         // allowed similarity x_foo, y_foo
236                         // or too many chars differ (x_foo, y_boo) or (xfoo, yboo)
237                         continue;
238                     }
239                     split_at = interned_name.chars().next().map(|c| c.len_utf8());
240                 }
241             }
242             span_lint_and_then(
243                 self.0.cx,
244                 SIMILAR_NAMES,
245                 span,
246                 "binding's name is too similar to existing binding",
247                 |diag| {
248                     diag.span_note(existing_name.span, "existing binding defined here");
249                     if let Some(split) = split_at {
250                         diag.span_help(
251                             span,
252                             &format!(
253                                 "separate the discriminating character by an \
254                                  underscore like: `{}_{}`",
255                                 &interned_name[..split],
256                                 &interned_name[split..]
257                             ),
258                         );
259                     }
260                 },
261             );
262             return;
263         }
264         self.0.names.push(ExistingName {
265             whitelist: get_whitelist(&interned_name).unwrap_or(&[]),
266             interned: interned_name,
267             span,
268             len: count,
269         });
270     }
271 }
272
273 impl<'a, 'b> SimilarNamesLocalVisitor<'a, 'b> {
274     /// ensure scoping rules work
275     fn apply<F: for<'c> Fn(&'c mut Self)>(&mut self, f: F) {
276         let n = self.names.len();
277         let single_char_count = self.single_char_names.len();
278         f(self);
279         self.names.truncate(n);
280         self.single_char_names.truncate(single_char_count);
281     }
282 }
283
284 impl<'a, 'tcx> Visitor<'tcx> for SimilarNamesLocalVisitor<'a, 'tcx> {
285     fn visit_local(&mut self, local: &'tcx Local) {
286         if let Some(ref init) = local.init {
287             self.apply(|this| walk_expr(this, &**init));
288         }
289         // add the pattern after the expression because the bindings aren't available
290         // yet in the init
291         // expression
292         SimilarNamesNameVisitor(self).visit_pat(&*local.pat);
293     }
294     fn visit_block(&mut self, blk: &'tcx Block) {
295         self.apply(|this| walk_block(this, blk));
296     }
297     fn visit_arm(&mut self, arm: &'tcx Arm) {
298         self.apply(|this| {
299             // just go through the first pattern, as either all patterns
300             // bind the same bindings or rustc would have errored much earlier
301             SimilarNamesNameVisitor(this).visit_pat(&arm.pats[0]);
302             this.apply(|this| walk_expr(this, &arm.body));
303         });
304     }
305     fn visit_item(&mut self, _: &Item) {
306         // do not recurse into inner items
307     }
308     fn visit_mac(&mut self, _mac: &Mac) {
309         // do not check macs
310     }
311 }
312
313 impl EarlyLintPass for NonExpressiveNames {
314     fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
315         if let ItemKind::Fn(ref decl, _, _, ref blk) = item.node {
316             do_check(self, cx, &item.attrs, decl, blk);
317         }
318     }
319
320     fn check_impl_item(&mut self, cx: &EarlyContext<'_>, item: &ImplItem) {
321         if let ImplItemKind::Method(ref sig, ref blk) = item.node {
322             do_check(self, cx, &item.attrs, &sig.decl, blk);
323         }
324     }
325 }
326
327 fn do_check(lint: &mut NonExpressiveNames, cx: &EarlyContext<'_>, attrs: &[Attribute], decl: &FnDecl, blk: &Block) {
328     if !attr::contains_name(attrs, "test") {
329         let mut visitor = SimilarNamesLocalVisitor {
330             names: Vec::new(),
331             cx,
332             lint,
333             single_char_names: Vec::new(),
334         };
335         // initialize with function arguments
336         for arg in &decl.inputs {
337             SimilarNamesNameVisitor(&mut visitor).visit_pat(&arg.pat);
338         }
339         // walk all other bindings
340         walk_block(&mut visitor, blk);
341     }
342 }
343
344 /// Precondition: `a_name.chars().count() < b_name.chars().count()`.
345 fn levenstein_not_1(a_name: &str, b_name: &str) -> bool {
346     debug_assert!(a_name.chars().count() < b_name.chars().count());
347     let mut a_chars = a_name.chars();
348     let mut b_chars = b_name.chars();
349     while let (Some(a), Some(b)) = (a_chars.next(), b_chars.next()) {
350         if a == b {
351             continue;
352         }
353         if let Some(b2) = b_chars.next() {
354             // check if there's just one character inserted
355             return a != b2 || a_chars.ne(b_chars);
356         } else {
357             // tuple
358             // ntuple
359             return true;
360         }
361     }
362     // for item in items
363     true
364 }