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