Skip to main content

ide/inlay_hints/
lifetime.rs

1//! Implementation of "lifetime elision" inlay hints:
2//! ```no_run
3//! fn example/* <'0> */(a: &/* '0 */()) {}
4//! ```
5use std::iter;
6
7use ide_db::{FxHashMap, famous_defs::FamousDefs, syntax_helpers::node_ext::walk_ty};
8use itertools::Itertools;
9use syntax::{SmolStr, format_smolstr};
10use syntax::{
11    SyntaxKind, SyntaxToken,
12    ast::{self, AstNode, HasGenericParams, HasName},
13};
14
15use crate::{
16    InlayHint, InlayHintPosition, InlayHintsConfig, InlayKind, LifetimeElisionHints,
17    inlay_hints::InlayHintCtx,
18};
19
20pub(super) fn fn_hints(
21    acc: &mut Vec<InlayHint>,
22    ctx: &mut InlayHintCtx,
23    fd: &FamousDefs<'_, '_>,
24    config: &InlayHintsConfig<'_>,
25    func: ast::Fn,
26) -> Option<()> {
27    if config.lifetime_elision_hints == LifetimeElisionHints::Never {
28        return None;
29    }
30
31    let param_list = func.param_list()?;
32    let generic_param_list = func.generic_param_list();
33    let ret_type = func.ret_type();
34    let gpl_append_range = func.name()?.syntax().text_range();
35    hints_(
36        acc,
37        ctx,
38        fd,
39        config,
40        param_list.params().filter_map(|it| {
41            Some((
42                it.pat().and_then(|it| match it {
43                    ast::Pat::IdentPat(p) => p.name(),
44                    _ => None,
45                }),
46                it.ty()?,
47            ))
48        }),
49        generic_param_list,
50        ret_type,
51        param_list.self_param(),
52        |acc, allocated_lifetimes| {
53            acc.push(InlayHint {
54                range: gpl_append_range,
55                kind: InlayKind::GenericParamList,
56                label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(),
57                text_edit: None,
58                position: InlayHintPosition::After,
59                pad_left: false,
60                pad_right: false,
61                resolve_parent: None,
62            })
63        },
64        true,
65    )
66}
67
68pub(super) fn fn_ptr_hints(
69    acc: &mut Vec<InlayHint>,
70    ctx: &mut InlayHintCtx,
71    fd: &FamousDefs<'_, '_>,
72    config: &InlayHintsConfig<'_>,
73    func: ast::FnPtrType,
74) -> Option<()> {
75    if config.lifetime_elision_hints == LifetimeElisionHints::Never {
76        return None;
77    }
78
79    let parent_for_binder = func
80        .syntax()
81        .ancestors()
82        .skip(1)
83        .take_while(|it| matches!(it.kind(), SyntaxKind::PAREN_TYPE | SyntaxKind::FOR_TYPE))
84        .find_map(ast::ForType::cast)
85        .and_then(|it| it.for_binder());
86
87    let param_list = func.param_list()?;
88    let generic_param_list = parent_for_binder.as_ref().and_then(|it| it.generic_param_list());
89    let ret_type = func.ret_type();
90    let for_kw = parent_for_binder.as_ref().and_then(|it| it.for_token());
91    hints_(
92        acc,
93        ctx,
94        fd,
95        config,
96        param_list.params().filter_map(|it| {
97            Some((
98                it.pat().and_then(|it| match it {
99                    ast::Pat::IdentPat(p) => p.name(),
100                    _ => None,
101                }),
102                it.ty()?,
103            ))
104        }),
105        generic_param_list,
106        ret_type,
107        None,
108        |acc, allocated_lifetimes| {
109            let has_for = for_kw.is_some();
110            let for_ = if has_for { "" } else { "for" };
111            acc.push(InlayHint {
112                range: for_kw.map_or_else(
113                    || func.syntax().first_token().unwrap().text_range(),
114                    |it| it.text_range(),
115                ),
116                kind: InlayKind::GenericParamList,
117                label: format!("{for_}<{}>", allocated_lifetimes.iter().format(", "),).into(),
118                text_edit: None,
119                position: if has_for {
120                    InlayHintPosition::After
121                } else {
122                    InlayHintPosition::Before
123                },
124                pad_left: false,
125                pad_right: true,
126                resolve_parent: None,
127            });
128        },
129        false,
130    )
131}
132
133pub(super) fn fn_path_hints(
134    acc: &mut Vec<InlayHint>,
135    ctx: &mut InlayHintCtx,
136    fd: &FamousDefs<'_, '_>,
137    config: &InlayHintsConfig<'_>,
138    func: &ast::PathType,
139) -> Option<()> {
140    if config.lifetime_elision_hints == LifetimeElisionHints::Never {
141        return None;
142    }
143
144    // FIXME: Support general path types
145    let (param_list, ret_type) = func.path().as_ref().and_then(path_as_fn)?;
146    let parent_for_binder = func
147        .syntax()
148        .ancestors()
149        .skip(1)
150        .take_while(|it| matches!(it.kind(), SyntaxKind::PAREN_TYPE | SyntaxKind::FOR_TYPE))
151        .find_map(ast::ForType::cast)
152        .and_then(|it| it.for_binder());
153
154    let generic_param_list = parent_for_binder.as_ref().and_then(|it| it.generic_param_list());
155    let for_kw = parent_for_binder.as_ref().and_then(|it| it.for_token());
156    hints_(
157        acc,
158        ctx,
159        fd,
160        config,
161        param_list.type_args().filter_map(|it| Some((None, it.ty()?))),
162        generic_param_list,
163        ret_type,
164        None,
165        |acc, allocated_lifetimes| {
166            let has_for = for_kw.is_some();
167            let for_ = if has_for { "" } else { "for" };
168            acc.push(InlayHint {
169                range: for_kw.map_or_else(
170                    || func.syntax().first_token().unwrap().text_range(),
171                    |it| it.text_range(),
172                ),
173                kind: InlayKind::GenericParamList,
174                label: format!("{for_}<{}>", allocated_lifetimes.iter().format(", "),).into(),
175                text_edit: None,
176                position: if has_for {
177                    InlayHintPosition::After
178                } else {
179                    InlayHintPosition::Before
180                },
181                pad_left: false,
182                pad_right: true,
183                resolve_parent: None,
184            });
185        },
186        false,
187    )
188}
189
190fn path_as_fn(path: &ast::Path) -> Option<(ast::ParenthesizedArgList, Option<ast::RetType>)> {
191    path.segment().and_then(|it| it.parenthesized_arg_list().zip(Some(it.ret_type())))
192}
193
194fn hints_(
195    acc: &mut Vec<InlayHint>,
196    ctx: &mut InlayHintCtx,
197    FamousDefs(_, _): &FamousDefs<'_, '_>,
198    config: &InlayHintsConfig<'_>,
199    params: impl Iterator<Item = (Option<ast::Name>, ast::Type)>,
200    generic_param_list: Option<ast::GenericParamList>,
201    ret_type: Option<ast::RetType>,
202    self_param: Option<ast::SelfParam>,
203    on_missing_gpl: impl FnOnce(&mut Vec<InlayHint>, &[SmolStr]),
204    mut is_trivial: bool,
205) -> Option<()> {
206    let is_elided = |lt: &Option<ast::Lifetime>| match lt {
207        Some(lt) => matches!(lt.text().as_str(), "'_"),
208        None => true,
209    };
210    let self_param = self_param.and_then(|it| {
211        if it.colon_token().is_none() {
212            return Some((it.amp_token(), it.lifetime()));
213        }
214        it.ty().map(|ty| {
215            let ref_type = ty.syntax().descendants().find_map(ast::RefType::cast);
216            let lifetime = ref_type
217                .as_ref()
218                .and_then(|it| it.lifetime())
219                .or_else(|| ty.syntax().descendants().find_map(ast::Lifetime::cast));
220            (ref_type.and_then(|it| it.amp_token()), lifetime)
221        })
222    });
223    let self_param = self_param.filter(|(amp, lt)| amp.is_some() || lt.is_some());
224
225    let mk_lt_hint = |t: SyntaxToken, label: String| InlayHint {
226        range: t.text_range(),
227        kind: InlayKind::Lifetime,
228        label: label.into(),
229        text_edit: None,
230        position: InlayHintPosition::After,
231        pad_left: false,
232        pad_right: true,
233        resolve_parent: None,
234    };
235
236    let potential_lt_refs = {
237        let mut acc: Vec<_> = vec![];
238        if let Some((amp_token, lifetime)) = self_param.clone() {
239            let is_elided = is_elided(&lifetime);
240            acc.push((None, amp_token, lifetime, is_elided));
241        }
242        params.for_each(|(name, ty)| {
243            // FIXME: check path types
244            walk_ty(&ty, &mut |ty| match ty {
245                ast::Type::RefType(r) => {
246                    let lifetime = r.lifetime();
247                    let is_elided = is_elided(&lifetime);
248                    acc.push((name.clone(), r.amp_token(), lifetime, is_elided));
249                    false
250                }
251                ast::Type::FnPtrType(_) => {
252                    is_trivial = false;
253                    true
254                }
255                ast::Type::PathType(t)
256                    if t.path()
257                        .and_then(|it| it.segment())
258                        .and_then(|it| it.parenthesized_arg_list())
259                        .is_some() =>
260                {
261                    is_trivial = false;
262                    true
263                }
264                _ => false,
265            })
266        });
267        acc
268    };
269
270    let mut used_names: FxHashMap<SmolStr, usize> =
271        ctx.lifetime_stacks.iter().flat_map(|it| it.iter()).cloned().zip(iter::repeat(0)).collect();
272    // allocate names
273    let mut gen_idx_name = {
274        let mut generic = (0u8..).map(|idx| match idx {
275            idx if idx < 10 => SmolStr::from_iter(['\'', (idx + 48) as char]),
276            idx => format_smolstr!("'{idx}"),
277        });
278        let ctx = &*ctx;
279        move || {
280            generic
281                .by_ref()
282                .find(|s| ctx.lifetime_stacks.iter().flat_map(|it| it.iter()).all(|n| n != s))
283                .unwrap_or_default()
284        }
285    };
286    let mut allocated_lifetimes = vec![];
287
288    {
289        let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
290        if self_param.is_some() && potential_lt_refs.next().is_some() {
291            allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
292                // self can't be used as a lifetime, so no need to check for collisions
293                "'self".into()
294            } else {
295                gen_idx_name()
296            });
297        }
298        potential_lt_refs.for_each(|(name, ..)| {
299            let name = match name {
300                Some(it) if config.param_names_for_lifetime_elision_hints => {
301                    if let Some(c) = used_names.get_mut(it.text().as_str()) {
302                        *c += 1;
303                        format_smolstr!("'{}{c}", it.text().as_str())
304                    } else {
305                        used_names.insert(it.text().as_str().into(), 0);
306                        format_smolstr!("'{}", it.text().as_str())
307                    }
308                }
309                _ => gen_idx_name(),
310            };
311            allocated_lifetimes.push(name);
312        });
313    }
314
315    // fetch output lifetime if elision rule applies
316    let output = match potential_lt_refs.as_slice() {
317        [(_, _, lifetime, _), ..] if self_param.is_some() || potential_lt_refs.len() == 1 => {
318            match lifetime {
319                Some(lt) => match lt.text().as_str() {
320                    "'_" => allocated_lifetimes.first().cloned(),
321                    "'static" => None,
322                    name => Some(name.into()),
323                },
324                None => allocated_lifetimes.first().cloned(),
325            }
326        }
327        [..] => None,
328    };
329
330    if allocated_lifetimes.is_empty() && output.is_none() {
331        return None;
332    }
333
334    // apply hints
335    // apply output if required
336    if let (Some(output_lt), Some(r)) = (&output, ret_type)
337        && let Some(ty) = r.ty()
338    {
339        walk_ty(&ty, &mut |ty| match ty {
340            ast::Type::RefType(ty) if ty.lifetime().is_none() => {
341                if let Some(amp) = ty.amp_token() {
342                    is_trivial = false;
343                    acc.push(mk_lt_hint(amp, output_lt.to_string()));
344                }
345                false
346            }
347            ast::Type::FnPtrType(_) => {
348                is_trivial = false;
349                true
350            }
351            ast::Type::PathType(t)
352                if t.path()
353                    .and_then(|it| it.segment())
354                    .and_then(|it| it.parenthesized_arg_list())
355                    .is_some() =>
356            {
357                is_trivial = false;
358                true
359            }
360            _ => false,
361        })
362    }
363
364    if config.lifetime_elision_hints == LifetimeElisionHints::SkipTrivial && is_trivial {
365        return None;
366    }
367
368    let mut a = allocated_lifetimes.iter();
369    for (_, amp_token, _, is_elided) in potential_lt_refs {
370        if is_elided {
371            let t = amp_token?;
372            let lt = a.next()?;
373            acc.push(mk_lt_hint(t, lt.to_string()));
374        }
375    }
376
377    // generate generic param list things
378    match (generic_param_list, allocated_lifetimes.as_slice()) {
379        (_, []) => (),
380        (Some(gpl), allocated_lifetimes) => {
381            let angle_tok = gpl.l_angle_token()?;
382            let is_empty = gpl.generic_params().next().is_none();
383            acc.push(InlayHint {
384                range: angle_tok.text_range(),
385                kind: InlayKind::Lifetime,
386                label: format!(
387                    "{}{}",
388                    allocated_lifetimes.iter().format(", "),
389                    if is_empty { "" } else { ", " }
390                )
391                .into(),
392                text_edit: None,
393                position: InlayHintPosition::After,
394                pad_left: false,
395                pad_right: true,
396                resolve_parent: None,
397            });
398        }
399        (None, allocated_lifetimes) => on_missing_gpl(acc, allocated_lifetimes),
400    }
401    if let Some(stack) = ctx.lifetime_stacks.last_mut() {
402        stack.extend(allocated_lifetimes);
403    }
404    Some(())
405}
406
407#[cfg(test)]
408mod tests {
409    use crate::{
410        InlayHintsConfig, LifetimeElisionHints,
411        inlay_hints::tests::{TEST_CONFIG, check, check_with_config},
412    };
413
414    #[test]
415    fn hints_lifetimes() {
416        check(
417            r#"
418fn empty() {}
419
420fn no_gpl(a: &()) {}
421 //^^^^^^<'0>
422          // ^'0
423fn empty_gpl<>(a: &()) {}
424      //    ^'0   ^'0
425fn partial<'b>(a: &(), b: &'b ()) {}
426//        ^'0, $  ^'0
427fn partial<'a>(a: &'a (), b: &()) {}
428//        ^'0, $             ^'0
429
430fn single_ret(a: &()) -> &() {}
431// ^^^^^^^^^^<'0>
432              // ^'0     ^'0
433fn full_mul(a: &(), b: &()) {}
434// ^^^^^^^^<'0, '1>
435            // ^'0     ^'1
436
437fn foo<'c>(a: &'c ()) -> &() {}
438                      // ^'c
439
440fn nested_in(a: &   &X< &()>) {}
441// ^^^^^^^^^<'0, '1, '2>
442              //^'0 ^'1 ^'2
443fn nested_out(a: &()) -> &   &X< &()>{}
444// ^^^^^^^^^^<'0>
445               //^'0     ^'0 ^'0 ^'0
446
447impl () {
448    fn foo(self, x: &()) -> &() {}
449    // ^^^<'0>
450                 // ^'0     ^'0
451    fn foo(&self) {}
452    // ^^^<'0>
453        // ^'0
454    fn foo(&self) -> &() {}
455    // ^^^<'0>
456        // ^'0       ^'0
457    fn foo(&self, a: &()) -> &() {}
458    // ^^^<'0, '1>
459        // ^'0       ^'1     ^'0
460    fn foo(self: &Self, a: &()) -> &() {}
461    // ^^^<'0, '1>
462              // ^'0       ^'1     ^'0
463
464}
465"#,
466        );
467    }
468
469    #[test]
470    fn hints_lifetimes_named() {
471        check_with_config(
472            InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
473            r#"
474fn nested_in<'named>(named: &        &X<      &()>) {}
475//          ^'named1, 'named2, 'named3, $
476                          //^'named1 ^'named2 ^'named3
477"#,
478        );
479    }
480
481    #[test]
482    fn hints_lifetimes_trivial_skip() {
483        check_with_config(
484            InlayHintsConfig {
485                lifetime_elision_hints: LifetimeElisionHints::SkipTrivial,
486                ..TEST_CONFIG
487            },
488            r#"
489fn no_gpl(a: &()) {}
490fn empty_gpl<>(a: &()) {}
491fn partial<'b>(a: &(), b: &'b ()) {}
492fn partial<'a>(a: &'a (), b: &()) {}
493
494fn single_ret(a: &()) -> &() {}
495// ^^^^^^^^^^<'0>
496              // ^'0     ^'0
497fn full_mul(a: &(), b: &()) {}
498
499fn foo<'c>(a: &'c ()) -> &() {}
500                      // ^'c
501
502fn nested_in(a: &   &X< &()>) {}
503fn nested_out(a: &()) -> &   &X< &()>{}
504// ^^^^^^^^^^<'0>
505               //^'0     ^'0 ^'0 ^'0
506
507impl () {
508    fn foo(&self) {}
509    fn foo(&self) -> &() {}
510    // ^^^<'0>
511        // ^'0       ^'0
512    fn foo(&self, a: &()) -> &() {}
513    // ^^^<'0, '1>
514        // ^'0       ^'1     ^'0
515}
516"#,
517        );
518    }
519
520    #[test]
521    fn no_collide() {
522        check_with_config(
523            InlayHintsConfig {
524                lifetime_elision_hints: LifetimeElisionHints::Always,
525                param_names_for_lifetime_elision_hints: true,
526                ..TEST_CONFIG
527            },
528            r#"
529impl<'foo> {
530    fn foo(foo: &()) {}
531    // ^^^ <'foo1>
532             // ^ 'foo1
533}
534"#,
535        );
536    }
537
538    #[test]
539    fn hints_lifetimes_fn_ptr() {
540        check_with_config(
541            InlayHintsConfig {
542                lifetime_elision_hints: LifetimeElisionHints::Always,
543                ..TEST_CONFIG
544            },
545            r#"
546fn fn_ptr(a: fn(&()) -> &fn(&()) -> &()) {}
547           //^^ for<'0>
548              //^'0
549                      //^'0
550                       //^^ for<'1>
551                          //^'1
552                                  //^'1
553fn fn_ptr2(a: for<'a> fn(&()) -> &()) {}
554               //^'0, $
555                       //^'0
556                               //^'0
557fn fn_trait(a: &impl Fn(&()) -> &()) {}
558// ^^^^^^^^<'0>
559            // ^'0
560                  // ^^ for<'1>
561                      //^'1
562                             // ^'1
563"#,
564        );
565    }
566
567    #[test]
568    fn hints_in_non_gen_defs() {
569        check_with_config(
570            InlayHintsConfig {
571                lifetime_elision_hints: LifetimeElisionHints::Always,
572                ..TEST_CONFIG
573            },
574            r#"
575const _: fn(&()) -> &();
576       //^^ for<'0>
577          //^'0
578                  //^'0
579"#,
580        );
581    }
582}