Skip to main content

ide/inlay_hints/
param_name.rs

1//! Implementation of "param name" inlay hints:
2//! ```no_run
3//! fn max(x: i32, y: i32) -> i32 { x + y }
4//! _ = max(/*x*/4, /*y*/4);
5//! ```
6
7use 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            // Only annotate hints for expressions that exist in the original file
44            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    // Show hint for the next expected (missing) argument if enabled
95    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            // Apply heuristics to hide obvious parameter hints
105            if should_hide_missing_param_hint(unary_function, function_name, param_name.as_str()) {
106                return Some(());
107            }
108
109            // Determine the position for the hint
110            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
149/// 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_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    // These are to be tested in the `parameter_hint_heuristics` test
193    // hide when:
194    // - the parameter name is a suffix of the function's name
195    // - the argument is a qualified constructing or call expression where the qualifier is an ADT
196    // - exact argument<->parameter match(ignoring leading and trailing underscore) or
197    //   parameter is a prefix/suffix of argument with _ splitting it off
198    // - param starts with `ra_fixture`
199    // - param is a well known name in a unary function
200    // - param is generated name
201
202    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
225/// 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't
227/// 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    }
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
256/// 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_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
288/// Check whether param_name and argument are the same or
289/// 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));
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        // paths
340        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 locals
345            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        // recurse
352        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}
364
365fn 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 std
368    param_name.len() == 1 || INSIGNIFICANT_PARAMETER_NAMES.contains(&param_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        // When calling foo() with no args, show hint for first param on the closing paren
749        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        // foo(1,) - show hint for 'a' on '1', and 'b' on the trailing comma
762        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        // foo(1, 2,) - show hints for a, b on args, and c on trailing comma
776        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        // S.foo(1,) - show hint for 'a' on '1', and 'b' on trailing comma
791        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        // When all args provided, no missing hint - just regular param hints
808        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        // 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}