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