• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 use clippy_utils::diagnostics::span_lint_and_then;
2 use clippy_utils::source::{position_before_rarrow, snippet_block, snippet_opt};
3 use if_chain::if_chain;
4 use rustc_errors::Applicability;
5 use rustc_hir::intravisit::FnKind;
6 use rustc_hir::{
7     AsyncGeneratorKind, Block, Body, Closure, Expr, ExprKind, FnDecl, FnRetTy, GeneratorKind, GenericArg, GenericBound,
8     ImplItem, Item, ItemKind, LifetimeName, Node, Term, TraitRef, Ty, TyKind, TypeBindingKind,
9 };
10 use rustc_lint::{LateContext, LateLintPass};
11 use rustc_session::{declare_lint_pass, declare_tool_lint};
12 use rustc_span::def_id::LocalDefId;
13 use rustc_span::{sym, Span};
14 
15 declare_clippy_lint! {
16     /// ### What it does
17     /// It checks for manual implementations of `async` functions.
18     ///
19     /// ### Why is this bad?
20     /// It's more idiomatic to use the dedicated syntax.
21     ///
22     /// ### Example
23     /// ```rust
24     /// use std::future::Future;
25     ///
26     /// fn foo() -> impl Future<Output = i32> { async { 42 } }
27     /// ```
28     /// Use instead:
29     /// ```rust
30     /// async fn foo() -> i32 { 42 }
31     /// ```
32     #[clippy::version = "1.45.0"]
33     pub MANUAL_ASYNC_FN,
34     style,
35     "manual implementations of `async` functions can be simplified using the dedicated syntax"
36 }
37 
38 declare_lint_pass!(ManualAsyncFn => [MANUAL_ASYNC_FN]);
39 
40 impl<'tcx> LateLintPass<'tcx> for ManualAsyncFn {
check_fn( &mut self, cx: &LateContext<'tcx>, kind: FnKind<'tcx>, decl: &'tcx FnDecl<'_>, body: &'tcx Body<'_>, span: Span, def_id: LocalDefId, )41     fn check_fn(
42         &mut self,
43         cx: &LateContext<'tcx>,
44         kind: FnKind<'tcx>,
45         decl: &'tcx FnDecl<'_>,
46         body: &'tcx Body<'_>,
47         span: Span,
48         def_id: LocalDefId,
49     ) {
50         if_chain! {
51             if let Some(header) = kind.header();
52             if !header.asyncness.is_async();
53             // Check that this function returns `impl Future`
54             if let FnRetTy::Return(ret_ty) = decl.output;
55             if let Some((trait_ref, output_lifetimes)) = future_trait_ref(cx, ret_ty);
56             if let Some(output) = future_output_ty(trait_ref);
57             if captures_all_lifetimes(decl.inputs, &output_lifetimes);
58             // Check that the body of the function consists of one async block
59             if let ExprKind::Block(block, _) = body.value.kind;
60             if block.stmts.is_empty();
61             if let Some(closure_body) = desugared_async_block(cx, block);
62             if let Node::Item(Item {vis_span, ..}) | Node::ImplItem(ImplItem {vis_span, ..}) =
63                 cx.tcx.hir().get_by_def_id(def_id);
64             then {
65                 let header_span = span.with_hi(ret_ty.span.hi());
66 
67                 span_lint_and_then(
68                     cx,
69                     MANUAL_ASYNC_FN,
70                     header_span,
71                     "this function can be simplified using the `async fn` syntax",
72                     |diag| {
73                         if_chain! {
74                             if let Some(vis_snip) = snippet_opt(cx, *vis_span);
75                             if let Some(header_snip) = snippet_opt(cx, header_span);
76                             if let Some(ret_pos) = position_before_rarrow(&header_snip);
77                             if let Some((ret_sugg, ret_snip)) = suggested_ret(cx, output);
78                             then {
79                                 let header_snip = if vis_snip.is_empty() {
80                                     format!("async {}", &header_snip[..ret_pos])
81                                 } else {
82                                     format!("{} async {}", vis_snip, &header_snip[vis_snip.len() + 1..ret_pos])
83                                 };
84 
85                                 let help = format!("make the function `async` and {ret_sugg}");
86                                 diag.span_suggestion(
87                                     header_span,
88                                     help,
89                                     format!("{header_snip}{ret_snip}"),
90                                     Applicability::MachineApplicable
91                                 );
92 
93                                 let body_snip = snippet_block(cx, closure_body.value.span, "..", Some(block.span));
94                                 diag.span_suggestion(
95                                     block.span,
96                                     "move the body of the async block to the enclosing function",
97                                     body_snip,
98                                     Applicability::MachineApplicable
99                                 );
100                             }
101                         }
102                     },
103                 );
104             }
105         }
106     }
107 }
108 
future_trait_ref<'tcx>( cx: &LateContext<'tcx>, ty: &'tcx Ty<'tcx>, ) -> Option<(&'tcx TraitRef<'tcx>, Vec<LifetimeName>)>109 fn future_trait_ref<'tcx>(
110     cx: &LateContext<'tcx>,
111     ty: &'tcx Ty<'tcx>,
112 ) -> Option<(&'tcx TraitRef<'tcx>, Vec<LifetimeName>)> {
113     if_chain! {
114         if let TyKind::OpaqueDef(item_id, bounds, false) = ty.kind;
115         let item = cx.tcx.hir().item(item_id);
116         if let ItemKind::OpaqueTy(opaque) = &item.kind;
117         if let Some(trait_ref) = opaque.bounds.iter().find_map(|bound| {
118             if let GenericBound::Trait(poly, _) = bound {
119                 Some(&poly.trait_ref)
120             } else {
121                 None
122             }
123         });
124         if trait_ref.trait_def_id() == cx.tcx.lang_items().future_trait();
125         then {
126             let output_lifetimes = bounds
127                 .iter()
128                 .filter_map(|bound| {
129                     if let GenericArg::Lifetime(lt) = bound {
130                         Some(lt.res)
131                     } else {
132                         None
133                     }
134                 })
135                 .collect();
136 
137             return Some((trait_ref, output_lifetimes));
138         }
139     }
140 
141     None
142 }
143 
future_output_ty<'tcx>(trait_ref: &'tcx TraitRef<'tcx>) -> Option<&'tcx Ty<'tcx>>144 fn future_output_ty<'tcx>(trait_ref: &'tcx TraitRef<'tcx>) -> Option<&'tcx Ty<'tcx>> {
145     if_chain! {
146         if let Some(segment) = trait_ref.path.segments.last();
147         if let Some(args) = segment.args;
148         if args.bindings.len() == 1;
149         let binding = &args.bindings[0];
150         if binding.ident.name == sym::Output;
151         if let TypeBindingKind::Equality { term: Term::Ty(output) } = binding.kind;
152         then {
153             return Some(output);
154         }
155     }
156 
157     None
158 }
159 
captures_all_lifetimes(inputs: &[Ty<'_>], output_lifetimes: &[LifetimeName]) -> bool160 fn captures_all_lifetimes(inputs: &[Ty<'_>], output_lifetimes: &[LifetimeName]) -> bool {
161     let input_lifetimes: Vec<LifetimeName> = inputs
162         .iter()
163         .filter_map(|ty| {
164             if let TyKind::Ref(lt, _) = ty.kind {
165                 Some(lt.res)
166             } else {
167                 None
168             }
169         })
170         .collect();
171 
172     // The lint should trigger in one of these cases:
173     // - There are no input lifetimes
174     // - There's only one output lifetime bound using `+ '_`
175     // - All input lifetimes are explicitly bound to the output
176     input_lifetimes.is_empty()
177         || (output_lifetimes.len() == 1 && matches!(output_lifetimes[0], LifetimeName::Infer))
178         || input_lifetimes
179             .iter()
180             .all(|in_lt| output_lifetimes.iter().any(|out_lt| in_lt == out_lt))
181 }
182 
desugared_async_block<'tcx>(cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) -> Option<&'tcx Body<'tcx>>183 fn desugared_async_block<'tcx>(cx: &LateContext<'tcx>, block: &'tcx Block<'tcx>) -> Option<&'tcx Body<'tcx>> {
184     if_chain! {
185         if let Some(block_expr) = block.expr;
186         if let Expr {
187             kind: ExprKind::Closure(&Closure { body, .. }),
188             ..
189         } = block_expr;
190         let closure_body = cx.tcx.hir().body(body);
191         if closure_body.generator_kind == Some(GeneratorKind::Async(AsyncGeneratorKind::Block));
192         then {
193             return Some(closure_body);
194         }
195     }
196 
197     None
198 }
199 
suggested_ret(cx: &LateContext<'_>, output: &Ty<'_>) -> Option<(&'static str, String)>200 fn suggested_ret(cx: &LateContext<'_>, output: &Ty<'_>) -> Option<(&'static str, String)> {
201     match output.kind {
202         TyKind::Tup(tys) if tys.is_empty() => {
203             let sugg = "remove the return type";
204             Some((sugg, String::new()))
205         },
206         _ => {
207             let sugg = "return the output of the future directly";
208             snippet_opt(cx, output.span).map(|snip| (sugg, format!(" -> {snip}")))
209         },
210     }
211 }
212