| //! Implementation of "lifetime elision" inlay hints: |
| //! ```no_run |
| //! fn example/* <'0> */(a: &/* '0 */()) {} |
| //! ``` |
| use std::iter; |
| |
| use ide_db::{FxHashMap, famous_defs::FamousDefs, syntax_helpers::node_ext::walk_ty}; |
| use itertools::Itertools; |
| use syntax::{SmolStr, format_smolstr}; |
| use syntax::{ |
| SyntaxKind, SyntaxToken, |
| ast::{self, AstNode, HasGenericParams, HasName}, |
| }; |
| |
| use crate::{ |
| InlayHint, InlayHintPosition, InlayHintsConfig, InlayKind, LifetimeElisionHints, |
| inlay_hints::InlayHintCtx, |
| }; |
| |
| pub(super) fn fn_hints( |
| acc: &mut Vec<InlayHint>, |
| ctx: &mut InlayHintCtx, |
| fd: &FamousDefs<'_, '_>, |
| config: &InlayHintsConfig, |
| func: ast::Fn, |
| ) -> Option<()> { |
| if config.lifetime_elision_hints == LifetimeElisionHints::Never { |
| return None; |
| } |
| |
| let param_list = func.param_list()?; |
| let generic_param_list = func.generic_param_list(); |
| let ret_type = func.ret_type(); |
| let self_param = param_list.self_param().filter(|it| it.amp_token().is_some()); |
| let gpl_append_range = func.name()?.syntax().text_range(); |
| hints_( |
| acc, |
| ctx, |
| fd, |
| config, |
| param_list.params().filter_map(|it| { |
| Some(( |
| it.pat().and_then(|it| match it { |
| ast::Pat::IdentPat(p) => p.name(), |
| _ => None, |
| }), |
| it.ty()?, |
| )) |
| }), |
| generic_param_list, |
| ret_type, |
| self_param, |
| |acc, allocated_lifetimes| { |
| acc.push(InlayHint { |
| range: gpl_append_range, |
| kind: InlayKind::GenericParamList, |
| label: format!("<{}>", allocated_lifetimes.iter().format(", "),).into(), |
| text_edit: None, |
| position: InlayHintPosition::After, |
| pad_left: false, |
| pad_right: false, |
| resolve_parent: None, |
| }) |
| }, |
| true, |
| ) |
| } |
| |
| pub(super) fn fn_ptr_hints( |
| acc: &mut Vec<InlayHint>, |
| ctx: &mut InlayHintCtx, |
| fd: &FamousDefs<'_, '_>, |
| config: &InlayHintsConfig, |
| func: ast::FnPtrType, |
| ) -> Option<()> { |
| if config.lifetime_elision_hints == LifetimeElisionHints::Never { |
| return None; |
| } |
| |
| let parent_for_binder = func |
| .syntax() |
| .ancestors() |
| .skip(1) |
| .take_while(|it| matches!(it.kind(), SyntaxKind::PAREN_TYPE | SyntaxKind::FOR_TYPE)) |
| .find_map(ast::ForType::cast) |
| .and_then(|it| it.for_binder()); |
| |
| let param_list = func.param_list()?; |
| let generic_param_list = parent_for_binder.as_ref().and_then(|it| it.generic_param_list()); |
| let ret_type = func.ret_type(); |
| let for_kw = parent_for_binder.as_ref().and_then(|it| it.for_token()); |
| hints_( |
| acc, |
| ctx, |
| fd, |
| config, |
| param_list.params().filter_map(|it| { |
| Some(( |
| it.pat().and_then(|it| match it { |
| ast::Pat::IdentPat(p) => p.name(), |
| _ => None, |
| }), |
| it.ty()?, |
| )) |
| }), |
| generic_param_list, |
| ret_type, |
| None, |
| |acc, allocated_lifetimes| { |
| let has_for = for_kw.is_some(); |
| let for_ = if has_for { "" } else { "for" }; |
| acc.push(InlayHint { |
| range: for_kw.map_or_else( |
| || func.syntax().first_token().unwrap().text_range(), |
| |it| it.text_range(), |
| ), |
| kind: InlayKind::GenericParamList, |
| label: format!("{for_}<{}>", allocated_lifetimes.iter().format(", "),).into(), |
| text_edit: None, |
| position: if has_for { |
| InlayHintPosition::After |
| } else { |
| InlayHintPosition::Before |
| }, |
| pad_left: false, |
| pad_right: true, |
| resolve_parent: None, |
| }); |
| }, |
| false, |
| ) |
| } |
| |
| pub(super) fn fn_path_hints( |
| acc: &mut Vec<InlayHint>, |
| ctx: &mut InlayHintCtx, |
| fd: &FamousDefs<'_, '_>, |
| config: &InlayHintsConfig, |
| func: &ast::PathType, |
| ) -> Option<()> { |
| if config.lifetime_elision_hints == LifetimeElisionHints::Never { |
| return None; |
| } |
| |
| // FIXME: Support general path types |
| let (param_list, ret_type) = func.path().as_ref().and_then(path_as_fn)?; |
| let parent_for_binder = func |
| .syntax() |
| .ancestors() |
| .skip(1) |
| .take_while(|it| matches!(it.kind(), SyntaxKind::PAREN_TYPE | SyntaxKind::FOR_TYPE)) |
| .find_map(ast::ForType::cast) |
| .and_then(|it| it.for_binder()); |
| |
| let generic_param_list = parent_for_binder.as_ref().and_then(|it| it.generic_param_list()); |
| let for_kw = parent_for_binder.as_ref().and_then(|it| it.for_token()); |
| hints_( |
| acc, |
| ctx, |
| fd, |
| config, |
| param_list.type_args().filter_map(|it| Some((None, it.ty()?))), |
| generic_param_list, |
| ret_type, |
| None, |
| |acc, allocated_lifetimes| { |
| let has_for = for_kw.is_some(); |
| let for_ = if has_for { "" } else { "for" }; |
| acc.push(InlayHint { |
| range: for_kw.map_or_else( |
| || func.syntax().first_token().unwrap().text_range(), |
| |it| it.text_range(), |
| ), |
| kind: InlayKind::GenericParamList, |
| label: format!("{for_}<{}>", allocated_lifetimes.iter().format(", "),).into(), |
| text_edit: None, |
| position: if has_for { |
| InlayHintPosition::After |
| } else { |
| InlayHintPosition::Before |
| }, |
| pad_left: false, |
| pad_right: true, |
| resolve_parent: None, |
| }); |
| }, |
| false, |
| ) |
| } |
| |
| fn path_as_fn(path: &ast::Path) -> Option<(ast::ParenthesizedArgList, Option<ast::RetType>)> { |
| path.segment().and_then(|it| it.parenthesized_arg_list().zip(Some(it.ret_type()))) |
| } |
| |
| fn hints_( |
| acc: &mut Vec<InlayHint>, |
| ctx: &mut InlayHintCtx, |
| FamousDefs(_, _): &FamousDefs<'_, '_>, |
| config: &InlayHintsConfig, |
| params: impl Iterator<Item = (Option<ast::Name>, ast::Type)>, |
| generic_param_list: Option<ast::GenericParamList>, |
| ret_type: Option<ast::RetType>, |
| self_param: Option<ast::SelfParam>, |
| on_missing_gpl: impl FnOnce(&mut Vec<InlayHint>, &[SmolStr]), |
| mut is_trivial: bool, |
| ) -> Option<()> { |
| let is_elided = |lt: &Option<ast::Lifetime>| match lt { |
| Some(lt) => matches!(lt.text().as_str(), "'_"), |
| None => true, |
| }; |
| |
| let mk_lt_hint = |t: SyntaxToken, label: String| InlayHint { |
| range: t.text_range(), |
| kind: InlayKind::Lifetime, |
| label: label.into(), |
| text_edit: None, |
| position: InlayHintPosition::After, |
| pad_left: false, |
| pad_right: true, |
| resolve_parent: None, |
| }; |
| |
| let potential_lt_refs = { |
| let mut acc: Vec<_> = vec![]; |
| if let Some(self_param) = &self_param { |
| let lifetime = self_param.lifetime(); |
| let is_elided = is_elided(&lifetime); |
| acc.push((None, self_param.amp_token(), lifetime, is_elided)); |
| } |
| params.for_each(|(name, ty)| { |
| // FIXME: check path types |
| walk_ty(&ty, &mut |ty| match ty { |
| ast::Type::RefType(r) => { |
| let lifetime = r.lifetime(); |
| let is_elided = is_elided(&lifetime); |
| acc.push((name.clone(), r.amp_token(), lifetime, is_elided)); |
| false |
| } |
| ast::Type::FnPtrType(_) => { |
| is_trivial = false; |
| true |
| } |
| ast::Type::PathType(t) => { |
| if t.path() |
| .and_then(|it| it.segment()) |
| .and_then(|it| it.parenthesized_arg_list()) |
| .is_some() |
| { |
| is_trivial = false; |
| true |
| } else { |
| false |
| } |
| } |
| _ => false, |
| }) |
| }); |
| acc |
| }; |
| |
| let mut used_names: FxHashMap<SmolStr, usize> = |
| ctx.lifetime_stacks.iter().flat_map(|it| it.iter()).cloned().zip(iter::repeat(0)).collect(); |
| // allocate names |
| let mut gen_idx_name = { |
| let mut generic = (0u8..).map(|idx| match idx { |
| idx if idx < 10 => SmolStr::from_iter(['\'', (idx + 48) as char]), |
| idx => format_smolstr!("'{idx}"), |
| }); |
| let ctx = &*ctx; |
| move || { |
| generic |
| .by_ref() |
| .find(|s| ctx.lifetime_stacks.iter().flat_map(|it| it.iter()).all(|n| n != s)) |
| .unwrap_or_default() |
| } |
| }; |
| let mut allocated_lifetimes = vec![]; |
| |
| { |
| let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided); |
| if self_param.is_some() && potential_lt_refs.next().is_some() { |
| allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { |
| // self can't be used as a lifetime, so no need to check for collisions |
| "'self".into() |
| } else { |
| gen_idx_name() |
| }); |
| } |
| potential_lt_refs.for_each(|(name, ..)| { |
| let name = match name { |
| Some(it) if config.param_names_for_lifetime_elision_hints => { |
| if let Some(c) = used_names.get_mut(it.text().as_str()) { |
| *c += 1; |
| format_smolstr!("'{}{c}", it.text().as_str()) |
| } else { |
| used_names.insert(it.text().as_str().into(), 0); |
| format_smolstr!("'{}", it.text().as_str()) |
| } |
| } |
| _ => gen_idx_name(), |
| }; |
| allocated_lifetimes.push(name); |
| }); |
| } |
| |
| // fetch output lifetime if elision rule applies |
| let output = match potential_lt_refs.as_slice() { |
| [(_, _, lifetime, _), ..] if self_param.is_some() || potential_lt_refs.len() == 1 => { |
| match lifetime { |
| Some(lt) => match lt.text().as_str() { |
| "'_" => allocated_lifetimes.first().cloned(), |
| "'static" => None, |
| name => Some(name.into()), |
| }, |
| None => allocated_lifetimes.first().cloned(), |
| } |
| } |
| [..] => None, |
| }; |
| |
| if allocated_lifetimes.is_empty() && output.is_none() { |
| return None; |
| } |
| |
| // apply hints |
| // apply output if required |
| if let (Some(output_lt), Some(r)) = (&output, ret_type) |
| && let Some(ty) = r.ty() |
| { |
| walk_ty(&ty, &mut |ty| match ty { |
| ast::Type::RefType(ty) if ty.lifetime().is_none() => { |
| if let Some(amp) = ty.amp_token() { |
| is_trivial = false; |
| acc.push(mk_lt_hint(amp, output_lt.to_string())); |
| } |
| false |
| } |
| ast::Type::FnPtrType(_) => { |
| is_trivial = false; |
| true |
| } |
| ast::Type::PathType(t) => { |
| if t.path() |
| .and_then(|it| it.segment()) |
| .and_then(|it| it.parenthesized_arg_list()) |
| .is_some() |
| { |
| is_trivial = false; |
| true |
| } else { |
| false |
| } |
| } |
| _ => false, |
| }) |
| } |
| |
| if config.lifetime_elision_hints == LifetimeElisionHints::SkipTrivial && is_trivial { |
| return None; |
| } |
| |
| let mut a = allocated_lifetimes.iter(); |
| for (_, amp_token, _, is_elided) in potential_lt_refs { |
| if is_elided { |
| let t = amp_token?; |
| let lt = a.next()?; |
| acc.push(mk_lt_hint(t, lt.to_string())); |
| } |
| } |
| |
| // generate generic param list things |
| match (generic_param_list, allocated_lifetimes.as_slice()) { |
| (_, []) => (), |
| (Some(gpl), allocated_lifetimes) => { |
| let angle_tok = gpl.l_angle_token()?; |
| let is_empty = gpl.generic_params().next().is_none(); |
| acc.push(InlayHint { |
| range: angle_tok.text_range(), |
| kind: InlayKind::Lifetime, |
| label: format!( |
| "{}{}", |
| allocated_lifetimes.iter().format(", "), |
| if is_empty { "" } else { ", " } |
| ) |
| .into(), |
| text_edit: None, |
| position: InlayHintPosition::After, |
| pad_left: false, |
| pad_right: true, |
| resolve_parent: None, |
| }); |
| } |
| (None, allocated_lifetimes) => on_missing_gpl(acc, allocated_lifetimes), |
| } |
| if let Some(stack) = ctx.lifetime_stacks.last_mut() { |
| stack.extend(allocated_lifetimes); |
| } |
| Some(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use crate::{ |
| InlayHintsConfig, LifetimeElisionHints, |
| inlay_hints::tests::{TEST_CONFIG, check, check_with_config}, |
| }; |
| |
| #[test] |
| fn hints_lifetimes() { |
| check( |
| r#" |
| fn empty() {} |
| |
| fn no_gpl(a: &()) {} |
| //^^^^^^<'0> |
| // ^'0 |
| fn empty_gpl<>(a: &()) {} |
| // ^'0 ^'0 |
| fn partial<'b>(a: &(), b: &'b ()) {} |
| // ^'0, $ ^'0 |
| fn partial<'a>(a: &'a (), b: &()) {} |
| // ^'0, $ ^'0 |
| |
| fn single_ret(a: &()) -> &() {} |
| // ^^^^^^^^^^<'0> |
| // ^'0 ^'0 |
| fn full_mul(a: &(), b: &()) {} |
| // ^^^^^^^^<'0, '1> |
| // ^'0 ^'1 |
| |
| fn foo<'c>(a: &'c ()) -> &() {} |
| // ^'c |
| |
| fn nested_in(a: & &X< &()>) {} |
| // ^^^^^^^^^<'0, '1, '2> |
| //^'0 ^'1 ^'2 |
| fn nested_out(a: &()) -> & &X< &()>{} |
| // ^^^^^^^^^^<'0> |
| //^'0 ^'0 ^'0 ^'0 |
| |
| impl () { |
| fn foo(&self) {} |
| // ^^^<'0> |
| // ^'0 |
| fn foo(&self) -> &() {} |
| // ^^^<'0> |
| // ^'0 ^'0 |
| fn foo(&self, a: &()) -> &() {} |
| // ^^^<'0, '1> |
| // ^'0 ^'1 ^'0 |
| } |
| "#, |
| ); |
| } |
| |
| #[test] |
| fn hints_lifetimes_named() { |
| check_with_config( |
| InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, |
| r#" |
| fn nested_in<'named>(named: & &X< &()>) {} |
| // ^'named1, 'named2, 'named3, $ |
| //^'named1 ^'named2 ^'named3 |
| "#, |
| ); |
| } |
| |
| #[test] |
| fn hints_lifetimes_trivial_skip() { |
| check_with_config( |
| InlayHintsConfig { |
| lifetime_elision_hints: LifetimeElisionHints::SkipTrivial, |
| ..TEST_CONFIG |
| }, |
| r#" |
| fn no_gpl(a: &()) {} |
| fn empty_gpl<>(a: &()) {} |
| fn partial<'b>(a: &(), b: &'b ()) {} |
| fn partial<'a>(a: &'a (), b: &()) {} |
| |
| fn single_ret(a: &()) -> &() {} |
| // ^^^^^^^^^^<'0> |
| // ^'0 ^'0 |
| fn full_mul(a: &(), b: &()) {} |
| |
| fn foo<'c>(a: &'c ()) -> &() {} |
| // ^'c |
| |
| fn nested_in(a: & &X< &()>) {} |
| fn nested_out(a: &()) -> & &X< &()>{} |
| // ^^^^^^^^^^<'0> |
| //^'0 ^'0 ^'0 ^'0 |
| |
| impl () { |
| fn foo(&self) {} |
| fn foo(&self) -> &() {} |
| // ^^^<'0> |
| // ^'0 ^'0 |
| fn foo(&self, a: &()) -> &() {} |
| // ^^^<'0, '1> |
| // ^'0 ^'1 ^'0 |
| } |
| "#, |
| ); |
| } |
| |
| #[test] |
| fn no_collide() { |
| check_with_config( |
| InlayHintsConfig { |
| lifetime_elision_hints: LifetimeElisionHints::Always, |
| param_names_for_lifetime_elision_hints: true, |
| ..TEST_CONFIG |
| }, |
| r#" |
| impl<'foo> { |
| fn foo(foo: &()) {} |
| // ^^^ <'foo1> |
| // ^ 'foo1 |
| } |
| "#, |
| ); |
| } |
| |
| #[test] |
| fn hints_lifetimes_fn_ptr() { |
| check_with_config( |
| InlayHintsConfig { |
| lifetime_elision_hints: LifetimeElisionHints::Always, |
| ..TEST_CONFIG |
| }, |
| r#" |
| fn fn_ptr(a: fn(&()) -> &fn(&()) -> &()) {} |
| //^^ for<'0> |
| //^'0 |
| //^'0 |
| //^^ for<'1> |
| //^'1 |
| //^'1 |
| fn fn_ptr2(a: for<'a> fn(&()) -> &()) {} |
| //^'0, $ |
| //^'0 |
| //^'0 |
| fn fn_trait(a: &impl Fn(&()) -> &()) {} |
| // ^^^^^^^^<'0> |
| // ^'0 |
| // ^^ for<'1> |
| //^'1 |
| // ^'1 |
| "#, |
| ); |
| } |
| |
| #[test] |
| fn hints_in_non_gen_defs() { |
| check_with_config( |
| InlayHintsConfig { |
| lifetime_elision_hints: LifetimeElisionHints::Always, |
| ..TEST_CONFIG |
| }, |
| r#" |
| const _: fn(&()) -> &(); |
| //^^ for<'0> |
| //^'0 |
| //^'0 |
| "#, |
| ); |
| } |
| } |