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_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('_');
202 if param_name.is_empty() {
203 return true;
204 }
205
206 if param_name.starts_with("ra_fixture") {
207 return true;
208 }
209
210 if unary_function {
211 if let Some(function_name) = function_name
212 && is_param_name_suffix_of_fn_name(param_name, function_name)
213 {
214 return true;
215 }
216 if is_obvious_param(param_name) {
217 return true;
218 }
219 }
220
221 is_argument_expr_similar_to_param_name(sema, argument, param_name)
222}
223
224fn should_hide_missing_param_hint(
228 unary_function: bool,
229 function_name: Option<&str>,
230 param_name: &str,
231) -> bool {
232 let param_name = param_name.trim_matches('_');
233 if param_name.is_empty() {
234 return true;
235 }
236
237 if param_name.starts_with("ra_fixture") {
238 return true;
239 }
240
241 if unary_function {
242 if let Some(function_name) = function_name
243 && is_param_name_suffix_of_fn_name(param_name, function_name)
244 {
245 return true;
246 }
247 if is_obvious_param(param_name) {
248 return true;
249 }
250 }
251
252 false
253}
254
255fn is_param_name_suffix_of_fn_name(param_name: &str, fn_name: &str) -> bool {
260 fn_name == param_name
261 || fn_name
262 .len()
263 .checked_sub(param_name.len())
264 .and_then(|at| fn_name.is_char_boundary(at).then(|| fn_name.split_at(at)))
265 .is_some_and(|(prefix, suffix)| {
266 suffix.eq_ignore_ascii_case(param_name) && prefix.ends_with('_')
267 })
268}
269
270fn is_argument_expr_similar_to_param_name(
271 sema: &Semantics<'_, RootDatabase>,
272 argument: &ast::Expr,
273 param_name: &str,
274) -> bool {
275 match get_segment_representation(argument) {
276 Some(Either::Left(argument)) => is_argument_similar_to_param_name(&argument, param_name),
277 Some(Either::Right(path)) => {
278 path.segment()
279 .and_then(|it| it.name_ref())
280 .is_some_and(|name_ref| name_ref.text().eq_ignore_ascii_case(param_name))
281 || is_adt_constructor_similar_to_param_name(sema, &path, param_name)
282 }
283 None => false,
284 }
285}
286
287pub(super) fn is_argument_similar_to_param_name(
290 argument: &[ast::NameRef],
291 param_name: &str,
292) -> bool {
293 debug_assert!(!argument.is_empty());
294 debug_assert!(!param_name.is_empty());
295 let param_name = param_name.split('_');
296 let argument = argument.iter().flat_map(|it| it.text_non_mutable().split('_'));
297
298 let prefix_match = zip(argument.clone(), param_name.clone())
299 .all(|(arg, param)| arg.eq_ignore_ascii_case(param));
300 let postfix_match = || {
301 zip(argument.rev(), param_name.rev()).all(|(arg, param)| arg.eq_ignore_ascii_case(param))
302 };
303 prefix_match || postfix_match()
304}
305
306pub(super) fn get_segment_representation(
307 expr: &ast::Expr,
308) -> Option<Either<Vec<ast::NameRef>, ast::Path>> {
309 match expr {
310 ast::Expr::MethodCallExpr(method_call_expr) => {
311 let receiver =
312 method_call_expr.receiver().and_then(|expr| get_segment_representation(&expr));
313 let name_ref = method_call_expr.name_ref()?;
314 if INSIGNIFICANT_METHOD_NAMES.contains(&name_ref.text().as_str()) {
315 return receiver;
316 }
317 Some(Either::Left(match receiver {
318 Some(Either::Left(mut left)) => {
319 left.push(name_ref);
320 left
321 }
322 Some(Either::Right(_)) | None => vec![name_ref],
323 }))
324 }
325 ast::Expr::FieldExpr(field_expr) => {
326 let expr = field_expr.expr().and_then(|expr| get_segment_representation(&expr));
327 let name_ref = field_expr.name_ref()?;
328 let res = match expr {
329 Some(Either::Left(mut left)) => {
330 left.push(name_ref);
331 left
332 }
333 Some(Either::Right(_)) | None => vec![name_ref],
334 };
335 Some(Either::Left(res))
336 }
337 ast::Expr::MacroExpr(macro_expr) => macro_expr.macro_call()?.path().map(Either::Right),
339 ast::Expr::RecordExpr(record_expr) => record_expr.path().map(Either::Right),
340 ast::Expr::PathExpr(path_expr) => {
341 let path = path_expr.path()?;
342 Some(match path.as_single_name_ref() {
344 None => Either::Right(path),
345 Some(name_ref) => Either::Left(vec![name_ref]),
346 })
347 }
348 ast::Expr::PrefixExpr(prefix_expr) if prefix_expr.op_kind() == Some(UnaryOp::Not) => None,
349 ast::Expr::PrefixExpr(prefix_expr) => get_segment_representation(&prefix_expr.expr()?),
351 ast::Expr::RefExpr(ref_expr) => get_segment_representation(&ref_expr.expr()?),
352 ast::Expr::CastExpr(cast_expr) => get_segment_representation(&cast_expr.expr()?),
353 ast::Expr::CallExpr(call_expr) => get_segment_representation(&call_expr.expr()?),
354 ast::Expr::AwaitExpr(await_expr) => get_segment_representation(&await_expr.expr()?),
355 ast::Expr::IndexExpr(index_expr) => get_segment_representation(&index_expr.base()?),
356 ast::Expr::ParenExpr(paren_expr) => get_segment_representation(&paren_expr.expr()?),
357 ast::Expr::TryExpr(try_expr) => get_segment_representation(&try_expr.expr()?),
358 _ => None,
360 }
361}
362
363fn is_obvious_param(param_name: &str) -> bool {
364 param_name.len() == 1 || INSIGNIFICANT_PARAMETER_NAMES.contains(¶m_name)
367}
368
369fn is_adt_constructor_similar_to_param_name(
370 sema: &Semantics<'_, RootDatabase>,
371 path: &ast::Path,
372 param_name: &str,
373) -> bool {
374 (|| match sema.resolve_path(path)? {
375 hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => {
376 Some(to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name)
377 }
378 hir::PathResolution::Def(hir::ModuleDef::Function(_) | hir::ModuleDef::EnumVariant(_)) => {
379 if to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name {
380 return Some(true);
381 }
382 let qual = path.qualifier()?;
383 match sema.resolve_path(&qual)? {
384 hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => {
385 Some(to_lower_snake_case(&qual.segment()?.name_ref()?.text()) == param_name)
386 }
387 _ => None,
388 }
389 }
390 _ => None,
391 })()
392 .unwrap_or(false)
393}
394
395#[cfg(test)]
396mod tests {
397 use crate::{
398 InlayHintsConfig,
399 inlay_hints::tests::{DISABLED_CONFIG, check_with_config},
400 };
401
402 #[track_caller]
403 fn check_params(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
404 check_with_config(
405 InlayHintsConfig { parameter_hints: true, ..DISABLED_CONFIG },
406 ra_fixture,
407 );
408 }
409
410 #[test]
411 fn param_hints_only() {
412 check_params(
413 r#"
414fn foo(a: i32, b: i32) -> i32 { a + b }
415fn main() {
416 let _x = foo(
417 4,
418 //^ a
419 4,
420 //^ b
421 );
422}"#,
423 );
424 }
425
426 #[test]
427 fn param_hints_on_closure() {
428 check_params(
429 r#"
430//- minicore: fn
431fn main() {
432 let clo = |a: u8, b: u8| a + b;
433 clo(
434 1,
435 //^ a
436 2,
437 //^ b
438 );
439}
440 "#,
441 );
442 }
443
444 #[test]
445 fn param_name_similar_to_fn_name_still_hints() {
446 check_params(
447 r#"
448fn max(x: i32, y: i32) -> i32 { x + y }
449fn main() {
450 let _x = max(
451 4,
452 //^ x
453 4,
454 //^ y
455 );
456}"#,
457 );
458 }
459
460 #[test]
461 fn param_name_similar_to_fn_name() {
462 check_params(
463 r#"
464fn param_with_underscore(with_underscore: i32) -> i32 { with_underscore }
465fn main() {
466 let _x = param_with_underscore(
467 4,
468 );
469}"#,
470 );
471 check_params(
472 r#"
473fn param_with_underscore(underscore: i32) -> i32 { underscore }
474fn main() {
475 let _x = param_with_underscore(
476 4,
477 );
478}"#,
479 );
480 }
481
482 #[test]
483 fn param_name_same_as_fn_name() {
484 check_params(
485 r#"
486fn foo(foo: i32) -> i32 { foo }
487fn main() {
488 let _x = foo(
489 4,
490 );
491}"#,
492 );
493 }
494
495 #[test]
496 fn never_hide_param_when_multiple_params() {
497 check_params(
498 r#"
499fn foo(foo: i32, bar: i32) -> i32 { bar + baz }
500fn main() {
501 let _x = foo(
502 4,
503 //^ foo
504 8,
505 //^ bar
506 );
507}"#,
508 );
509 }
510
511 #[test]
512 fn param_hints_look_through_as_ref_and_clone() {
513 check_params(
514 r#"
515fn foo(bar: i32, baz: f32) {}
516
517fn main() {
518 let bar = 3;
519 let baz = &"baz";
520 let fez = 1.0;
521 foo(bar.clone(), bar.clone());
522 //^^^^^^^^^^^ baz
523 foo(bar.as_ref(), bar.as_ref());
524 //^^^^^^^^^^^^ baz
525}
526"#,
527 );
528 }
529
530 #[test]
531 fn self_param_hints() {
532 check_params(
533 r#"
534struct Foo;
535
536impl Foo {
537 fn foo(self: Self) {}
538 fn bar(self: &Self) {}
539}
540
541fn main() {
542 Foo::foo(Foo);
543 //^^^ self
544 Foo::bar(&Foo);
545 //^^^^ self
546}
547"#,
548 )
549 }
550
551 #[test]
552 fn param_name_hints_show_for_literals() {
553 check_params(
554 r#"pub fn test(a: i32, b: i32) -> [i32; 2] { [a, b] }
555fn main() {
556 test(
557 0xa_b,
558 //^^^^^ a
559 0xa_b,
560 //^^^^^ b
561 );
562}"#,
563 )
564 }
565
566 #[test]
567 fn param_name_hints_show_after_empty_arg() {
568 check_params(
569 r#"pub fn test(a: i32, b: i32, c: i32) {}
570fn main() {
571 test(, 2,);
572 //^ b
573 test(, , 3);
574 //^ c
575}"#,
576 )
577 }
578
579 #[test]
580 fn function_call_parameter_hint() {
581 check_params(
582 r#"
583//- minicore: option
584struct FileId {}
585struct SmolStr {}
586
587struct TextRange {}
588struct SyntaxKind {}
589struct NavigationTarget {}
590
591struct Test {}
592
593impl Test {
594 fn method(&self, mut param: i32) -> i32 { param * 2 }
595
596 fn from_syntax(
597 file_id: FileId,
598 name: SmolStr,
599 focus_range: Option<TextRange>,
600 full_range: TextRange,
601 kind: SyntaxKind,
602 docs: Option<String>,
603 ) -> NavigationTarget {
604 NavigationTarget {}
605 }
606}
607
608fn test_func(mut foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 {
609 foo + bar
610}
611
612fn main() {
613 let not_literal = 1;
614 let _: i32 = test_func(1, 2, "hello", 3, not_literal);
615 //^ foo ^ bar ^^^^^^^ msg ^^^^^^^^^^^ last
616 let t: Test = Test {};
617 t.method(123);
618 //^^^ param
619 Test::method(&t, 3456);
620 //^^ self ^^^^ param
621 Test::from_syntax(
622 FileId {},
623 "impl".into(),
624 //^^^^^^^^^^^^^ name
625 None,
626 //^^^^ focus_range
627 TextRange {},
628 //^^^^^^^^^^^^ full_range
629 SyntaxKind {},
630 //^^^^^^^^^^^^^ kind
631 None,
632 //^^^^ docs
633 );
634}"#,
635 );
636 }
637
638 #[test]
639 fn parameter_hint_heuristics() {
640 check_params(
641 r#"
642fn check(ra_fixture_thing: &str) {}
643
644fn map(f: i32) {}
645fn filter(predicate: i32) {}
646
647fn strip_suffix(suffix: &str) {}
648fn stripsuffix(suffix: &str) {}
649fn same(same: u32) {}
650fn same2(_same2: u32) {}
651
652fn enum_matches_param_name(completion_kind: CompletionKind) {}
653
654fn foo(param: u32) {}
655fn bar(param_eter: u32) {}
656fn baz(a_d_e: u32) {}
657fn far(loop_: u32) {}
658fn faz(r#loop: u32) {}
659
660enum CompletionKind {
661 Keyword,
662}
663
664fn non_ident_pat((a, b): (u32, u32)) {}
665
666fn main() {
667 const PARAM: u32 = 0;
668 foo(PARAM);
669 foo(!PARAM);
670 // ^^^^^^ param
671 check("");
672
673 map(0);
674 filter(0);
675
676 strip_suffix("");
677 stripsuffix("");
678 //^^ suffix
679 same(0);
680 same2(0);
681
682 enum_matches_param_name(CompletionKind::Keyword);
683
684 let param = 0;
685 foo(param);
686 foo(param as _);
687 let param_end = 0;
688 foo(param_end);
689 let start_param = 0;
690 foo(start_param);
691 let param2 = 0;
692 foo(param2);
693 //^^^^^^ param
694
695 macro_rules! param {
696 () => {};
697 };
698 foo(param!());
699
700 let param_eter = 0;
701 bar(param_eter);
702 let param_eter_end = 0;
703 bar(param_eter_end);
704 let start_param_eter = 0;
705 bar(start_param_eter);
706 let param_eter2 = 0;
707 bar(param_eter2);
708 //^^^^^^^^^^^ param_eter
709 let loop_level = 0;
710 far(loop_level);
711 faz(loop_level);
712
713 non_ident_pat((0, 0));
714
715 baz(a.d.e);
716 baz(a.dc.e);
717 // ^^^^^^ a_d_e
718 baz(ac.d.e);
719 // ^^^^^^ a_d_e
720 baz(a.d.ec);
721 // ^^^^^^ a_d_e
722}"#,
723 );
724 }
725
726 #[track_caller]
727 fn check_missing_params(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
728 check_with_config(
729 InlayHintsConfig {
730 parameter_hints: true,
731 parameter_hints_for_missing_arguments: true,
732 ..DISABLED_CONFIG
733 },
734 ra_fixture,
735 );
736 }
737
738 #[test]
739 fn missing_param_hint_empty_call() {
740 check_missing_params(
742 r#"
743fn foo(a: i32, b: i32) -> i32 { a + b }
744fn main() {
745 foo();
746 //^ a
747}"#,
748 );
749 }
750
751 #[test]
752 fn missing_param_hint_after_first_arg() {
753 check_missing_params(
755 r#"
756fn foo(a: i32, b: i32) -> i32 { a + b }
757fn main() {
758 foo(1,);
759 //^ a
760 //^ b
761}"#,
762 );
763 }
764
765 #[test]
766 fn missing_param_hint_partial_args() {
767 check_missing_params(
769 r#"
770fn foo(a: i32, b: i32, c: i32) -> i32 { a + b + c }
771fn main() {
772 foo(1, 2,);
773 //^ a
774 //^ b
775 //^ c
776}"#,
777 );
778 }
779
780 #[test]
781 fn missing_param_hint_method_call() {
782 check_missing_params(
784 r#"
785struct S;
786impl S {
787 fn foo(&self, a: i32, b: i32) -> i32 { a + b }
788}
789fn main() {
790 S.foo(1,);
791 //^ a
792 //^ b
793}"#,
794 );
795 }
796
797 #[test]
798 fn missing_param_hint_no_hint_when_complete() {
799 check_missing_params(
801 r#"
802fn foo(a: i32, b: i32) -> i32 { a + b }
803fn main() {
804 foo(1, 2);
805 //^ a
806 //^ b
807}"#,
808 );
809 }
810
811 #[test]
812 fn missing_param_hint_respects_heuristics() {
813 check_missing_params(
815 r#"
816fn foo(foo: i32) -> i32 { foo }
817fn main() {
818 foo();
819}"#,
820 );
821 }
822}