]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/non_expressive_names.rs
Merge branch 'macro-use' into HEAD
[rust.git] / clippy_lints / src / non_expressive_names.rs
1 use rustc::lint::*;
2 use rustc::{declare_lint, lint_array};
3 use syntax::codemap::Span;
4 use syntax::symbol::LocalInternedString;
5 use syntax::ast::*;
6 use syntax::attr;
7 use syntax::visit::{walk_block, walk_expr, walk_pat, Visitor};
8 use crate::utils::{in_macro, span_lint, span_lint_and_then};
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 #[cfg_attr(rustfmt, 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, _) => for field in fields {
110                 if !field.node.is_shorthand {
111                     self.visit_pat(&field.node.pat);
112                 }
113             },
114             _ => walk_pat(self, pat),
115         }
116     }
117 }
118
119 fn get_whitelist(interned_name: &str) -> Option<&'static [&'static str]> {
120     for &allow in WHITELIST {
121         if whitelisted(interned_name, allow) {
122             return Some(allow);
123         }
124     }
125     None
126 }
127
128 fn whitelisted(interned_name: &str, list: &[&str]) -> bool {
129     list.iter()
130         .any(|&name| interned_name.starts_with(name) || interned_name.ends_with(name))
131 }
132
133 impl<'a, 'tcx, 'b> SimilarNamesNameVisitor<'a, 'tcx, 'b> {
134     fn check_short_name(&mut self, c: char, span: Span) {
135         // make sure we ignore shadowing
136         if self.0.single_char_names.contains(&c) {
137             return;
138         }
139         self.0.single_char_names.push(c);
140         if self.0.single_char_names.len() as u64 >= self.0.lint.single_char_binding_names_threshold {
141             span_lint(
142                 self.0.cx,
143                 MANY_SINGLE_CHAR_NAMES,
144                 span,
145                 &format!("{}th binding whose name is just one char", self.0.single_char_names.len()),
146             );
147         }
148     }
149     fn check_name(&mut self, span: Span, name: Name) {
150         if in_macro(span) {
151             return;
152         }
153         let interned_name = name.as_str();
154         if interned_name.chars().any(char::is_uppercase) {
155             return;
156         }
157         if interned_name.chars().all(|c| c.is_digit(10) || c == '_') {
158             span_lint(
159                 self.0.cx,
160                 JUST_UNDERSCORES_AND_DIGITS,
161                 span,
162                 "consider choosing a more descriptive name",
163             );
164             return;
165         }
166         let count = interned_name.chars().count();
167         if count < 3 {
168             if count == 1 {
169                 let c = interned_name.chars().next().expect("already checked");
170                 self.check_short_name(c, span);
171             }
172             return;
173         }
174         for existing_name in &self.0.names {
175             if whitelisted(&interned_name, existing_name.whitelist) {
176                 continue;
177             }
178             let mut split_at = None;
179             if existing_name.len > count {
180                 if existing_name.len - count != 1 || levenstein_not_1(&interned_name, &existing_name.interned) {
181                     continue;
182                 }
183             } else if existing_name.len < count {
184                 if count - existing_name.len != 1 || levenstein_not_1(&existing_name.interned, &interned_name) {
185                     continue;
186                 }
187             } else {
188                 let mut interned_chars = interned_name.chars();
189                 let mut existing_chars = existing_name.interned.chars();
190                 let first_i = interned_chars
191                     .next()
192                     .expect("we know we have at least one char");
193                 let first_e = existing_chars
194                     .next()
195                     .expect("we know we have at least one char");
196                 let eq_or_numeric = |(a, b): (char, char)| a == b || a.is_numeric() && b.is_numeric();
197
198                 if eq_or_numeric((first_i, first_e)) {
199                     let last_i = interned_chars
200                         .next_back()
201                         .expect("we know we have at least two chars");
202                     let last_e = existing_chars
203                         .next_back()
204                         .expect("we know we have at least two chars");
205                     if eq_or_numeric((last_i, last_e)) {
206                         if interned_chars
207                             .zip(existing_chars)
208                             .filter(|&ie| !eq_or_numeric(ie))
209                             .count() != 1
210                         {
211                             continue;
212                         }
213                     } else {
214                         let second_last_i = interned_chars
215                             .next_back()
216                             .expect("we know we have at least three chars");
217                         let second_last_e = existing_chars
218                             .next_back()
219                             .expect("we know we have at least three chars");
220                         if !eq_or_numeric((second_last_i, second_last_e)) || second_last_i == '_'
221                             || !interned_chars.zip(existing_chars).all(eq_or_numeric)
222                         {
223                             // allowed similarity foo_x, foo_y
224                             // or too many chars differ (foo_x, boo_y) or (foox, booy)
225                             continue;
226                         }
227                         split_at = interned_name.char_indices().rev().next().map(|(i, _)| i);
228                     }
229                 } else {
230                     let second_i = interned_chars
231                         .next()
232                         .expect("we know we have at least two chars");
233                     let second_e = existing_chars
234                         .next()
235                         .expect("we know we have at least two chars");
236                     if !eq_or_numeric((second_i, second_e)) || second_i == '_'
237                         || !interned_chars.zip(existing_chars).all(eq_or_numeric)
238                     {
239                         // allowed similarity x_foo, y_foo
240                         // or too many chars differ (x_foo, y_boo) or (xfoo, yboo)
241                         continue;
242                     }
243                     split_at = interned_name.chars().next().map(|c| c.len_utf8());
244                 }
245             }
246             span_lint_and_then(
247                 self.0.cx,
248                 SIMILAR_NAMES,
249                 span,
250                 "binding's name is too similar to existing binding",
251                 |diag| {
252                     diag.span_note(existing_name.span, "existing binding defined here");
253                     if let Some(split) = split_at {
254                         diag.span_help(
255                             span,
256                             &format!(
257                                 "separate the discriminating character by an \
258                                  underscore like: `{}_{}`",
259                                 &interned_name[..split],
260                                 &interned_name[split..]
261                             ),
262                         );
263                     }
264                 },
265             );
266             return;
267         }
268         self.0.names.push(ExistingName {
269             whitelist: get_whitelist(&interned_name).unwrap_or(&[]),
270             interned: interned_name,
271             span,
272             len: count,
273         });
274     }
275 }
276
277 impl<'a, 'b> SimilarNamesLocalVisitor<'a, 'b> {
278     /// ensure scoping rules work
279     fn apply<F: for<'c> Fn(&'c mut Self)>(&mut self, f: F) {
280         let n = self.names.len();
281         let single_char_count = self.single_char_names.len();
282         f(self);
283         self.names.truncate(n);
284         self.single_char_names.truncate(single_char_count);
285     }
286 }
287
288 impl<'a, 'tcx> Visitor<'tcx> for SimilarNamesLocalVisitor<'a, 'tcx> {
289     fn visit_local(&mut self, local: &'tcx Local) {
290         if let Some(ref init) = local.init {
291             self.apply(|this| walk_expr(this, &**init));
292         }
293         // add the pattern after the expression because the bindings aren't available
294         // yet in the init
295         // expression
296         SimilarNamesNameVisitor(self).visit_pat(&*local.pat);
297     }
298     fn visit_block(&mut self, blk: &'tcx Block) {
299         self.apply(|this| walk_block(this, blk));
300     }
301     fn visit_arm(&mut self, arm: &'tcx Arm) {
302         self.apply(|this| {
303             // just go through the first pattern, as either all patterns
304             // bind the same bindings or rustc would have errored much earlier
305             SimilarNamesNameVisitor(this).visit_pat(&arm.pats[0]);
306             this.apply(|this| walk_expr(this, &arm.body));
307         });
308     }
309     fn visit_item(&mut self, _: &Item) {
310         // do not recurse into inner items
311     }
312 }
313
314 impl EarlyLintPass for NonExpressiveNames {
315     fn check_item(&mut self, cx: &EarlyContext, item: &Item) {
316         if let ItemKind::Fn(ref decl, _, _, ref blk) = item.node {
317             do_check(self, cx, &item.attrs, decl, blk);
318         }
319     }
320
321     fn check_impl_item(&mut self, cx: &EarlyContext, item: &ImplItem) {
322         if let ImplItemKind::Method(ref sig, ref blk) = item.node {
323             do_check(self, cx, &item.attrs, &sig.decl, blk);
324         }
325     }
326
327 }
328
329 fn do_check(lint: &mut NonExpressiveNames, cx: &EarlyContext, attrs: &[Attribute], decl: &FnDecl, blk: &Block) {
330     if !attr::contains_name(attrs, "test") {
331         let mut visitor = SimilarNamesLocalVisitor {
332             names: Vec::new(),
333             cx,
334             lint,
335             single_char_names: Vec::new(),
336         };
337         // initialize with function arguments
338         for arg in &decl.inputs {
339             SimilarNamesNameVisitor(&mut visitor).visit_pat(&arg.pat);
340         }
341         // walk all other bindings
342         walk_block(&mut visitor, blk);
343     }
344 }
345
346 /// Precondition: `a_name.chars().count() < b_name.chars().count()`.
347 fn levenstein_not_1(a_name: &str, b_name: &str) -> bool {
348     debug_assert!(a_name.chars().count() < b_name.chars().count());
349     let mut a_chars = a_name.chars();
350     let mut b_chars = b_name.chars();
351     while let (Some(a), Some(b)) = (a_chars.next(), b_chars.next()) {
352         if a == b {
353             continue;
354         }
355         if let Some(b2) = b_chars.next() {
356             // check if there's just one character inserted
357             return a != b2 || a_chars.ne(b_chars);
358         } else {
359             // tuple
360             // ntuple
361             return true;
362         }
363     }
364     // for item in items
365     true
366 }