Unnamed repository; edit this file 'description' to name the repository.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# Author: NNB <[email protected]>

"ui.background" = { bg = "base00" }
"ui.menu" = { fg = "base05", bg = "base01" }
"ui.menu.selected" = { fg = "base01", bg = "base04" }
"ui.linenr" = { fg = "base03", bg = "base01" }
"ui.popup" = { bg = "base01" }
"ui.window" = { bg = "base01" }
"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
"ui.selection" = { bg = "base02" }
"comment" = { fg = "base03", modifiers = ["italic"] }
"ui.statusline" = { fg = "base04", bg = "base01" }
"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
"ui.virtual.whitespace" = "base03"
"ui.text" = "base05"
"operator" = "base05"
"ui.text.focus" = "base05"
"variable" = "base08"
"constant.numeric" = "base09"
"constant" = "base09"
"attribute" = "base09"
"type" = "base0A"
"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
"string"  = "base0B"
"variable.other.member" = "base0B"
"constant.character.escape" = "base0C"
"function" = "base0D"
"constructor" = "base0D"
"special" = "base0D"
"keyword" = "base0E"
"label" = "base0E"
"namespace" = "base0E"
"ui.help" = { fg = "base06", bg = "base01" }

"markup.heading" = "base0D"
"markup.list" = "base08"
"markup.bold" = { fg = "base0A", modifiers = ["bold"] }
"markup.italic" = { fg = "base0E", modifiers = ["italic"] }
"markup.strikethrough" = { modifiers = ["crossed_out"] }
"markup.link.url" = { fg = "base09", modifiers = ["underlined"] }
"markup.link.text" = "base08"
"markup.quote" = "base0C"
"markup.raw" = "base0B"

"diff.plus" = "base0B"
"diff.delta" = "base09"
"diff.minus" = "base08"

"diagnostic" = { modifiers = ["underlined"] }
"ui.gutter" = { bg = "base01" }
"info" = "base0D"
"hint" = "base03"
"debug" = "base03"
"warning" = "base09"
"error" = "base08"

"ui.bufferline" = { fg = "base04", bg = "base01" }
"ui.bufferline.active" = { fg = "base07", bg = "base00" }

[palette]
base00 = "#f8f8f8" # Default Background
base01 = "#e8e8e8" # Lighter Background (Used for status bars, line number and folding marks)
base02 = "#d8d8d8" # Selection Background
base03 = "#b8b8b8" # Comments, Invisibles, Line Highlighting
base04 = "#585858" # Dark Foreground (Used for status bars)
base05 = "#383838" # Default Foreground, Caret, Delimiters, Operators
base06 = "#282828" # Light Foreground (Not often used)
base07 = "#181818" # Light Background (Not often used)
base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url
base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background
base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted
base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes
base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings
base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed
base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
ef='#n246'>246 247 248 249 250 251 252 253 254 255 256 257
use ide_db::{
    assists::{AssistId, AssistKind},
    base_db::FileId,
    defs::Definition,
    search::FileReference,
    syntax_helpers::node_ext::full_path_of_name_ref,
};
use syntax::{
    ast::{self, NameLike, NameRef},
    AstNode, SyntaxKind, TextRange,
};

use crate::{AssistContext, Assists};

// Assist: unnecessary_async
//
// Removes the `async` mark from functions which have no `.await` in their body.
// Looks for calls to the functions and removes the `.await` on the call site.
//
// ```
// pub async f$0n foo() {}
// pub async fn bar() { foo().await }
// ```
// ->
// ```
// pub fn foo() {}
// pub async fn bar() { foo() }
// ```
pub(crate) fn unnecessary_async(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
    let function: ast::Fn = ctx.find_node_at_offset()?;

    // Do nothing if the cursor is not on the prototype. This is so that the check does not pollute
    // when the user asks us for assists when in the middle of the function body.
    // We consider the prototype to be anything that is before the body of the function.
    let cursor_position = ctx.offset();
    if cursor_position >= function.body()?.syntax().text_range().start() {
        return None;
    }
    // Do nothing if the function isn't async.
    if let None = function.async_token() {
        return None;
    }
    // Do nothing if the function has an `await` expression in its body.
    if function.body()?.syntax().descendants().find_map(ast::AwaitExpr::cast).is_some() {
        return None;
    }

    // Remove the `async` keyword plus whitespace after it, if any.
    let async_range = {
        let async_token = function.async_token()?;
        let next_token = async_token.next_token()?;
        if matches!(next_token.kind(), SyntaxKind::WHITESPACE) {
            TextRange::new(async_token.text_range().start(), next_token.text_range().end())
        } else {
            async_token.text_range()
        }
    };

    // Otherwise, we may remove the `async` keyword.
    acc.add(
        AssistId("unnecessary_async", AssistKind::QuickFix),
        "Remove unnecessary async",
        async_range,
        |edit| {
            // Remove async on the function definition.
            edit.replace(async_range, "");

            // Remove all `.await`s from calls to the function we remove `async` from.
            if let Some(fn_def) = ctx.sema.to_def(&function) {
                for await_expr in find_all_references(ctx, &Definition::Function(fn_def))
                    // Keep only references that correspond NameRefs.
                    .filter_map(|(_, reference)| match reference.name {
                        NameLike::NameRef(nameref) => Some(nameref),
                        _ => None,
                    })
                    // Keep only references that correspond to await expressions
                    .filter_map(|nameref| find_await_expression(ctx, &nameref))
                {
                    if let Some(await_token) = &await_expr.await_token() {
                        edit.replace(await_token.text_range(), "");
                    }
                    if let Some(dot_token) = &await_expr.dot_token() {
                        edit.replace(dot_token.text_range(), "");
                    }
                }
            }
        },
    )
}

