Skip to content

Commit 44c84a8

Browse files
committed
Add convert_ufcs_to_method assist
1 parent ae65912 commit 44c84a8

File tree

3 files changed

+232
-0
lines changed

3 files changed

+232
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use syntax::{
2+
ast::{self, make, AstNode, HasArgList},
3+
TextRange,
4+
};
5+
6+
use crate::{AssistContext, AssistId, AssistKind, Assists};
7+
8+
// Assist: convert_ufcs_to_method
9+
//
10+
// Transforms universal function call syntax into a method call.
11+
//
12+
// ```
13+
// fn main() {
14+
// std::ops::Add::add$0(1, 2);
15+
// }
16+
// # mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } }
17+
// ```
18+
// ->
19+
// ```
20+
// fn main() {
21+
// 1.add(2);
22+
// }
23+
// # mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } }
24+
// ```
25+
pub(crate) fn convert_ufcs_to_method(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
26+
let call = ctx.find_node_at_offset::<ast::CallExpr>()?;
27+
let ast::Expr::PathExpr(path_expr) = call.expr()? else { return None };
28+
let path = path_expr.path()?;
29+
30+
let cursor_in_range = path.syntax().text_range().contains_range(ctx.selection_trimmed());
31+
if !cursor_in_range {
32+
return None;
33+
}
34+
35+
let args = call.arg_list()?;
36+
let l_paren = args.l_paren_token()?;
37+
let mut args_iter = args.args();
38+
let first_arg = args_iter.next()?;
39+
let second_arg = args_iter.next();
40+
41+
_ = path.qualifier()?;
42+
let method_name = path.segment()?.name_ref()?;
43+
44+
let res = ctx.sema.resolve_path(&path)?;
45+
let hir::PathResolution::Def(hir::ModuleDef::Function(fun)) = res else { return None };
46+
if !fun.has_self_param(ctx.sema.db) {
47+
return None;
48+
}
49+
50+
// `core::ops::Add::add(` -> ``
51+
let delete_path =
52+
TextRange::new(path.syntax().text_range().start(), l_paren.text_range().end());
53+
54+
// Parens around `expr` if needed
55+
let parens = needs_parens_as_receiver(&first_arg).then(|| {
56+
let range = first_arg.syntax().text_range();
57+
(range.start(), range.end())
58+
});
59+
60+
// `, ` -> `.add(`
61+
let replace_comma = TextRange::new(
62+
first_arg.syntax().text_range().end(),
63+
second_arg
64+
.map(|a| a.syntax().text_range().start())
65+
.unwrap_or_else(|| first_arg.syntax().text_range().end()),
66+
);
67+
68+
acc.add(
69+
AssistId("convert_ufcs_to_method", AssistKind::RefactorRewrite),
70+
"Convert UFCS to a method call",
71+
call.syntax().text_range(),
72+
|edit| {
73+
edit.delete(delete_path);
74+
if let Some((open, close)) = parens {
75+
edit.insert(open, "(");
76+
edit.insert(close, ")");
77+
}
78+
edit.replace(replace_comma, format!(".{method_name}("));
79+
},
80+
)
81+
}
82+
83+
fn needs_parens_as_receiver(expr: &ast::Expr) -> bool {
84+
// Make `(expr).dummy()`
85+
let dummy_call = make::expr_method_call(
86+
make::expr_paren(expr.clone()),
87+
make::name_ref("dummy"),
88+
make::arg_list([]),
89+
);
90+
91+
// Get the `expr` clone with the right parent back
92+
// (unreachable!s are fine since we've just constructed the expression)
93+
let ast::Expr::MethodCallExpr(call) = &dummy_call else { unreachable!() };
94+
let Some(receiver) = call.receiver() else { unreachable!() };
95+
let ast::Expr::ParenExpr(parens) = receiver else { unreachable!() };
96+
let Some(expr) = parens.expr() else { unreachable!() };
97+
98+
expr.needs_parens_in(dummy_call.syntax().clone())
99+
}
100+
101+
#[cfg(test)]
102+
mod tests {
103+
use crate::tests::{check_assist, check_assist_not_applicable};
104+
105+
use super::*;
106+
107+
#[test]
108+
fn ufcs2method_simple() {
109+
check_assist(
110+
convert_ufcs_to_method,
111+
r#"
112+
struct S;
113+
impl S { fn f(self, S: S) {} }
114+
fn f() { S::$0f(S, S); }"#,
115+
r#"
116+
struct S;
117+
impl S { fn f(self, S: S) {} }
118+
fn f() { S.f(S); }"#,
119+
);
120+
}
121+
122+
#[test]
123+
fn ufcs2method_trait() {
124+
check_assist(
125+
convert_ufcs_to_method,
126+
r#"
127+
//- minicore: add
128+
fn f() { <u32 as core::ops::Add>::$0add(2, 2); }"#,
129+
r#"
130+
fn f() { 2.add(2); }"#,
131+
);
132+
133+
check_assist(
134+
convert_ufcs_to_method,
135+
r#"
136+
//- minicore: add
137+
fn f() { core::ops::Add::$0add(2, 2); }"#,
138+
r#"
139+
fn f() { 2.add(2); }"#,
140+
);
141+
142+
check_assist(
143+
convert_ufcs_to_method,
144+
r#"
145+
//- minicore: add
146+
use core::ops::Add;
147+
fn f() { <_>::$0add(2, 2); }"#,
148+
r#"
149+
use core::ops::Add;
150+
fn f() { 2.add(2); }"#,
151+
);
152+
}
153+
154+
#[test]
155+
fn ufcs2method_single_arg() {
156+
check_assist(
157+
convert_ufcs_to_method,
158+
r#"
159+
struct S;
160+
impl S { fn f(self) {} }
161+
fn f() { S::$0f(S); }"#,
162+
r#"
163+
struct S;
164+
impl S { fn f(self) {} }
165+
fn f() { S.f(); }"#,
166+
);
167+
}
168+
169+
#[test]
170+
fn ufcs2method_parens() {
171+
check_assist(
172+
convert_ufcs_to_method,
173+
r#"
174+
//- minicore: deref
175+
struct S;
176+
impl core::ops::Deref for S {
177+
type Target = S;
178+
fn deref(&self) -> &S { self }
179+
}
180+
fn f() { core::ops::Deref::$0deref(&S); }"#,
181+
r#"
182+
struct S;
183+
impl core::ops::Deref for S {
184+
type Target = S;
185+
fn deref(&self) -> &S { self }
186+
}
187+
fn f() { (&S).deref(); }"#,
188+
);
189+
}
190+
191+
#[test]
192+
fn ufcs2method_doesnt_apply_with_cursor_not_on_path() {
193+
check_assist_not_applicable(
194+
convert_ufcs_to_method,
195+
r#"
196+
//- minicore: add
197+
fn f() { core::ops::Add::add(2,$0 2); }"#,
198+
);
199+
}
200+
201+
#[test]
202+
fn ufcs2method_doesnt_apply_with_no_self() {
203+
check_assist_not_applicable(
204+
convert_ufcs_to_method,
205+
r#"
206+
struct S;
207+
impl S { fn assoc(S: S, S: S) {} }
208+
fn f() { S::assoc$0(S, S); }"#,
209+
);
210+
}
211+
}

