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