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