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