Skip to main content

hir_expand/
fixup.rs

1//! To make attribute macros work reliably when typing, we need to take care to
2//! fix up syntax errors in the code we're passing to them.
3
4use intern::sym;
5use rustc_hash::{FxHashMap, FxHashSet};
6use span::{
7    ErasedFileAstId, FIXUP_ERASED_FILE_AST_ID_MARKER, ROOT_ERASED_FILE_AST_ID, Span, SpanAnchor,
8    SyntaxContext,
9};
10use stdx::never;
11use syntax::{
12    SyntaxElement, SyntaxKind, SyntaxNode, TextRange, TextSize,
13    ast::{self, AstNode, HasLoopBody},
14    match_ast,
15};
16use syntax_bridge::DocCommentDesugarMode;
17use thin_vec::ThinVec;
18use tt::{Spacing, TransformTtAction, transform_tt};
19
20use crate::{
21    span_map::SpanMap,
22    tt::{self, Ident, Leaf, Punct, TopSubtree},
23};
24
25/// The result of calculating fixes for a syntax node -- a bunch of changes
26/// (appending to and replacing nodes), the information that is needed to
27/// reverse those changes afterwards, and a token map.
28#[derive(Debug, Default)]
29pub(crate) struct SyntaxFixups {
30    pub(crate) append: FxHashMap<SyntaxElement, Vec<Leaf>>,
31    pub(crate) remove: FxHashSet<SyntaxElement>,
32    pub(crate) undo_info: SyntaxFixupUndoInfo,
33}
34
35/// This is the information needed to reverse the fixups.
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct SyntaxFixupUndoInfo {
38    original: Option<ThinVec<TopSubtree>>,
39}
40
41impl SyntaxFixupUndoInfo {
42    pub(crate) const NONE: Self = SyntaxFixupUndoInfo { original: None };
43}
44
45// We mark spans with `FIXUP_DUMMY_AST_ID` to indicate that they are fake.
46const FIXUP_DUMMY_AST_ID: ErasedFileAstId = FIXUP_ERASED_FILE_AST_ID_MARKER;
47const FIXUP_DUMMY_RANGE: TextRange = TextRange::empty(TextSize::new(0));
48// If the fake span has this range end, that means that the range start is an index into the
49// `original` list in `SyntaxFixupUndoInfo`.
50const FIXUP_DUMMY_RANGE_END: TextSize = TextSize::new(!0);
51
52pub(crate) fn fixup_syntax(
53    span_map: SpanMap<'_>,
54    node: &SyntaxNode,
55    call_site: Span,
56    mode: DocCommentDesugarMode,
57) -> SyntaxFixups {
58    let mut append = FxHashMap::<SyntaxElement, _>::default();
59    let mut remove = FxHashSet::<SyntaxElement>::default();
60    let mut preorder = node.preorder();
61    let mut original = ThinVec::new();
62    let dummy_range = FIXUP_DUMMY_RANGE;
63    let fake_span = |range| {
64        let span = span_map.span_for_range(range);
65        Span {
66            range: dummy_range,
67            anchor: SpanAnchor { ast_id: FIXUP_DUMMY_AST_ID, ..span.anchor },
68            ctx: span.ctx,
69        }
70    };
71    while let Some(event) = preorder.next() {
72        let syntax::WalkEvent::Enter(node) = event else { continue };
73
74        let node_range = node.text_range();
75        if can_handle_error(&node) && has_error_to_handle(&node) {
76            remove.insert(node.clone().into());
77            // the node contains an error node, we have to completely replace it by something valid
78            let original_tree =
79                syntax_bridge::syntax_node_to_token_tree(&node, span_map, call_site, mode);
80            let idx = original.len() as u32;
81            original.push(original_tree);
82            let span = span_map.span_for_range(node_range);
83            let replacement = Leaf::Ident(Ident {
84                sym: sym::__ra_fixup,
85                span: Span {
86                    range: TextRange::new(TextSize::new(idx), FIXUP_DUMMY_RANGE_END),
87                    anchor: SpanAnchor { ast_id: FIXUP_DUMMY_AST_ID, ..span.anchor },
88                    ctx: span.ctx,
89                },
90                is_raw: tt::IdentIsRaw::No,
91            });
92            append.insert(node.clone().into(), vec![replacement]);
93            preorder.skip_subtree();
94            continue;
95        }
96        // In some other situations, we can fix things by just appending some tokens.
97        match_ast! {
98            match node {
99                ast::FieldExpr(it) => {
100                    if it.name_ref().is_none() {
101                        // incomplete field access: some_expr.|
102                        append.insert(node.clone().into(), vec![
103                            Leaf::Ident(Ident {
104                                sym: sym::__ra_fixup,
105                                span: fake_span(node_range),
106                                is_raw: tt::IdentIsRaw::No
107                            }),
108                        ]);
109                    }
110                },
111                ast::ExprStmt(it) => {
112                    let needs_semi = it.semicolon_token().is_none() && it.expr().is_some_and(|e| e.syntax().kind() != SyntaxKind::BLOCK_EXPR);
113                    if needs_semi {
114                        append.insert(node.clone().into(), vec![
115                            Leaf::Punct(Punct {
116                                char: ';',
117                                spacing: Spacing::Alone,
118                                span: fake_span(node_range),
119                            }),
120                        ]);
121                    }
122                },
123                ast::LetStmt(it) => {
124                    if it.semicolon_token().is_none() {
125                        append.insert(node.clone().into(), vec![
126                            Leaf::Punct(Punct {
127                                char: ';',
128                                spacing: Spacing::Alone,
129                                span: fake_span(node_range)
130                            }),
131                        ]);
132                    }
133                },
134                ast::IfExpr(it) => {
135                    if it.condition().is_none() {
136                        // insert placeholder token after the if token
137                        let if_token = match it.if_token() {
138                            Some(t) => t,
139                            None => continue,
140                        };
141                        append.insert(if_token.into(), vec![
142                            Leaf::Ident(Ident {
143                                sym: sym::__ra_fixup,
144                                span: fake_span(node_range),
145                                is_raw: tt::IdentIsRaw::No
146                            }),
147                        ]);
148                    }
149                    if it.then_branch().is_none() {
150                        append.insert(node.clone().into(), vec![
151                            Leaf::Punct(Punct {
152                                char: '{',
153                                spacing: Spacing::Alone,
154                                span: fake_span(node_range)
155                            }),
156                            Leaf::Punct(Punct {
157                                char: '}',
158                                spacing: Spacing::Alone,
159                                span: fake_span(node_range)
160                            }),
161                        ]);
162                    }
163                },
164                ast::WhileExpr(it) => {
165                    if it.condition().is_none() {
166                        // insert placeholder token after the while token
167                        let while_token = match it.while_token() {
168                            Some(t) => t,
169                            None => continue,
170                        };
171                        append.insert(while_token.into(), vec![
172                            Leaf::Ident(Ident {
173                                sym: sym::__ra_fixup,
174                                span: fake_span(node_range),
175                                is_raw: tt::IdentIsRaw::No
176                            }),
177                        ]);
178                    }
179                    if it.loop_body().is_none() {
180                        append.insert(node.clone().into(), vec![
181                            Leaf::Punct(Punct {
182                                char: '{',
183                                spacing: Spacing::Alone,
184                                span: fake_span(node_range)
185                            }),
186                            Leaf::Punct(Punct {
187                                char: '}',
188                                spacing: Spacing::Alone,
189                                span: fake_span(node_range)
190                            }),
191                        ]);
192                    }
193                },
194                ast::LoopExpr(it) => {
195                    if it.loop_body().is_none() {
196                        append.insert(node.clone().into(), vec![
197                            Leaf::Punct(Punct {
198                                char: '{',
199                                spacing: Spacing::Alone,
200                                span: fake_span(node_range)
201                            }),
202                            Leaf::Punct(Punct {
203                                char: '}',
204                                spacing: Spacing::Alone,
205                                span: fake_span(node_range)
206                            }),
207                        ]);
208                    }
209                },
210                ast::MatchExpr(it) => {
211                    if it.expr().is_none() {
212                        let match_token = match it.match_token() {
213                            Some(t) => t,
214                            None => continue
215                        };
216                        append.insert(match_token.into(), vec![
217                            Leaf::Ident(Ident {
218                                sym: sym::__ra_fixup,
219                                span: fake_span(node_range),
220                                is_raw: tt::IdentIsRaw::No
221                            }),
222                        ]);
223                    }
224                    if it.match_arm_list().is_none() {
225                        // No match arms
226                        append.insert(node.clone().into(), vec![
227                            Leaf::Punct(Punct {
228                                char: '{',
229                                spacing: Spacing::Alone,
230                                span: fake_span(node_range)
231                            }),
232                            Leaf::Punct(Punct {
233                                char: '}',
234                                spacing: Spacing::Alone,
235                                span: fake_span(node_range)
236                            }),
237                        ]);
238                    }
239                },
240                ast::ForExpr(it) => {
241                    let for_token = match it.for_token() {
242                        Some(token) => token,
243                        None => continue
244                    };
245
246                    let [pat, in_token, iter] = [
247                         sym::underscore,
248                         sym::in_,
249                         sym::__ra_fixup,
250                    ].map(|sym|
251                        Leaf::Ident(Ident {
252                            sym,
253                            span: fake_span(node_range),
254                            is_raw: tt::IdentIsRaw::No
255                        }),
256                    );
257
258                    if it.pat().is_none() && it.in_token().is_none() && it.iterable().is_none() {
259                        append.insert(for_token.into(), vec![pat, in_token, iter]);
260                    // does something funky -- see test case for_no_pat
261                    } else if it.pat().is_none() {
262                        append.insert(for_token.into(), vec![pat]);
263                    }
264
265                    if it.loop_body().is_none() {
266                        append.insert(node.clone().into(), vec![
267                            Leaf::Punct(Punct {
268                                char: '{',
269                                spacing: Spacing::Alone,
270                                span: fake_span(node_range)
271                            }),
272                            Leaf::Punct(Punct {
273                                char: '}',
274                                spacing: Spacing::Alone,
275                                span: fake_span(node_range)
276                            }),
277                        ]);
278                    }
279                },
280                ast::RecordExprField(it) => {
281                    if let Some(colon) = it.colon_token()
282                        && it.name_ref().is_some() && it.expr().is_none() {
283                            append.insert(colon.into(), vec![
284                                Leaf::Ident(Ident {
285                                    sym: sym::__ra_fixup,
286                                    span: fake_span(node_range),
287                                    is_raw: tt::IdentIsRaw::No
288                                })
289                            ]);
290                        }
291                },
292                ast::Path(it) => {
293                    if let Some(colon) = it.coloncolon_token()
294                        && it.segment().is_none() {
295                            append.insert(colon.into(), vec![
296                                Leaf::Ident(Ident {
297                                    sym: sym::__ra_fixup,
298                                    span: fake_span(node_range),
299                                    is_raw: tt::IdentIsRaw::No
300                                })
301                            ]);
302                        }
303                },
304                ast::ClosureExpr(it) => {
305                    if it.body().is_none() {
306                        append.insert(node.into(), vec![
307                            Leaf::Ident(Ident {
308                                sym: sym::__ra_fixup,
309                                span: fake_span(node_range),
310                                is_raw: tt::IdentIsRaw::No
311                            })
312                        ]);
313                    }
314                },
315                _ => (),
316            }
317        }
318    }
319    original.shrink_to_fit();
320    let needs_fixups = !append.is_empty() || !original.is_empty();
321    SyntaxFixups {
322        append,
323        remove,
324        undo_info: SyntaxFixupUndoInfo { original: needs_fixups.then_some(original) },
325    }
326}
327
328fn has_error(node: &SyntaxNode) -> bool {
329    node.children().any(|c| c.kind() == SyntaxKind::ERROR)
330}
331
332fn can_handle_error(node: &SyntaxNode) -> bool {
333    ast::Expr::can_cast(node.kind())
334}
335
336fn has_error_to_handle(node: &SyntaxNode) -> bool {
337    has_error(node) || node.children().any(|c| !can_handle_error(&c) && has_error_to_handle(&c))
338}
339
340pub(crate) fn reverse_fixups(tt: &mut TopSubtree, undo_info: &SyntaxFixupUndoInfo) {
341    let Some(undo_info) = &undo_info.original else { return };
342    let undo_info = &**undo_info;
343    let top_subtree = tt.top_subtree();
344    let open_span = top_subtree.delimiter.open;
345    let close_span = top_subtree.delimiter.close;
346    if never!(
347        close_span.anchor.ast_id == FIXUP_DUMMY_AST_ID
348            || open_span.anchor.ast_id == FIXUP_DUMMY_AST_ID
349    ) {
350        let span = |file_id| Span {
351            range: TextRange::empty(TextSize::new(0)),
352            anchor: SpanAnchor { file_id, ast_id: ROOT_ERASED_FILE_AST_ID },
353            ctx: SyntaxContext::root(span::Edition::Edition2015),
354        };
355        tt.set_top_subtree_delimiter_span(tt::DelimSpan {
356            open: span(open_span.anchor.file_id),
357            close: span(close_span.anchor.file_id),
358        });
359    }
360    reverse_fixups_(tt, undo_info);
361}
362
363fn reverse_fixups_(tt: &mut TopSubtree, undo_info: &[TopSubtree]) {
364    transform_tt(tt, |tt| match tt {
365        tt::TokenTree::Leaf(leaf) => {
366            let span = leaf.span();
367            let is_real_leaf = span.anchor.ast_id != FIXUP_DUMMY_AST_ID;
368            let is_replaced_node = span.range.end() == FIXUP_DUMMY_RANGE_END;
369            if !is_real_leaf && !is_replaced_node {
370                return TransformTtAction::remove();
371            }
372
373            if !is_real_leaf {
374                // we have a fake node here, we need to replace it again with the original
375                let original = &undo_info[u32::from(leaf.span().range.start()) as usize];
376                TransformTtAction::ReplaceWith(original.view().strip_invisible())
377            } else {
378                // just a normal leaf
379                TransformTtAction::Keep
380            }
381        }
382        tt::TokenTree::Subtree(tt) => {
383            // fixup should only create matching delimiters, but proc macros
384            // could just copy the span to one of the delimiters. We don't want
385            // to leak the dummy ID, so we remove both.
386            if tt.delimiter.close.anchor.ast_id == FIXUP_DUMMY_AST_ID
387                || tt.delimiter.open.anchor.ast_id == FIXUP_DUMMY_AST_ID
388            {
389                return TransformTtAction::remove();
390            }
391            TransformTtAction::Keep
392        }
393    });
394}
395
396#[cfg(test)]
397mod tests {
398    use expect_test::{Expect, expect};
399    use span::{Edition, EditionedFileId, FileId};
400    use syntax::TextRange;
401    use syntax_bridge::DocCommentDesugarMode;
402
403    use crate::{
404        fixup::reverse_fixups,
405        span_map::{RealSpanMap, SpanMap},
406        tt,
407    };
408
409    // The following three functions are only meant to check partial structural equivalence of
410    // `TokenTree`s, see the last assertion in `check()`.
411    fn check_leaf_eq(a: &tt::Leaf, b: &tt::Leaf) -> bool {
412        match (a, b) {
413            (tt::Leaf::Literal(a), tt::Leaf::Literal(b)) => a.text_and_suffix == b.text_and_suffix,
414            (tt::Leaf::Punct(a), tt::Leaf::Punct(b)) => a.char == b.char,
415            (tt::Leaf::Ident(a), tt::Leaf::Ident(b)) => a.sym == b.sym,
416            _ => false,
417        }
418    }
419
420    fn check_subtree_eq(a: &tt::TopSubtree, b: &tt::TopSubtree) -> bool {
421        let a = a.view().as_token_trees();
422        let b = b.view().as_token_trees();
423        a.len() == b.len()
424            && std::iter::zip(a.iter_flat_tokens(), b.iter_flat_tokens())
425                .all(|(a, b)| check_tt_eq(&a, &b))
426    }
427
428    fn check_tt_eq(a: &tt::TokenTree, b: &tt::TokenTree) -> bool {
429        match (a, b) {
430            (tt::TokenTree::Leaf(a), tt::TokenTree::Leaf(b)) => check_leaf_eq(a, b),
431            (tt::TokenTree::Subtree(a), tt::TokenTree::Subtree(b)) => {
432                a.delimiter.kind == b.delimiter.kind
433            }
434            _ => false,
435        }
436    }
437
438    #[track_caller]
439    fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str, mut expect: Expect) {
440        let parsed = syntax::SourceFile::parse(ra_fixture, span::Edition::CURRENT);
441        let span_map = SpanMap::RealSpanMap(&RealSpanMap::absolute(EditionedFileId::new(
442            FileId::from_raw(0),
443            Edition::CURRENT,
444        )));
445        let fixups = super::fixup_syntax(
446            span_map,
447            &parsed.syntax_node(),
448            span_map.span_for_range(TextRange::empty(0.into())),
449            DocCommentDesugarMode::Mbe,
450        );
451        let mut tt = syntax_bridge::syntax_node_to_token_tree_modified(
452            &parsed.syntax_node(),
453            span_map,
454            fixups.append,
455            fixups.remove,
456            span_map.span_for_range(TextRange::empty(0.into())),
457            DocCommentDesugarMode::Mbe,
458            |_, _| (true, Vec::new()),
459        );
460
461        let actual = format!("{tt}\n");
462
463        expect.indent(false);
464        expect.assert_eq(&actual);
465
466        // the fixed-up tree should be syntactically valid
467        let (parse, _) = syntax_bridge::token_tree_to_syntax_node(
468            &tt,
469            syntax_bridge::TopEntryPoint::MacroItems,
470            &mut |_| parser::Edition::CURRENT,
471        );
472        assert!(
473            parse.errors().is_empty(),
474            "parse has syntax errors. parse tree:\n{:#?}",
475            parse.syntax_node()
476        );
477
478        // the fixed-up tree should not contain braces as punct
479        // FIXME: should probably instead check that it's a valid punctuation character
480        for x in tt.token_trees().iter_flat_tokens() {
481            match x {
482                ::tt::TokenTree::Leaf(::tt::Leaf::Punct(punct)) => {
483                    assert!(!matches!(punct.char, '{' | '}' | '(' | ')' | '[' | ']'))
484                }
485                _ => (),
486            }
487        }
488
489        reverse_fixups(&mut tt, &fixups.undo_info);
490
491        // the fixed-up + reversed version should be equivalent to the original input
492        // modulo token IDs and `Punct`s' spacing.
493        let original_as_tt = syntax_bridge::syntax_node_to_token_tree(
494            &parsed.syntax_node(),
495            span_map,
496            span_map.span_for_range(TextRange::empty(0.into())),
497            DocCommentDesugarMode::Mbe,
498        );
499        assert!(
500            check_subtree_eq(&tt, &original_as_tt),
501            "different token tree:\n{tt:?}\n\n{original_as_tt:?}"
502        );
503    }
504
505    #[test]
506    fn just_for_token() {
507        check(
508            r#"
509fn foo() {
510    for
511}
512"#,
513            expect![[r#"
514fn foo () {for _ in __ra_fixup {}}
515"#]],
516        )
517    }
518
519    #[test]
520    fn for_no_iter_pattern() {
521        check(
522            r#"
523fn foo() {
524    for {}
525}
526"#,
527            expect![[r#"
528fn foo () {for _ in __ra_fixup {}}
529"#]],
530        )
531    }
532
533    #[test]
534    fn for_no_body() {
535        check(
536            r#"
537fn foo() {
538    for bar in qux
539}
540"#,
541            expect![[r#"
542fn foo () {for bar in qux {}}
543"#]],
544        )
545    }
546
547    // FIXME: https://github.com/rust-lang/rust-analyzer/pull/12937#discussion_r937633695
548    #[test]
549    fn for_no_pat() {
550        check(
551            r#"
552fn foo() {
553    for in qux {
554
555    }
556}
557"#,
558            expect![[r#"
559fn foo () {__ra_fixup}
560"#]],
561        )
562    }
563
564    #[test]
565    fn match_no_expr_no_arms() {
566        check(
567            r#"
568fn foo() {
569    match
570}
571"#,
572            expect![[r#"
573fn foo () {match __ra_fixup {}}
574"#]],
575        )
576    }
577
578    #[test]
579    fn match_expr_no_arms() {
580        check(
581            r#"
582fn foo() {
583    match it {
584
585    }
586}
587"#,
588            expect![[r#"
589fn foo () {match it {}}
590"#]],
591        )
592    }
593
594    #[test]
595    fn match_no_expr() {
596        check(
597            r#"
598fn foo() {
599    match {
600        _ => {}
601    }
602}
603"#,
604            expect![[r#"
605fn foo () {match __ra_fixup {}}
606"#]],
607        )
608    }
609
610    #[test]
611    fn incomplete_field_expr_1() {
612        check(
613            r#"
614fn foo() {
615    a.
616}
617"#,
618            expect![[r#"
619fn foo () {a . __ra_fixup}
620"#]],
621        )
622    }
623
624    #[test]
625    fn incomplete_field_expr_2() {
626        check(
627            r#"
628fn foo() {
629    a.;
630}
631"#,
632            expect![[r#"
633fn foo () {a .__ra_fixup ;}
634"#]],
635        )
636    }
637
638    #[test]
639    fn incomplete_field_expr_3() {
640        check(
641            r#"
642fn foo() {
643    a.;
644    bar();
645}
646"#,
647            expect![[r#"
648fn foo () {a .__ra_fixup ; bar () ;}
649"#]],
650        )
651    }
652
653    #[test]
654    fn incomplete_let() {
655        check(
656            r#"
657fn foo() {
658    let it = a
659}
660"#,
661            expect![[r#"
662fn foo () {let it = a ;}
663"#]],
664        )
665    }
666
667    #[test]
668    fn incomplete_field_expr_in_let() {
669        check(
670            r#"
671fn foo() {
672    let it = a.
673}
674"#,
675            expect![[r#"
676fn foo () {let it = a . __ra_fixup ;}
677"#]],
678        )
679    }
680
681    #[test]
682    fn field_expr_before_call() {
683        // another case that easily happens while typing
684        check(
685            r#"
686fn foo() {
687    a.b
688    bar();
689}
690"#,
691            expect![[r#"
692fn foo () {a . b ; bar () ;}
693"#]],
694        )
695    }
696
697    #[test]
698    fn extraneous_comma() {
699        check(
700            r#"
701fn foo() {
702    bar(,);
703}
704"#,
705            expect![[r#"
706fn foo () {__ra_fixup ;}
707"#]],
708        )
709    }
710
711    #[test]
712    fn fixup_if_1() {
713        check(
714            r#"
715fn foo() {
716    if a
717}
718"#,
719            expect![[r#"
720fn foo () {if a {}}
721"#]],
722        )
723    }
724
725    #[test]
726    fn fixup_if_2() {
727        check(
728            r#"
729fn foo() {
730    if
731}
732"#,
733            expect![[r#"
734fn foo () {if __ra_fixup {}}
735"#]],
736        )
737    }
738
739    #[test]
740    fn fixup_if_3() {
741        check(
742            r#"
743fn foo() {
744    if {}
745}
746"#,
747            expect![[r#"
748fn foo () {if __ra_fixup {} {}}
749"#]],
750        )
751    }
752
753    #[test]
754    fn fixup_while_1() {
755        check(
756            r#"
757fn foo() {
758    while
759}
760"#,
761            expect![[r#"
762fn foo () {while __ra_fixup {}}
763"#]],
764        )
765    }
766
767    #[test]
768    fn fixup_while_2() {
769        check(
770            r#"
771fn foo() {
772    while foo
773}
774"#,
775            expect![[r#"
776fn foo () {while foo {}}
777"#]],
778        )
779    }
780    #[test]
781    fn fixup_while_3() {
782        check(
783            r#"
784fn foo() {
785    while {}
786}
787"#,
788            expect![[r#"
789fn foo () {while __ra_fixup {}}
790"#]],
791        )
792    }
793
794    #[test]
795    fn fixup_loop() {
796        check(
797            r#"
798fn foo() {
799    loop
800}
801"#,
802            expect![[r#"
803fn foo () {loop {}}
804"#]],
805        )
806    }
807
808    #[test]
809    fn fixup_path() {
810        check(
811            r#"
812fn foo() {
813    path::
814}
815"#,
816            expect![[r#"
817fn foo () {path :: __ra_fixup}
818"#]],
819        )
820    }
821
822    #[test]
823    fn fixup_record_ctor_field() {
824        check(
825            r#"
826fn foo() {
827    R { f: }
828}
829"#,
830            expect![[r#"
831fn foo () {R {f : __ra_fixup}}
832"#]],
833        )
834    }
835
836    #[test]
837    fn no_fixup_record_ctor_field() {
838        check(
839            r#"
840fn foo() {
841    R { f: a }
842}
843"#,
844            expect![[r#"
845fn foo () {R {f : a}}
846"#]],
847        )
848    }
849
850    #[test]
851    fn fixup_arg_list() {
852        check(
853            r#"
854fn foo() {
855    foo(a
856}
857"#,
858            expect![[r#"
859fn foo () {foo (a)}
860"#]],
861        );
862        check(
863            r#"
864fn foo() {
865    bar.foo(a
866}
867"#,
868            expect![[r#"
869fn foo () {bar . foo (a)}
870"#]],
871        );
872    }
873
874    #[test]
875    fn fixup_closure() {
876        check(
877            r#"
878fn foo() {
879    ||
880}
881"#,
882            expect![[r#"
883fn foo () {|| __ra_fixup}
884"#]],
885        );
886    }
887
888    #[test]
889    fn fixup_regression_() {
890        check(
891            r#"
892fn foo() {
893    {}
894    {}
895}
896"#,
897            expect![[r#"
898fn foo () {{} {}}
899"#]],
900        );
901    }
902}