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