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().split('_'));298 let argument = argument.map(|it| it.strip_prefix("r#").unwrap_or(it));299300 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}307308pub(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 left323 }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 left334 }335 Some(Either::Right(_)) | None => vec![name_ref],336 };337 Some(Either::Left(res))338 }339 // paths340 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 // single segment paths are likely locals345 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 // recurse352 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 // ast::Expr::ClosureExpr(closure_expr) => todo!(),361 _ => None,362 }363}364365fn is_obvious_param(param_name: &str) -> bool {366 // avoid displaying hints for common functions like map, filter, etc.367 // or other obvious words used in std368 param_name.len() == 1 || INSIGNIFICANT_PARAMETER_NAMES.contains(¶m_name)369}370371fn 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}396397#[cfg(test)]398mod tests {399 use crate::{400 InlayHintsConfig,401 inlay_hints::tests::{DISABLED_CONFIG, check_with_config},402 };403404 #[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 }411412 #[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 //^ a421 4,422 //^ b423 );424}"#,425 );426 }427428 #[test]429 fn param_hints_on_closure() {430 check_params(431 r#"432//- minicore: fn433fn main() {434 let clo = |a: u8, b: u8| a + b;435 clo(436 1,437 //^ a438 2,439 //^ b440 );441}442 "#,443 );444 }445446 #[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 //^ x455 4,456 //^ y457 );458}"#,459 );460 }461462 #[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 }483484 #[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 }496497 #[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 //^ foo506 8,507 //^ bar508 );509}"#,510 );511 }512513 #[test]514 fn param_hints_look_through_as_ref_and_clone() {515 check_params(516 r#"517fn foo(bar: i32, baz: f32) {}518519fn main() {520 let bar = 3;521 let baz = &"baz";522 let fez = 1.0;523 foo(bar.clone(), bar.clone());524 //^^^^^^^^^^^ baz525 foo(bar.as_ref(), bar.as_ref());526 //^^^^^^^^^^^^ baz527}528"#,529 );530 }531532 #[test]533 fn self_param_hints() {534 check_params(535 r#"536struct Foo;537538impl Foo {539 fn foo(self: Self) {}540 fn bar(self: &Self) {}541}542543fn main() {544 Foo::foo(Foo);545 //^^^ self546 Foo::bar(&Foo);547 //^^^^ self548}549"#,550 )551 }552553 #[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 //^^^^^ a561 0xa_b,562 //^^^^^ b563 );564}"#,565 )566 }567568 #[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 //^ b575 test(, , 3);576 //^ c577}"#,578 )579 }580581 #[test]582 fn function_call_parameter_hint() {583 check_params(584 r#"585//- minicore: option586struct FileId {}587struct SmolStr {}588589struct TextRange {}590struct SyntaxKind {}591struct NavigationTarget {}592593struct Test {}594595impl Test {596 fn method(&self, mut param: i32) -> i32 { param * 2 }597598 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}609610fn test_func(mut foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 {611 foo + bar612}613async fn test_async(foo: i32, _: i32) {}614615fn main() {616 let not_literal = 1;617 let _: i32 = test_func(1, 2, "hello", 3, not_literal);618 //^ foo ^ bar ^^^^^^^ msg ^^^^^^^^^^^ last619 let t: Test = Test {};620 t.method(123);621 //^^^ param622 Test::method(&t, 3456);623 //^^ self ^^^^ param624 Test::from_syntax(625 FileId {},626 "impl".into(),627 //^^^^^^^^^^^^^ name628 None,629 //^^^^ focus_range630 TextRange {},631 //^^^^^^^^^^^^ full_range632 SyntaxKind {},633 //^^^^^^^^^^^^^ kind634 None,635 //^^^^ docs636 );637 test_async(1, 2)638 //^ foo639}"#,640 );641 }642643 #[test]644 fn parameter_hint_heuristics() {645 check_params(646 r#"647fn check(ra_fixture_thing: &str) {}648649fn map(f: i32) {}650fn filter(predicate: i32) {}651652fn strip_suffix(suffix: &str) {}653fn stripsuffix(suffix: &str) {}654fn same(same: u32) {}655fn same2(_same2: u32) {}656657fn enum_matches_param_name(completion_kind: CompletionKind) {}658659fn foo(param: u32) {}660fn bar(param_eter: u32) {}661fn baz(a_d_e: u32) {}662fn far(loop_: u32) {}663fn faz(r#loop: u32) {}664665enum CompletionKind {666 Keyword,667}668669fn non_ident_pat((a, b): (u32, u32)) {}670671fn main() {672 const PARAM: u32 = 0;673 foo(PARAM);674 foo(!PARAM);675 // ^^^^^^ param676 check("");677678 map(0);679 filter(0);680681 strip_suffix("");682 stripsuffix("");683 //^^ suffix684 same(0);685 same2(0);686687 enum_matches_param_name(CompletionKind::Keyword);688689 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 //^^^^^^ param699700 macro_rules! param {701 () => {};702 };703 foo(param!());704705 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_eter714 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);720721 non_ident_pat((0, 0));722723 baz(a.d.e);724 baz(a.dc.e);725 // ^^^^^^ a_d_e726 baz(ac.d.e);727 // ^^^^^^ a_d_e728 baz(a.d.ec);729 // ^^^^^^ a_d_e730}"#,731 );732 }733734 #[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_CONFIG741 },742 ra_fixture,743 );744 }745746 #[test]747 fn missing_param_hint_empty_call() {748 // When calling foo() with no args, show hint for first param on the closing paren749 check_missing_params(750 r#"751fn foo(a: i32, b: i32) -> i32 { a + b }752fn main() {753 foo();754 //^ a755}"#,756 );757 }758759 #[test]760 fn missing_param_hint_after_first_arg() {761 // foo(1,) - show hint for 'a' on '1', and 'b' on the trailing comma762 check_missing_params(763 r#"764fn foo(a: i32, b: i32) -> i32 { a + b }765fn main() {766 foo(1,);767 //^ a768 //^ b769}"#,770 );771 }772773 #[test]774 fn missing_param_hint_partial_args() {775 // foo(1, 2,) - show hints for a, b on args, and c on trailing comma776 check_missing_params(777 r#"778fn foo(a: i32, b: i32, c: i32) -> i32 { a + b + c }779fn main() {780 foo(1, 2,);781 //^ a782 //^ b783 //^ c784}"#,785 );786 }787788 #[test]789 fn missing_param_hint_method_call() {790 // S.foo(1,) - show hint for 'a' on '1', and 'b' on trailing comma791 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 //^ a800 //^ b801}"#,802 );803 }804805 #[test]806 fn missing_param_hint_no_hint_when_complete() {807 // When all args provided, no missing hint - just regular param hints808 check_missing_params(809 r#"810fn foo(a: i32, b: i32) -> i32 { a + b }811fn main() {812 foo(1, 2);813 //^ a814 //^ b815}"#,816 );817 }818819 #[test]820 fn missing_param_hint_respects_heuristics() {821 // The hint should be hidden if it matches heuristics (e.g., single param unary fn with same name)822 check_missing_params(823 r#"824fn foo(foo: i32) -> i32 { foo }825fn main() {826 foo();827}"#,828 );829 }830}