Skip to content

Commit e963846

Browse files
committed
Auto merge of rust-lang#14816 - justahero:rust-langgh-14626, r=Veykril
feat: Assist to replace generic with impl trait This adds a new assist named "Replace named generic with impl". It is the inverse operation to the existing "Replace impl trait with generic" assist. It allows to refactor the following statement: ```rust // 👇 cursor fn new<T$0: ToString>(input: T) -> Self {} ``` to be transformed into: ```rust fn new(input: impl ToString) -> Self {} ``` * adds new helper function `impl_trait_type` to create AST node * add method to remove an existing generic param type from param list Closes rust-lang#14626
2 parents 8589a2d + e78df83 commit e963846

File tree

5 files changed

+247
-0
lines changed

5 files changed

+247
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
use hir::Semantics;
2+
use ide_db::{
3+
base_db::{FileId, FileRange},
4+
defs::Definition,
5+
search::SearchScope,
6+
RootDatabase,
7+
};
8+
use syntax::{
9+
ast::{self, make::impl_trait_type, HasGenericParams, HasName, HasTypeBounds},
10+
ted, AstNode,
11+
};
12+
13+
use crate::{AssistContext, AssistId, AssistKind, Assists};
14+
15+
// Assist: replace_named_generic_with_impl
16+
//
17+
// Replaces named generic with an `impl Trait` in function argument.
18+
//
19+
// ```
20+
// fn new<P$0: AsRef<Path>>(location: P) -> Self {}
21+
// ```
22+
// ->
23+
// ```
24+
// fn new(location: impl AsRef<Path>) -> Self {}
25+
// ```
26+
pub(crate) fn replace_named_generic_with_impl(
27+
acc: &mut Assists,
28+
ctx: &AssistContext<'_>,
29+
) -> Option<()> {
30+
// finds `<P: AsRef<Path>>`
31+
let type_param = ctx.find_node_at_offset::<ast::TypeParam>()?;
32+
// returns `P`
33+
let type_param_name = type_param.name()?;
34+
35+
// The list of type bounds / traits: `AsRef<Path>`
36+
let type_bound_list = type_param.type_bound_list()?;
37+
38+
let fn_ = type_param.syntax().ancestors().find_map(ast::Fn::cast)?;
39+
let params = fn_
40+
.param_list()?
41+
.params()
42+
.filter_map(|param| {
43+
// function parameter type needs to match generic type name
44+
if let ast::Type::PathType(path_type) = param.ty()? {
45+
let left = path_type.path()?.segment()?.name_ref()?.ident_token()?.to_string();
46+
let right = type_param_name.to_string();
47+
if left == right {
48+
Some(param)
49+
} else {
50+
None
51+
}
52+
} else {
53+
None
54+
}
55+
})
56+
.collect::<Vec<_>>();
57+
58+
if params.is_empty() {
59+
return None;
60+
}
61+
62+
let type_param_hir_def = ctx.sema.to_def(&type_param)?;
63+
let type_param_def = Definition::GenericParam(hir::GenericParam::TypeParam(type_param_hir_def));
64+
65+
if is_referenced_outside(&ctx.sema, type_param_def, &fn_, ctx.file_id()) {
66+
return None;
67+
}
68+
69+
let target = type_param.syntax().text_range();
70+
71+
acc.add(
72+
AssistId("replace_named_generic_with_impl", AssistKind::RefactorRewrite),
73+
"Replace named generic with impl",
74+
target,
75+
|edit| {
76+
let type_param = edit.make_mut(type_param);
77+
let fn_ = edit.make_mut(fn_);
78+
79+
// get all params
80+
let param_types = params
81+
.iter()
82+
.filter_map(|param| match param.ty() {
83+
Some(ast::Type::PathType(param_type)) => Some(edit.make_mut(param_type)),
84+
_ => None,
85+
})
86+
.collect::<Vec<_>>();
87+
88+
if let Some(generic_params) = fn_.generic_param_list() {
89+
generic_params.remove_generic_param(ast::GenericParam::TypeParam(type_param));
90+
if generic_params.generic_params().count() == 0 {
91+
ted::remove(generic_params.syntax());
92+
}
93+
}
94+
95+
// get type bounds in signature type: `P` -> `impl AsRef<Path>`
96+
let new_bounds = impl_trait_type(type_bound_list);
97+
for param_type in param_types.iter().rev() {
98+
ted::replace(param_type.syntax(), new_bounds.clone_for_update().syntax());
99+
}
100+
},
101+
)
102+
}
103+
104+
fn is_referenced_outside(
105+
sema: &Semantics<'_, RootDatabase>,
106+
type_param_def: Definition,
107+
fn_: &ast::Fn,
108+
file_id: FileId,
109+
) -> bool {
110+
// limit search scope to function body & return type
111+
let search_ranges = vec![
112+
fn_.body().map(|body| body.syntax().text_range()),
113+
fn_.ret_type().map(|ret_type| ret_type.syntax().text_range()),
114+
];
115+
116+
search_ranges.into_iter().flatten().any(|search_range| {
117+
let file_range = FileRange { file_id, range: search_range };
118+
!type_param_def.usages(sema).in_scope(SearchScope::file_range(file_range)).all().is_empty()
119+
})
120+
}
121+
122+
#[cfg(test)]
123+
mod tests {
124+
use super::*;
125+
126+
use crate::tests::{check_assist, check_assist_not_applicable};
127+
128+
#[test]
129+
fn replace_generic_moves_into_function() {
130+
check_assist(
131+
replace_named_generic_with_impl,
132+
r#"fn new<T$0: ToString>(input: T) -> Self {}"#,
133+
r#"fn new(input: impl ToString) -> Self {}"#,
134+
);
135+
}
136+
137+
#[test]
138+
fn replace_generic_with_inner_associated_type() {
139+
check_assist(
140+
replace_named_generic_with_impl,
141+
r#"fn new<P$0: AsRef<Path>>(input: P) -> Self {}"#,
142+
r#"fn new(input: impl AsRef<Path>) -> Self {}"#,
143+
);
144+
}
145+
146+
#[test]
147+
fn replace_generic_trait_applies_to_all_matching_params() {
148+
check_assist(
149+
replace_named_generic_with_impl,
150+
r#"fn new<T$0: ToString>(a: T, b: T) -> Self {}"#,
151+
r#"fn new(a: impl ToString, b: impl ToString) -> Self {}"#,
152+
);
153+
}
154+
155+
#[test]
156+
fn replace_generic_with_multiple_generic_params() {
157+
check_assist(
158+
replace_named_generic_with_impl,
159+
r#"fn new<P: AsRef<Path>, T$0: ToString>(t: T, p: P) -> Self {}"#,
160+
r#"fn new<P: AsRef<Path>>(t: impl ToString, p: P) -> Self {}"#,
161+
);
162+
check_assist(
163+
replace_named_generic_with_impl,
164+
r#"fn new<T$0: ToString, P: AsRef<Path>>(t: T, p: P) -> Self {}"#,
165+
r#"fn new<P: AsRef<Path>>(t: impl ToString, p: P) -> Self {}"#,
166+
);
167+
check_assist(
168+
replace_named_generic_with_impl,
169+
r#"fn new<A: Send, B$0: ToString, C: Debug>(a: A, b: B, c: C) -> Self {}"#,
170+
r#"fn new<A: Send, C: Debug>(a: A, b: impl ToString, c: C) -> Self {}"#,
171+
);
172+
}
173+
174+
#[test]
175+
fn replace_generic_with_multiple_trait_bounds() {
176+
check_assist(
177+
replace_named_generic_with_impl,
178+
r#"fn new<P$0: Send + Sync>(p: P) -> Self {}"#,
179+
r#"fn new(p: impl Send + Sync) -> Self {}"#,
180+
);
181+
}
182+
183+
#[test]
184+
fn replace_generic_not_applicable_if_param_used_as_return_type() {
185+
check_assist_not_applicable(
186+
replace_named_generic_with_impl,
187+
r#"fn new<P$0: Send + Sync>(p: P) -> P {}"#,
188+
);
189+
}
190+
191+
#[test]
192+
fn replace_generic_not_applicable_if_param_used_in_fn_body() {
193+
check_assist_not_applicable(
194+
replace_named_generic_with_impl,
195+
r#"fn new<P$0: ToString>(p: P) { let x: &dyn P = &O; }"#,
196+
);
197+
}
198+
199+
#[test]
200+
fn replace_generic_ignores_another_function_with_same_param_type() {
201+
check_assist(
202+
replace_named_generic_with_impl,
203+
r#"
204+
fn new<P$0: Send + Sync>(p: P) {}
205+
fn hello<P: Debug>(p: P) { println!("{:?}", p); }
206+
"#,
207+
r#"
208+
fn new(p: impl Send + Sync) {}
209+
fn hello<P: Debug>(p: P) { println!("{:?}", p); }
210+
"#,
211+
);
212+
}
213+
}

