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