ide_assists/handlers/
unnecessary_async.rs

1use ide_db::{
2    EditionedFileId,
3    assists::AssistId,
4    defs::Definition,
5    search::{FileReference, FileReferenceNode},
6    syntax_helpers::node_ext::full_path_of_name_ref,
7};
8use syntax::{
9    AstNode, SyntaxKind, TextRange,
10    ast::{self, NameRef},
11};
12
13use crate::{AssistContext, Assists};
14
15// FIXME: This ought to be a diagnostic lint.
16
17// Assist: unnecessary_async
18//
19// Removes the `async` mark from functions which have no `.await` in their body.
20// Looks for calls to the functions and removes the `.await` on the call site.
21//
22// ```
23// pub asy$0nc fn foo() {}
24// pub async fn bar() { foo().await }
25// ```
26// ->
27// ```
28// pub fn foo() {}
29// pub async fn bar() { foo() }
30// ```
31pub(crate) fn unnecessary_async(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
32    let function: ast::Fn = ctx.find_node_at_offset()?;
33
34    // Do nothing if the cursor isn't on the async token.
35    let async_token = function.async_token()?;
36    if !async_token.text_range().contains_inclusive(ctx.offset()) {
37        return None;
38    }
39    // Do nothing if the function has an `await` expression in its body.
40    if function.body()?.syntax().descendants().find_map(ast::AwaitExpr::cast).is_some() {
41        return None;
42    }
43    // Do nothing if the method is a member of trait.
44    if let Some(impl_) = function.syntax().ancestors().nth(2).and_then(ast::Impl::cast)
45        && impl_.trait_().is_some()
46    {
47        return None;
48    }
49
50    // Remove the `async` keyword plus whitespace after it, if any.
51    let async_range = {
52        let async_token = function.async_token()?;
53        let next_token = async_token.next_token()?;
54        if matches!(next_token.kind(), SyntaxKind::WHITESPACE) {
55            TextRange::new(async_token.text_range().start(), next_token.text_range().end())
56        } else {
57            async_token.text_range()
58        }
59    };
60
61    // Otherwise, we may remove the `async` keyword.
62    acc.add(
63        AssistId::quick_fix("unnecessary_async"),
64        "Remove unnecessary async",
65        async_range,
66        |edit| {
67            // Remove async on the function definition.
68            edit.replace(async_range, "");
69
70            // Remove all `.await`s from calls to the function we remove `async` from.
71            if let Some(fn_def) = ctx.sema.to_def(&function) {
72                for await_expr in find_all_references(ctx, &Definition::Function(fn_def))
73                    // Keep only references that correspond NameRefs.
74                    .filter_map(|(_, reference)| match reference.name {
75                        FileReferenceNode::NameRef(nameref) => Some(nameref),
76                        _ => None,
77                    })
78                    // Keep only references that correspond to await expressions
79                    .filter_map(|nameref| find_await_expression(ctx, &nameref))
80                {
81                    if let Some(await_token) = &await_expr.await_token() {
82                        edit.replace(await_token.text_range(), "");
83                    }
84                    if let Some(dot_token) = &await_expr.dot_token() {
85                        edit.replace(dot_token.text_range(), "");
86                    }
87                }
88            }
89        },
90    )
91}
92
93fn find_all_references(
94    ctx: &AssistContext<'_>,
95    def: &Definition,
96) -> impl Iterator<Item = (EditionedFileId, FileReference)> {
97    def.usages(&ctx.sema).all().into_iter().flat_map(|(file_id, references)| {
98        references.into_iter().map(move |reference| (file_id, reference))
99    })
100}
101
102/// Finds the await expression for the given `NameRef`.
103/// If no await expression is found, returns None.
104fn find_await_expression(ctx: &AssistContext<'_>, nameref: &NameRef) -> Option<ast::AwaitExpr> {
105    // From the nameref, walk up the tree to the await expression.
106    let await_expr = if let Some(path) = full_path_of_name_ref(nameref) {
107        // Function calls.
108        path.syntax()
109            .parent()
110            .and_then(ast::PathExpr::cast)?
111            .syntax()
112            .parent()
113            .and_then(ast::CallExpr::cast)?
114            .syntax()
115            .parent()
116            .and_then(ast::AwaitExpr::cast)
117    } else {
118        // Method calls.
119        nameref
120            .syntax()
121            .parent()
122            .and_then(ast::MethodCallExpr::cast)?
123            .syntax()
124            .parent()
125            .and_then(ast::AwaitExpr::cast)
126    };
127
128    ctx.sema.original_ast_node(await_expr?)
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    use crate::tests::{check_assist, check_assist_not_applicable};
136
137    #[test]
138    fn applies_on_empty_function() {
139        check_assist(unnecessary_async, "pub asy$0nc fn f() {}", "pub fn f() {}")
140    }
141
142    #[test]
143    fn applies_and_removes_whitespace() {
144        check_assist(unnecessary_async, "pub async$0       fn f() {}", "pub fn f() {}")
145    }
146
147    #[test]
148    fn applies_on_function_with_a_non_await_expr() {
149        check_assist(unnecessary_async, "pub asy$0nc fn f() { f2() }", "pub fn f() { f2() }")
150    }
151
152    #[test]
153    fn does_not_apply_on_function_with_an_await_expr() {
154        check_assist_not_applicable(unnecessary_async, "pub asy$0nc fn f() { f2().await }")
155    }
156
157    #[test]
158    fn applies_and_removes_await_on_reference() {
159        check_assist(
160            unnecessary_async,
161            r#"
162pub async fn f4() { }
163pub asy$0nc fn f2() { }
164pub async fn f() { f2().await }
165pub async fn f3() { f2().await }"#,
166            r#"
167pub async fn f4() { }
168pub fn f2() { }
169pub async fn f() { f2() }
170pub async fn f3() { f2() }"#,
171        )
172    }
173
174    #[test]
175    fn applies_and_removes_await_from_within_module() {
176        check_assist(
177            unnecessary_async,
178            r#"
179pub async fn f4() { }
180mod a { pub asy$0nc fn f2() { } }
181pub async fn f() { a::f2().await }
182pub async fn f3() { a::f2().await }"#,
183            r#"
184pub async fn f4() { }
185mod a { pub fn f2() { } }
186pub async fn f() { a::f2() }
187pub async fn f3() { a::f2() }"#,
188        )
189    }
190
191    #[test]
192    fn applies_and_removes_await_on_inner_await() {
193        check_assist(
194            unnecessary_async,
195            // Ensure that it is the first await on the 3rd line that is removed
196            r#"
197pub async fn f() { f2().await }
198pub asy$0nc fn f2() -> i32 { 1 }
199pub async fn f3() { f4(f2().await).await }
200pub async fn f4(i: i32) { }"#,
201            r#"
202pub async fn f() { f2() }
203pub fn f2() -> i32 { 1 }
204pub async fn f3() { f4(f2()).await }
205pub async fn f4(i: i32) { }"#,
206        )
207    }
208
209    #[test]
210    fn applies_and_removes_await_on_outer_await() {
211        check_assist(
212            unnecessary_async,
213            // Ensure that it is the second await on the 3rd line that is removed
214            r#"
215pub async fn f() { f2().await }
216pub async$0 fn f2(i: i32) { }
217pub async fn f3() { f2(f4().await).await }
218pub async fn f4() -> i32 { 1 }"#,
219            r#"
220pub async fn f() { f2() }
221pub fn f2(i: i32) { }
222pub async fn f3() { f2(f4().await) }
223pub async fn f4() -> i32 { 1 }"#,
224        )
225    }
226
227    #[test]
228    fn applies_on_method_call() {
229        check_assist(
230            unnecessary_async,
231            r#"
232pub struct S { }
233impl S { pub async$0 fn f2(&self) { } }
234pub async fn f(s: &S) { s.f2().await }"#,
235            r#"
236pub struct S { }
237impl S { pub fn f2(&self) { } }
238pub async fn f(s: &S) { s.f2() }"#,
239        )
240    }
241
242    #[test]
243    fn does_not_apply_on_function_with_a_nested_await_expr() {
244        check_assist_not_applicable(
245            unnecessary_async,
246            "async$0 fn f() { if true { loop { f2().await } } }",
247        )
248    }
249
250    #[test]
251    fn does_not_apply_when_not_on_async_token() {
252        check_assist_not_applicable(unnecessary_async, "pub async fn$0 f() { f2() }")
253    }
254
255    #[test]
256    fn does_not_apply_on_async_trait_method() {
257        check_assist_not_applicable(
258            unnecessary_async,
259            r#"
260trait Trait {
261    async fn foo();
262}
263impl Trait for () {
264    $0async fn foo() {}
265}"#,
266        );
267    }
268}