Skip to content

Commit 0cda9fc

Browse files
committed
implement assist to add missing lifetime to function
1 parent 83ba420 commit 0cda9fc

File tree

3 files changed

+351
-0
lines changed

3 files changed

+351
-0
lines changed
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
use ide_db::FxHashSet;
2+
use itertools::Itertools;
3+
use syntax::ast::{
4+
self, AstNode, GenericArgList, GenericParamList, HasGenericParams, HasName, HasTypeBounds,
5+
ParamList, TypeBoundList, WhereClause,
6+
};
7+
8+
use crate::{AssistContext, AssistId, AssistKind, Assists};
9+
10+
// fn f(s: &'a str) {
11+
// println!("{s}");
12+
// }
13+
14+
// Assist: add_lifetime_to_function
15+
//
16+
// Adds a new lifetime to a struct, enum or union.
17+
//
18+
// ```
19+
// fn print(s: &$0'a str) {
20+
// println!("{s}");
21+
// }
22+
// ```
23+
// ->
24+
// ```
25+
// fn print<'a>(s: &'a str) {
26+
// println!("{s}");
27+
// }
28+
// ```
29+
pub(crate) fn add_lifetime_to_function(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
30+
let function = ctx.find_node_at_offset::<ast::Fn>()?;
31+
let target = function.syntax().text_range();
32+
33+
let lifetimes_used_in_arguments = function
34+
.param_list()
35+
.into_iter()
36+
.flat_map(lifetimes_from_param_list)
37+
.chain(function.ret_type().into_iter().flat_map(|ty| ty.ty()).flat_map(lifetimes_from_type))
38+
.chain(function.where_clause().into_iter().flat_map(lifetimes_from_where_clause));
39+
let lifetimes_in_generic_list: FxHashSet<_> = function
40+
.generic_param_list()
41+
.iter()
42+
.flat_map(|gparams| gparams.lifetime_params())
43+
.flat_map(|lifetime_param| lifetime_param.lifetime())
44+
.map(|lifetime| lifetime.to_string())
45+
.collect();
46+
47+
let mut lifetimes_to_add = lifetimes_used_in_arguments
48+
.filter(|life| !lifetimes_in_generic_list.contains(life))
49+
.peekable();
50+
lifetimes_to_add.peek()?;
51+
52+
acc.add(
53+
AssistId("add_lifetime_to_function", AssistKind::QuickFix),
54+
"Add lifetime to function",
55+
target,
56+
|edit| {
57+
let mut lifetimes_to_add: Vec<_> = lifetimes_to_add.collect();
58+
lifetimes_to_add.sort();
59+
lifetimes_to_add.dedup();
60+
let lifetime_list = lifetimes_to_add.into_iter().join(", ");
61+
match function.generic_param_list() {
62+
Some(gen_param) => {
63+
if let Some(lifetime_end) = gen_param.lifetime_params().last() {
64+
edit.insert(
65+
lifetime_end.syntax().text_range().end(),
66+
format!(", {lifetime_list}"),
67+
);
68+
} else if let Some(generic_start) = gen_param.generic_params().next() {
69+
edit.insert(
70+
generic_start.syntax().text_range().start(),
71+
format!("{lifetime_list}, "),
72+
);
73+
}
74+
}
75+
None => {
76+
if let Some(name) = function.name() {
77+
edit.insert(name.syntax().text_range().end(), format!("<{lifetime_list}>"));
78+
}
79+
}
80+
}
81+
},
82+
)
83+
}
84+
85+
fn lifetimes_from_type(ty: ast::Type) -> Vec<String> {
86+
match ty {
87+
ast::Type::ArrayType(arr_ty) => arr_ty.ty().map(lifetimes_from_type).unwrap_or_default(),
88+
ast::Type::DynTraitType(dynt_ty) => dynt_ty
89+
// dyn Trait<'a, &'b Struct<'c>>
90+
// ^^^^^^^^^^^^^^^^^^^^
91+
.type_bound_list()
92+
.iter()
93+
// dyn Trait<'a, &'b Struct<'c>>
94+
// ^^, ^^^^^^^^^^^^^^
95+
.flat_map(|list| list.bounds())
96+
.flat_map(|bound| {
97+
bound
98+
// dyn Trait<'a, &'b Struct<'c>>
99+
// ^^^^^^^^^^^^^^
100+
.ty()
101+
.into_iter()
102+
// dyn Trait<'a, &'b Struct<'c>>
103+
// ^^ ^^
104+
.flat_map(lifetimes_from_type)
105+
// dyn Trait<'a, &'b Struct<'c>>
106+
// ^^
107+
.chain(bound.lifetime().map(|life| life.to_string()))
108+
})
109+
.collect(),
110+
ast::Type::FnPtrType(fn_ptr_ty) => fn_ptr_ty
111+
// fn(param1: &'a str, param2: &'b dyn Trait<'c>) -> &'d str
112+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
113+
.param_list()
114+
.into_iter()
115+
// fn(param1: &'a str, param2: &'b dyn Trait<'c>) -> &'d str
116+
// ^^^^^^^^^^^^^^^, ^^^^^^^^^^^^^^^^^^^^^^^^^
117+
.flat_map(|param_list| param_list.params())
118+
// fn(param1: &'a str, param2: &'b dyn Trait<'c>) -> &'d str
119+
// ^^^^^^^, ^^^^^^^^^^^^^^^^^
120+
.filter_map(|param| param.ty())
121+
// fn(param1: &'a str, param2: &'b dyn Trait<'c>) -> &'d str
122+
// ^^, ^^ ^^
123+
.flat_map(lifetimes_from_type)
124+
.chain(
125+
fn_ptr_ty
126+
// fn(param1: &'a str, param2: &'b dyn Trait<'c>) -> &'d str
127+
// ^^^^^^^
128+
.ret_type()
129+
.and_then(|ty| ty.ty())
130+
.into_iter()
131+
// fn(param1: &'a str, param2: &'b dyn Trait<'c>) -> &'d str
132+
// ^^
133+
.flat_map(lifetimes_from_type),
134+
)
135+
.collect(),
136+
// Note: we collect the lifetimes from where bounds that have an `for` restriction on them.
137+
// We could also add any lifetimes we find inside ForTypes to the `generic_param_list` of
138+
// the `for` bound, but we instead add them to the function; it's less confusing.
139+
ast::Type::ForType(for_ty) => {
140+
let for_lifetimes: FxHashSet<_> = for_ty
141+
.generic_param_list()
142+
.into_iter()
143+
.flat_map(lifetimes_from_generic_params)
144+
.collect();
145+
for_ty
146+
.ty()
147+
.into_iter()
148+
.flat_map(lifetimes_from_type)
149+
// If a type was declared on the `for<'a>` bound, then we should not report it as
150+
// missing.
151+
.filter(|life| !for_lifetimes.contains(life))
152+
.collect()
153+
}
154+
ast::Type::ImplTraitType(impl_trait_ty) => impl_trait_ty
155+
.type_bound_list()
156+
.into_iter()
157+
.flat_map(lifetimes_from_type_bounds)
158+
.collect(),
159+
// _ doesn't have any lifetimes
160+
ast::Type::InferType(_) => vec![],
161+
// macro calls also don't have lifetimes
162+
ast::Type::MacroType(_) => vec![],
163+
// ! is equally blessed with no lifetimes
164+
ast::Type::NeverType(_) => vec![],
165+
ast::Type::ParenType(paren_ty) => {
166+
paren_ty.ty().map(lifetimes_from_type).unwrap_or_default()
167+
}
168+
ast::Type::PathType(path_ty) => path_ty
169+
.path()
170+
.unwrap()
171+
.segments()
172+
.flat_map(|seg| {
173+
seg.generic_arg_list()
174+
.into_iter()
175+
.flat_map(lifetimes_from_generic_args)
176+
.chain(seg.param_list().into_iter().flat_map(lifetimes_from_param_list))
177+
.chain(
178+
seg.ret_type()
179+
.and_then(|ty| ty.ty())
180+
.into_iter()
181+
.flat_map(lifetimes_from_type),
182+
)
183+
})
184+
.collect(),
185+
ast::Type::PtrType(ptr_ty) => ptr_ty.ty().map(lifetimes_from_type).unwrap_or_default(),
186+
ast::Type::RefType(ref_ty) => ref_ty
187+
.lifetime()
188+
.iter()
189+
.map(|life| life.to_string())
190+
.chain(ref_ty.ty().map(lifetimes_from_type).unwrap_or_default())
191+
.collect(),
192+
ast::Type::SliceType(slice_ty) => {
193+
slice_ty.ty().map(lifetimes_from_type).unwrap_or_default()
194+
}
195+
ast::Type::TupleType(tpl_ty) => tpl_ty.fields().flat_map(lifetimes_from_type).collect(),
196+
}
197+
}
198+
199+
fn lifetimes_from_generic_params(generic_params: GenericParamList) -> impl Iterator<Item = String> {
200+
generic_params
201+
.lifetime_params()
202+
.flat_map(|lifetime_param| lifetime_param.lifetime())
203+
.map(|lifetime| lifetime.to_string())
204+
}
205+
206+
fn lifetimes_from_type_bounds(type_bounds: TypeBoundList) -> impl Iterator<Item = String> {
207+
type_bounds.bounds().flat_map(|bound| {
208+
bound
209+
.ty()
210+
.into_iter()
211+
.flat_map(lifetimes_from_type)
212+
.chain(bound.lifetime().map(|life| life.to_string()))
213+
})
214+
}
215+
216+
fn lifetimes_from_param_list(param_list: ParamList) -> impl Iterator<Item = String> {
217+
param_list
218+
.params()
219+
.filter_map(|param| param.ty())
220+
.flat_map(lifetimes_from_type)
221+
.map(|life| life.to_string())
222+
}
223+
224+
fn lifetimes_from_generic_args(generic_args: GenericArgList) -> Vec<String> {
225+
generic_args
226+
.generic_args()
227+
.flat_map(|arg| match arg {
228+
ast::GenericArg::AssocTypeArg(_assoc_ty_arg) => vec![],
229+
ast::GenericArg::ConstArg(_) => vec![],
230+
ast::GenericArg::LifetimeArg(lifetime_arg) => vec![lifetime_arg.to_string()],
231+
ast::GenericArg::TypeArg(ty_arg) => {
232+
ty_arg.ty().map(lifetimes_from_type).unwrap_or_default()
233+
}
234+
})
235+
.collect()
236+
}
237+
238+
fn lifetimes_from_where_clause(clause: WhereClause) -> impl Iterator<Item = String> {
239+
clause.predicates().flat_map(|pred| {
240+
pred.generic_param_list()
241+
.into_iter()
242+
.flat_map(lifetimes_from_generic_params)
243+
.chain(pred.ty().into_iter().flat_map(lifetimes_from_type))
244+
.chain(pred.lifetime().map(|life| life.to_string()))
245+
.chain(pred.type_bound_list().into_iter().flat_map(lifetimes_from_type_bounds))
246+
})
247+
}
248+
249+
#[cfg(test)]
250+
mod tests {
251+
use crate::tests::{check_assist, check_assist_not_applicable};
252+
253+
use super::*;
254+
255+
#[test]
256+
fn test_add_lifetime_to_function() {
257+
check_assist(
258+
add_lifetime_to_function,
259+
r#"fn f(s: &$0'a str) {}"#,
260+
r#"fn f<'a>(s: &'a str) {}"#,
261+
);
262+
}
263+
264+
#[test]
265+
fn test_dont_add_lifetime_to_good_function() {
266+
check_assist_not_applicable(add_lifetime_to_function, r#"fn f<'a>(s: &$0'a str) {}"#);
267+
}
268+
269+
#[test]
270+
fn test_add_lifetime_to_generic_function() {
271+
check_assist(
272+
add_lifetime_to_function,
273+
r#"fn f<T>(s: &$0'a str, t: T) {}"#,
274+
r#"fn f<'a, T>(s: &'a str, t: T) {}"#,
275+
);
276+
}
277+
278+
#[test]
279+
fn test_add_lifetime_to_lifetime_function() {
280+
check_assist(
281+
add_lifetime_to_function,
282+
r#"fn f<'b>(s: &$0'a str, s2: &'b str) {}"#,
283+
r#"fn f<'b, 'a>(s: &'a str, s2: &'b str) {}"#,
284+
);
285+
}
286+
287+
#[test]
288+
fn test_add_lifetime_to_lifetime_generic_function() {
289+
check_assist(
290+
add_lifetime_to_function,
291+
r#"fn f<'a, T>(s: &'a str, s2: &$0'b T) {}"#,
292+
r#"fn f<'a, 'b, T>(s: &'a str, s2: &'b T) {}"#,
293+
);
294+
}
295+
296+
#[test]
297+
fn test_add_position_2_lifetime_to_lifetime_function() {
298+
check_assist(
299+
add_lifetime_to_function,
300+
r#"fn f<'a>(s: &'a str, s2: &$0'b str) {}"#,
301+
r#"fn f<'a, 'b>(s: &'a str, s2: &'b str) {}"#,
302+
);
303+
}
304+
305+
#[test]
306+
fn test_add_2_lifetimes_to_function() {
307+
check_assist(
308+
add_lifetime_to_function,
309+
r#"fn f(s: &'a str, s2: &$0'b str) {}"#,
310+
r#"fn f<'a, 'b>(s: &'a str, s2: &'b str) {}"#,
311+
);
312+
}
313+
314+
#[test]
315+
fn test_add_lifetime_to_complicated_function() {
316+
check_assist(
317+
add_lifetime_to_function,
318+
r#"fn f<T>(id: T) where T: Displa$0y + 'a {}"#,
319+
r#"fn f<'a, T>(id: T) where T: Display + 'a {}"#,
320+
);
321+
check_assist(
322+
add_lifetime_to_function,
323+
r#"fn f(things: &[Thing<'$0a>]) {}"#,
324+
r#"fn f<'a>(things: &[Thing<'a>]) {}"#,
325+
);
326+
check_assist(
327+
add_lifetime_to_function,
328+
r#"fn f<T, ID>(id: ID) where ID: for<'db> Lookup<Database<'db> = dyn DefDatabase + 'db, Data = T> + '$0a {}"#,
329+
r#"fn f<'a, T, ID>(id: ID) where ID: for<'db> Lookup<Database<'db> = dyn DefDatabase + 'db, Data = T> + 'a {}"#,
330+
);
331+
}
332+
}

crates/ide-assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ mod handlers {
104104
mod add_braces;
105105
mod add_explicit_type;
106106
mod add_label_to_loop;
107+
mod add_lifetime_to_function;
107108
mod add_lifetime_to_type;
108109
mod add_missing_impl_members;
109110
mod add_missing_match_arms;
@@ -227,6 +228,7 @@ mod handlers {
227228
add_explicit_type::add_explicit_type,
228229
add_label_to_loop::add_label_to_loop,
229230
add_missing_match_arms::add_missing_match_arms,
231+
add_lifetime_to_function::add_lifetime_to_function,
230232
add_lifetime_to_type::add_lifetime_to_type,
231233
add_return_type::add_return_type,
232234
add_turbo_fish::add_turbo_fish,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,23 @@ fn main() {
150150
)
151151
}
152152

153+
#[test]
154+
fn doctest_add_lifetime_to_function() {
155+
check_doc_test(
156+
"add_lifetime_to_function",
157+
r#####"
158+
fn print(s: &$0'a str) {
159+
println!("{s}");
160+
}
161+
"#####,
162+
r#####"
163+
fn print<'a>(s: &'a str) {
164+
println!("{s}");
165+
}
166+
"#####,
167+
)
168+
}
169+
153170
#[test]
154171
fn doctest_add_lifetime_to_type() {
155172
check_doc_test(

0 commit comments

Comments
 (0)