1use std::iter::zip;
8
9use either::Either;
10use hir::{EditionedFileId, Semantics};
11use ide_db::{RootDatabase, famous_defs::FamousDefs};
12
13use stdx::to_lower_snake_case;
14use syntax::T;
15use syntax::ast::{self, AstNode, HasArgList, HasName, UnaryOp};
16
17use crate::{InlayHint, InlayHintLabel, InlayHintPosition, InlayHintsConfig, InlayKind};
18
19pub(super) fn hints(
20 acc: &mut Vec<InlayHint>,
21 FamousDefs(sema, krate): &FamousDefs<'_, '_>,
22 config: &InlayHintsConfig<'_>,
23 file_id: EditionedFileId,
24 expr: ast::Expr,
25) -> Option<()> {
26 if !config.parameter_hints {
27 return None;
28 }
29
30 let (callable, arg_list) = get_callable(sema, &expr)?;
31 let unary_function = callable.n_params() == 1;
32 let function_name = match callable.kind() {
33 hir::CallableKind::Function(function) => Some(function.name(sema.db)),
34 _ => None,
35 };
36 let function_name = function_name.as_ref().map(|it| it.as_str());
37 let hints = callable
38 .params()
39 .into_iter()
40 .zip(arg_list.args())
41 .filter_map(|(p, arg)| {
42 let range = sema.original_range_opt(arg.syntax())?;
44 if range.file_id != file_id {
45 return None;
46 }
47 let param_name = p.name(sema.db)?;
48 Some((p, param_name, arg, range))
49 })
50 .filter(|(_, param_name, arg, _)| {
51 !should_hide_param_name_hint(
52 sema,
53 unary_function,
54 function_name,
55 param_name.as_str(),
56 arg,
57 )
58 })
59 .map(|(param, param_name, _, hir::FileRange { range, .. })| {
60 let colon = if config.render_colons { ":" } else { "" };
61 let label = InlayHintLabel::simple(
62 format!("{}{colon}", param_name.display(sema.db, krate.edition(sema.db))),
63 None,
64 config.lazy_location_opt(|| {
65 let source = sema.source(param)?;
66 let name_syntax = match source.value.as_ref() {
67 Either::Left(pat) => pat.name(),
68 Either::Right(param) => match param.pat()? {
69 ast::Pat::IdentPat(it) => it.name(),
70 _ => None,
71 },
72 }?;
73 sema.original_range_opt(name_syntax.syntax()).map(|frange| ide_db::FileRange {
74 file_id: frange.file_id.file_id(sema.db),
75 range: frange.range,
76 })
77 }),
78 );
79 InlayHint {
80 range,
81 kind: InlayKind::Parameter,
82 label,
83 text_edit: None,
84 position: InlayHintPosition::Before,
85 pad_left: false,
86 pad_right: true,
87 resolve_parent: Some(expr.syntax().text_range()),
88 }
89 });
90
91 acc.extend(hints);
92
93 if config.parameter_hints_for_missing_arguments {
95 let provided_args_count = arg_list.args().count();
96 let params = callable.params();
97 let total_params = params.len();
98
99 if provided_args_count < total_params
100 && let Some(next_param) = params.get(provided_args_count)
101 && let Some(param_name) = next_param.name(sema.db)
102 {
103 if should_hide_missing_param_hint(unary_function, function_name, param_name.as_str()) {
105 return Some(());
106 }
107
108 if let Some(hint_range) = missing_arg_hint_position(&arg_list) {
110 let colon = if config.render_colons { ":" } else { "" };
111 let label = InlayHintLabel::simple(
112 format!("{}{}", param_name.display(sema.db, krate.edition(sema.db)), colon),
113 None,
114 config.lazy_location_opt(|| {
115 let source = sema.source(next_param.clone())?;
116 let name_syntax = match source.value.as_ref() {
117 Either::Left(pat) => pat.name(),
118 Either::Right(param) => match param.pat()? {
119 ast::Pat::IdentPat(it) => it.name(),
120 _ => None,
121 },
122 }?;
123 sema.original_range_opt(name_syntax.syntax()).map(|frange| {
124 ide_db::FileRange {
125 file_id: frange.file_id.file_id(sema.db),
126 range: frange.range,
127 }
128 })
129 }),
130 );
131 acc.push(InlayHint {
132 range: hint_range,
133 kind: InlayKind::Parameter,
134 label,
135 text_edit: None,
136 position: InlayHintPosition::Before,
137 pad_left: true,
138 pad_right: false,
139 resolve_parent: Some(expr.syntax().text_range()),
140 });
141 }
142 }
143 }
144
145 Some(())
146}
147
148fn missing_arg_hint_position(arg_list: &ast::ArgList) -> Option<syntax::TextRange> {
151 arg_list
154 .syntax()
155 .children_with_tokens()
156 .filter_map(|it| it.into_token())
157 .find(|t| t.kind() == T![')'])
158 .map(|t| t.text_range())
159}
160
161fn get_callable<'db>(
162 sema: &Semantics<'db, RootDatabase>,
163 expr: &ast::Expr,
164) -> Option<(hir::Callable<'db>, ast::ArgList)> {
165 match expr {
166 ast::Expr::CallExpr(expr) => {
167 let descended = sema.descend_node_into_attributes(expr.clone()).pop();
168 let expr = descended.as_ref().unwrap_or(expr);
169 sema.type_of_expr(&expr.expr()?)?.original.as_callable(sema.db).zip(expr.arg_list())
170 }
171 ast::Expr::MethodCallExpr(expr) => {
172 let descended = sema.descend_node_into_attributes(expr.clone()).pop();
173 let expr = descended.as_ref().unwrap_or(expr);
174 sema.resolve_method_call_as_callable(expr).zip(expr.arg_list())
175 }
176 _ => None,
177 }
178}
179
180const INSIGNIFICANT_METHOD_NAMES: &[&str] = &["clone", "as_ref", "into"];
181const INSIGNIFICANT_PARAMETER_NAMES: &[&str] =
182 &["predicate", "value", "pat", "rhs", "other", "msg", "op"];
183
184fn should_hide_param_name_hint(
185 sema: &Semantics<'_, RootDatabase>,
186 unary_function: bool,
187 function_name: Option<&str>,
188 param_name: &str,
189 argument: &ast::Expr,
190) -> bool {
191 let param_name = param_name.trim_matches('_');
201 if param_name.is_empty() {
202 return true;
203 }
204
205 if param_name.starts_with("ra_fixture") {
206 return true;
207 }
208
209 if unary_function {
210 if let Some(function_name) = function_name
211 && is_param_name_suffix_of_fn_name(param_name, function_name)
212 {
213 return true;
214 }
215 if is_obvious_param(param_name) {
216 return true;
217 }
218 }
219
220 is_argument_expr_similar_to_param_name(sema, argument, param_name)
221}
222
223fn should_hide_missing_param_hint(
227 unary_function: bool,
228 function_name: Option<&str>,
229 param_name: &str,
230) -> bool {
231 let param_name = param_name.trim_matches('_');
232 if param_name.is_empty() {
233 return true;
234 }
235
236 if param_name.starts_with("ra_fixture") {
237 return true;
238 }
239
240 if unary_function {
241 if let Some(function_name) = function_name
242 && is_param_name_suffix_of_fn_name(param_name, function_name)
243 {
244 return true;
245 }
246 if is_obvious_param(param_name) {
247 return true;
248 }
249 }
250
251 false
252}
253
254fn is_param_name_suffix_of_fn_name(param_name: &str, fn_name: &str) -> bool {
259 fn_name == param_name
260 || fn_name
261 .len()
262 .checked_sub(param_name.len())
263 .and_then(|at| fn_name.is_char_boundary(at).then(|| fn_name.split_at(at)))
264 .is_some_and(|(prefix, suffix)| {
265 suffix.eq_ignore_ascii_case(param_name) && prefix.ends_with('_')
266 })
267}
268
269fn is_argument_expr_similar_to_param_name(
270 sema: &Semantics<'_, RootDatabase>,
271 argument: &ast::Expr,
272 param_name: &str,
273) -> bool {
274 match get_segment_representation(argument) {
275 Some(Either::Left(argument)) => is_argument_similar_to_param_name(&argument, param_name),
276 Some(Either::Right(path)) => {
277 path.segment()
278 .and_then(|it| it.name_ref())
279 .is_some_and(|name_ref| name_ref.text().eq_ignore_ascii_case(param_name))
280 || is_adt_constructor_similar_to_param_name(sema, &path, param_name)
281 }
282 None => false,
283 }
284}
285
286pub(super) fn is_argument_similar_to_param_name(
289 argument: &[ast::NameRef],
290 param_name: &str,
291) -> bool {
292 debug_assert!(!argument.is_empty());
293 debug_assert!(!param_name.is_empty());
294 let param_name = param_name.split('_');
295 let argument = argument.iter().flat_map(|it| it.text_non_mutable().split('_'));
296
297 let prefix_match = zip(argument.clone(), param_name.clone())
298 .all(|(arg, param)| arg.eq_ignore_ascii_case(param));
299 let postfix_match = || {
300 zip(argument.rev(), param_name.rev()).all(|(arg, param)| arg.eq_ignore_ascii_case(param))
301 };
302 prefix_match || postfix_match()
303}
304
305pub(super) fn get_segment_representation(
306 expr: &ast::Expr,
307) -> Option<Either<Vec<ast::NameRef>, ast::Path>> {
308 match expr {
309 ast::Expr::MethodCallExpr(method_call_expr) => {
310 let receiver =
311 method_call_expr.receiver().and_then(|expr| get_segment_representation(&expr));
312 let name_ref = method_call_expr.name_ref()?;
313 if INSIGNIFICANT_METHOD_NAMES.contains(&name_ref.text().as_str()) {
314 return receiver;
315 }
316 Some(Either::Left(match receiver {
317 Some(Either::Left(mut left)) => {
318 left.push(name_ref);
319 left
320 }
321 Some(Either::Right(_)) | None => vec![name_ref],
322 }))
323 }
324 ast::Expr::FieldExpr(field_expr) => {
325 let expr = field_expr.expr().and_then(|expr| get_segment_representation(&expr));
326 let name_ref = field_expr.name_ref()?;
327 let res = match expr {
328 Some(Either::Left(mut left)) => {
329 left.push(name_ref);
330 left
331 }
332 Some(Either::Right(_)) | None => vec![name_ref],
333 };
334 Some(Either::Left(res))
335 }
336 ast::Expr::MacroExpr(macro_expr) => macro_expr.macro_call()?.path().map(Either::Right),
338 ast::Expr::RecordExpr(record_expr) => record_expr.path().map(Either::Right),
339 ast::Expr::PathExpr(path_expr) => {
340 let path = path_expr.path()?;
341 Some(match path.as_single_name_ref() {
343 None => Either::Right(path),
344 Some(name_ref) => Either::Left(vec![name_ref]),
345 })
346 }
347 ast::Expr::PrefixExpr(prefix_expr) if prefix_expr.op_kind() == Some(UnaryOp::Not) => None,
348 ast::Expr::PrefixExpr(prefix_expr) => get_segment_representation(&prefix_expr.expr()?),
350 ast::Expr::RefExpr(ref_expr) => get_segment_representation(&ref_expr.expr()?),
351 ast::Expr::CastExpr(cast_expr) => get_segment_representation(&cast_expr.expr()?),
352 ast::Expr::CallExpr(call_expr) => get_segment_representation(&call_expr.expr()?),
353 ast::Expr::AwaitExpr(await_expr) => get_segment_representation(&await_expr.expr()?),
354 ast::Expr::IndexExpr(index_expr) => get_segment_representation(&index_expr.base()?),
355 ast::Expr::ParenExpr(paren_expr) => get_segment_representation(&paren_expr.expr()?),
356 ast::Expr::TryExpr(try_expr) => get_segment_representation(&try_expr.expr()?),
357 _ => None,
359 }
360}
361
362fn is_obvious_param(param_name: &str) -> bool {
363 param_name.len() == 1 || INSIGNIFICANT_PARAMETER_NAMES.contains(¶m_name)
366}
367
368fn is_adt_constructor_similar_to_param_name(
369 sema: &Semantics<'_, RootDatabase>,
370 path: &ast::Path,
371 param_name: &str,
372) -> bool {
373 (|| match sema.resolve_path(path)? {
374 hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => {
375 Some(to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name)
376 }
377 hir::PathResolution::Def(hir::ModuleDef::Function(_) | hir::ModuleDef::Variant(_)) => {
378 if to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name {
379 return Some(true);
380 }
381 let qual = path.qualifier()?;
382 match sema.resolve_path(&qual)? {
383 hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => {
384 Some(to_lower_snake_case(&qual.segment()?.name_ref()?.text()) == param_name)
385 }
386 _ => None,
387 }
388 }
389 _ => None,
390 })()
391 .unwrap_or(false)
392}
393
394#[cfg(test)]
395mod tests {
396 use crate::{
397 InlayHintsConfig,
398 inlay_hints::tests::{DISABLED_CONFIG, check_with_config},
399 };
400
401 #[track_caller]
402 fn check_params(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
403 check_with_config(
404 InlayHintsConfig { parameter_hints: true, ..DISABLED_CONFIG },
405 ra_fixture,
406 );
407 }
408
409 #[test]
410 fn param_hints_only() {
411 check_params(
412 r#"
413fn foo(a: i32, b: i32) -> i32 { a + b }
414fn main() {
415 let _x = foo(
416 4,
417 //^ a
418 4,
419 //^ b
420 );
421}"#,
422 );
423 }
424
425 #[test]
426 fn param_hints_on_closure() {
427 check_params(
428 r#"
429fn main() {
430 let clo = |a: u8, b: u8| a + b;
431 clo(
432 1,
433 //^ a
434 2,
435 //^ b
436 );
437}
438 "#,
439 );
440 }
441
442 #[test]
443 fn param_name_similar_to_fn_name_still_hints() {
444 check_params(
445 r#"
446fn max(x: i32, y: i32) -> i32 { x + y }
447fn main() {
448 let _x = max(
449 4,
450 //^ x
451 4,
452 //^ y
453 );
454}"#,
455 );
456 }
457
458 #[test]
459 fn param_name_similar_to_fn_name() {
460 check_params(
461 r#"
462fn param_with_underscore(with_underscore: i32) -> i32 { with_underscore }
463fn main() {
464 let _x = param_with_underscore(
465 4,
466 );
467}"#,
468 );
469 check_params(
470 r#"
471fn param_with_underscore(underscore: i32) -> i32 { underscore }
472fn main() {
473 let _x = param_with_underscore(
474 4,
475 );
476}"#,
477 );
478 }
479
480 #[test]
481 fn param_name_same_as_fn_name() {
482 check_params(
483 r#"
484fn foo(foo: i32) -> i32 { foo }
485fn main() {
486 let _x = foo(
487 4,
488 );
489}"#,
490 );
491 }
492
493 #[test]
494 fn never_hide_param_when_multiple_params() {
495 check_params(
496 r#"
497fn foo(foo: i32, bar: i32) -> i32 { bar + baz }
498fn main() {
499 let _x = foo(
500 4,
501 //^ foo
502 8,
503 //^ bar
504 );
505}"#,
506 );
507 }
508
509 #[test]
510 fn param_hints_look_through_as_ref_and_clone() {
511 check_params(
512 r#"
513fn foo(bar: i32, baz: f32) {}
514
515fn main() {
516 let bar = 3;
517 let baz = &"baz";
518 let fez = 1.0;
519 foo(bar.clone(), bar.clone());
520 //^^^^^^^^^^^ baz
521 foo(bar.as_ref(), bar.as_ref());
522 //^^^^^^^^^^^^ baz
523}
524"#,
525 );
526 }
527
528 #[test]
529 fn self_param_hints() {
530 check_params(
531 r#"
532struct Foo;
533
534impl Foo {
535 fn foo(self: Self) {}
536 fn bar(self: &Self) {}
537}
538
539fn main() {
540 Foo::foo(Foo);
541 //^^^ self
542 Foo::bar(&Foo);
543 //^^^^ self
544}
545"#,
546 )
547 }
548
549 #[test]
550 fn param_name_hints_show_for_literals() {
551 check_params(
552 r#"pub fn test(a: i32, b: i32) -> [i32; 2] { [a, b] }
553fn main() {
554 test(
555 0xa_b,
556 //^^^^^ a
557 0xa_b,
558 //^^^^^ b
559 );
560}"#,
561 )
562 }
563
564 #[test]
565 fn function_call_parameter_hint() {
566 check_params(
567 r#"
568//- minicore: option
569struct FileId {}
570struct SmolStr {}
571
572struct TextRange {}
573struct SyntaxKind {}
574struct NavigationTarget {}
575
576struct Test {}
577
578impl Test {
579 fn method(&self, mut param: i32) -> i32 { param * 2 }
580
581 fn from_syntax(
582 file_id: FileId,
583 name: SmolStr,
584 focus_range: Option<TextRange>,
585 full_range: TextRange,
586 kind: SyntaxKind,
587 docs: Option<String>,
588 ) -> NavigationTarget {
589 NavigationTarget {}
590 }
591}
592
593fn test_func(mut foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 {
594 foo + bar
595}
596
597fn main() {
598 let not_literal = 1;
599 let _: i32 = test_func(1, 2, "hello", 3, not_literal);
600 //^ foo ^ bar ^^^^^^^ msg ^^^^^^^^^^^ last
601 let t: Test = Test {};
602 t.method(123);
603 //^^^ param
604 Test::method(&t, 3456);
605 //^^ self ^^^^ param
606 Test::from_syntax(
607 FileId {},
608 "impl".into(),
609 //^^^^^^^^^^^^^ name
610 None,
611 //^^^^ focus_range
612 TextRange {},
613 //^^^^^^^^^^^^ full_range
614 SyntaxKind {},
615 //^^^^^^^^^^^^^ kind
616 None,
617 //^^^^ docs
618 );
619}"#,
620 );
621 }
622
623 #[test]
624 fn parameter_hint_heuristics() {
625 check_params(
626 r#"
627fn check(ra_fixture_thing: &str) {}
628
629fn map(f: i32) {}
630fn filter(predicate: i32) {}
631
632fn strip_suffix(suffix: &str) {}
633fn stripsuffix(suffix: &str) {}
634fn same(same: u32) {}
635fn same2(_same2: u32) {}
636
637fn enum_matches_param_name(completion_kind: CompletionKind) {}
638
639fn foo(param: u32) {}
640fn bar(param_eter: u32) {}
641fn baz(a_d_e: u32) {}
642fn far(loop_: u32) {}
643fn faz(r#loop: u32) {}
644
645enum CompletionKind {
646 Keyword,
647}
648
649fn non_ident_pat((a, b): (u32, u32)) {}
650
651fn main() {
652 const PARAM: u32 = 0;
653 foo(PARAM);
654 foo(!PARAM);
655 // ^^^^^^ param
656 check("");
657
658 map(0);
659 filter(0);
660
661 strip_suffix("");
662 stripsuffix("");
663 //^^ suffix
664 same(0);
665 same2(0);
666
667 enum_matches_param_name(CompletionKind::Keyword);
668
669 let param = 0;
670 foo(param);
671 foo(param as _);
672 let param_end = 0;
673 foo(param_end);
674 let start_param = 0;
675 foo(start_param);
676 let param2 = 0;
677 foo(param2);
678 //^^^^^^ param
679
680 macro_rules! param {
681 () => {};
682 };
683 foo(param!());
684
685 let param_eter = 0;
686 bar(param_eter);
687 let param_eter_end = 0;
688 bar(param_eter_end);
689 let start_param_eter = 0;
690 bar(start_param_eter);
691 let param_eter2 = 0;
692 bar(param_eter2);
693 //^^^^^^^^^^^ param_eter
694 let loop_level = 0;
695 far(loop_level);
696 faz(loop_level);
697
698 non_ident_pat((0, 0));
699
700 baz(a.d.e);
701 baz(a.dc.e);
702 // ^^^^^^ a_d_e
703 baz(ac.d.e);
704 // ^^^^^^ a_d_e
705 baz(a.d.ec);
706 // ^^^^^^ a_d_e
707}"#,
708 );
709 }
710
711 #[track_caller]
712 fn check_missing_params(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
713 check_with_config(
714 InlayHintsConfig {
715 parameter_hints: true,
716 parameter_hints_for_missing_arguments: true,
717 ..DISABLED_CONFIG
718 },
719 ra_fixture,
720 );
721 }
722
723 #[test]
724 fn missing_param_hint_empty_call() {
725 check_missing_params(
727 r#"
728fn foo(a: i32, b: i32) -> i32 { a + b }
729fn main() {
730 foo();
731 //^ a
732}"#,
733 );
734 }
735
736 #[test]
737 fn missing_param_hint_after_first_arg() {
738 check_missing_params(
740 r#"
741fn foo(a: i32, b: i32) -> i32 { a + b }
742fn main() {
743 foo(1,);
744 //^ a
745 //^ b
746}"#,
747 );
748 }
749
750 #[test]
751 fn missing_param_hint_partial_args() {
752 check_missing_params(
754 r#"
755fn foo(a: i32, b: i32, c: i32) -> i32 { a + b + c }
756fn main() {
757 foo(1, 2,);
758 //^ a
759 //^ b
760 //^ c
761}"#,
762 );
763 }
764
765 #[test]
766 fn missing_param_hint_method_call() {
767 check_missing_params(
769 r#"
770struct S;
771impl S {
772 fn foo(&self, a: i32, b: i32) -> i32 { a + b }
773}
774fn main() {
775 S.foo(1,);
776 //^ a
777 //^ b
778}"#,
779 );
780 }
781
782 #[test]
783 fn missing_param_hint_no_hint_when_complete() {
784 check_missing_params(
786 r#"
787fn foo(a: i32, b: i32) -> i32 { a + b }
788fn main() {
789 foo(1, 2);
790 //^ a
791 //^ b
792}"#,
793 );
794 }
795
796 #[test]
797 fn missing_param_hint_respects_heuristics() {
798 check_missing_params(
800 r#"
801fn foo(foo: i32) -> i32 { foo }
802fn main() {
803 foo();
804}"#,
805 );
806 }
807}