crates/ide-assists/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ mod handlers {
193193
mod replace_arith_op;
194194
mod introduce_named_generic;
195195
mod replace_let_with_if_let;
196+
mod replace_named_generic_with_impl;
196197
mod replace_qualified_name_with_use;
197198
mod replace_string_with_char;
198199
mod replace_turbofish_with_explicit_type;
@@ -299,6 +300,7 @@ mod handlers {
299300
replace_let_with_if_let::replace_let_with_if_let,
300301
replace_method_eager_lazy::replace_with_eager_method,
301302
replace_method_eager_lazy::replace_with_lazy_method,
303+
replace_named_generic_with_impl::replace_named_generic_with_impl,
302304
replace_turbofish_with_explicit_type::replace_turbofish_with_explicit_type,
303305
replace_qualified_name_with_use::replace_qualified_name_with_use,
304306
replace_arith_op::replace_arith_with_wrapping,

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

+13
Original file line numberDiff line numberDiff line change
@@ -2338,6 +2338,19 @@ fn handle(action: Action) {
23382338
)
23392339
}
23402340

2341+
#[test]
2342+
fn doctest_replace_named_generic_with_impl() {
2343+
check_doc_test(
2344+
"replace_named_generic_with_impl",
2345+
r#####"
2346+
fn new<P$0: AsRef<Path>>(location: P) -> Self {}
2347+
"#####,
2348+
r#####"
2349+
fn new(location: impl AsRef<Path>) -> Self {}
2350+
"#####,
2351+
)
2352+
}
2353+
23412354
#[test]
23422355
fn doctest_replace_qualified_name_with_use() {
23432356
check_doc_test(

crates/syntax/src/ast/edit_in_place.rs

+15
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,21 @@ impl ast::GenericParamList {
236236
}
237237
}
238238

239+
/// Removes the existing generic param
240+
pub fn remove_generic_param(&self, generic_param: ast::GenericParam) {
241+
if let Some(previous) = generic_param.syntax().prev_sibling() {
242+
if let Some(next_token) = previous.next_sibling_or_token() {
243+
ted::remove_all(next_token..=generic_param.syntax().clone().into());
244+
}
245+
} else if let Some(next) = generic_param.syntax().next_sibling() {
246+
if let Some(next_token) = next.prev_sibling_or_token() {
247+
ted::remove_all(generic_param.syntax().clone().into()..=next_token);
248+
}
249+
} else {
250+
ted::remove(generic_param.syntax());
251+
}
252+
}
253+
239254
/// Constructs a matching [`ast::GenericArgList`]
240255
pub fn to_generic_args(&self) -> ast::GenericArgList {
241256
let args = self.generic_params().filter_map(|param| match param {

crates/syntax/src/ast/make.rs

+4
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ pub fn impl_trait(
232232
ast_from_text(&format!("impl{ty_params_str} {trait_} for {ty}{ty_genargs_str} {{}}"))
233233
}
234234

235+
pub fn impl_trait_type(bounds: ast::TypeBoundList) -> ast::ImplTraitType {
236+
ast_from_text(&format!("fn f(x: impl {bounds}) {{}}"))
237+
}
238+
235239
pub fn path_segment(name_ref: ast::NameRef) -> ast::PathSegment {
236240
ast_from_text(&format!("type __ = {name_ref};"))
237241
}

0 commit comments

Comments
 (0)