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