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