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