]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/manual_map.rs
Fix clippy let expressions fallout
[rust.git] / clippy_lints / src / manual_map.rs
1 use crate::{map_unit_fn::OPTION_MAP_UNIT_FN, matches::MATCH_AS_REF};
2 use clippy_utils::diagnostics::span_lint_and_sugg;
3 use clippy_utils::higher;
4 use clippy_utils::source::{snippet_with_applicability, snippet_with_context};
5 use clippy_utils::ty::{is_type_diagnostic_item, peel_mid_ty_refs_is_mutable};
6 use clippy_utils::{
7     can_move_expr_to_closure, in_constant, is_else_clause, is_lang_ctor, is_lint_allowed, path_to_local_id,
8     peel_hir_expr_refs,
9 };
10 use rustc_ast::util::parser::PREC_POSTFIX;
11 use rustc_errors::Applicability;
12 use rustc_hir::LangItem::{OptionNone, OptionSome};
13 use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, HirId, Mutability, Pat, PatKind};
14 use rustc_lint::{LateContext, LateLintPass, LintContext};
15 use rustc_middle::lint::in_external_macro;
16 use rustc_session::{declare_lint_pass, declare_tool_lint};
17 use rustc_span::{sym, SyntaxContext};
18
19 declare_clippy_lint! {
20     /// ### What it does
21     /// Checks for usages of `match` which could be implemented using `map`
22     ///
23     /// ### Why is this bad?
24     /// Using the `map` method is clearer and more concise.
25     ///
26     /// ### Example
27     /// ```rust
28     /// match Some(0) {
29     ///     Some(x) => Some(x + 1),
30     ///     None => None,
31     /// };
32     /// ```
33     /// Use instead:
34     /// ```rust
35     /// Some(0).map(|x| x + 1);
36     /// ```
37     pub MANUAL_MAP,
38     style,
39     "reimplementation of `map`"
40 }
41
42 declare_lint_pass!(ManualMap => [MANUAL_MAP]);
43
44 impl LateLintPass<'_> for ManualMap {
45     #[allow(clippy::too_many_lines)]
46     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
47         if let Some(higher::IfLet {
48             let_pat,
49             let_expr,
50             if_then,
51             if_else: Some(if_else),
52         }) = higher::IfLet::hir(cx, expr)
53         {
54             manage_lint(cx, expr, (&let_pat.kind, if_then), (&PatKind::Wild, if_else), let_expr);
55         }
56
57         if let ExprKind::Match(scrutinee, [then @ Arm { guard: None, .. }, r#else @ Arm { guard: None, .. }], _) =
58             expr.kind
59         {
60             manage_lint(
61                 cx,
62                 expr,
63                 (&then.pat.kind, then.body),
64                 (&r#else.pat.kind, r#else.body),
65                 scrutinee,
66             );
67         }
68     }
69 }
70
71 fn manage_lint<'tcx>(
72     cx: &LateContext<'tcx>,
73     expr: &'tcx Expr<'_>,
74     then: (&'tcx PatKind<'_>, &'tcx Expr<'_>),
75     r#else: (&'tcx PatKind<'_>, &'tcx Expr<'_>),
76     scrut: &'tcx Expr<'_>,
77 ) {
78     if in_external_macro(cx.sess(), expr.span) || in_constant(cx, expr.hir_id) {
79         return;
80     }
81
82     let (scrutinee_ty, ty_ref_count, ty_mutability) = peel_mid_ty_refs_is_mutable(cx.typeck_results().expr_ty(scrut));
83     if !(is_type_diagnostic_item(cx, scrutinee_ty, sym::option_type)
84         && is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(expr), sym::option_type))
85     {
86         return;
87     }
88
89     let (then_pat, then_expr) = then;
90     let (else_pat, else_expr) = r#else;
91
92     let expr_ctxt = expr.span.ctxt();
93     let (some_expr, some_pat, pat_ref_count, is_wild_none) = match (
94         try_parse_pattern(cx, then_pat, expr_ctxt),
95         try_parse_pattern(cx, else_pat, expr_ctxt),
96     ) {
97         (Some(OptionPat::Wild), Some(OptionPat::Some { pattern, ref_count })) if is_none_expr(cx, then_expr) => {
98             (else_expr, pattern, ref_count, true)
99         },
100         (Some(OptionPat::None), Some(OptionPat::Some { pattern, ref_count })) if is_none_expr(cx, then_expr) => {
101             (else_expr, pattern, ref_count, false)
102         },
103         (Some(OptionPat::Some { pattern, ref_count }), Some(OptionPat::Wild)) if is_none_expr(cx, else_expr) => {
104             (then_expr, pattern, ref_count, true)
105         },
106         (Some(OptionPat::Some { pattern, ref_count }), Some(OptionPat::None)) if is_none_expr(cx, else_expr) => {
107             (then_expr, pattern, ref_count, false)
108         },
109         _ => return,
110     };
111
112     // Top level or patterns aren't allowed in closures.
113     if matches!(some_pat.kind, PatKind::Or(_)) {
114         return;
115     }
116
117     let some_expr = match get_some_expr(cx, some_expr, expr_ctxt) {
118         Some(expr) => expr,
119         None => return,
120     };
121
122     if cx.typeck_results().expr_ty(some_expr) == cx.tcx.types.unit && !is_lint_allowed(cx, OPTION_MAP_UNIT_FN, expr.hir_id) {
123         return;
124     }
125
126     // `map` won't perform any adjustments.
127     if !cx.typeck_results().expr_adjustments(some_expr).is_empty() {
128         return;
129     }
130
131     if !can_move_expr_to_closure(cx, some_expr) {
132         return;
133     }
134
135     // Determine which binding mode to use.
136     let explicit_ref = some_pat.contains_explicit_ref_binding();
137     let binding_ref = explicit_ref.or_else(|| (ty_ref_count != pat_ref_count).then(|| ty_mutability));
138
139     let as_ref_str = match binding_ref {
140         Some(Mutability::Mut) => ".as_mut()",
141         Some(Mutability::Not) => ".as_ref()",
142         None => "",
143     };
144
145     let mut app = Applicability::MachineApplicable;
146
147     // Remove address-of expressions from the scrutinee. Either `as_ref` will be called, or
148     // it's being passed by value.
149     let scrutinee = peel_hir_expr_refs(scrut).0;
150     let (scrutinee_str, _) = snippet_with_context(cx, scrutinee.span, expr_ctxt, "..", &mut app);
151     let scrutinee_str = if scrutinee.span.ctxt() == expr.span.ctxt() && scrutinee.precedence().order() < PREC_POSTFIX {
152         format!("({})", scrutinee_str)
153     } else {
154         scrutinee_str.into()
155     };
156
157     let body_str = if let PatKind::Binding(annotation, id, some_binding, None) = some_pat.kind {
158         match can_pass_as_func(cx, id, some_expr) {
159             Some(func) if func.span.ctxt() == some_expr.span.ctxt() => {
160                 snippet_with_applicability(cx, func.span, "..", &mut app).into_owned()
161             },
162             _ => {
163                 if path_to_local_id(some_expr, id)
164                     && !is_lint_allowed(cx, MATCH_AS_REF, expr.hir_id)
165                     && binding_ref.is_some()
166                 {
167                     return;
168                 }
169
170                 // `ref` and `ref mut` annotations were handled earlier.
171                 let annotation = if matches!(annotation, BindingAnnotation::Mutable) {
172                     "mut "
173                 } else {
174                     ""
175                 };
176                 format!(
177                     "|{}{}| {}",
178                     annotation,
179                     some_binding,
180                     snippet_with_context(cx, some_expr.span, expr_ctxt, "..", &mut app).0
181                 )
182             },
183         }
184     } else if !is_wild_none && explicit_ref.is_none() {
185         // TODO: handle explicit reference annotations.
186         format!(
187             "|{}| {}",
188             snippet_with_context(cx, some_pat.span, expr_ctxt, "..", &mut app).0,
189             snippet_with_context(cx, some_expr.span, expr_ctxt, "..", &mut app).0
190         )
191     } else {
192         // Refutable bindings and mixed reference annotations can't be handled by `map`.
193         return;
194     };
195
196     span_lint_and_sugg(
197         cx,
198         MANUAL_MAP,
199         expr.span,
200         "manual implementation of `Option::map`",
201         "try this",
202         if is_else_clause(cx.tcx, expr) {
203             format!("{{ {}{}.map({}) }}", scrutinee_str, as_ref_str, body_str)
204         } else {
205             format!("{}{}.map({})", scrutinee_str, as_ref_str, body_str)
206         },
207         app,
208     );
209 }
210
211 // Checks whether the expression could be passed as a function, or whether a closure is needed.
212 // Returns the function to be passed to `map` if it exists.
213 fn can_pass_as_func(cx: &LateContext<'tcx>, binding: HirId, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
214     match expr.kind {
215         ExprKind::Call(func, [arg])
216             if path_to_local_id (arg, binding) && cx.typeck_results().expr_adjustments(arg).is_empty() =>
217         {
218             Some(func)
219         },
220         _ => None,
221     }
222 }
223
224 enum OptionPat<'a> {
225     Wild,
226     None,
227     Some {
228         // The pattern contained in the `Some` tuple.
229         pattern: &'a Pat<'a>,
230         // The number of references before the `Some` tuple.
231         // e.g. `&&Some(_)` has a ref count of 2.
232         ref_count: usize,
233     },
234 }
235
236 // Try to parse into a recognized `Option` pattern.
237 // i.e. `_`, `None`, `Some(..)`, or a reference to any of those.
238 fn try_parse_pattern(
239     cx: &LateContext<'tcx>,
240     pat_kind: &'tcx PatKind<'_>,
241     ctxt: SyntaxContext,
242 ) -> Option<OptionPat<'tcx>> {
243     fn f(
244         cx: &LateContext<'tcx>,
245         pat_kind: &'tcx PatKind<'_>,
246         ref_count: usize,
247         ctxt: SyntaxContext,
248     ) -> Option<OptionPat<'tcx>> {
249         match pat_kind {
250             PatKind::Wild => Some(OptionPat::Wild),
251             PatKind::Ref(ref_pat, _) => f(cx, &ref_pat.kind, ref_count + 1, ctxt),
252             PatKind::Path(ref qpath) if is_lang_ctor(cx, qpath, OptionNone) => Some(OptionPat::None),
253             PatKind::TupleStruct(ref qpath, [pattern], _) if is_lang_ctor(cx, qpath, OptionSome) => {
254                 Some(OptionPat::Some { pattern, ref_count })
255             },
256             _ => None,
257         }
258     }
259     f(cx, pat_kind, 0, ctxt)
260 }
261
262 // Checks for an expression wrapped by the `Some` constructor. Returns the contained expression.
263 fn get_some_expr(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, ctxt: SyntaxContext) -> Option<&'tcx Expr<'tcx>> {
264     // TODO: Allow more complex expressions.
265     match expr.kind {
266         ExprKind::Call(
267             Expr {
268                 kind: ExprKind::Path(ref qpath),
269                 ..
270             },
271             [arg],
272         ) if ctxt == expr.span.ctxt() && is_lang_ctor(cx, qpath, OptionSome) => Some(arg),
273         ExprKind::Block(
274             Block {
275                 stmts: [],
276                 expr: Some(expr),
277                 ..
278             },
279             _,
280         ) => get_some_expr(cx, expr, ctxt),
281         _ => None,
282     }
283 }
284
285 // Checks for the `None` value.
286 fn is_none_expr(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool {
287     match expr.kind {
288         ExprKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
289         ExprKind::Block(
290             Block {
291                 stmts: [],
292                 expr: Some(expr),
293                 ..
294             },
295             _,
296         ) => is_none_expr(cx, expr),
297         _ => false,
298     }
299 }