//! 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, 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, 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, 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)> { path.segment().and_then(|it| it.parenthesized_arg_list().zip(Some(it.ret_type()))) } fn hints_( acc: &mut Vec, ctx: &mut InlayHintCtx, FamousDefs(_, _): &FamousDefs<'_, '_>, config: &InlayHintsConfig<'_>, params: impl Iterator, ast::Type)>, generic_param_list: Option, ret_type: Option, self_param: Option, on_missing_gpl: impl FnOnce(&mut Vec, &[SmolStr]), mut is_trivial: bool, ) -> Option<()> { let is_elided = |lt: &Option| 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 = 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 "#, ); } }