crates/ide-assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ mod handlers {
126126
mod convert_to_guarded_return;
127127
mod convert_two_arm_bool_match_to_matches_macro;
128128
mod convert_while_to_loop;
129+
mod convert_ufcs_to_method;
129130
mod destructure_tuple_binding;
130131
mod expand_glob_import;
131132
mod extract_expressions_from_format_string;
@@ -218,6 +219,7 @@ mod handlers {
218219
convert_bool_then::convert_bool_then_to_if,
219220
convert_bool_then::convert_if_to_bool_then,
220221
convert_comment_block::convert_comment_block,
222+
convert_ufcs_to_method::convert_ufcs_to_method,
221223
convert_integer_literal::convert_integer_literal,
222224
convert_into_to_from::convert_into_to_from,
223225
convert_iter_for_each_to_for::convert_iter_for_each_to_for,

crates/ide-assists/src/tests/generated.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,25 @@ fn main() {
554554
)
555555
}
556556

557+
#[test]
558+
fn doctest_convert_ufcs_to_method() {
559+
check_doc_test(
560+
"convert_ufcs_to_method",
561+
r#####"
562+
fn main() {
563+
std::ops::Add::add$0(1, 2);
564+
}
565+
mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } }
566+
"#####,
567+
r#####"
568+
fn main() {
569+
1.add(2);
570+
}
571+
mod std { pub mod ops { pub trait Add { fn add(self, _: Self) {} } impl Add for i32 {} } }
572+
"#####,
573+
)
574+
}
575+
557576
#[test]
558577
fn doctest_convert_while_to_loop() {
559578
check_doc_test(

0 commit comments

Comments
 (0)