fn find_all_references(
    ctx: &AssistContext,
    def: &Definition,
) -> impl Iterator<Item = (FileId, FileReference)> {
    def.usages(&ctx.sema).all().into_iter().flat_map(|(file_id, references)| {
        references.into_iter().map(move |reference| (file_id, reference))
    })
}

/// Finds the await expression for the given `NameRef`.
/// If no await expression is found, returns None.
fn find_await_expression(ctx: &AssistContext, nameref: &NameRef) -> Option<ast::AwaitExpr> {
    // From the nameref, walk up the tree to the await expression.
    let await_expr = if let Some(path) = full_path_of_name_ref(&nameref) {
        // Function calls.
        path.syntax()
            .parent()
            .and_then(ast::PathExpr::cast)?
            .syntax()
            .parent()
            .and_then(ast::CallExpr::cast)?
            .syntax()
            .parent()
            .and_then(ast::AwaitExpr::cast)
    } else {
        // Method calls.
        nameref
            .syntax()
            .parent()
            .and_then(ast::MethodCallExpr::cast)?
            .syntax()
            .parent()
            .and_then(ast::AwaitExpr::cast)
    };

    ctx.sema.original_ast_node(await_expr?)
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::tests::{check_assist, check_assist_not_applicable};

    #[test]
    fn applies_on_empty_function() {
        check_assist(unnecessary_async, "pub async f$0n f() {}", "pub fn f() {}")
    }

    #[test]
    fn applies_and_removes_whitespace() {
        check_assist(unnecessary_async, "pub async       f$0n f() {}", "pub fn f() {}")
    }

    #[test]
    fn does_not_apply_on_non_async_function() {
        check_assist_not_applicable(unnecessary_async, "pub f$0n f() {}")
    }

    #[test]
    fn applies_on_function_with_a_non_await_expr() {
        check_assist(unnecessary_async, "pub async f$0n f() { f2() }", "pub fn f() { f2() }")
    }

    #[test]
    fn does_not_apply_on_function_with_an_await_expr() {
        check_assist_not_applicable(unnecessary_async, "pub async f$0n f() { f2().await }")
    }

    #[test]
    fn applies_and_removes_await_on_reference() {
        check_assist(
            unnecessary_async,
            r#"
pub async fn f4() { }
pub async f$0n f2() { }
pub async fn f() { f2().await }
pub async fn f3() { f2().await }"#,
            r#"
pub async fn f4() { }
pub fn f2() { }
pub async fn f() { f2() }
pub async fn f3() { f2() }"#,
        )
    }

    #[test]
    fn applies_and_removes_await_from_within_module() {
        check_assist(
            unnecessary_async,
            r#"
pub async fn f4() { }
mod a { pub async f$0n f2() { } }
pub async fn f() { a::f2().await }
pub async fn f3() { a::f2().await }"#,
            r#"
pub async fn f4() { }
mod a { pub fn f2() { } }
pub async fn f() { a::f2() }
pub async fn f3() { a::f2() }"#,
        )
    }

    #[test]
    fn applies_and_removes_await_on_inner_await() {
        check_assist(
            unnecessary_async,
            // Ensure that it is the first await on the 3rd line that is removed
            r#"
pub async fn f() { f2().await }
pub async f$0n f2() -> i32 { 1 }
pub async fn f3() { f4(f2().await).await }
pub async fn f4(i: i32) { }"#,
            r#"
pub async fn f() { f2() }
pub fn f2() -> i32 { 1 }
pub async fn f3() { f4(f2()).await }
pub async fn f4(i: i32) { }"#,
        )
    }

    #[test]
    fn applies_and_removes_await_on_outer_await() {
        check_assist(
            unnecessary_async,
            // Ensure that it is the second await on the 3rd line that is removed
            r#"
pub async fn f() { f2().await }
pub async f$0n f2(i: i32) { }
pub async fn f3() { f2(f4().await).await }
pub async fn f4() -> i32 { 1 }"#,
            r#"
pub async fn f() { f2() }
pub fn f2(i: i32) { }
pub async fn f3() { f2(f4().await) }
pub async fn f4() -> i32 { 1 }"#,
        )
    }

    #[test]
    fn applies_on_method_call() {
        check_assist(
            unnecessary_async,
            r#"
pub struct S { }
impl S { pub async f$0n f2(&self) { } }
pub async fn f(s: &S) { s.f2().await }"#,
            r#"
pub struct S { }
impl S { pub fn f2(&self) { } }
pub async fn f(s: &S) { s.f2() }"#,
        )
    }

    #[test]
    fn does_not_apply_on_function_with_a_nested_await_expr() {
        check_assist_not_applicable(
            unnecessary_async,
            "async f$0n f() { if true { loop { f2().await } } }",
        )
    }

    #[test]
    fn does_not_apply_when_not_on_prototype() {
        check_assist_not_applicable(unnecessary_async, "pub async fn f() { $0f2() }")
    }
}