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    #[allow(deprecated)]
347    if never!(
348        close_span.anchor.ast_id == FIXUP_DUMMY_AST_ID
349            || open_span.anchor.ast_id == FIXUP_DUMMY_AST_ID
350    ) {
351        let span = |file_id| Span {
352            range: TextRange::empty(TextSize::new(0)),
353            anchor: SpanAnchor { file_id, ast_id: ROOT_ERASED_FILE_AST_ID },
354            ctx: SyntaxContext::root(span::Edition::Edition2015),
355        };
356        tt.set_top_subtree_delimiter_span(tt::DelimSpan {
357            open: span(open_span.anchor.file_id),
358            close: span(close_span.anchor.file_id),
359        });
360    }
361    reverse_fixups_(tt, undo_info);
362}
363
364fn reverse_fixups_(tt: &mut TopSubtree, undo_info: &[TopSubtree]) {
365    transform_tt(tt, |tt| match tt {
366        tt::TokenTree::Leaf(leaf) => {
367            let span = leaf.span();
368            let is_real_leaf = span.anchor.ast_id != FIXUP_DUMMY_AST_ID;
369            let is_replaced_node = span.range.end() == FIXUP_DUMMY_RANGE_END;
370            if !is_real_leaf && !is_replaced_node {
371                return TransformTtAction::remove();
372            }
373
374            if !is_real_leaf {
375                // we have a fake node here, we need to replace it again with the original
376                let original = &undo_info[u32::from(leaf.span().range.start()) as usize];
377                TransformTtAction::ReplaceWith(original.view().strip_invisible())
378            } else {
379                // just a normal leaf
380                TransformTtAction::Keep
381            }
382        }
383        tt::TokenTree::Subtree(tt) => {
384            // fixup should only create matching delimiters, but proc macros
385            // could just copy the span to one of the delimiters. We don't want
386            // to leak the dummy ID, so we remove both.
387            if tt.delimiter.close.anchor.ast_id == FIXUP_DUMMY_AST_ID
388                || tt.delimiter.open.anchor.ast_id == FIXUP_DUMMY_AST_ID
389            {
390                return TransformTtAction::remove();
391            }
392            TransformTtAction::Keep
393        }
394    });
395}
396
397#[cfg(test)]
398mod tests {
399    use expect_test::{Expect, expect};
400    use span::{Edition, EditionedFileId, FileId};
401    use syntax::TextRange;
402    use syntax_bridge::DocCommentDesugarMode;
403
404    use crate::{
405        fixup::reverse_fixups,
406        span_map::{RealSpanMap, SpanMap},
407        tt,
408    };
409
410    // The following three functions are only meant to check partial structural equivalence of
411    // `TokenTree`s, see the last assertion in `check()`.
412    fn check_leaf_eq(a: &tt::Leaf, b: &tt::Leaf) -> bool {
413        match (a, b) {
414            (tt::Leaf::Literal(a), tt::Leaf::Literal(b)) => a.text_and_suffix == b.text_and_suffix,
415            (tt::Leaf::Punct(a), tt::Leaf::Punct(b)) => a.char == b.char,
416            (tt::Leaf::Ident(a), tt::Leaf::Ident(b)) => a.sym == b.sym,
417            _ => false,
418        }
419    }
420
421    fn check_subtree_eq(a: &tt::TopSubtree, b: &tt::TopSubtree) -> bool {
422        let a = a.view().as_token_trees().iter_flat_tokens();
423        let b = b.view().as_token_trees().iter_flat_tokens();
424        a.len() == b.len() && std::iter::zip(a, b).all(|(a, b)| check_tt_eq(&a, &b))
425    }
426
427    fn check_tt_eq(a: &tt::TokenTree, b: &tt::TokenTree) -> bool {
428        match (a, b) {
429            (tt::TokenTree::Leaf(a), tt::TokenTree::Leaf(b)) => check_leaf_eq(a, b),
430            (tt::TokenTree::Subtree(a), tt::TokenTree::Subtree(b)) => {
431                a.delimiter.kind == b.delimiter.kind
432            }
433            _ => false,
434        }
435    }
436
437    #[track_caller]
438    fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str, mut expect: Expect) {
439        let parsed = syntax::SourceFile::parse(ra_fixture, span::Edition::CURRENT);
440        let span_map = SpanMap::RealSpanMap(&RealSpanMap::absolute(EditionedFileId::new(
441            FileId::from_raw(0),
442            Edition::CURRENT,
443        )));
444        let fixups = super::fixup_syntax(
445            span_map,
446            &parsed.syntax_node(),
447            span_map.span_for_range(TextRange::empty(0.into())),
448            DocCommentDesugarMode::Mbe,
449        );
450        let mut tt = syntax_bridge::syntax_node_to_token_tree_modified(
451            &parsed.syntax_node(),
452            span_map,
453            fixups.append,
454            fixups.remove,
455            span_map.span_for_range(TextRange::empty(0.into())),
456            DocCommentDesugarMode::Mbe,
457            |_, _| (true, Vec::new()),
458        );
459
460        let actual = format!("{tt}\n");
461
462        expect.indent(false);
463        expect.assert_eq(&actual);
464
465        // the fixed-up tree should be syntactically valid
466        let (parse, _) = syntax_bridge::token_tree_to_syntax_node(
467            &tt,
468            syntax_bridge::TopEntryPoint::MacroItems,
469            &mut |_| parser::Edition::CURRENT,
470        );
471        assert!(
472            parse.errors().is_empty(),
473            "parse has syntax errors. parse tree:\n{:#?}",
474            parse.syntax_node()
475        );
476
477        // the fixed-up tree should not contain braces as punct
478        // FIXME: should probably instead check that it's a valid punctuation character
479        for x in tt.token_trees().iter_flat_tokens() {
480            match x {
481                ::tt::TokenTree::Leaf(::tt::Leaf::Punct(punct)) => {
482                    assert!(!matches!(punct.char, '{' | '}' | '(' | ')' | '[' | ']'))
483                }
484                _ => (),
485            }
486        }
487
488        reverse_fixups(&mut tt, &fixups.undo_info);
489
490        // the fixed-up + reversed version should be equivalent to the original input
491        // modulo token IDs and `Punct`s' spacing.
492        let original_as_tt = syntax_bridge::syntax_node_to_token_tree(
493            &parsed.syntax_node(),
494            span_map,
495            span_map.span_for_range(TextRange::empty(0.into())),
496            DocCommentDesugarMode::Mbe,
497        );
498        assert!(
499            check_subtree_eq(&tt, &original_as_tt),
500            "different token tree:\n{tt:?}\n\n{original_as_tt:?}"
501        );
502    }
503
504    #[test]
505    fn just_for_token() {
506        check(
507            r#"
508fn foo() {
509    for
510}
511"#,
512            expect![[r#"
513fn foo () {for _ in __ra_fixup {}}
514"#]],
515        )
516    }
517
518    #[test]
519    fn for_no_iter_pattern() {
520        check(
521            r#"
522fn foo() {
523    for {}
524}
525"#,
526            expect![[r#"
527fn foo () {for _ in __ra_fixup {}}
528"#]],
529        )
530    }
531
532    #[test]
533    fn for_no_body() {
534        check(
535            r#"
536fn foo() {
537    for bar in qux
538}
539"#,
540            expect![[r#"
541fn foo () {for bar in qux {}}
542"#]],
543        )
544    }
545
546    // FIXME: https://github.com/rust-lang/rust-analyzer/pull/12937#discussion_r937633695
547    #[test]
548    fn for_no_pat() {
549        check(
550            r#"
551fn foo() {
552    for in qux {
553
554    }
555}
556"#,
557            expect![[r#"
558fn foo () {__ra_fixup}
559"#]],
560        )
561    }
562
563    #[test]
564    fn match_no_expr_no_arms() {
565        check(
566            r#"
567fn foo() {
568    match
569}
570"#,
571            expect![[r#"
572fn foo () {match __ra_fixup {}}
573"#]],
574        )
575    }
576
577    #[test]
578    fn match_expr_no_arms() {
579        check(
580            r#"
581fn foo() {
582    match it {
583
584    }
585}
586"#,
587            expect![[r#"
588fn foo () {match it {}}
589"#]],
590        )
591    }
592
593    #[test]
594    fn match_no_expr() {
595        check(
596            r#"
597fn foo() {
598    match {
599        _ => {}
600    }
601}
602"#,
603            expect![[r#"
604fn foo () {match __ra_fixup {}}
605"#]],
606        )
607    }
608
609    #[test]
610    fn incomplete_field_expr_1() {
611        check(
612            r#"
613fn foo() {
614    a.
615}
616"#,
617            expect![[r#"
618fn foo () {a . __ra_fixup}
619"#]],
620        )
621    }
622
623    #[test]
624    fn incomplete_field_expr_2() {
625        check(
626            r#"
627fn foo() {
628    a.;
629}
630"#,
631            expect![[r#"
632fn foo () {a .__ra_fixup ;}
633"#]],
634        )
635    }
636
637    #[test]
638    fn incomplete_field_expr_3() {
639        check(
640            r#"
641fn foo() {
642    a.;
643    bar();
644}
645"#,
646            expect![[r#"
647fn foo () {a .__ra_fixup ; bar () ;}
648"#]],
649        )
650    }
651
652    #[test]
653    fn incomplete_let() {
654        check(
655            r#"
656fn foo() {
657    let it = a
658}
659"#,
660            expect![[r#"
661fn foo () {let it = a ;}
662"#]],
663        )
664    }
665
666    #[test]
667    fn incomplete_field_expr_in_let() {
668        check(
669            r#"
670fn foo() {
671    let it = a.
672}
673"#,
674            expect![[r#"
675fn foo () {let it = a . __ra_fixup ;}
676"#]],
677        )
678    }
679
680    #[test]
681    fn field_expr_before_call() {
682        // another case that easily happens while typing
683        check(
684            r#"
685fn foo() {
686    a.b
687    bar();
688}
689"#,
690            expect![[r#"
691fn foo () {a . b ; bar () ;}
692"#]],
693        )
694    }
695
696    #[test]
697    fn extraneous_comma() {
698        check(
699            r#"
700fn foo() {
701    bar(,);
702}
703"#,
704            expect![[r#"
705fn foo () {__ra_fixup ;}
706"#]],
707        )
708    }
709
710    #[test]
711    fn fixup_if_1() {
712        check(
713            r#"
714fn foo() {
715    if a
716}
717"#,
718            expect![[r#"
719fn foo () {if a {}}
720"#]],
721        )
722    }
723
724    #[test]
725    fn fixup_if_2() {
726        check(
727            r#"
728fn foo() {
729    if
730}
731"#,
732            expect![[r#"
733fn foo () {if __ra_fixup {}}
734"#]],
735        )
736    }
737
738    #[test]
739    fn fixup_if_3() {
740        check(
741            r#"
742fn foo() {
743    if {}
744}
745"#,
746            expect![[r#"
747fn foo () {if __ra_fixup {} {}}
748"#]],
749        )
750    }
751
752    #[test]
753    fn fixup_while_1() {
754        check(
755            r#"
756fn foo() {
757    while
758}
759"#,
760            expect![[r#"
761fn foo () {while __ra_fixup {}}
762"#]],
763        )
764    }
765
766    #[test]
767    fn fixup_while_2() {
768        check(
769            r#"
770fn foo() {
771    while foo
772}
773"#,
774            expect![[r#"
775fn foo () {while foo {}}
776"#]],
777        )
778    }
779    #[test]
780    fn fixup_while_3() {
781        check(
782            r#"
783fn foo() {
784    while {}
785}
786"#,
787            expect![[r#"
788fn foo () {while __ra_fixup {}}
789"#]],
790        )
791    }
792
793    #[test]
794    fn fixup_loop() {
795        check(
796            r#"
797fn foo() {
798    loop
799}
800"#,
801            expect![[r#"
802fn foo () {loop {}}
803"#]],
804        )
805    }
806
807    #[test]
808    fn fixup_path() {
809        check(
810            r#"
811fn foo() {
812    path::
813}
814"#,
815            expect![[r#"
816fn foo () {path :: __ra_fixup}
817"#]],
818        )
819    }
820
821    #[test]
822    fn fixup_record_ctor_field() {
823        check(
824            r#"
825fn foo() {
826    R { f: }
827}
828"#,
829            expect![[r#"
830fn foo () {R {f : __ra_fixup}}
831"#]],
832        )
833    }
834
835    #[test]
836    fn no_fixup_record_ctor_field() {
837        check(
838            r#"
839fn foo() {
840    R { f: a }
841}
842"#,
843            expect![[r#"
844fn foo () {R {f : a}}
845"#]],
846        )
847    }
848
849    #[test]
850    fn fixup_arg_list() {
851        check(
852            r#"
853fn foo() {
854    foo(a
855}
856"#,
857            expect![[r#"
858fn foo () {foo (a)}
859"#]],
860        );
861        check(
862            r#"
863fn foo() {
864    bar.foo(a
865}
866"#,
867            expect![[r#"
868fn foo () {bar . foo (a)}
869"#]],
870        );
871    }
872
873    #[test]
874    fn fixup_closure() {
875        check(
876            r#"
877fn foo() {
878    ||
879}
880"#,
881            expect![[r#"
882fn foo () {|| __ra_fixup}
883"#]],
884        );
885    }
886
887    #[test]
888    fn fixup_regression_() {
889        check(
890            r#"
891fn foo() {
892    {}
893    {}
894}
895"#,
896            expect![[r#"
897fn foo () {{} {}}
898"#]],
899        );
900    }
901}