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