Auto merge of #13825 - WaffleLapkin:ufcs_to_method_call_and_back, r=Veykril

feat: Add `unqualify_method_call` assist

...which is the inverse of `qualify_method_call` assist.

![Peek 2022-12-22 22-47](https://user-images.githubusercontent.com/38225716/209206554-8f067206-6fa6-48f8-849e-f6d36ee2e5a1.gif)

Optional future work:
- import the trait if needed
- remove excess references when auto-ref is possible
This commit is contained in:
bors 2023-01-09 14:37:40 +00:00
commit d33fa38cc9
3 changed files with 232 additions and 0 deletions

View file

@ -0,0 +1,211 @@
use syntax::{
ast::{self, make, AstNode, HasArgList},
TextRange,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: unqualify_method_call
//
// 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 unqualify_method_call(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let call = ctx.find_node_at_offset::<ast::CallExpr>()?;
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("unqualify_method_call", AssistKind::RefactorRewrite),
"Unqualify 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 unqualify_method_call_simple() {
check_assist(
unqualify_method_call,
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 unqualify_method_call_trait() {
check_assist(
unqualify_method_call,
r#"
//- minicore: add
fn f() { <u32 as core::ops::Add>::$0add(2, 2); }"#,
r#"
fn f() { 2.add(2); }"#,
);
check_assist(
unqualify_method_call,
r#"
//- minicore: add
fn f() { core::ops::Add::$0add(2, 2); }"#,
r#"
fn f() { 2.add(2); }"#,
);
check_assist(
unqualify_method_call,
r#"
//- minicore: add
use core::ops::Add;
fn f() { <_>::$0add(2, 2); }"#,
r#"
use core::ops::Add;
fn f() { 2.add(2); }"#,
);
}
#[test]
fn unqualify_method_call_single_arg() {
check_assist(
unqualify_method_call,
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 unqualify_method_call_parens() {
check_assist(
unqualify_method_call,
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 unqualify_method_call_doesnt_apply_with_cursor_not_on_path() {
check_assist_not_applicable(
unqualify_method_call,
r#"
//- minicore: add
fn f() { core::ops::Add::add(2,$0 2); }"#,
);
}
#[test]
fn unqualify_method_call_doesnt_apply_with_no_self() {
check_assist_not_applicable(
unqualify_method_call,
r#"
struct S;
impl S { fn assoc(S: S, S: S) {} }
fn f() { S::assoc$0(S, S); }"#,
);
}
}

View file

@ -202,6 +202,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] {
@ -308,6 +309,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

View file

@ -2566,6 +2566,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(