]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | use crate::utils::visitors::LocalUsedVisitor; |
2 | use crate::utils::{path_to_local, span_lint_and_then, SpanlessEq}; | |
3 | use if_chain::if_chain; | |
4 | use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res}; | |
5 | use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, Pat, PatKind, QPath, StmtKind, UnOp}; | |
6 | use rustc_lint::{LateContext, LateLintPass}; | |
7 | use rustc_middle::ty::{DefIdTree, TyCtxt, TypeckResults}; | |
8 | use rustc_session::{declare_lint_pass, declare_tool_lint}; | |
9 | use rustc_span::{MultiSpan, Span}; | |
10 | ||
11 | declare_clippy_lint! { | |
12 | /// **What it does:** Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together | |
13 | /// without adding any branches. | |
14 | /// | |
15 | /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only | |
16 | /// cases where merging would most likely make the code more readable. | |
17 | /// | |
18 | /// **Why is this bad?** It is unnecessarily verbose and complex. | |
19 | /// | |
20 | /// **Known problems:** None. | |
21 | /// | |
22 | /// **Example:** | |
23 | /// | |
24 | /// ```rust | |
25 | /// fn func(opt: Option<Result<u64, String>>) { | |
26 | /// let n = match opt { | |
27 | /// Some(n) => match n { | |
28 | /// Ok(n) => n, | |
29 | /// _ => return, | |
30 | /// } | |
31 | /// None => return, | |
32 | /// }; | |
33 | /// } | |
34 | /// ``` | |
35 | /// Use instead: | |
36 | /// ```rust | |
37 | /// fn func(opt: Option<Result<u64, String>>) { | |
38 | /// let n = match opt { | |
39 | /// Some(Ok(n)) => n, | |
40 | /// _ => return, | |
41 | /// }; | |
42 | /// } | |
43 | /// ``` | |
44 | pub COLLAPSIBLE_MATCH, | |
45 | style, | |
46 | "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together." | |
47 | } | |
48 | ||
49 | declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]); | |
50 | ||
51 | impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch { | |
52 | fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { | |
53 | if let ExprKind::Match(_expr, arms, _source) = expr.kind { | |
54 | if let Some(wild_arm) = arms.iter().rfind(|arm| arm_is_wild_like(arm, cx.tcx)) { | |
55 | for arm in arms { | |
56 | check_arm(arm, wild_arm, cx); | |
57 | } | |
58 | } | |
59 | } | |
60 | } | |
61 | } | |
62 | ||
63 | fn check_arm<'tcx>(arm: &Arm<'tcx>, wild_outer_arm: &Arm<'tcx>, cx: &LateContext<'tcx>) { | |
64 | if_chain! { | |
65 | let expr = strip_singleton_blocks(arm.body); | |
66 | if let ExprKind::Match(expr_in, arms_inner, _) = expr.kind; | |
67 | // the outer arm pattern and the inner match | |
68 | if expr_in.span.ctxt() == arm.pat.span.ctxt(); | |
69 | // there must be no more than two arms in the inner match for this lint | |
70 | if arms_inner.len() == 2; | |
71 | // no if guards on the inner match | |
72 | if arms_inner.iter().all(|arm| arm.guard.is_none()); | |
73 | // match expression must be a local binding | |
74 | // match <local> { .. } | |
75 | if let Some(binding_id) = path_to_local(strip_ref_operators(expr_in, cx.typeck_results())); | |
76 | // one of the branches must be "wild-like" | |
77 | if let Some(wild_inner_arm_idx) = arms_inner.iter().rposition(|arm_inner| arm_is_wild_like(arm_inner, cx.tcx)); | |
78 | let (wild_inner_arm, non_wild_inner_arm) = | |
79 | (&arms_inner[wild_inner_arm_idx], &arms_inner[1 - wild_inner_arm_idx]); | |
80 | if !pat_contains_or(non_wild_inner_arm.pat); | |
81 | // the binding must come from the pattern of the containing match arm | |
82 | // ..<local>.. => match <local> { .. } | |
83 | if let Some(binding_span) = find_pat_binding(arm.pat, binding_id); | |
84 | // the "wild-like" branches must be equal | |
85 | if SpanlessEq::new(cx).eq_expr(wild_inner_arm.body, wild_outer_arm.body); | |
86 | // the binding must not be used in the if guard | |
87 | let mut used_visitor = LocalUsedVisitor::new(cx, binding_id); | |
88 | if match arm.guard { | |
89 | None => true, | |
90 | Some(Guard::If(expr) | Guard::IfLet(_, expr)) => !used_visitor.check_expr(expr), | |
91 | }; | |
92 | // ...or anywhere in the inner match | |
93 | if !arms_inner.iter().any(|arm| used_visitor.check_arm(arm)); | |
94 | then { | |
95 | span_lint_and_then( | |
96 | cx, | |
97 | COLLAPSIBLE_MATCH, | |
98 | expr.span, | |
99 | "unnecessary nested match", | |
100 | |diag| { | |
101 | let mut help_span = MultiSpan::from_spans(vec![binding_span, non_wild_inner_arm.pat.span]); | |
102 | help_span.push_span_label(binding_span, "replace this binding".into()); | |
103 | help_span.push_span_label(non_wild_inner_arm.pat.span, "with this pattern".into()); | |
104 | diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern"); | |
105 | }, | |
106 | ); | |
107 | } | |
108 | } | |
109 | } | |
110 | ||
111 | fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> { | |
112 | while let ExprKind::Block(block, _) = expr.kind { | |
113 | match (block.stmts, block.expr) { | |
114 | ([stmt], None) => match stmt.kind { | |
115 | StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e, | |
116 | _ => break, | |
117 | }, | |
118 | ([], Some(e)) => expr = e, | |
119 | _ => break, | |
120 | } | |
121 | } | |
122 | expr | |
123 | } | |
124 | ||
125 | /// A "wild-like" pattern is wild ("_") or `None`. | |
126 | /// For this lint to apply, both the outer and inner match expressions | |
127 | /// must have "wild-like" branches that can be combined. | |
128 | fn arm_is_wild_like(arm: &Arm<'_>, tcx: TyCtxt<'_>) -> bool { | |
129 | if arm.guard.is_some() { | |
130 | return false; | |
131 | } | |
132 | match arm.pat.kind { | |
133 | PatKind::Binding(..) | PatKind::Wild => true, | |
134 | PatKind::Path(QPath::Resolved(None, path)) if is_none_ctor(path.res, tcx) => true, | |
135 | _ => false, | |
136 | } | |
137 | } | |
138 | ||
139 | fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> { | |
140 | let mut span = None; | |
141 | pat.walk_short(|p| match &p.kind { | |
142 | // ignore OR patterns | |
143 | PatKind::Or(_) => false, | |
144 | PatKind::Binding(_bm, _, _ident, _) => { | |
145 | let found = p.hir_id == hir_id; | |
146 | if found { | |
147 | span = Some(p.span); | |
148 | } | |
149 | !found | |
150 | }, | |
151 | _ => true, | |
152 | }); | |
153 | span | |
154 | } | |
155 | ||
156 | fn pat_contains_or(pat: &Pat<'_>) -> bool { | |
157 | let mut result = false; | |
158 | pat.walk(|p| { | |
159 | let is_or = matches!(p.kind, PatKind::Or(_)); | |
160 | result |= is_or; | |
161 | !is_or | |
162 | }); | |
163 | result | |
164 | } | |
165 | ||
166 | fn is_none_ctor(res: Res, tcx: TyCtxt<'_>) -> bool { | |
167 | if let Some(none_id) = tcx.lang_items().option_none_variant() { | |
168 | if let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = res { | |
169 | if let Some(variant_id) = tcx.parent(id) { | |
170 | return variant_id == none_id; | |
171 | } | |
172 | } | |
173 | } | |
174 | false | |
175 | } | |
176 | ||
177 | /// Removes `AddrOf` operators (`&`) or deref operators (`*`), but only if a reference type is | |
178 | /// dereferenced. An overloaded deref such as `Vec` to slice would not be removed. | |
179 | fn strip_ref_operators<'hir>(mut expr: &'hir Expr<'hir>, typeck_results: &TypeckResults<'_>) -> &'hir Expr<'hir> { | |
180 | loop { | |
181 | match expr.kind { | |
182 | ExprKind::AddrOf(_, _, e) => expr = e, | |
183 | ExprKind::Unary(UnOp::Deref, e) if typeck_results.expr_ty(e).is_ref() => expr = e, | |
184 | _ => break, | |
185 | } | |
186 | } | |
187 | expr | |
188 | } |