1use 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#[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#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct SyntaxFixupUndoInfo {
38 original: Option<Arc<Box<[TopSubtree]>>>,
40}
41
42impl SyntaxFixupUndoInfo {
43 pub(crate) const NONE: Self = SyntaxFixupUndoInfo { original: None };
44}
45
46const FIXUP_DUMMY_AST_ID: ErasedFileAstId = FIXUP_ERASED_FILE_AST_ID_MARKER;
48const FIXUP_DUMMY_RANGE: TextRange = TextRange::empty(TextSize::new(0));
49const 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 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 match_ast! {
99 match node {
100 ast::FieldExpr(it) => {
101 if it.name_ref().is_none() {
102 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 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 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 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 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 } 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
375fn transform_tt<'a, 'b>(
379 tt: &'a mut Vec<tt::TokenTree>,
380 mut callback: impl FnMut(&mut tt::TokenTree) -> TransformTtAction<'b>,
381) {
382 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 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 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 let original = &undo_info[u32::from(leaf.span().range.start()) as usize];
444 TransformTtAction::ReplaceWith(original.view().strip_invisible())
445 } else {
446 TransformTtAction::Keep
448 }
449 }
450 tt::TokenTree::Subtree(tt) => {
451 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 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 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 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 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 #[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 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}