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