1use 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 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 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 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".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 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 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 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}