]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/booleans.rs
Auto merge of #3790 - ljedrz:HirIdify_intravisit, r=phansch
[rust.git] / clippy_lints / src / booleans.rs
1 use crate::utils::{
2     get_trait_def_id, implements_trait, in_macro, match_type, paths, snippet_opt, span_lint_and_then, SpanlessEq,
3 };
4 use rustc::hir::intravisit::*;
5 use rustc::hir::*;
6 use rustc::lint::{LateContext, LateLintPass, LintArray, LintPass};
7 use rustc::{declare_tool_lint, lint_array};
8 use rustc_data_structures::thin_vec::ThinVec;
9 use rustc_errors::Applicability;
10 use syntax::ast::{LitKind, DUMMY_NODE_ID};
11 use syntax::source_map::{dummy_spanned, Span, DUMMY_SP};
12
13 /// **What it does:** Checks for boolean expressions that can be written more
14 /// concisely.
15 ///
16 /// **Why is this bad?** Readability of boolean expressions suffers from
17 /// unnecessary duplication.
18 ///
19 /// **Known problems:** Ignores short circuiting behavior of `||` and
20 /// `&&`. Ignores `|`, `&` and `^`.
21 ///
22 /// **Example:**
23 /// ```rust
24 /// if a && true  // should be: if a
25 /// if !(a == b)  // should be: if a != b
26 /// ```
27 declare_clippy_lint! {
28     pub NONMINIMAL_BOOL,
29     complexity,
30     "boolean expressions that can be written more concisely"
31 }
32
33 /// **What it does:** Checks for boolean expressions that contain terminals that
34 /// can be eliminated.
35 ///
36 /// **Why is this bad?** This is most likely a logic bug.
37 ///
38 /// **Known problems:** Ignores short circuiting behavior.
39 ///
40 /// **Example:**
41 /// ```rust
42 /// if a && b || a { ... }
43 /// ```
44 /// The `b` is unnecessary, the expression is equivalent to `if a`.
45 declare_clippy_lint! {
46     pub LOGIC_BUG,
47     correctness,
48     "boolean expressions that contain terminals which can be eliminated"
49 }
50
51 // For each pairs, both orders are considered.
52 const METHODS_WITH_NEGATION: [(&str, &str); 2] = [("is_some", "is_none"), ("is_err", "is_ok")];
53
54 #[derive(Copy, Clone)]
55 pub struct NonminimalBool;
56
57 impl LintPass for NonminimalBool {
58     fn get_lints(&self) -> LintArray {
59         lint_array!(NONMINIMAL_BOOL, LOGIC_BUG)
60     }
61
62     fn name(&self) -> &'static str {
63         "NonminimalBool"
64     }
65 }
66
67 impl<'a, 'tcx> LateLintPass<'a, 'tcx> for NonminimalBool {
68     fn check_fn(
69         &mut self,
70         cx: &LateContext<'a, 'tcx>,
71         _: intravisit::FnKind<'tcx>,
72         _: &'tcx FnDecl,
73         body: &'tcx Body,
74         _: Span,
75         _: HirId,
76     ) {
77         NonminimalBoolVisitor { cx }.visit_body(body)
78     }
79 }
80
81 struct NonminimalBoolVisitor<'a, 'tcx: 'a> {
82     cx: &'a LateContext<'a, 'tcx>,
83 }
84
85 use quine_mc_cluskey::Bool;
86 struct Hir2Qmm<'a, 'tcx: 'a, 'v> {
87     terminals: Vec<&'v Expr>,
88     cx: &'a LateContext<'a, 'tcx>,
89 }
90
91 impl<'a, 'tcx, 'v> Hir2Qmm<'a, 'tcx, 'v> {
92     fn extract(&mut self, op: BinOpKind, a: &[&'v Expr], mut v: Vec<Bool>) -> Result<Vec<Bool>, String> {
93         for a in a {
94             if let ExprKind::Binary(binop, lhs, rhs) = &a.node {
95                 if binop.node == op {
96                     v = self.extract(op, &[lhs, rhs], v)?;
97                     continue;
98                 }
99             }
100             v.push(self.run(a)?);
101         }
102         Ok(v)
103     }
104
105     fn run(&mut self, e: &'v Expr) -> Result<Bool, String> {
106         // prevent folding of `cfg!` macros and the like
107         if !in_macro(e.span) {
108             match &e.node {
109                 ExprKind::Unary(UnNot, inner) => return Ok(Bool::Not(box self.run(inner)?)),
110                 ExprKind::Binary(binop, lhs, rhs) => match &binop.node {
111                     BinOpKind::Or => return Ok(Bool::Or(self.extract(BinOpKind::Or, &[lhs, rhs], Vec::new())?)),
112                     BinOpKind::And => return Ok(Bool::And(self.extract(BinOpKind::And, &[lhs, rhs], Vec::new())?)),
113                     _ => (),
114                 },
115                 ExprKind::Lit(lit) => match lit.node {
116                     LitKind::Bool(true) => return Ok(Bool::True),
117                     LitKind::Bool(false) => return Ok(Bool::False),
118                     _ => (),
119                 },
120                 _ => (),
121             }
122         }
123         for (n, expr) in self.terminals.iter().enumerate() {
124             if SpanlessEq::new(self.cx).ignore_fn().eq_expr(e, expr) {
125                 #[allow(clippy::cast_possible_truncation)]
126                 return Ok(Bool::Term(n as u8));
127             }
128             let negated = match &e.node {
129                 ExprKind::Binary(binop, lhs, rhs) => {
130                     if !implements_ord(self.cx, lhs) {
131                         continue;
132                     }
133
134                     let mk_expr = |op| Expr {
135                         id: DUMMY_NODE_ID,
136                         hir_id: DUMMY_HIR_ID,
137                         span: DUMMY_SP,
138                         attrs: ThinVec::new(),
139                         node: ExprKind::Binary(dummy_spanned(op), lhs.clone(), rhs.clone()),
140                     };
141                     match binop.node {
142                         BinOpKind::Eq => mk_expr(BinOpKind::Ne),
143                         BinOpKind::Ne => mk_expr(BinOpKind::Eq),
144                         BinOpKind::Gt => mk_expr(BinOpKind::Le),
145                         BinOpKind::Ge => mk_expr(BinOpKind::Lt),
146                         BinOpKind::Lt => mk_expr(BinOpKind::Ge),
147                         BinOpKind::Le => mk_expr(BinOpKind::Gt),
148                         _ => continue,
149                     }
150                 },
151                 _ => continue,
152             };
153             if SpanlessEq::new(self.cx).ignore_fn().eq_expr(&negated, expr) {
154                 #[allow(clippy::cast_possible_truncation)]
155                 return Ok(Bool::Not(Box::new(Bool::Term(n as u8))));
156             }
157         }
158         let n = self.terminals.len();
159         self.terminals.push(e);
160         if n < 32 {
161             #[allow(clippy::cast_possible_truncation)]
162             Ok(Bool::Term(n as u8))
163         } else {
164             Err("too many literals".to_owned())
165         }
166     }
167 }
168
169 struct SuggestContext<'a, 'tcx: 'a, 'v> {
170     terminals: &'v [&'v Expr],
171     cx: &'a LateContext<'a, 'tcx>,
172     output: String,
173     simplified: bool,
174 }
175
176 impl<'a, 'tcx, 'v> SuggestContext<'a, 'tcx, 'v> {
177     fn snip(&self, e: &Expr) -> Option<String> {
178         snippet_opt(self.cx, e.span)
179     }
180
181     fn simplify_not(&self, expr: &Expr) -> Option<String> {
182         match &expr.node {
183             ExprKind::Binary(binop, lhs, rhs) => {
184                 if !implements_ord(self.cx, lhs) {
185                     return None;
186                 }
187
188                 match binop.node {
189                     BinOpKind::Eq => Some(" != "),
190                     BinOpKind::Ne => Some(" == "),
191                     BinOpKind::Lt => Some(" >= "),
192                     BinOpKind::Gt => Some(" <= "),
193                     BinOpKind::Le => Some(" > "),
194                     BinOpKind::Ge => Some(" < "),
195                     _ => None,
196                 }
197                 .and_then(|op| Some(format!("{}{}{}", self.snip(lhs)?, op, self.snip(rhs)?)))
198             },
199             ExprKind::MethodCall(path, _, args) if args.len() == 1 => {
200                 let type_of_receiver = self.cx.tables.expr_ty(&args[0]);
201                 if !match_type(self.cx, type_of_receiver, &paths::OPTION)
202                     && !match_type(self.cx, type_of_receiver, &paths::RESULT)
203                 {
204                     return None;
205                 }
206                 METHODS_WITH_NEGATION
207                     .iter()
208                     .cloned()
209                     .flat_map(|(a, b)| vec![(a, b), (b, a)])
210                     .find(|&(a, _)| a == path.ident.as_str())
211                     .and_then(|(_, neg_method)| Some(format!("{}.{}()", self.snip(&args[0])?, neg_method)))
212             },
213             _ => None,
214         }
215     }
216
217     fn recurse(&mut self, suggestion: &Bool) -> Option<()> {
218         use quine_mc_cluskey::Bool::*;
219         match suggestion {
220             True => {
221                 self.output.push_str("true");
222             },
223             False => {
224                 self.output.push_str("false");
225             },
226             Not(inner) => match **inner {
227                 And(_) | Or(_) => {
228                     self.output.push('!');
229                     self.output.push('(');
230                     self.recurse(inner);
231                     self.output.push(')');
232                 },
233                 Term(n) => {
234                     let terminal = self.terminals[n as usize];
235                     if let Some(str) = self.simplify_not(terminal) {
236                         self.simplified = true;
237                         self.output.push_str(&str)
238                     } else {
239                         self.output.push('!');
240                         let snip = self.snip(terminal)?;
241                         self.output.push_str(&snip);
242                     }
243                 },
244                 True | False | Not(_) => {
245                     self.output.push('!');
246                     self.recurse(inner)?;
247                 },
248             },
249             And(v) => {
250                 for (index, inner) in v.iter().enumerate() {
251                     if index > 0 {
252                         self.output.push_str(" && ");
253                     }
254                     if let Or(_) = *inner {
255                         self.output.push('(');
256                         self.recurse(inner);
257                         self.output.push(')');
258                     } else {
259                         self.recurse(inner);
260                     }
261                 }
262             },
263             Or(v) => {
264                 for (index, inner) in v.iter().enumerate() {
265                     if index > 0 {
266                         self.output.push_str(" || ");
267                     }
268                     self.recurse(inner);
269                 }
270             },
271             &Term(n) => {
272                 let snip = self.snip(self.terminals[n as usize])?;
273                 self.output.push_str(&snip);
274             },
275         }
276         Some(())
277     }
278 }
279
280 // The boolean part of the return indicates whether some simplifications have been applied.
281 fn suggest(cx: &LateContext<'_, '_>, suggestion: &Bool, terminals: &[&Expr]) -> (String, bool) {
282     let mut suggest_context = SuggestContext {
283         terminals,
284         cx,
285         output: String::new(),
286         simplified: false,
287     };
288     suggest_context.recurse(suggestion);
289     (suggest_context.output, suggest_context.simplified)
290 }
291
292 fn simple_negate(b: Bool) -> Bool {
293     use quine_mc_cluskey::Bool::*;
294     match b {
295         True => False,
296         False => True,
297         t @ Term(_) => Not(Box::new(t)),
298         And(mut v) => {
299             for el in &mut v {
300                 *el = simple_negate(::std::mem::replace(el, True));
301             }
302             Or(v)
303         },
304         Or(mut v) => {
305             for el in &mut v {
306                 *el = simple_negate(::std::mem::replace(el, True));
307             }
308             And(v)
309         },
310         Not(inner) => *inner,
311     }
312 }
313
314 #[derive(Default)]
315 struct Stats {
316     terminals: [usize; 32],
317     negations: usize,
318     ops: usize,
319 }
320
321 fn terminal_stats(b: &Bool) -> Stats {
322     fn recurse(b: &Bool, stats: &mut Stats) {
323         match b {
324             True | False => stats.ops += 1,
325             Not(inner) => {
326                 match **inner {
327                     And(_) | Or(_) => stats.ops += 1, // brackets are also operations
328                     _ => stats.negations += 1,
329                 }
330                 recurse(inner, stats);
331             },
332             And(v) | Or(v) => {
333                 stats.ops += v.len() - 1;
334                 for inner in v {
335                     recurse(inner, stats);
336                 }
337             },
338             &Term(n) => stats.terminals[n as usize] += 1,
339         }
340     }
341     use quine_mc_cluskey::Bool::*;
342     let mut stats = Stats::default();
343     recurse(b, &mut stats);
344     stats
345 }
346
347 impl<'a, 'tcx> NonminimalBoolVisitor<'a, 'tcx> {
348     fn bool_expr(&self, e: &Expr) {
349         let mut h2q = Hir2Qmm {
350             terminals: Vec::new(),
351             cx: self.cx,
352         };
353         if let Ok(expr) = h2q.run(e) {
354             if h2q.terminals.len() > 8 {
355                 // QMC has exponentially slow behavior as the number of terminals increases
356                 // 8 is reasonable, it takes approximately 0.2 seconds.
357                 // See #825
358                 return;
359             }
360
361             let stats = terminal_stats(&expr);
362             let mut simplified = expr.simplify();
363             for simple in Bool::Not(Box::new(expr.clone())).simplify() {
364                 match simple {
365                     Bool::Not(_) | Bool::True | Bool::False => {},
366                     _ => simplified.push(Bool::Not(Box::new(simple.clone()))),
367                 }
368                 let simple_negated = simple_negate(simple);
369                 if simplified.iter().any(|s| *s == simple_negated) {
370                     continue;
371                 }
372                 simplified.push(simple_negated);
373             }
374             let mut improvements = Vec::new();
375             'simplified: for suggestion in &simplified {
376                 let simplified_stats = terminal_stats(suggestion);
377                 let mut improvement = false;
378                 for i in 0..32 {
379                     // ignore any "simplifications" that end up requiring a terminal more often
380                     // than in the original expression
381                     if stats.terminals[i] < simplified_stats.terminals[i] {
382                         continue 'simplified;
383                     }
384                     if stats.terminals[i] != 0 && simplified_stats.terminals[i] == 0 {
385                         span_lint_and_then(
386                             self.cx,
387                             LOGIC_BUG,
388                             e.span,
389                             "this boolean expression contains a logic bug",
390                             |db| {
391                                 db.span_help(
392                                     h2q.terminals[i].span,
393                                     "this expression can be optimized out by applying boolean operations to the \
394                                      outer expression",
395                                 );
396                                 db.span_suggestion(
397                                     e.span,
398                                     "it would look like the following",
399                                     suggest(self.cx, suggestion, &h2q.terminals).0,
400                                     // nonminimal_bool can produce minimal but
401                                     // not human readable expressions (#3141)
402                                     Applicability::Unspecified,
403                                 );
404                             },
405                         );
406                         // don't also lint `NONMINIMAL_BOOL`
407                         return;
408                     }
409                     // if the number of occurrences of a terminal decreases or any of the stats
410                     // decreases while none increases
411                     improvement |= (stats.terminals[i] > simplified_stats.terminals[i])
412                         || (stats.negations > simplified_stats.negations && stats.ops == simplified_stats.ops)
413                         || (stats.ops > simplified_stats.ops && stats.negations == simplified_stats.negations);
414                 }
415                 if improvement {
416                     improvements.push(suggestion);
417                 }
418             }
419             let nonminimal_bool_lint = |suggestions: Vec<_>| {
420                 span_lint_and_then(
421                     self.cx,
422                     NONMINIMAL_BOOL,
423                     e.span,
424                     "this boolean expression can be simplified",
425                     |db| {
426                         db.span_suggestions(
427                             e.span,
428                             "try",
429                             suggestions.into_iter(),
430                             // nonminimal_bool can produce minimal but
431                             // not human readable expressions (#3141)
432                             Applicability::Unspecified,
433                         );
434                     },
435                 );
436             };
437             if improvements.is_empty() {
438                 let suggest = suggest(self.cx, &expr, &h2q.terminals);
439                 if suggest.1 {
440                     nonminimal_bool_lint(vec![suggest.0])
441                 }
442             } else {
443                 nonminimal_bool_lint(
444                     improvements
445                         .into_iter()
446                         .map(|suggestion| suggest(self.cx, suggestion, &h2q.terminals).0)
447                         .collect(),
448                 );
449             }
450         }
451     }
452 }
453
454 impl<'a, 'tcx> Visitor<'tcx> for NonminimalBoolVisitor<'a, 'tcx> {
455     fn visit_expr(&mut self, e: &'tcx Expr) {
456         if in_macro(e.span) {
457             return;
458         }
459         match &e.node {
460             ExprKind::Binary(binop, _, _) if binop.node == BinOpKind::Or || binop.node == BinOpKind::And => {
461                 self.bool_expr(e)
462             },
463             ExprKind::Unary(UnNot, inner) => {
464                 if self.cx.tables.node_types()[inner.hir_id].is_bool() {
465                     self.bool_expr(e);
466                 } else {
467                     walk_expr(self, e);
468                 }
469             },
470             _ => walk_expr(self, e),
471         }
472     }
473     fn nested_visit_map<'this>(&'this mut self) -> NestedVisitorMap<'this, 'tcx> {
474         NestedVisitorMap::None
475     }
476 }
477
478 fn implements_ord<'a, 'tcx>(cx: &'a LateContext<'a, 'tcx>, expr: &Expr) -> bool {
479     let ty = cx.tables.expr_ty(expr);
480     get_trait_def_id(cx, &paths::ORD).map_or(false, |id| implements_trait(cx, ty, id, &[]))
481 }