]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/matches.rs
Merge pull request #2813 from terry90/master
[rust.git] / clippy_lints / src / matches.rs
1 use rustc::hir::*;
2 use rustc::lint::*;
3 use rustc::ty::{self, Ty};
4 use std::cmp::Ordering;
5 use std::collections::Bound;
6 use syntax::ast::LitKind;
7 use syntax::codemap::Span;
8 use crate::utils::paths;
9 use crate::utils::{expr_block, in_external_macro, is_allowed, is_expn_of, match_qpath, match_type, multispan_sugg,
10             remove_blocks, snippet, span_lint_and_sugg, span_lint_and_then, span_note_and_lint, walk_ptrs_ty};
11 use crate::utils::sugg::Sugg;
12 use crate::consts::{constant, Constant};
13
14 /// **What it does:** Checks for matches with a single arm where an `if let`
15 /// will usually suffice.
16 ///
17 /// **Why is this bad?** Just readability – `if let` nests less than a `match`.
18 ///
19 /// **Known problems:** None.
20 ///
21 /// **Example:**
22 /// ```rust
23 /// match x {
24 ///     Some(ref foo) => bar(foo),
25 ///     _ => ()
26 /// }
27 /// ```
28 declare_clippy_lint! {
29     pub SINGLE_MATCH,
30     style,
31     "a match statement with a single nontrivial arm (i.e. where the other arm \
32      is `_ => {}`) instead of `if let`"
33 }
34
35 /// **What it does:** Checks for matches with a two arms where an `if let` will
36 /// usually suffice.
37 ///
38 /// **Why is this bad?** Just readability – `if let` nests less than a `match`.
39 ///
40 /// **Known problems:** Personal style preferences may differ.
41 ///
42 /// **Example:**
43 /// ```rust
44 /// match x {
45 ///     Some(ref foo) => bar(foo),
46 ///     _ => bar(other_ref),
47 /// }
48 /// ```
49 declare_clippy_lint! {
50     pub SINGLE_MATCH_ELSE,
51     pedantic,
52     "a match statement with a two arms where the second arm's pattern is a wildcard \
53      instead of `if let`"
54 }
55
56 /// **What it does:** Checks for matches where all arms match a reference,
57 /// suggesting to remove the reference and deref the matched expression
58 /// instead. It also checks for `if let &foo = bar` blocks.
59 ///
60 /// **Why is this bad?** It just makes the code less readable. That reference
61 /// destructuring adds nothing to the code.
62 ///
63 /// **Known problems:** None.
64 ///
65 /// **Example:**
66 /// ```rust
67 /// match x {
68 ///     &A(ref y) => foo(y),
69 ///     &B => bar(),
70 ///     _ => frob(&x),
71 /// }
72 /// ```
73 declare_clippy_lint! {
74     pub MATCH_REF_PATS,
75     style,
76     "a match or `if let` with all arms prefixed with `&` instead of deref-ing the match expression"
77 }
78
79 /// **What it does:** Checks for matches where match expression is a `bool`. It
80 /// suggests to replace the expression with an `if...else` block.
81 ///
82 /// **Why is this bad?** It makes the code less readable.
83 ///
84 /// **Known problems:** None.
85 ///
86 /// **Example:**
87 /// ```rust
88 /// let condition: bool = true;
89 /// match condition {
90 ///     true => foo(),
91 ///     false => bar(),
92 /// }
93 /// ```
94 declare_clippy_lint! {
95     pub MATCH_BOOL,
96     style,
97     "a match on a boolean expression instead of an `if..else` block"
98 }
99
100 /// **What it does:** Checks for overlapping match arms.
101 ///
102 /// **Why is this bad?** It is likely to be an error and if not, makes the code
103 /// less obvious.
104 ///
105 /// **Known problems:** None.
106 ///
107 /// **Example:**
108 /// ```rust
109 /// let x = 5;
110 /// match x {
111 ///     1 ... 10 => println!("1 ... 10"),
112 ///     5 ... 15 => println!("5 ... 15"),
113 ///     _ => (),
114 /// }
115 /// ```
116 declare_clippy_lint! {
117     pub MATCH_OVERLAPPING_ARM,
118     style,
119     "a match with overlapping arms"
120 }
121
122 /// **What it does:** Checks for arm which matches all errors with `Err(_)`
123 /// and take drastic actions like `panic!`.
124 ///
125 /// **Why is this bad?** It is generally a bad practice, just like
126 /// catching all exceptions in java with `catch(Exception)`
127 ///
128 /// **Known problems:** None.
129 ///
130 /// **Example:**
131 /// ```rust
132 /// let x : Result(i32, &str) = Ok(3);
133 /// match x {
134 ///     Ok(_) => println!("ok"),
135 ///     Err(_) => panic!("err"),
136 /// }
137 /// ```
138 declare_clippy_lint! {
139     pub MATCH_WILD_ERR_ARM,
140     style,
141     "a match with `Err(_)` arm and take drastic actions"
142 }
143
144 /// **What it does:** Checks for match which is used to add a reference to an
145 /// `Option` value.
146 ///
147 /// **Why is this bad?** Using `as_ref()` or `as_mut()` instead is shorter.
148 ///
149 /// **Known problems:** None.
150 ///
151 /// **Example:**
152 /// ```rust
153 /// let x: Option<()> = None;
154 /// let r: Option<&()> = match x {
155 ///   None => None,
156 ///   Some(ref v) => Some(v),
157 /// };
158 /// ```
159 declare_clippy_lint! {
160     pub MATCH_AS_REF,
161     complexity,
162     "a match on an Option value instead of using `as_ref()` or `as_mut`"
163 }
164
165 #[allow(missing_copy_implementations)]
166 pub struct MatchPass;
167
168 impl LintPass for MatchPass {
169     fn get_lints(&self) -> LintArray {
170         lint_array!(
171             SINGLE_MATCH,
172             MATCH_REF_PATS,
173             MATCH_BOOL,
174             SINGLE_MATCH_ELSE,
175             MATCH_OVERLAPPING_ARM,
176             MATCH_WILD_ERR_ARM,
177             MATCH_AS_REF
178         )
179     }
180 }
181
182 impl<'a, 'tcx> LateLintPass<'a, 'tcx> for MatchPass {
183     fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr) {
184         if in_external_macro(cx, expr.span) {
185             return;
186         }
187         if let ExprMatch(ref ex, ref arms, MatchSource::Normal) = expr.node {
188             check_single_match(cx, ex, arms, expr);
189             check_match_bool(cx, ex, arms, expr);
190             check_overlapping_arms(cx, ex, arms);
191             check_wild_err_arm(cx, ex, arms);
192             check_match_as_ref(cx, ex, arms, expr);
193         }
194         if let ExprMatch(ref ex, ref arms, _) = expr.node {
195             check_match_ref_pats(cx, ex, arms, expr);
196         }
197     }
198 }
199
200 #[cfg_attr(rustfmt, rustfmt_skip)]
201 fn check_single_match(cx: &LateContext, ex: &Expr, arms: &[Arm], expr: &Expr) {
202     if arms.len() == 2 &&
203       arms[0].pats.len() == 1 && arms[0].guard.is_none() &&
204       arms[1].pats.len() == 1 && arms[1].guard.is_none() {
205         let els = remove_blocks(&arms[1].body);
206         let els = if is_unit_expr(els) {
207             None
208         } else if let ExprBlock(_, _) = els.node {
209             // matches with blocks that contain statements are prettier as `if let + else`
210             Some(els)
211         } else {
212             // allow match arms with just expressions
213             return;
214         };
215         let ty = cx.tables.expr_ty(ex);
216         if ty.sty != ty::TyBool || is_allowed(cx, MATCH_BOOL, ex.id) {
217             check_single_match_single_pattern(cx, ex, arms, expr, els);
218             check_single_match_opt_like(cx, ex, arms, expr, ty, els);
219         }
220     }
221 }
222
223 fn check_single_match_single_pattern(cx: &LateContext, ex: &Expr, arms: &[Arm], expr: &Expr, els: Option<&Expr>) {
224     if arms[1].pats[0].node == PatKind::Wild {
225         report_single_match_single_pattern(cx, ex, arms, expr, els);
226     }
227 }
228
229 fn report_single_match_single_pattern(cx: &LateContext, ex: &Expr, arms: &[Arm], expr: &Expr, els: Option<&Expr>) {
230     let lint = if els.is_some() {
231         SINGLE_MATCH_ELSE
232     } else {
233         SINGLE_MATCH
234     };
235     let els_str = els.map_or(String::new(), |els| format!(" else {}", expr_block(cx, els, None, "..")));
236     span_lint_and_sugg(
237         cx,
238         lint,
239         expr.span,
240         "you seem to be trying to use match for destructuring a single pattern. Consider using `if \
241          let`",
242         "try this",
243         format!(
244             "if let {} = {} {}{}",
245             snippet(cx, arms[0].pats[0].span, ".."),
246             snippet(cx, ex.span, ".."),
247             expr_block(cx, &arms[0].body, None, ".."),
248             els_str
249         ),
250     );
251 }
252
253 fn check_single_match_opt_like(cx: &LateContext, ex: &Expr, arms: &[Arm], expr: &Expr, ty: Ty, els: Option<&Expr>) {
254     // list of candidate Enums we know will never get any more members
255     let candidates = &[
256         (&paths::COW, "Borrowed"),
257         (&paths::COW, "Cow::Borrowed"),
258         (&paths::COW, "Cow::Owned"),
259         (&paths::COW, "Owned"),
260         (&paths::OPTION, "None"),
261         (&paths::RESULT, "Err"),
262         (&paths::RESULT, "Ok"),
263     ];
264
265     let path = match arms[1].pats[0].node {
266         PatKind::TupleStruct(ref path, ref inner, _) => {
267             // contains any non wildcard patterns? e.g. Err(err)
268             if inner.iter().any(|pat| pat.node != PatKind::Wild) {
269                 return;
270             }
271             print::to_string(print::NO_ANN, |s| s.print_qpath(path, false))
272         },
273         PatKind::Binding(BindingAnnotation::Unannotated, _, ident, None) => ident.node.to_string(),
274         PatKind::Path(ref path) => print::to_string(print::NO_ANN, |s| s.print_qpath(path, false)),
275         _ => return,
276     };
277
278     for &(ty_path, pat_path) in candidates {
279         if path == *pat_path && match_type(cx, ty, ty_path) {
280             report_single_match_single_pattern(cx, ex, arms, expr, els);
281         }
282     }
283 }
284
285 fn check_match_bool(cx: &LateContext, ex: &Expr, arms: &[Arm], expr: &Expr) {
286     // type of expression == bool
287     if cx.tables.expr_ty(ex).sty == ty::TyBool {
288         span_lint_and_then(
289             cx,
290             MATCH_BOOL,
291             expr.span,
292             "you seem to be trying to match on a boolean expression",
293             move |db| {
294                 if arms.len() == 2 && arms[0].pats.len() == 1 {
295                     // no guards
296                     let exprs = if let PatKind::Lit(ref arm_bool) = arms[0].pats[0].node {
297                         if let ExprLit(ref lit) = arm_bool.node {
298                             match lit.node {
299                                 LitKind::Bool(true) => Some((&*arms[0].body, &*arms[1].body)),
300                                 LitKind::Bool(false) => Some((&*arms[1].body, &*arms[0].body)),
301                                 _ => None,
302                             }
303                         } else {
304                             None
305                         }
306                     } else {
307                         None
308                     };
309
310                     if let Some((true_expr, false_expr)) = exprs {
311                         let sugg = match (is_unit_expr(true_expr), is_unit_expr(false_expr)) {
312                             (false, false) => Some(format!(
313                                 "if {} {} else {}",
314                                 snippet(cx, ex.span, "b"),
315                                 expr_block(cx, true_expr, None, ".."),
316                                 expr_block(cx, false_expr, None, "..")
317                             )),
318                             (false, true) => Some(format!(
319                                 "if {} {}",
320                                 snippet(cx, ex.span, "b"),
321                                 expr_block(cx, true_expr, None, "..")
322                             )),
323                             (true, false) => {
324                                 let test = Sugg::hir(cx, ex, "..");
325                                 Some(format!("if {} {}", !test, expr_block(cx, false_expr, None, "..")))
326                             },
327                             (true, true) => None,
328                         };
329
330                         if let Some(sugg) = sugg {
331                             db.span_suggestion(expr.span, "consider using an if/else expression", sugg);
332                         }
333                     }
334                 }
335             },
336         );
337     }
338 }
339
340 fn check_overlapping_arms<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, ex: &'tcx Expr, arms: &'tcx [Arm]) {
341     if arms.len() >= 2 && cx.tables.expr_ty(ex).is_integral() {
342         let ranges = all_ranges(cx, arms);
343         let type_ranges = type_ranges(&ranges);
344         if !type_ranges.is_empty() {
345             if let Some((start, end)) = overlapping(&type_ranges) {
346                 span_note_and_lint(
347                     cx,
348                     MATCH_OVERLAPPING_ARM,
349                     start.span,
350                     "some ranges overlap",
351                     end.span,
352                     "overlaps with this",
353                 );
354             }
355         }
356     }
357 }
358
359 fn check_wild_err_arm(cx: &LateContext, ex: &Expr, arms: &[Arm]) {
360     let ex_ty = walk_ptrs_ty(cx.tables.expr_ty(ex));
361     if match_type(cx, ex_ty, &paths::RESULT) {
362         for arm in arms {
363             if let PatKind::TupleStruct(ref path, ref inner, _) = arm.pats[0].node {
364                 let path_str = print::to_string(print::NO_ANN, |s| s.print_qpath(path, false));
365                 if_chain! {
366                     if path_str == "Err";
367                     if inner.iter().any(|pat| pat.node == PatKind::Wild);
368                     if let ExprBlock(ref block, _) = arm.body.node;
369                     if is_panic_block(block);
370                     then {
371                         // `Err(_)` arm with `panic!` found
372                         span_note_and_lint(cx,
373                                            MATCH_WILD_ERR_ARM,
374                                            arm.pats[0].span,
375                                            "Err(_) will match all errors, maybe not a good idea",
376                                            arm.pats[0].span,
377                                            "to remove this warning, match each error seperately \
378                                             or use unreachable macro");
379                     }
380                 }
381             }
382         }
383     }
384 }
385
386 // If the block contains only a `panic!` macro (as expression or statement)
387 fn is_panic_block(block: &Block) -> bool {
388     match (&block.expr, block.stmts.len(), block.stmts.first()) {
389         (&Some(ref exp), 0, _) => {
390             is_expn_of(exp.span, "panic").is_some() && is_expn_of(exp.span, "unreachable").is_none()
391         },
392         (&None, 1, Some(stmt)) => {
393             is_expn_of(stmt.span, "panic").is_some() && is_expn_of(stmt.span, "unreachable").is_none()
394         },
395         _ => false,
396     }
397 }
398
399 fn check_match_ref_pats(cx: &LateContext, ex: &Expr, arms: &[Arm], expr: &Expr) {
400     if has_only_ref_pats(arms) {
401         let mut suggs = Vec::new();
402         let (title, msg) = if let ExprAddrOf(Mutability::MutImmutable, ref inner) = ex.node {
403             suggs.push((ex.span, Sugg::hir(cx, inner, "..").to_string()));
404             (
405                 "you don't need to add `&` to both the expression and the patterns",
406                 "try",
407             )
408         } else {
409             suggs.push((ex.span, Sugg::hir(cx, ex, "..").deref().to_string()));
410             (
411                 "you don't need to add `&` to all patterns",
412                 "instead of prefixing all patterns with `&`, you can dereference the expression",
413             )
414         };
415
416         suggs.extend(arms.iter().flat_map(|a| &a.pats).filter_map(|p| {
417             if let PatKind::Ref(ref refp, _) = p.node {
418                 Some((p.span, snippet(cx, refp.span, "..").to_string()))
419             } else {
420                 None
421             }
422         }));
423
424         span_lint_and_then(cx, MATCH_REF_PATS, expr.span, title, |db| {
425             multispan_sugg(db, msg.to_owned(), suggs);
426         });
427     }
428 }
429
430 fn check_match_as_ref(cx: &LateContext, ex: &Expr, arms: &[Arm], expr: &Expr) {
431     if arms.len() == 2 &&
432         arms[0].pats.len() == 1 && arms[0].guard.is_none() &&
433         arms[1].pats.len() == 1 && arms[1].guard.is_none() {
434         let arm_ref: Option<BindingAnnotation> = if is_none_arm(&arms[0]) {
435             is_ref_some_arm(&arms[1])
436         } else if is_none_arm(&arms[1]) {
437             is_ref_some_arm(&arms[0])
438         } else {
439             None
440         };
441         if let Some(rb) = arm_ref {
442             let suggestion = if rb == BindingAnnotation::Ref { "as_ref" } else { "as_mut" };
443             span_lint_and_sugg(
444                 cx,
445                 MATCH_AS_REF,
446                 expr.span,
447                 &format!("use {}() instead", suggestion),
448                 "try this",
449                 format!("{}.{}()", snippet(cx, ex.span, "_"), suggestion)
450             )
451         }
452     }
453 }
454
455 /// Get all arms that are unbounded `PatRange`s.
456 fn all_ranges<'a, 'tcx>(
457     cx: &LateContext<'a, 'tcx>,
458     arms: &'tcx [Arm],
459 ) -> Vec<SpannedRange<Constant>> {
460     arms.iter()
461         .flat_map(|arm| {
462             if let Arm {
463                 ref pats,
464                 guard: None,
465                 ..
466             } = *arm
467             {
468                 pats.iter()
469             } else {
470                 [].iter()
471             }.filter_map(|pat| {
472                 if let PatKind::Range(ref lhs, ref rhs, ref range_end) = pat.node {
473                     let lhs = constant(cx, cx.tables, lhs)?.0;
474                     let rhs = constant(cx, cx.tables, rhs)?.0;
475                     let rhs = match *range_end {
476                         RangeEnd::Included => Bound::Included(rhs),
477                         RangeEnd::Excluded => Bound::Excluded(rhs),
478                     };
479                     return Some(SpannedRange { span: pat.span, node: (lhs, rhs) });
480                 }
481
482                 if let PatKind::Lit(ref value) = pat.node {
483                     let value = constant(cx, cx.tables, value)?.0;
484                     return Some(SpannedRange { span: pat.span, node: (value.clone(), Bound::Included(value)) });
485                 }
486
487                 None
488             })
489         })
490         .collect()
491 }
492
493 #[derive(Debug, Eq, PartialEq)]
494 pub struct SpannedRange<T> {
495     pub span: Span,
496     pub node: (T, Bound<T>),
497 }
498
499 type TypedRanges = Vec<SpannedRange<u128>>;
500
501 /// Get all `Int` ranges or all `Uint` ranges. Mixed types are an error anyway
502 /// and other types than
503 /// `Uint` and `Int` probably don't make sense.
504 fn type_ranges(ranges: &[SpannedRange<Constant>]) -> TypedRanges {
505     ranges
506         .iter()
507         .filter_map(|range| match range.node {
508             (
509                 Constant::Int(start),
510                 Bound::Included(Constant::Int(end)),
511             ) => Some(SpannedRange {
512                 span: range.span,
513                 node: (start, Bound::Included(end)),
514             }),
515             (
516                 Constant::Int(start),
517                 Bound::Excluded(Constant::Int(end)),
518             ) => Some(SpannedRange {
519                 span: range.span,
520                 node: (start, Bound::Excluded(end)),
521             }),
522             (
523                 Constant::Int(start),
524                 Bound::Unbounded,
525             ) => Some(SpannedRange {
526                 span: range.span,
527                 node: (start, Bound::Unbounded),
528             }),
529             _ => None,
530         })
531         .collect()
532 }
533
534 fn is_unit_expr(expr: &Expr) -> bool {
535     match expr.node {
536         ExprTup(ref v) if v.is_empty() => true,
537         ExprBlock(ref b, _) if b.stmts.is_empty() && b.expr.is_none() => true,
538         _ => false,
539     }
540 }
541
542 // Checks if arm has the form `None => None`
543 fn is_none_arm(arm: &Arm) -> bool {
544     match arm.pats[0].node {
545         PatKind::Path(ref path) if match_qpath(path, &paths::OPTION_NONE) => true,
546         _ => false,
547     }
548 }
549
550 // Checks if arm has the form `Some(ref v) => Some(v)` (checks for `ref` and `ref mut`)
551 fn is_ref_some_arm(arm: &Arm) -> Option<BindingAnnotation> {
552     if_chain! {
553         if let PatKind::TupleStruct(ref path, ref pats, _) = arm.pats[0].node;
554         if pats.len() == 1 && match_qpath(path, &paths::OPTION_SOME);
555         if let PatKind::Binding(rb, _, ref ident, _) = pats[0].node;
556         if rb == BindingAnnotation::Ref || rb == BindingAnnotation::RefMut;
557         if let ExprCall(ref e, ref args) = remove_blocks(&arm.body).node;
558         if let ExprPath(ref some_path) = e.node;
559         if match_qpath(some_path, &paths::OPTION_SOME) && args.len() == 1;
560         if let ExprPath(ref qpath) = args[0].node;
561         if let &QPath::Resolved(_, ref path2) = qpath;
562         if path2.segments.len() == 1 && ident.node == path2.segments[0].name;
563         then {
564             return Some(rb)
565         }
566     }
567     None
568 }
569
570 fn has_only_ref_pats(arms: &[Arm]) -> bool {
571     let mapped = arms.iter()
572         .flat_map(|a| &a.pats)
573         .map(|p| {
574             match p.node {
575                 PatKind::Ref(..) => Some(true), // &-patterns
576                 PatKind::Wild => Some(false),   // an "anything" wildcard is also fine
577                 _ => None,                      // any other pattern is not fine
578             }
579         })
580         .collect::<Option<Vec<bool>>>();
581     // look for Some(v) where there's at least one true element
582     mapped.map_or(false, |v| v.iter().any(|el| *el))
583 }
584
585 pub fn overlapping<T>(ranges: &[SpannedRange<T>]) -> Option<(&SpannedRange<T>, &SpannedRange<T>)>
586 where
587     T: Copy + Ord,
588 {
589     #[derive(Copy, Clone, Debug, Eq, PartialEq)]
590     enum Kind<'a, T: 'a> {
591         Start(T, &'a SpannedRange<T>),
592         End(Bound<T>, &'a SpannedRange<T>),
593     }
594
595     impl<'a, T: Copy> Kind<'a, T> {
596         fn range(&self) -> &'a SpannedRange<T> {
597             match *self {
598                 Kind::Start(_, r) | Kind::End(_, r) => r,
599             }
600         }
601
602         fn value(self) -> Bound<T> {
603             match self {
604                 Kind::Start(t, _) => Bound::Included(t),
605                 Kind::End(t, _) => t,
606             }
607         }
608     }
609
610     impl<'a, T: Copy + Ord> PartialOrd for Kind<'a, T> {
611         fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
612             Some(self.cmp(other))
613         }
614     }
615
616     impl<'a, T: Copy + Ord> Ord for Kind<'a, T> {
617         fn cmp(&self, other: &Self) -> Ordering {
618             match (self.value(), other.value()) {
619                 (Bound::Included(a), Bound::Included(b)) | (Bound::Excluded(a), Bound::Excluded(b)) => a.cmp(&b),
620                 // Range patterns cannot be unbounded (yet)
621                 (Bound::Unbounded, _) | (_, Bound::Unbounded) => unimplemented!(),
622                 (Bound::Included(a), Bound::Excluded(b)) => match a.cmp(&b) {
623                     Ordering::Equal => Ordering::Greater,
624                     other => other,
625                 },
626                 (Bound::Excluded(a), Bound::Included(b)) => match a.cmp(&b) {
627                     Ordering::Equal => Ordering::Less,
628                     other => other,
629                 },
630             }
631         }
632     }
633
634     let mut values = Vec::with_capacity(2 * ranges.len());
635
636     for r in ranges {
637         values.push(Kind::Start(r.node.0, r));
638         values.push(Kind::End(r.node.1, r));
639     }
640
641     values.sort();
642
643     for (a, b) in values.iter().zip(values.iter().skip(1)) {
644         match (a, b) {
645             (&Kind::Start(_, ra), &Kind::End(_, rb)) => if ra.node != rb.node {
646                 return Some((ra, rb));
647             },
648             (&Kind::End(a, _), &Kind::Start(b, _)) if a != Bound::Included(b) => (),
649             _ => return Some((a.range(), b.range())),
650         }
651     }
652
653     None
654 }