ide_assists/handlers/
introduce_named_lifetime.rs

1use ide_db::FxHashSet;
2use syntax::{
3    AstNode, TextRange,
4    ast::{self, HasGenericParams, edit_in_place::GenericParamsOwnerEdit, make},
5    ted::{self, Position},
6};
7
8use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder};
9
10static ASSIST_NAME: &str = "introduce_named_lifetime";
11static ASSIST_LABEL: &str = "Introduce named lifetime";
12
13// Assist: introduce_named_lifetime
14//
15// Change an anonymous lifetime to a named lifetime.
16//
17// ```
18// impl Cursor<'_$0> {
19//     fn node(self) -> &SyntaxNode {
20//         match self {
21//             Cursor::Replace(node) | Cursor::Before(node) => node,
22//         }
23//     }
24// }
25// ```
26// ->
27// ```
28// impl<'a> Cursor<'a> {
29//     fn node(self) -> &SyntaxNode {
30//         match self {
31//             Cursor::Replace(node) | Cursor::Before(node) => node,
32//         }
33//     }
34// }
35// ```
36pub(crate) fn introduce_named_lifetime(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
37    // FIXME: How can we handle renaming any one of multiple anonymous lifetimes?
38    // FIXME: should also add support for the case fun(f: &Foo) -> &$0Foo
39    let lifetime =
40        ctx.find_node_at_offset::<ast::Lifetime>().filter(|lifetime| lifetime.text() == "'_")?;
41    let lifetime_loc = lifetime.lifetime_ident_token()?.text_range();
42
43    if let Some(fn_def) = lifetime.syntax().ancestors().find_map(ast::Fn::cast) {
44        generate_fn_def_assist(acc, fn_def, lifetime_loc, lifetime)
45    } else if let Some(impl_def) = lifetime.syntax().ancestors().find_map(ast::Impl::cast) {
46        generate_impl_def_assist(acc, impl_def, lifetime_loc, lifetime)
47    } else {
48        None
49    }
50}
51
52/// Generate the assist for the fn def case
53fn generate_fn_def_assist(
54    acc: &mut Assists,
55    fn_def: ast::Fn,
56    lifetime_loc: TextRange,
57    lifetime: ast::Lifetime,
58) -> Option<()> {
59    let param_list: ast::ParamList = fn_def.param_list()?;
60    let new_lifetime_param = generate_unique_lifetime_param_name(fn_def.generic_param_list())?;
61    let self_param =
62        // use the self if it's a reference and has no explicit lifetime
63        param_list.self_param().filter(|p| p.lifetime().is_none() && p.amp_token().is_some());
64    // compute the location which implicitly has the same lifetime as the anonymous lifetime
65    let loc_needing_lifetime = if let Some(self_param) = self_param {
66        // if we have a self reference, use that
67        Some(NeedsLifetime::SelfParam(self_param))
68    } else {
69        // otherwise, if there's a single reference parameter without a named lifetime, use that
70        let fn_params_without_lifetime: Vec<_> = param_list
71            .params()
72            .filter_map(|param| match param.ty() {
73                Some(ast::Type::RefType(ascribed_type)) if ascribed_type.lifetime().is_none() => {
74                    Some(NeedsLifetime::RefType(ascribed_type))
75                }
76                _ => None,
77            })
78            .collect();
79        match fn_params_without_lifetime.len() {
80            1 => Some(fn_params_without_lifetime.into_iter().next()?),
81            0 => None,
82            // multiple unnamed is invalid. assist is not applicable
83            _ => return None,
84        }
85    };
86    acc.add(AssistId::refactor(ASSIST_NAME), ASSIST_LABEL, lifetime_loc, |builder| {
87        let fn_def = builder.make_mut(fn_def);
88        let lifetime = builder.make_mut(lifetime);
89        let loc_needing_lifetime =
90            loc_needing_lifetime.and_then(|it| it.make_mut(builder).to_position());
91
92        fn_def.get_or_create_generic_param_list().add_generic_param(
93            make::lifetime_param(new_lifetime_param.clone()).clone_for_update().into(),
94        );
95        ted::replace(lifetime.syntax(), new_lifetime_param.clone_for_update().syntax());
96        if let Some(position) = loc_needing_lifetime {
97            ted::insert(position, new_lifetime_param.clone_for_update().syntax());
98        }
99    })
100}
101
102/// Generate the assist for the impl def case
103fn generate_impl_def_assist(
104    acc: &mut Assists,
105    impl_def: ast::Impl,
106    lifetime_loc: TextRange,
107    lifetime: ast::Lifetime,
108) -> Option<()> {
109    let new_lifetime_param = generate_unique_lifetime_param_name(impl_def.generic_param_list())?;
110    acc.add(AssistId::refactor(ASSIST_NAME), ASSIST_LABEL, lifetime_loc, |builder| {
111        let impl_def = builder.make_mut(impl_def);
112        let lifetime = builder.make_mut(lifetime);
113
114        impl_def.get_or_create_generic_param_list().add_generic_param(
115            make::lifetime_param(new_lifetime_param.clone()).clone_for_update().into(),
116        );
117        ted::replace(lifetime.syntax(), new_lifetime_param.clone_for_update().syntax());
118    })
119}
120
121/// Given a type parameter list, generate a unique lifetime parameter name
122/// which is not in the list
123fn generate_unique_lifetime_param_name(
124    existing_type_param_list: Option<ast::GenericParamList>,
125) -> Option<ast::Lifetime> {
126    match existing_type_param_list {
127        Some(type_params) => {
128            let used_lifetime_params: FxHashSet<_> =
129                type_params.lifetime_params().map(|p| p.syntax().text().to_string()).collect();
130            ('a'..='z').map(|it| format!("'{it}")).find(|it| !used_lifetime_params.contains(it))
131        }
132        None => Some("'a".to_owned()),
133    }
134    .map(|it| make::lifetime(&it))
135}
136
137enum NeedsLifetime {
138    SelfParam(ast::SelfParam),
139    RefType(ast::RefType),
140}
141
142impl NeedsLifetime {
143    fn make_mut(self, builder: &mut SourceChangeBuilder) -> Self {
144        match self {
145            Self::SelfParam(it) => Self::SelfParam(builder.make_mut(it)),
146            Self::RefType(it) => Self::RefType(builder.make_mut(it)),
147        }
148    }
149
150    fn to_position(self) -> Option<Position> {
151        match self {
152            Self::SelfParam(it) => Some(Position::after(it.amp_token()?)),
153            Self::RefType(it) => Some(Position::after(it.amp_token()?)),
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::tests::{check_assist, check_assist_not_applicable};
162
163    #[test]
164    fn test_example_case() {
165        check_assist(
166            introduce_named_lifetime,
167            r#"impl Cursor<'_$0> {
168                fn node(self) -> &SyntaxNode {
169                    match self {
170                        Cursor::Replace(node) | Cursor::Before(node) => node,
171                    }
172                }
173            }"#,
174            r#"impl<'a> Cursor<'a> {
175                fn node(self) -> &SyntaxNode {
176                    match self {
177                        Cursor::Replace(node) | Cursor::Before(node) => node,
178                    }
179                }
180            }"#,
181        );
182    }
183
184    #[test]
185    fn test_example_case_simplified() {
186        check_assist(
187            introduce_named_lifetime,
188            r#"impl Cursor<'_$0> {"#,
189            r#"impl<'a> Cursor<'a> {"#,
190        );
191    }
192
193    #[test]
194    fn test_example_case_cursor_after_tick() {
195        check_assist(
196            introduce_named_lifetime,
197            r#"impl Cursor<'$0_> {"#,
198            r#"impl<'a> Cursor<'a> {"#,
199        );
200    }
201
202    #[test]
203    fn test_impl_with_other_type_param() {
204        check_assist(
205            introduce_named_lifetime,
206            "impl<I> fmt::Display for SepByBuilder<'_$0, I>
207        where
208            I: Iterator,
209            I::Item: fmt::Display,
210        {",
211            "impl<I, 'a> fmt::Display for SepByBuilder<'a, I>
212        where
213            I: Iterator,
214            I::Item: fmt::Display,
215        {",
216        )
217    }
218
219    #[test]
220    fn test_example_case_cursor_before_tick() {
221        check_assist(
222            introduce_named_lifetime,
223            r#"impl Cursor<$0'_> {"#,
224            r#"impl<'a> Cursor<'a> {"#,
225        );
226    }
227
228    #[test]
229    fn test_not_applicable_cursor_position() {
230        check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor<'_>$0 {"#);
231        check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor$0<'_> {"#);
232    }
233
234    #[test]
235    fn test_not_applicable_lifetime_already_name() {
236        check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor<'a$0> {"#);
237        check_assist_not_applicable(introduce_named_lifetime, r#"fn my_fun<'a>() -> X<'a$0>"#);
238    }
239
240    #[test]
241    fn test_with_type_parameter() {
242        check_assist(
243            introduce_named_lifetime,
244            r#"impl<T> Cursor<T, '_$0>"#,
245            r#"impl<T, 'a> Cursor<T, 'a>"#,
246        );
247    }
248
249    #[test]
250    fn test_with_existing_lifetime_name_conflict() {
251        check_assist(
252            introduce_named_lifetime,
253            r#"impl<'a, 'b> Cursor<'a, 'b, '_$0>"#,
254            r#"impl<'a, 'b, 'c> Cursor<'a, 'b, 'c>"#,
255        );
256    }
257
258    #[test]
259    fn test_function_return_value_anon_lifetime_param() {
260        check_assist(
261            introduce_named_lifetime,
262            r#"fn my_fun() -> X<'_$0>"#,
263            r#"fn my_fun<'a>() -> X<'a>"#,
264        );
265    }
266
267    #[test]
268    fn test_function_return_value_anon_reference_lifetime() {
269        check_assist(
270            introduce_named_lifetime,
271            r#"fn my_fun() -> &'_$0 X"#,
272            r#"fn my_fun<'a>() -> &'a X"#,
273        );
274    }
275
276    #[test]
277    fn test_function_param_anon_lifetime() {
278        check_assist(
279            introduce_named_lifetime,
280            r#"fn my_fun(x: X<'_$0>)"#,
281            r#"fn my_fun<'a>(x: X<'a>)"#,
282        );
283    }
284
285    #[test]
286    fn test_function_add_lifetime_to_params() {
287        check_assist(
288            introduce_named_lifetime,
289            r#"fn my_fun(f: &Foo) -> X<'_$0>"#,
290            r#"fn my_fun<'a>(f: &'a Foo) -> X<'a>"#,
291        );
292    }
293
294    #[test]
295    fn test_function_add_lifetime_to_params_in_presence_of_other_lifetime() {
296        check_assist(
297            introduce_named_lifetime,
298            r#"fn my_fun<'other>(f: &Foo, b: &'other Bar) -> X<'_$0>"#,
299            r#"fn my_fun<'other, 'a>(f: &'a Foo, b: &'other Bar) -> X<'a>"#,
300        );
301    }
302
303    #[test]
304    fn test_function_not_applicable_without_self_and_multiple_unnamed_param_lifetimes() {
305        // this is not permitted under lifetime elision rules
306        check_assist_not_applicable(
307            introduce_named_lifetime,
308            r#"fn my_fun(f: &Foo, b: &Bar) -> X<'_$0>"#,
309        );
310    }
311
312    #[test]
313    fn test_function_add_lifetime_to_self_ref_param() {
314        check_assist(
315            introduce_named_lifetime,
316            r#"fn my_fun<'other>(&self, f: &Foo, b: &'other Bar) -> X<'_$0>"#,
317            r#"fn my_fun<'other, 'a>(&'a self, f: &Foo, b: &'other Bar) -> X<'a>"#,
318        );
319    }
320
321    #[test]
322    fn test_function_add_lifetime_to_param_with_non_ref_self() {
323        check_assist(
324            introduce_named_lifetime,
325            r#"fn my_fun<'other>(self, f: &Foo, b: &'other Bar) -> X<'_$0>"#,
326            r#"fn my_fun<'other, 'a>(self, f: &'a Foo, b: &'other Bar) -> X<'a>"#,
327        );
328    }
329
330    #[test]
331    fn test_function_add_lifetime_to_self_ref_mut() {
332        check_assist(
333            introduce_named_lifetime,
334            r#"fn foo(&mut self) -> &'_$0 ()"#,
335            r#"fn foo<'a>(&'a mut self) -> &'a ()"#,
336        );
337    }
338}