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