Skip to main content

ide_assists/handlers/
introduce_named_lifetime.rs

1use ide_db::{FileId, FxHashSet};
2use syntax::{
3    AstNode, SmolStr, T, TextRange, ToSmolStr,
4    ast::{self, HasGenericParams, HasName},
5    format_smolstr,
6    syntax_editor::{Element, Position, SyntaxEditor},
7};
8
9use crate::{AssistContext, AssistId, Assists};
10
11static ASSIST_NAME: &str = "introduce_named_lifetime";
12static ASSIST_LABEL: &str = "Introduce named lifetime";
13
14// Assist: introduce_named_lifetime
15//
16// Change an anonymous lifetime to a named lifetime.
17//
18// ```
19// impl Cursor<'_$0> {
20//     fn node(self) -> &SyntaxNode {
21//         match self {
22//             Cursor::Replace(node) | Cursor::Before(node) => node,
23//         }
24//     }
25// }
26// ```
27// ->
28// ```
29// impl<'a> Cursor<'a> {
30//     fn node(self) -> &SyntaxNode {
31//         match self {
32//             Cursor::Replace(node) | Cursor::Before(node) => node,
33//         }
34//     }
35// }
36// ```
37pub(crate) fn introduce_named_lifetime(
38    acc: &mut Assists,
39    ctx: &AssistContext<'_, '_>,
40) -> Option<()> {
41    // FIXME: How can we handle renaming any one of multiple anonymous lifetimes?
42    // FIXME: should also add support for the case fun(f: &Foo) -> &$0Foo
43    let lifetime =
44        ctx.find_node_at_offset::<ast::Lifetime>().filter(|lifetime| lifetime.text() == "'_")?;
45    let file_id = ctx.vfs_file_id();
46    let lifetime_loc = lifetime.lifetime_ident_token()?.text_range();
47
48    if let Some(fn_def) = lifetime.syntax().ancestors().find_map(ast::Fn::cast) {
49        generate_fn_def_assist(acc, fn_def, lifetime_loc, lifetime, file_id)
50    } else if let Some(impl_def) = lifetime.syntax().ancestors().find_map(ast::Impl::cast) {
51        generate_impl_def_assist(acc, impl_def, lifetime_loc, lifetime, file_id)
52    } else {
53        None
54    }
55}
56
57/// Given a type parameter list, generate a unique lifetime parameter name
58/// which is not in the list
59fn generate_unique_lifetime_param_name(
60    existing_params: Option<ast::GenericParamList>,
61) -> Option<SmolStr> {
62    let used_lifetime_param: FxHashSet<SmolStr> = existing_params
63        .iter()
64        .flat_map(|params| params.lifetime_params())
65        .map(|p| p.syntax().text().to_smolstr())
66        .collect();
67    ('a'..='z').map(|c| format_smolstr!("'{c}")).find(|lt| !used_lifetime_param.contains(lt))
68}
69
70fn generate_fn_def_assist(
71    acc: &mut Assists,
72    fn_def: ast::Fn,
73    lifetime_loc: TextRange,
74    lifetime: ast::Lifetime,
75    file_id: FileId,
76) -> Option<()> {
77    let param_list = fn_def.param_list()?;
78    let new_lifetime_name = generate_unique_lifetime_param_name(fn_def.generic_param_list())?;
79    let self_param =
80        param_list.self_param().filter(|p| p.lifetime().is_none() && p.amp_token().is_some());
81
82    let loc_needing_lifetime = if let Some(self_param) = self_param {
83        Some(NeedsLifetime::SelfParam(self_param))
84    } else {
85        let unnamed_refs: Vec<_> = param_list
86            .params()
87            .filter_map(|param| match param.ty() {
88                Some(ast::Type::RefType(ref_type)) if ref_type.lifetime().is_none() => {
89                    Some(NeedsLifetime::RefType(ref_type))
90                }
91                _ => None,
92            })
93            .collect();
94
95        match unnamed_refs.len() {
96            1 => Some(unnamed_refs.into_iter().next()?),
97            0 => None,
98            _ => return None,
99        }
100    };
101
102    acc.add(AssistId::refactor(ASSIST_NAME), ASSIST_LABEL, lifetime_loc, |edit| {
103        let editor = edit.make_editor(fn_def.syntax());
104        let make = editor.make();
105
106        if let Some(generic_list) = fn_def.generic_param_list() {
107            insert_lifetime_param(&editor, &generic_list, &new_lifetime_name);
108        } else {
109            insert_new_generic_param_list_fn(&editor, &fn_def, &new_lifetime_name);
110        }
111
112        editor.replace(lifetime.syntax(), make.lifetime(&new_lifetime_name).syntax());
113
114        if let Some(pos) = loc_needing_lifetime.and_then(|l| l.to_position()) {
115            editor.insert_all(
116                pos,
117                vec![
118                    make.lifetime(&new_lifetime_name).syntax().clone().into(),
119                    make.whitespace(" ").into(),
120                ],
121            );
122        }
123
124        edit.add_file_edits(file_id, editor);
125    })
126}
127
128fn insert_new_generic_param_list_fn(
129    editor: &SyntaxEditor,
130    fn_def: &ast::Fn,
131    lifetime_name: &str,
132) -> Option<()> {
133    let make = editor.make();
134    let name = fn_def.name()?;
135
136    editor.insert_all(
137        Position::after(name.syntax()),
138        vec![
139            make.token(T![<]).syntax_element(),
140            make.lifetime(lifetime_name).syntax().syntax_element(),
141            make.token(T![>]).syntax_element(),
142        ],
143    );
144
145    Some(())
146}
147
148enum NeedsLifetime {
149    SelfParam(ast::SelfParam),
150    RefType(ast::RefType),
151}
152
153impl NeedsLifetime {
154    fn to_position(self) -> Option<Position> {
155        match self {
156            Self::SelfParam(it) => Some(Position::after(it.amp_token()?)),
157            Self::RefType(it) => Some(Position::after(it.amp_token()?)),
158        }
159    }
160}
161
162fn generate_impl_def_assist(
163    acc: &mut Assists,
164    impl_def: ast::Impl,
165    lifetime_loc: TextRange,
166    lifetime: ast::Lifetime,
167    file_id: FileId,
168) -> Option<()> {
169    let new_lifetime_name = generate_unique_lifetime_param_name(impl_def.generic_param_list())?;
170
171    acc.add(AssistId::refactor(ASSIST_NAME), ASSIST_LABEL, lifetime_loc, |edit| {
172        let editor = edit.make_editor(impl_def.syntax());
173        let make = editor.make();
174
175        if let Some(generic_list) = impl_def.generic_param_list() {
176            insert_lifetime_param(&editor, &generic_list, &new_lifetime_name);
177        } else {
178            insert_new_generic_param_list_imp(&editor, &impl_def, &new_lifetime_name);
179        }
180
181        editor.replace(lifetime.syntax(), make.lifetime(&new_lifetime_name).syntax());
182
183        edit.add_file_edits(file_id, editor);
184    })
185}
186
187fn insert_new_generic_param_list_imp(
188    editor: &SyntaxEditor,
189    impl_: &ast::Impl,
190    lifetime_name: &str,
191) -> Option<()> {
192    let make = editor.make();
193    let impl_kw = impl_.impl_token()?;
194
195    editor.insert_all(
196        Position::after(impl_kw),
197        vec![
198            make.token(T![<]).syntax_element(),
199            make.lifetime(lifetime_name).syntax().syntax_element(),
200            make.token(T![>]).syntax_element(),
201        ],
202    );
203
204    Some(())
205}
206
207fn insert_lifetime_param(
208    editor: &SyntaxEditor,
209    generic_list: &ast::GenericParamList,
210    lifetime_name: &str,
211) -> Option<()> {
212    let make = editor.make();
213    let r_angle = generic_list.r_angle_token()?;
214    let needs_comma = generic_list.generic_params().next().is_some();
215
216    let mut elements = Vec::new();
217
218    if needs_comma {
219        elements.push(make.token(T![,]).syntax_element());
220        elements.push(make.whitespace(" ").syntax_element());
221    }
222
223    let lifetime = make.lifetime(lifetime_name);
224    elements.push(lifetime.syntax().clone().into());
225
226    editor.insert_all(Position::before(r_angle), elements);
227    Some(())
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::tests::{check_assist, check_assist_not_applicable};
234
235    #[test]
236    fn test_example_case() {
237        check_assist(
238            introduce_named_lifetime,
239            r#"impl Cursor<'_$0> {
240                fn node(self) -> &SyntaxNode {
241                    match self {
242                        Cursor::Replace(node) | Cursor::Before(node) => node,
243                    }
244                }
245            }"#,
246            r#"impl<'a> Cursor<'a> {
247                fn node(self) -> &SyntaxNode {
248                    match self {
249                        Cursor::Replace(node) | Cursor::Before(node) => node,
250                    }
251                }
252            }"#,
253        );
254    }
255
256    #[test]
257    fn test_example_case_simplified() {
258        check_assist(
259            introduce_named_lifetime,
260            r#"impl Cursor<'_$0> {"#,
261            r#"impl<'a> Cursor<'a> {"#,
262        );
263    }
264
265    #[test]
266    fn test_example_case_cursor_after_tick() {
267        check_assist(
268            introduce_named_lifetime,
269            r#"impl Cursor<'$0_> {"#,
270            r#"impl<'a> Cursor<'a> {"#,
271        );
272    }
273
274    #[test]
275    fn test_impl_with_other_type_param() {
276        check_assist(
277            introduce_named_lifetime,
278            "impl<I> fmt::Display for SepByBuilder<'_$0, I>
279        where
280            I: Iterator,
281            I::Item: fmt::Display,
282        {",
283            "impl<I, 'a> fmt::Display for SepByBuilder<'a, I>
284        where
285            I: Iterator,
286            I::Item: fmt::Display,
287        {",
288        )
289    }
290
291    #[test]
292    fn test_example_case_cursor_before_tick() {
293        check_assist(
294            introduce_named_lifetime,
295            r#"impl Cursor<$0'_> {"#,
296            r#"impl<'a> Cursor<'a> {"#,
297        );
298    }
299
300    #[test]
301    fn test_not_applicable_cursor_position() {
302        check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor<'_>$0 {"#);
303        check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor$0<'_> {"#);
304    }
305
306    #[test]
307    fn test_not_applicable_lifetime_already_name() {
308        check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor<'a$0> {"#);
309        check_assist_not_applicable(introduce_named_lifetime, r#"fn my_fun<'a>() -> X<'a$0>"#);
310    }
311
312    #[test]
313    fn test_with_type_parameter() {
314        check_assist(
315            introduce_named_lifetime,
316            r#"impl<T> Cursor<T, '_$0>"#,
317            r#"impl<T, 'a> Cursor<T, 'a>"#,
318        );
319    }
320
321    #[test]
322    fn test_with_existing_lifetime_name_conflict() {
323        check_assist(
324            introduce_named_lifetime,
325            r#"impl<'a, 'b> Cursor<'a, 'b, '_$0>"#,
326            r#"impl<'a, 'b, 'c> Cursor<'a, 'b, 'c>"#,
327        );
328    }
329
330    #[test]
331    fn test_function_return_value_anon_lifetime_param() {
332        check_assist(
333            introduce_named_lifetime,
334            r#"fn my_fun() -> X<'_$0>"#,
335            r#"fn my_fun<'a>() -> X<'a>"#,
336        );
337    }
338
339    #[test]
340    fn test_function_return_value_anon_reference_lifetime() {
341        check_assist(
342            introduce_named_lifetime,
343            r#"fn my_fun() -> &'_$0 X"#,
344            r#"fn my_fun<'a>() -> &'a X"#,
345        );
346    }
347
348    #[test]
349    fn test_function_param_anon_lifetime() {
350        check_assist(
351            introduce_named_lifetime,
352            r#"fn my_fun(x: X<'_$0>)"#,
353            r#"fn my_fun<'a>(x: X<'a>)"#,
354        );
355    }
356
357    #[test]
358    fn test_function_add_lifetime_to_params() {
359        check_assist(
360            introduce_named_lifetime,
361            r#"fn my_fun(f: &Foo) -> X<'_$0>"#,
362            r#"fn my_fun<'a>(f: &'a Foo) -> X<'a>"#,
363        );
364    }
365
366    #[test]
367    fn test_function_add_lifetime_to_params_in_presence_of_other_lifetime() {
368        check_assist(
369            introduce_named_lifetime,
370            r#"fn my_fun<'other>(f: &Foo, b: &'other Bar) -> X<'_$0>"#,
371            r#"fn my_fun<'other, 'a>(f: &'a Foo, b: &'other Bar) -> X<'a>"#,
372        );
373    }
374
375    #[test]
376    fn test_function_not_applicable_without_self_and_multiple_unnamed_param_lifetimes() {
377        // this is not permitted under lifetime elision rules
378        check_assist_not_applicable(
379            introduce_named_lifetime,
380            r#"fn my_fun(f: &Foo, b: &Bar) -> X<'_$0>"#,
381        );
382    }
383
384    #[test]
385    fn test_function_add_lifetime_to_self_ref_param() {
386        check_assist(
387            introduce_named_lifetime,
388            r#"fn my_fun<'other>(&self, f: &Foo, b: &'other Bar) -> X<'_$0>"#,
389            r#"fn my_fun<'other, 'a>(&'a self, f: &Foo, b: &'other Bar) -> X<'a>"#,
390        );
391    }
392
393    #[test]
394    fn test_function_add_lifetime_to_param_with_non_ref_self() {
395        check_assist(
396            introduce_named_lifetime,
397            r#"fn my_fun<'other>(self, f: &Foo, b: &'other Bar) -> X<'_$0>"#,
398            r#"fn my_fun<'other, 'a>(self, f: &'a Foo, b: &'other Bar) -> X<'a>"#,
399        );
400    }
401
402    #[test]
403    fn test_function_add_lifetime_to_self_ref_mut() {
404        check_assist(
405            introduce_named_lifetime,
406            r#"fn foo(&mut self) -> &'_$0 ()"#,
407            r#"fn foo<'a>(&'a mut self) -> &'a ()"#,
408        );
409    }
410}