1//! Implementation of "param name" inlay hints:2//! ```no_run3//! fn max(x: i32, y: i32) -> i32 { x + y }4//! _ = max(/*x*/4, /*y*/4);5//! ```67use std::iter::zip;89use either::Either;10use hir::{EditionedFileId, Semantics, name};11use ide_db::{RootDatabase, famous_defs::FamousDefs};1213use stdx::to_lower_snake_case;14use syntax::T;15use syntax::ast::{self, AstNode, HasArgList, HasName, UnaryOp};1617use crate::{InlayHint, InlayHintLabel, InlayHintPosition, InlayHintsConfig, InlayKind};1819pub(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 }2930 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 = callable38 .params()39 .into_iter()40 .zip(arg_list.args_maybe_empty())41 .filter_map(|(p, arg)| {42 let arg = arg?;43 // Only annotate hints for expressions that exist in the original file44 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 });9192 acc.extend(hints);9394 // Show hint for the next expected (missing) argument if enabled95 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();99100 if provided_args_count < total_params101 && let Some(next_param) = params.get(provided_args_count)102 && let Some(param_name) = next_param.name(sema.db)103 {104 // Apply heuristics to hide obvious parameter hints105 if should_hide_missing_param_hint(unary_function, function_name, param_name.as_str()) {106 return Some(());107 }108109 // Determine the position for the hint110 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 }145146 Some(())147}148149/// Determines the position where the hint for a missing argument should be placed.150/// Returns the range of the token where the hint should appear.151fn missing_arg_hint_position(arg_list: &ast::ArgList) -> Option<syntax::TextRange> {152 // Always place the hint on the closing paren, so it appears before `)`.153 // This way `foo()` becomes `foo(a)` visually with the hint.154 arg_list155 .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}161162fn 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}180181const INSIGNIFICANT_METHOD_NAMES: &[&str] = &["clone", "as_ref", "into"];182const INSIGNIFICANT_PARAMETER_NAMES: &[&str] =183 &["predicate", "value", "pat", "rhs", "other", "msg", "op"];184185fn 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 // These are to be tested in the `parameter_hint_heuristics` test193 // hide when:194 // - the parameter name is a suffix of the function's name195 // - the argument is a qualified constructing or call expression where the qualifier is an ADT196 // - exact argument<->parameter match(ignoring leading and trailing underscore) or197 // parameter is a prefix/suffix of argument with _ splitting it off198 // - param starts with `ra_fixture`199 // - param is a well known name in a unary function200 // - param is generated name201202 let param_name = param_name.trim_matches('_');203 if param_name.is_empty() {204 return true;205 }206207 if param_name.starts_with("ra_fixture") || name::is_generated(param_name) {208 return true;209 }210211 if unary_function {212 if let Some(function_name) = function_name213 && 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 }221222 is_argument_expr_similar_to_param_name(sema, argument, param_name)223}224225/// Determines whether to hide the parameter hint for a missing argument.226/// This is a simplified version of `should_hide_param_name_hint` that doesn't227/// require an actual argument expression.228fn 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 }237238 if param_name.starts_with("ra_fixture") {239 return true;240 }241242 if unary_function {243 if let Some(function_name) = function_name244 && 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 }252253 false254}255256/// Hide the parameter name of a unary function if it is a `_` - prefixed suffix of the function's name, or equal.257///258/// `fn strip_suffix(suffix)` will be hidden.259/// `fn stripsuffix(suffix)` will not be hidden.260fn is_param_name_suffix_of_fn_name(param_name: &str, fn_name: &str) -> bool {261 fn_name == param_name262 || fn_name263 .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}270271fn 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}287288/// Check whether param_name and argument are the same or289/// whether param_name is a prefix/suffix of argument(split at `_`).290pub(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_non_mutable().split('_'));298299 let prefix_match = zip(argument.clone(), param_name.clone())300 .all(|(arg, param)| arg.eq_ignore_ascii_case(param));301 let postfix_match = || {302 zip(argument.rev(), param_name.rev()).all(|(arg, param)| arg.eq_ignore_ascii_case(param))303 };304 prefix_match || postfix_match()305}306307pub(super) fn get_segment_representation(308 expr: &ast::Expr,309) -> Option<Either<Vec<ast::NameRef>, ast::Path>> {310 match expr {311 ast::Expr::MethodCallExpr(method_call_expr) => {312 let receiver =313 method_call_expr.receiver().and_then(|expr| get_segment_representation(&expr));314 let name_ref = method_call_expr.name_ref()?;315 if INSIGNIFICANT_METHOD_NAMES.contains(&name_ref.text().as_str()) {316 return receiver;317 }318 Some(Either::Left(match receiver {319 Some(Either::Left(mut left)) => {320 left.push(name_ref);321 left322 }323 Some(Either::Right(_)) | None => vec![name_ref],324 }))325 }326 ast::Expr::FieldExpr(field_expr) => {327 let expr = field_expr.expr().and_then(|expr| get_segment_representation(&expr));328 let name_ref = field_expr.name_ref()?;329 let res = match expr {330 Some(Either::Left(mut left)) => {331 left.push(name_ref);332 left333 }334 Some(Either::Right(_)) | None => vec![name_ref],335 };336 Some(Either::Left(res))337 }338 // paths339 ast::Expr::MacroExpr(macro_expr) => macro_expr.macro_call()?.path().map(Either::Right),340 ast::Expr::RecordExpr(record_expr) => record_expr.path().map(Either::Right),341 ast::Expr::PathExpr(path_expr) => {342 let path = path_expr.path()?;343 // single segment paths are likely locals344 Some(match path.as_single_name_ref() {345 None => Either::Right(path),346 Some(name_ref) => Either::Left(vec![name_ref]),347 })348 }349 ast::Expr::PrefixExpr(prefix_expr) if prefix_expr.op_kind() == Some(UnaryOp::Not) => None,350 // recurse351 ast::Expr::PrefixExpr(prefix_expr) => get_segment_representation(&prefix_expr.expr()?),352 ast::Expr::RefExpr(ref_expr) => get_segment_representation(&ref_expr.expr()?),353 ast::Expr::CastExpr(cast_expr) => get_segment_representation(&cast_expr.expr()?),354 ast::Expr::CallExpr(call_expr) => get_segment_representation(&call_expr.expr()?),355 ast::Expr::AwaitExpr(await_expr) => get_segment_representation(&await_expr.expr()?),356 ast::Expr::IndexExpr(index_expr) => get_segment_representation(&index_expr.base()?),357 ast::Expr::ParenExpr(paren_expr) => get_segment_representation(&paren_expr.expr()?),358 ast::Expr::TryExpr(try_expr) => get_segment_representation(&try_expr.expr()?),359 // ast::Expr::ClosureExpr(closure_expr) => todo!(),360 _ => None,361 }362}363364fn is_obvious_param(param_name: &str) -> bool {365 // avoid displaying hints for common functions like map, filter, etc.366 // or other obvious words used in std367 param_name.len() == 1 || INSIGNIFICANT_PARAMETER_NAMES.contains(¶m_name)368}369370fn is_adt_constructor_similar_to_param_name(371 sema: &Semantics<'_, RootDatabase>,372 path: &ast::Path,373 param_name: &str,374) -> bool {375 (|| match sema.resolve_path(path)? {376 hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => {377 Some(to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name)378 }379 hir::PathResolution::Def(hir::ModuleDef::Function(_) | hir::ModuleDef::EnumVariant(_)) => {380 if to_lower_snake_case(&path.segment()?.name_ref()?.text()) == param_name {381 return Some(true);382 }383 let qual = path.qualifier()?;384 match sema.resolve_path(&qual)? {385 hir::PathResolution::Def(hir::ModuleDef::Adt(_)) => {386 Some(to_lower_snake_case(&qual.segment()?.name_ref()?.text()) == param_name)387 }388 _ => None,389 }390 }391 _ => None,392 })()393 .unwrap_or(false)394}395396#[cfg(test)]397mod tests {398 use crate::{399 InlayHintsConfig,400 inlay_hints::tests::{DISABLED_CONFIG, check_with_config},401 };402403 #[track_caller]404 fn check_params(#[rust_analyzer::rust_fixture] ra_fixture: &str) {405 check_with_config(406 InlayHintsConfig { parameter_hints: true, ..DISABLED_CONFIG },407 ra_fixture,408 );409 }410411 #[test]412 fn param_hints_only() {413 check_params(414 r#"415fn foo(a: i32, b: i32) -> i32 { a + b }416fn main() {417 let _x = foo(418 4,419 //^ a420 4,421 //^ b422 );423}"#,424 );425 }426427 #[test]428 fn param_hints_on_closure() {429 check_params(430 r#"431//- minicore: fn432fn main() {433 let clo = |a: u8, b: u8| a + b;434 clo(435 1,436 //^ a437 2,438 //^ b439 );440}441 "#,442 );443 }444445 #[test]446 fn param_name_similar_to_fn_name_still_hints() {447 check_params(448 r#"449fn max(x: i32, y: i32) -> i32 { x + y }450fn main() {451 let _x = max(452 4,453 //^ x454 4,455 //^ y456 );457}"#,458 );459 }460461 #[test]462 fn param_name_similar_to_fn_name() {463 check_params(464 r#"465fn param_with_underscore(with_underscore: i32) -> i32 { with_underscore }466fn main() {467 let _x = param_with_underscore(468 4,469 );470}"#,471 );472 check_params(473 r#"474fn param_with_underscore(underscore: i32) -> i32 { underscore }475fn main() {476 let _x = param_with_underscore(477 4,478 );479}"#,480 );481 }482483 #[test]484 fn param_name_same_as_fn_name() {485 check_params(486 r#"487fn foo(foo: i32) -> i32 { foo }488fn main() {489 let _x = foo(490 4,491 );492}"#,493 );494 }495496 #[test]497 fn never_hide_param_when_multiple_params() {498 check_params(499 r#"500fn foo(foo: i32, bar: i32) -> i32 { bar + baz }501fn main() {502 let _x = foo(503 4,504 //^ foo505 8,506 //^ bar507 );508}"#,509 );510 }511512 #[test]513 fn param_hints_look_through_as_ref_and_clone() {514 check_params(515 r#"516fn foo(bar: i32, baz: f32) {}517518fn main() {519 let bar = 3;520 let baz = &"baz";521 let fez = 1.0;522 foo(bar.clone(), bar.clone());523 //^^^^^^^^^^^ baz524 foo(bar.as_ref(), bar.as_ref());525 //^^^^^^^^^^^^ baz526}527"#,528 );529 }530531 #[test]532 fn self_param_hints() {533 check_params(534 r#"535struct Foo;536537impl Foo {538 fn foo(self: Self) {}539 fn bar(self: &Self) {}540}541542fn main() {543 Foo::foo(Foo);544 //^^^ self545 Foo::bar(&Foo);546 //^^^^ self547}548"#,549 )550 }551552 #[test]553 fn param_name_hints_show_for_literals() {554 check_params(555 r#"pub fn test(a: i32, b: i32) -> [i32; 2] { [a, b] }556fn main() {557 test(558 0xa_b,559 //^^^^^ a560 0xa_b,561 //^^^^^ b562 );563}"#,564 )565 }566567 #[test]568 fn param_name_hints_show_after_empty_arg() {569 check_params(570 r#"pub fn test(a: i32, b: i32, c: i32) {}571fn main() {572 test(, 2,);573 //^ b574 test(, , 3);575 //^ c576}"#,577 )578 }579580 #[test]581 fn function_call_parameter_hint() {582 check_params(583 r#"584//- minicore: option585struct FileId {}586struct SmolStr {}587588struct TextRange {}589struct SyntaxKind {}590struct NavigationTarget {}591592struct Test {}593594impl Test {595 fn method(&self, mut param: i32) -> i32 { param * 2 }596597 fn from_syntax(598 file_id: FileId,599 name: SmolStr,600 focus_range: Option<TextRange>,601 full_range: TextRange,602 kind: SyntaxKind,603 docs: Option<String>,604 ) -> NavigationTarget {605 NavigationTarget {}606 }607}608609fn test_func(mut foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 {610 foo + bar611}612async fn test_async(foo: i32, _: i32) {}613614fn main() {615 let not_literal = 1;616 let _: i32 = test_func(1, 2, "hello", 3, not_literal);617 //^ foo ^ bar ^^^^^^^ msg ^^^^^^^^^^^ last618 let t: Test = Test {};619 t.method(123);620 //^^^ param621 Test::method(&t, 3456);622 //^^ self ^^^^ param623 Test::from_syntax(624 FileId {},625 "impl".into(),626 //^^^^^^^^^^^^^ name627 None,628 //^^^^ focus_range629 TextRange {},630 //^^^^^^^^^^^^ full_range631 SyntaxKind {},632 //^^^^^^^^^^^^^ kind633 None,634 //^^^^ docs635 );636 test_async(1, 2)637 //^ foo638}"#,639 );640 }641642 #[test]643 fn parameter_hint_heuristics() {644 check_params(645 r#"646fn check(ra_fixture_thing: &str) {}647648fn map(f: i32) {}649fn filter(predicate: i32) {}650651fn strip_suffix(suffix: &str) {}652fn stripsuffix(suffix: &str) {}653fn same(same: u32) {}654fn same2(_same2: u32) {}655656fn enum_matches_param_name(completion_kind: CompletionKind) {}657658fn foo(param: u32) {}659fn bar(param_eter: u32) {}660fn baz(a_d_e: u32) {}661fn far(loop_: u32) {}662fn faz(r#loop: u32) {}663664enum CompletionKind {665 Keyword,666}667668fn non_ident_pat((a, b): (u32, u32)) {}669670fn main() {671 const PARAM: u32 = 0;672 foo(PARAM);673 foo(!PARAM);674 // ^^^^^^ param675 check("");676677 map(0);678 filter(0);679680 strip_suffix("");681 stripsuffix("");682 //^^ suffix683 same(0);684 same2(0);685686 enum_matches_param_name(CompletionKind::Keyword);687688 let param = 0;689 foo(param);690 foo(param as _);691 let param_end = 0;692 foo(param_end);693 let start_param = 0;694 foo(start_param);695 let param2 = 0;696 foo(param2);697 //^^^^^^ param698699 macro_rules! param {700 () => {};701 };702 foo(param!());703704 let param_eter = 0;705 bar(param_eter);706 let param_eter_end = 0;707 bar(param_eter_end);708 let start_param_eter = 0;709 bar(start_param_eter);710 let param_eter2 = 0;711 bar(param_eter2);712 //^^^^^^^^^^^ param_eter713 let loop_level = 0;714 far(loop_level);715 faz(loop_level);716717 non_ident_pat((0, 0));718719 baz(a.d.e);720 baz(a.dc.e);721 // ^^^^^^ a_d_e722 baz(ac.d.e);723 // ^^^^^^ a_d_e724 baz(a.d.ec);725 // ^^^^^^ a_d_e726}"#,727 );728 }729730 #[track_caller]731 fn check_missing_params(#[rust_analyzer::rust_fixture] ra_fixture: &str) {732 check_with_config(733 InlayHintsConfig {734 parameter_hints: true,735 parameter_hints_for_missing_arguments: true,736 ..DISABLED_CONFIG737 },738 ra_fixture,739 );740 }741742 #[test]743 fn missing_param_hint_empty_call() {744 // When calling foo() with no args, show hint for first param on the closing paren745 check_missing_params(746 r#"747fn foo(a: i32, b: i32) -> i32 { a + b }748fn main() {749 foo();750 //^ a751}"#,752 );753 }754755 #[test]756 fn missing_param_hint_after_first_arg() {757 // foo(1,) - show hint for 'a' on '1', and 'b' on the trailing comma758 check_missing_params(759 r#"760fn foo(a: i32, b: i32) -> i32 { a + b }761fn main() {762 foo(1,);763 //^ a764 //^ b765}"#,766 );767 }768769 #[test]770 fn missing_param_hint_partial_args() {771 // foo(1, 2,) - show hints for a, b on args, and c on trailing comma772 check_missing_params(773 r#"774fn foo(a: i32, b: i32, c: i32) -> i32 { a + b + c }775fn main() {776 foo(1, 2,);777 //^ a778 //^ b779 //^ c780}"#,781 );782 }783784 #[test]785 fn missing_param_hint_method_call() {786 // S.foo(1,) - show hint for 'a' on '1', and 'b' on trailing comma787 check_missing_params(788 r#"789struct S;790impl S {791 fn foo(&self, a: i32, b: i32) -> i32 { a + b }792}793fn main() {794 S.foo(1,);795 //^ a796 //^ b797}"#,798 );799 }800801 #[test]802 fn missing_param_hint_no_hint_when_complete() {803 // When all args provided, no missing hint - just regular param hints804 check_missing_params(805 r#"806fn foo(a: i32, b: i32) -> i32 { a + b }807fn main() {808 foo(1, 2);809 //^ a810 //^ b811}"#,812 );813 }814815 #[test]816 fn missing_param_hint_respects_heuristics() {817 // The hint should be hidden if it matches heuristics (e.g., single param unary fn with same name)818 check_missing_params(819 r#"820fn foo(foo: i32) -> i32 { foo }821fn main() {822 foo();823}"#,824 );825 }826}