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