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