]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/non_expressive_names.rs
3bdf2c38d2372c91604baa3c5f985d78c464e701
[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     fn name(&self) -> &'static str {
76         "NoneExpressiveNames"
77     }
78 }
79
80 struct ExistingName {
81     interned: LocalInternedString,
82     span: Span,
83     len: usize,
84     whitelist: &'static [&'static str],
85 }
86
87 struct SimilarNamesLocalVisitor<'a, 'tcx: 'a> {
88     names: Vec<ExistingName>,
89     cx: &'a EarlyContext<'tcx>,
90     lint: &'a NonExpressiveNames,
91     single_char_names: Vec<char>,
92 }
93
94 // this list contains lists of names that are allowed to be similar
95 // the assumption is that no name is ever contained in multiple lists.
96 #[rustfmt::skip]
97 const WHITELIST: &[&[&str]] = &[
98     &["parsed", "parser"],
99     &["lhs", "rhs"],
100     &["tx", "rx"],
101     &["set", "get"],
102     &["args", "arms"],
103     &["qpath", "path"],
104     &["lit", "lint"],
105 ];
106
107 struct SimilarNamesNameVisitor<'a: 'b, 'tcx: 'a, 'b>(&'b mut SimilarNamesLocalVisitor<'a, 'tcx>);
108
109 impl<'a, 'tcx: 'a, 'b> Visitor<'tcx> for SimilarNamesNameVisitor<'a, 'tcx, 'b> {
110     fn visit_pat(&mut self, pat: &'tcx Pat) {
111         match pat.node {
112             PatKind::Ident(_, ident, _) => self.check_name(ident.span, ident.name),
113             PatKind::Struct(_, ref fields, _) => {
114                 for field in fields {
115                     if !field.node.is_shorthand {
116                         self.visit_pat(&field.node.pat);
117                     }
118                 }
119             },
120             _ => walk_pat(self, pat),
121         }
122     }
123     fn visit_mac(&mut self, _mac: &Mac) {
124         // do not check macs
125     }
126 }
127
128 fn get_whitelist(interned_name: &str) -> Option<&'static [&'static str]> {
129     for &allow in WHITELIST {
130         if whitelisted(interned_name, allow) {
131             return Some(allow);
132         }
133     }
134     None
135 }
136
137 fn whitelisted(interned_name: &str, list: &[&str]) -> bool {
138     list.iter()
139         .any(|&name| interned_name.starts_with(name) || interned_name.ends_with(name))
140 }
141
142 impl<'a, 'tcx, 'b> SimilarNamesNameVisitor<'a, 'tcx, 'b> {
143     fn check_short_name(&mut self, c: char, span: Span) {
144         // make sure we ignore shadowing
145         if self.0.single_char_names.contains(&c) {
146             return;
147         }
148         self.0.single_char_names.push(c);
149         if self.0.single_char_names.len() as u64 >= self.0.lint.single_char_binding_names_threshold {
150             span_lint(
151                 self.0.cx,
152                 MANY_SINGLE_CHAR_NAMES,
153                 span,
154                 &format!(
155                     "{}th binding whose name is just one char",
156                     self.0.single_char_names.len()
157                 ),
158             );
159         }
160     }
161     fn check_name(&mut self, span: Span, name: Name) {
162         let interned_name = name.as_str();
163         if interned_name.chars().any(char::is_uppercase) {
164             return;
165         }
166         if interned_name.chars().all(|c| c.is_digit(10) || c == '_') {
167             span_lint(
168                 self.0.cx,
169                 JUST_UNDERSCORES_AND_DIGITS,
170                 span,
171                 "consider choosing a more descriptive name",
172             );
173             return;
174         }
175         let count = interned_name.chars().count();
176         if count < 3 {
177             if count == 1 {
178                 let c = interned_name.chars().next().expect("already checked");
179                 self.check_short_name(c, span);
180             }
181             return;
182         }
183         for existing_name in &self.0.names {
184             if whitelisted(&interned_name, existing_name.whitelist) {
185                 continue;
186             }
187             let mut split_at = None;
188             if existing_name.len > count {
189                 if existing_name.len - count != 1 || levenstein_not_1(&interned_name, &existing_name.interned) {
190                     continue;
191                 }
192             } else if existing_name.len < count {
193                 if count - existing_name.len != 1 || levenstein_not_1(&existing_name.interned, &interned_name) {
194                     continue;
195                 }
196             } else {
197                 let mut interned_chars = interned_name.chars();
198                 let mut existing_chars = existing_name.interned.chars();
199                 let first_i = interned_chars.next().expect("we know we have at least one char");
200                 let first_e = existing_chars.next().expect("we know we have at least one char");
201                 let eq_or_numeric = |(a, b): (char, char)| a == b || a.is_numeric() && b.is_numeric();
202
203                 if eq_or_numeric((first_i, first_e)) {
204                     let last_i = interned_chars.next_back().expect("we know we have at least two chars");
205                     let last_e = existing_chars.next_back().expect("we know we have at least two chars");
206                     if eq_or_numeric((last_i, last_e)) {
207                         if interned_chars
208                             .zip(existing_chars)
209                             .filter(|&ie| !eq_or_numeric(ie))
210                             .count()
211                             != 1
212                         {
213                             continue;
214                         }
215                     } else {
216                         let second_last_i = interned_chars
217                             .next_back()
218                             .expect("we know we have at least three chars");
219                         let second_last_e = existing_chars
220                             .next_back()
221                             .expect("we know we have at least three chars");
222                         if !eq_or_numeric((second_last_i, second_last_e))
223                             || second_last_i == '_'
224                             || !interned_chars.zip(existing_chars).all(eq_or_numeric)
225                         {
226                             // allowed similarity foo_x, foo_y
227                             // or too many chars differ (foo_x, boo_y) or (foox, booy)
228                             continue;
229                         }
230                         split_at = interned_name.char_indices().rev().next().map(|(i, _)| i);
231                     }
232                 } else {
233                     let second_i = interned_chars.next().expect("we know we have at least two chars");
234                     let second_e = existing_chars.next().expect("we know we have at least two chars");
235                     if !eq_or_numeric((second_i, second_e))
236                         || 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     fn visit_mac(&mut self, _mac: &Mac) {
313         // do not check macs
314     }
315 }
316
317 impl EarlyLintPass for NonExpressiveNames {
318     fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
319         if let ItemKind::Fn(ref decl, _, _, ref blk) = item.node {
320             do_check(self, cx, &item.attrs, decl, blk);
321         }
322     }
323
324     fn check_impl_item(&mut self, cx: &EarlyContext<'_>, item: &ImplItem) {
325         if let ImplItemKind::Method(ref sig, ref blk) = item.node {
326             do_check(self, cx, &item.attrs, &sig.decl, blk);
327         }
328     }
329 }
330
331 fn do_check(lint: &mut NonExpressiveNames, cx: &EarlyContext<'_>, attrs: &[Attribute], decl: &FnDecl, blk: &Block) {
332     if !attr::contains_name(attrs, "test") {
333         let mut visitor = SimilarNamesLocalVisitor {
334             names: Vec::new(),
335             cx,
336             lint,
337             single_char_names: Vec::new(),
338         };
339         // initialize with function arguments
340         for arg in &decl.inputs {
341             SimilarNamesNameVisitor(&mut visitor).visit_pat(&arg.pat);
342         }
343         // walk all other bindings
344         walk_block(&mut visitor, blk);
345     }
346 }
347
348 /// Precondition: `a_name.chars().count() < b_name.chars().count()`.
349 fn levenstein_not_1(a_name: &str, b_name: &str) -> bool {
350     debug_assert!(a_name.chars().count() < b_name.chars().count());
351     let mut a_chars = a_name.chars();
352     let mut b_chars = b_name.chars();
353     while let (Some(a), Some(b)) = (a_chars.next(), b_chars.next()) {
354         if a == b {
355             continue;
356         }
357         if let Some(b2) = b_chars.next() {
358             // check if there's just one character inserted
359             return a != b2 || a_chars.ne(b_chars);
360         } else {
361             // tuple
362             // ntuple
363             return true;
364         }
365     }
366     // for item in items
367     true
368 }