From 44c84a8d2848c9fae4d496c0d9dc9e8c98c8f1ef Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Tue, 6 Dec 2022 17:34:47 +0000 Subject: [PATCH 1/2] Add `convert_ufcs_to_method` assist --- .../src/handlers/convert_ufcs_to_method.rs | 211 ++++++++++++++++++ crates/ide-assists/src/lib.rs | 2 + crates/ide-assists/src/tests/generated.rs | 19 ++ 3 files changed, 232 insertions(+) create mode 100644 crates/ide-assists/src/handlers/convert_ufcs_to_method.rs diff --git a/crates/ide-assists/src/handlers/convert_ufcs_to_method.rs b/crates/ide-assists/src/handlers/convert_ufcs_to_method.rs new file mode 100644 index 0000000000..8704e40b0d --- /dev/null +++ b/crates/ide-assists/src/handlers/convert_ufcs_to_method.rs @@ -0,0 +1,211 @@ +use syntax::{ + ast::{self, make, AstNode, HasArgList}, + TextRange, +}; + +use crate::{AssistContext, AssistId, AssistKind, Assists}; + +// Assist: convert_ufcs_to_method +// +// Transforms universal function call syntax into a method call. +// +// ``` +// fn main() { +// std::ops::Add::add$0(1, 2); +// } +// # mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } +// ``` +// -> +// ``` +// fn main() { +// 1.add(2); +// } +// # mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } +// ``` +pub(crate) fn convert_ufcs_to_method(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let call = ctx.find_node_at_offset::()?; + let ast::Expr::PathExpr(path_expr) = call.expr()? else { return None }; + let path = path_expr.path()?; + + let cursor_in_range = path.syntax().text_range().contains_range(ctx.selection_trimmed()); + if !cursor_in_range { + return None; + } + + let args = call.arg_list()?; + let l_paren = args.l_paren_token()?; + let mut args_iter = args.args(); + let first_arg = args_iter.next()?; + let second_arg = args_iter.next(); + + _ = path.qualifier()?; + let method_name = path.segment()?.name_ref()?; + + let res = ctx.sema.resolve_path(&path)?; + let hir::PathResolution::Def(hir::ModuleDef::Function(fun)) = res else { return None }; + if !fun.has_self_param(ctx.sema.db) { + return None; + } + + // `core::ops::Add::add(` -> `` + let delete_path = + TextRange::new(path.syntax().text_range().start(), l_paren.text_range().end()); + + // Parens around `expr` if needed + let parens = needs_parens_as_receiver(&first_arg).then(|| { + let range = first_arg.syntax().text_range(); + (range.start(), range.end()) + }); + + // `, ` -> `.add(` + let replace_comma = TextRange::new( + first_arg.syntax().text_range().end(), + second_arg + .map(|a| a.syntax().text_range().start()) + .unwrap_or_else(|| first_arg.syntax().text_range().end()), + ); + + acc.add( + AssistId("convert_ufcs_to_method", AssistKind::RefactorRewrite), + "Convert UFCS to a method call", + call.syntax().text_range(), + |edit| { + edit.delete(delete_path); + if let Some((open, close)) = parens { + edit.insert(open, "("); + edit.insert(close, ")"); + } + edit.replace(replace_comma, format!(".{method_name}(")); + }, + ) +} + +fn needs_parens_as_receiver(expr: &ast::Expr) -> bool { + // Make `(expr).dummy()` + let dummy_call = make::expr_method_call( + make::expr_paren(expr.clone()), + make::name_ref("dummy"), + make::arg_list([]), + ); + + // Get the `expr` clone with the right parent back + // (unreachable!s are fine since we've just constructed the expression) + let ast::Expr::MethodCallExpr(call) = &dummy_call else { unreachable!() }; + let Some(receiver) = call.receiver() else { unreachable!() }; + let ast::Expr::ParenExpr(parens) = receiver else { unreachable!() }; + let Some(expr) = parens.expr() else { unreachable!() }; + + expr.needs_parens_in(dummy_call.syntax().clone()) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn ufcs2method_simple() { + check_assist( + convert_ufcs_to_method, + r#" +struct S; +impl S { fn f(self, S: S) {} } +fn f() { S::$0f(S, S); }"#, + r#" +struct S; +impl S { fn f(self, S: S) {} } +fn f() { S.f(S); }"#, + ); + } + + #[test] + fn ufcs2method_trait() { + check_assist( + convert_ufcs_to_method, + r#" +//- minicore: add +fn f() { ::$0add(2, 2); }"#, + r#" +fn f() { 2.add(2); }"#, + ); + + check_assist( + convert_ufcs_to_method, + r#" +//- minicore: add +fn f() { core::ops::Add::$0add(2, 2); }"#, + r#" +fn f() { 2.add(2); }"#, + ); + + check_assist( + convert_ufcs_to_method, + r#" +//- minicore: add +use core::ops::Add; +fn f() { <_>::$0add(2, 2); }"#, + r#" +use core::ops::Add; +fn f() { 2.add(2); }"#, + ); + } + + #[test] + fn ufcs2method_single_arg() { + check_assist( + convert_ufcs_to_method, + r#" + struct S; + impl S { fn f(self) {} } + fn f() { S::$0f(S); }"#, + r#" + struct S; + impl S { fn f(self) {} } + fn f() { S.f(); }"#, + ); + } + + #[test] + fn ufcs2method_parens() { + check_assist( + convert_ufcs_to_method, + r#" +//- minicore: deref +struct S; +impl core::ops::Deref for S { + type Target = S; + fn deref(&self) -> &S { self } +} +fn f() { core::ops::Deref::$0deref(&S); }"#, + r#" +struct S; +impl core::ops::Deref for S { + type Target = S; + fn deref(&self) -> &S { self } +} +fn f() { (&S).deref(); }"#, + ); + } + + #[test] + fn ufcs2method_doesnt_apply_with_cursor_not_on_path() { + check_assist_not_applicable( + convert_ufcs_to_method, + r#" +//- minicore: add +fn f() { core::ops::Add::add(2,$0 2); }"#, + ); + } + + #[test] + fn ufcs2method_doesnt_apply_with_no_self() { + check_assist_not_applicable( + convert_ufcs_to_method, + r#" +struct S; +impl S { fn assoc(S: S, S: S) {} } +fn f() { S::assoc$0(S, S); }"#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index f7ac9d8fd6..6da51cfa4f 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -126,6 +126,7 @@ mod handlers { mod convert_to_guarded_return; mod convert_two_arm_bool_match_to_matches_macro; mod convert_while_to_loop; + mod convert_ufcs_to_method; mod destructure_tuple_binding; mod expand_glob_import; mod extract_expressions_from_format_string; @@ -218,6 +219,7 @@ mod handlers { convert_bool_then::convert_bool_then_to_if, convert_bool_then::convert_if_to_bool_then, convert_comment_block::convert_comment_block, + convert_ufcs_to_method::convert_ufcs_to_method, convert_integer_literal::convert_integer_literal, convert_into_to_from::convert_into_to_from, convert_iter_for_each_to_for::convert_iter_for_each_to_for, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 210df6999d..d84f343c55 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -554,6 +554,25 @@ fn main() { ) } +#[test] +fn doctest_convert_ufcs_to_method() { + check_doc_test( + "convert_ufcs_to_method", + r#####" +fn main() { + std::ops::Add::add$0(1, 2); +} +mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } +"#####, + r#####" +fn main() { + 1.add(2); +} +mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } +"#####, + ) +} + #[test] fn doctest_convert_while_to_loop() { check_doc_test( From c782353a907ea121dcae670a5f0d8e14b8865b19 Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Mon, 9 Jan 2023 13:59:02 +0000 Subject: [PATCH 2/2] Rename assist: `convert_ufcs_to_method` => `unqualify_method_call` --- ..._to_method.rs => unqualify_method_call.rs} | 36 +++++++++--------- crates/ide-assists/src/lib.rs | 4 +- crates/ide-assists/src/tests/generated.rs | 38 +++++++++---------- 3 files changed, 39 insertions(+), 39 deletions(-) rename crates/ide-assists/src/handlers/{convert_ufcs_to_method.rs => unqualify_method_call.rs} (85%) diff --git a/crates/ide-assists/src/handlers/convert_ufcs_to_method.rs b/crates/ide-assists/src/handlers/unqualify_method_call.rs similarity index 85% rename from crates/ide-assists/src/handlers/convert_ufcs_to_method.rs rename to crates/ide-assists/src/handlers/unqualify_method_call.rs index 8704e40b0d..e9d4e270cd 100644 --- a/crates/ide-assists/src/handlers/convert_ufcs_to_method.rs +++ b/crates/ide-assists/src/handlers/unqualify_method_call.rs @@ -5,7 +5,7 @@ use syntax::{ use crate::{AssistContext, AssistId, AssistKind, Assists}; -// Assist: convert_ufcs_to_method +// Assist: unqualify_method_call // // Transforms universal function call syntax into a method call. // @@ -22,7 +22,7 @@ use crate::{AssistContext, AssistId, AssistKind, Assists}; // } // # mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } // ``` -pub(crate) fn convert_ufcs_to_method(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { +pub(crate) fn unqualify_method_call(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { let call = ctx.find_node_at_offset::()?; let ast::Expr::PathExpr(path_expr) = call.expr()? else { return None }; let path = path_expr.path()?; @@ -66,8 +66,8 @@ pub(crate) fn convert_ufcs_to_method(acc: &mut Assists, ctx: &AssistContext<'_>) ); acc.add( - AssistId("convert_ufcs_to_method", AssistKind::RefactorRewrite), - "Convert UFCS to a method call", + AssistId("unqualify_method_call", AssistKind::RefactorRewrite), + "Unqualify method call", call.syntax().text_range(), |edit| { edit.delete(delete_path); @@ -105,9 +105,9 @@ mod tests { use super::*; #[test] - fn ufcs2method_simple() { + fn unqualify_method_call_simple() { check_assist( - convert_ufcs_to_method, + unqualify_method_call, r#" struct S; impl S { fn f(self, S: S) {} } @@ -120,9 +120,9 @@ fn f() { S.f(S); }"#, } #[test] - fn ufcs2method_trait() { + fn unqualify_method_call_trait() { check_assist( - convert_ufcs_to_method, + unqualify_method_call, r#" //- minicore: add fn f() { ::$0add(2, 2); }"#, @@ -131,7 +131,7 @@ fn f() { 2.add(2); }"#, ); check_assist( - convert_ufcs_to_method, + unqualify_method_call, r#" //- minicore: add fn f() { core::ops::Add::$0add(2, 2); }"#, @@ -140,7 +140,7 @@ fn f() { 2.add(2); }"#, ); check_assist( - convert_ufcs_to_method, + unqualify_method_call, r#" //- minicore: add use core::ops::Add; @@ -152,9 +152,9 @@ fn f() { 2.add(2); }"#, } #[test] - fn ufcs2method_single_arg() { + fn unqualify_method_call_single_arg() { check_assist( - convert_ufcs_to_method, + unqualify_method_call, r#" struct S; impl S { fn f(self) {} } @@ -167,9 +167,9 @@ fn f() { 2.add(2); }"#, } #[test] - fn ufcs2method_parens() { + fn unqualify_method_call_parens() { check_assist( - convert_ufcs_to_method, + unqualify_method_call, r#" //- minicore: deref struct S; @@ -189,9 +189,9 @@ fn f() { (&S).deref(); }"#, } #[test] - fn ufcs2method_doesnt_apply_with_cursor_not_on_path() { + fn unqualify_method_call_doesnt_apply_with_cursor_not_on_path() { check_assist_not_applicable( - convert_ufcs_to_method, + unqualify_method_call, r#" //- minicore: add fn f() { core::ops::Add::add(2,$0 2); }"#, @@ -199,9 +199,9 @@ fn f() { core::ops::Add::add(2,$0 2); }"#, } #[test] - fn ufcs2method_doesnt_apply_with_no_self() { + fn unqualify_method_call_doesnt_apply_with_no_self() { check_assist_not_applicable( - convert_ufcs_to_method, + unqualify_method_call, r#" struct S; impl S { fn assoc(S: S, S: S) {} } diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 6da51cfa4f..6747b95007 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -126,7 +126,6 @@ mod handlers { mod convert_to_guarded_return; mod convert_two_arm_bool_match_to_matches_macro; mod convert_while_to_loop; - mod convert_ufcs_to_method; mod destructure_tuple_binding; mod expand_glob_import; mod extract_expressions_from_format_string; @@ -202,6 +201,7 @@ mod handlers { mod unnecessary_async; mod unwrap_block; mod unwrap_result_return_type; + mod unqualify_method_call; mod wrap_return_type_in_result; pub(crate) fn all() -> &'static [Handler] { @@ -219,7 +219,6 @@ mod handlers { convert_bool_then::convert_bool_then_to_if, convert_bool_then::convert_if_to_bool_then, convert_comment_block::convert_comment_block, - convert_ufcs_to_method::convert_ufcs_to_method, convert_integer_literal::convert_integer_literal, convert_into_to_from::convert_into_to_from, convert_iter_for_each_to_for::convert_iter_for_each_to_for, @@ -308,6 +307,7 @@ mod handlers { unwrap_block::unwrap_block, unwrap_result_return_type::unwrap_result_return_type, unwrap_tuple::unwrap_tuple, + unqualify_method_call::unqualify_method_call, wrap_return_type_in_result::wrap_return_type_in_result, // These are manually sorted for better priorities. By default, // priority is determined by the size of the target range (smaller diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index d84f343c55..2644e7dd11 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -554,25 +554,6 @@ fn main() { ) } -#[test] -fn doctest_convert_ufcs_to_method() { - check_doc_test( - "convert_ufcs_to_method", - r#####" -fn main() { - std::ops::Add::add$0(1, 2); -} -mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } -"#####, - r#####" -fn main() { - 1.add(2); -} -mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } -"#####, - ) -} - #[test] fn doctest_convert_while_to_loop() { check_doc_test( @@ -2552,6 +2533,25 @@ pub async fn bar() { foo() } ) } +#[test] +fn doctest_unqualify_method_call() { + check_doc_test( + "unqualify_method_call", + r#####" +fn main() { + std::ops::Add::add$0(1, 2); +} +mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } +"#####, + r#####" +fn main() { + 1.add(2); +} +mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } } +"#####, + ) +} + #[test] fn doctest_unwrap_block() { check_doc_test(