]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/booleans.rs
Close code block in docstring
[rust.git] / clippy_lints / src / booleans.rs
1 use rustc::lint::{LintArray, LateLintPass, LateContext, LintPass};
2 use rustc::hir::*;
3 use rustc::hir::intravisit::*;
4 use syntax::ast::{LitKind, DUMMY_NODE_ID};
5 use syntax::codemap::{DUMMY_SP, dummy_spanned};
6 use syntax::util::ThinVec;
7 use utils::{span_lint_and_then, in_macro, snippet_opt, SpanlessEq};
8
9 /// **What it does:** Checks for boolean expressions that can be written more
10 /// concisely.
11 ///
12 /// **Why is this bad?** Readability of boolean expressions suffers from
13 /// unnecessary duplication.
14 ///
15 /// **Known problems:** Ignores short circuiting behavior of `||` and
16 /// `&&`. Ignores `|`, `&` and `^`.
17 ///
18 /// **Example:**
19 /// ```rust
20 /// if a && true  // should be: if a
21 /// if !(a == b)  // should be: if a != b
22 /// ```
23 declare_lint! {
24     pub NONMINIMAL_BOOL,
25     Allow,
26     "boolean expressions that can be written more concisely"
27 }
28
29 /// **What it does:** Checks for boolean expressions that contain terminals that
30 /// can be eliminated.
31 ///
32 /// **Why is this bad?** This is most likely a logic bug.
33 ///
34 /// **Known problems:** Ignores short circuiting behavior.
35 ///
36 /// **Example:**
37 /// ```rust
38 /// if a && b || a { ... }
39 /// ```
40 /// The `b` is unnecessary, the expression is equivalent to `if a`.
41 declare_lint! {
42     pub LOGIC_BUG,
43     Warn,
44     "boolean expressions that contain terminals which can be eliminated"
45 }
46
47 #[derive(Copy,Clone)]
48 pub struct NonminimalBool;
49
50 impl LintPass for NonminimalBool {
51     fn get_lints(&self) -> LintArray {
52         lint_array!(NONMINIMAL_BOOL, LOGIC_BUG)
53     }
54 }
55
56 impl LateLintPass for NonminimalBool {
57     fn check_item(&mut self, cx: &LateContext, item: &Item) {
58         NonminimalBoolVisitor(cx).visit_item(item)
59     }
60 }
61
62 struct NonminimalBoolVisitor<'a, 'tcx: 'a>(&'a LateContext<'a, 'tcx>);
63
64 use quine_mc_cluskey::Bool;
65 struct Hir2Qmm<'a, 'tcx: 'a, 'v> {
66     terminals: Vec<&'v Expr>,
67     cx: &'a LateContext<'a, 'tcx>,
68 }
69
70 impl<'a, 'tcx, 'v> Hir2Qmm<'a, 'tcx, 'v> {
71     fn extract(&mut self, op: BinOp_, a: &[&'v Expr], mut v: Vec<Bool>) -> Result<Vec<Bool>, String> {
72         for a in a {
73             if let ExprBinary(binop, ref lhs, ref rhs) = a.node {
74                 if binop.node == op {
75                     v = self.extract(op, &[lhs, rhs], v)?;
76                     continue;
77                 }
78             }
79             v.push(self.run(a)?);
80         }
81         Ok(v)
82     }
83
84     fn run(&mut self, e: &'v Expr) -> Result<Bool, String> {
85         // prevent folding of `cfg!` macros and the like
86         if !in_macro(self.cx, e.span) {
87             match e.node {
88                 ExprUnary(UnNot, ref inner) => return Ok(Bool::Not(box self.run(inner)?)),
89                 ExprBinary(binop, ref lhs, ref rhs) => {
90                     match binop.node {
91                         BiOr => return Ok(Bool::Or(self.extract(BiOr, &[lhs, rhs], Vec::new())?)),
92                         BiAnd => return Ok(Bool::And(self.extract(BiAnd, &[lhs, rhs], Vec::new())?)),
93                         _ => (),
94                     }
95                 }
96                 ExprLit(ref lit) => {
97                     match lit.node {
98                         LitKind::Bool(true) => return Ok(Bool::True),
99                         LitKind::Bool(false) => return Ok(Bool::False),
100                         _ => (),
101                     }
102                 }
103                 _ => (),
104             }
105         }
106         for (n, expr) in self.terminals.iter().enumerate() {
107             if SpanlessEq::new(self.cx).ignore_fn().eq_expr(e, expr) {
108                 #[allow(cast_possible_truncation)]
109                 return Ok(Bool::Term(n as u8));
110             }
111             let negated = match e.node {
112                 ExprBinary(binop, ref lhs, ref rhs) => {
113                     let mk_expr = |op| {
114                         Expr {
115                             id: DUMMY_NODE_ID,
116                             span: DUMMY_SP,
117                             attrs: ThinVec::new(),
118                             node: ExprBinary(dummy_spanned(op), lhs.clone(), rhs.clone()),
119                         }
120                     };
121                     match binop.node {
122                         BiEq => mk_expr(BiNe),
123                         BiNe => mk_expr(BiEq),
124                         BiGt => mk_expr(BiLe),
125                         BiGe => mk_expr(BiLt),
126                         BiLt => mk_expr(BiGe),
127                         BiLe => mk_expr(BiGt),
128                         _ => continue,
129                     }
130                 }
131                 _ => continue,
132             };
133             if SpanlessEq::new(self.cx).ignore_fn().eq_expr(&negated, expr) {
134                 #[allow(cast_possible_truncation)]
135                 return Ok(Bool::Not(Box::new(Bool::Term(n as u8))));
136             }
137         }
138         let n = self.terminals.len();
139         self.terminals.push(e);
140         if n < 32 {
141             #[allow(cast_possible_truncation)]
142             Ok(Bool::Term(n as u8))
143         } else {
144             Err("too many literals".to_owned())
145         }
146     }
147 }
148
149 fn suggest(cx: &LateContext, suggestion: &Bool, terminals: &[&Expr]) -> String {
150     fn recurse(brackets: bool, cx: &LateContext, suggestion: &Bool, terminals: &[&Expr], mut s: String) -> String {
151         use quine_mc_cluskey::Bool::*;
152         let snip = |e: &Expr| snippet_opt(cx, e.span).expect("don't try to improve booleans created by macros");
153         match *suggestion {
154             True => {
155                 s.push_str("true");
156                 s
157             }
158             False => {
159                 s.push_str("false");
160                 s
161             }
162             Not(ref inner) => {
163                 match **inner {
164                     And(_) | Or(_) => {
165                         s.push('!');
166                         recurse(true, cx, inner, terminals, s)
167                     }
168                     Term(n) => {
169                         if let ExprBinary(binop, ref lhs, ref rhs) = terminals[n as usize].node {
170                             let op = match binop.node {
171                                 BiEq => " != ",
172                                 BiNe => " == ",
173                                 BiLt => " >= ",
174                                 BiGt => " <= ",
175                                 BiLe => " > ",
176                                 BiGe => " < ",
177                                 _ => {
178                                     s.push('!');
179                                     return recurse(true, cx, inner, terminals, s);
180                                 }
181                             };
182                             s.push_str(&snip(lhs));
183                             s.push_str(op);
184                             s.push_str(&snip(rhs));
185                             s
186                         } else {
187                             s.push('!');
188                             recurse(false, cx, inner, terminals, s)
189                         }
190                     }
191                     _ => {
192                         s.push('!');
193                         recurse(false, cx, inner, terminals, s)
194                     }
195                 }
196             }
197             And(ref v) => {
198                 if brackets {
199                     s.push('(');
200                 }
201                 if let Or(_) = v[0] {
202                     s = recurse(true, cx, &v[0], terminals, s);
203                 } else {
204                     s = recurse(false, cx, &v[0], terminals, s);
205                 }
206                 for inner in &v[1..] {
207                     s.push_str(" && ");
208                     if let Or(_) = *inner {
209                         s = recurse(true, cx, inner, terminals, s);
210                     } else {
211                         s = recurse(false, cx, inner, terminals, s);
212                     }
213                 }
214                 if brackets {
215                     s.push(')');
216                 }
217                 s
218             }
219             Or(ref v) => {
220                 if brackets {
221                     s.push('(');
222                 }
223                 s = recurse(false, cx, &v[0], terminals, s);
224                 for inner in &v[1..] {
225                     s.push_str(" || ");
226                     s = recurse(false, cx, inner, terminals, s);
227                 }
228                 if brackets {
229                     s.push(')');
230                 }
231                 s
232             }
233             Term(n) => {
234                 if brackets {
235                     if let ExprBinary(..) = terminals[n as usize].node {
236                         s.push('(');
237                     }
238                 }
239                 s.push_str(&snip(terminals[n as usize]));
240                 if brackets {
241                     if let ExprBinary(..) = terminals[n as usize].node {
242                         s.push(')');
243                     }
244                 }
245                 s
246             }
247         }
248     }
249     recurse(false, cx, suggestion, terminals, String::new())
250 }
251
252 fn simple_negate(b: Bool) -> Bool {
253     use quine_mc_cluskey::Bool::*;
254     match b {
255         True => False,
256         False => True,
257         t @ Term(_) => Not(Box::new(t)),
258         And(mut v) => {
259             for el in &mut v {
260                 *el = simple_negate(::std::mem::replace(el, True));
261             }
262             Or(v)
263         }
264         Or(mut v) => {
265             for el in &mut v {
266                 *el = simple_negate(::std::mem::replace(el, True));
267             }
268             And(v)
269         }
270         Not(inner) => *inner,
271     }
272 }
273
274 #[derive(Default)]
275 struct Stats {
276     terminals: [usize; 32],
277     negations: usize,
278     ops: usize,
279 }
280
281 fn terminal_stats(b: &Bool) -> Stats {
282     fn recurse(b: &Bool, stats: &mut Stats) {
283         match *b {
284             True | False => stats.ops += 1,
285             Not(ref inner) => {
286                 match **inner {
287                     And(_) | Or(_) => stats.ops += 1, // brackets are also operations
288                     _ => stats.negations += 1,
289                 }
290                 recurse(inner, stats);
291             }
292             And(ref v) | Or(ref v) => {
293                 stats.ops += v.len() - 1;
294                 for inner in v {
295                     recurse(inner, stats);
296                 }
297             }
298             Term(n) => stats.terminals[n as usize] += 1,
299         }
300     }
301     use quine_mc_cluskey::Bool::*;
302     let mut stats = Stats::default();
303     recurse(b, &mut stats);
304     stats
305 }
306
307 impl<'a, 'tcx> NonminimalBoolVisitor<'a, 'tcx> {
308     fn bool_expr(&self, e: &Expr) {
309         let mut h2q = Hir2Qmm {
310             terminals: Vec::new(),
311             cx: self.0,
312         };
313         if let Ok(expr) = h2q.run(e) {
314
315             if h2q.terminals.len() > 8 {
316                 // QMC has exponentially slow behavior as the number of terminals increases
317                 // 8 is reasonable, it takes approximately 0.2 seconds.
318                 // See #825
319                 return;
320             }
321
322             let stats = terminal_stats(&expr);
323             let mut simplified = expr.simplify();
324             for simple in Bool::Not(Box::new(expr.clone())).simplify() {
325                 match simple {
326                     Bool::Not(_) | Bool::True | Bool::False => {}
327                     _ => simplified.push(Bool::Not(Box::new(simple.clone()))),
328                 }
329                 let simple_negated = simple_negate(simple);
330                 if simplified.iter().any(|s| *s == simple_negated) {
331                     continue;
332                 }
333                 simplified.push(simple_negated);
334             }
335             let mut improvements = Vec::new();
336             'simplified: for suggestion in &simplified {
337                 let simplified_stats = terminal_stats(suggestion);
338                 let mut improvement = false;
339                 for i in 0..32 {
340                     // ignore any "simplifications" that end up requiring a terminal more often
341                     // than in the original expression
342                     if stats.terminals[i] < simplified_stats.terminals[i] {
343                         continue 'simplified;
344                     }
345                     if stats.terminals[i] != 0 && simplified_stats.terminals[i] == 0 {
346                         span_lint_and_then(self.0,
347                                            LOGIC_BUG,
348                                            e.span,
349                                            "this boolean expression contains a logic bug",
350                                            |db| {
351                             db.span_help(h2q.terminals[i].span,
352                                          "this expression can be optimized out by applying boolean operations to the \
353                                           outer expression");
354                             db.span_suggestion(e.span,
355                                                "it would look like the following",
356                                                suggest(self.0, suggestion, &h2q.terminals));
357                         });
358                         // don't also lint `NONMINIMAL_BOOL`
359                         return;
360                     }
361                     // if the number of occurrences of a terminal decreases or any of the stats
362                     // decreases while none increases
363                     improvement |= (stats.terminals[i] > simplified_stats.terminals[i]) ||
364                                    (stats.negations > simplified_stats.negations &&
365                                     stats.ops == simplified_stats.ops) ||
366                                    (stats.ops > simplified_stats.ops && stats.negations == simplified_stats.negations);
367                 }
368                 if improvement {
369                     improvements.push(suggestion);
370                 }
371             }
372             if !improvements.is_empty() {
373                 span_lint_and_then(self.0,
374                                    NONMINIMAL_BOOL,
375                                    e.span,
376                                    "this boolean expression can be simplified",
377                                    |db| {
378                     for suggestion in &improvements {
379                         db.span_suggestion(e.span, "try", suggest(self.0, suggestion, &h2q.terminals));
380                     }
381                 });
382             }
383         }
384     }
385 }
386
387 impl<'a, 'v, 'tcx> Visitor<'v> for NonminimalBoolVisitor<'a, 'tcx> {
388     fn visit_expr(&mut self, e: &'v Expr) {
389         if in_macro(self.0, e.span) {
390             return;
391         }
392         match e.node {
393             ExprBinary(binop, _, _) if binop.node == BiOr || binop.node == BiAnd => self.bool_expr(e),
394             ExprUnary(UnNot, ref inner) => {
395                 if self.0.tcx.node_types()[&inner.id].is_bool() {
396                     self.bool_expr(e);
397                 } else {
398                     walk_expr(self, e);
399                 }
400             }
401             _ => walk_expr(self, e),
402         }
403     }
404 }