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