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