1use cel_parser::trivia::{line_column_to_byte, line_start_byte_offsets};
40use proc_macro2::LineColumn;
41
42use crate::ast::{
43 BindingDecl, CellDecl, ConditionalBranch, ConditionalDecl, ExprSpan, OutDecl, RelationshipDecl,
44 RequireBlock, RequirementDecl, Sheet, SourceDecl,
45};
46
47trait TriviaTarget {
49 fn span(&self) -> ExprSpan;
50 fn set_leading_comment(&mut self, comment: crate::ast::Comment);
51 fn set_trailing_line_comment(&mut self, comment: crate::ast::Comment);
55 fn set_blank_line_before(&mut self, value: bool);
56}
57
58impl TriviaTarget for crate::ast::SheetItem {
59 fn span(&self) -> ExprSpan {
60 crate::ast::SheetItem::span(self)
61 }
62 fn set_leading_comment(&mut self, comment: crate::ast::Comment) {
63 crate::ast::SheetItem::set_leading_comment(self, comment)
64 }
65 fn set_trailing_line_comment(&mut self, comment: crate::ast::Comment) {
66 crate::ast::SheetItem::set_trailing_line_comment(self, comment)
67 }
68 fn set_blank_line_before(&mut self, value: bool) {
69 crate::ast::SheetItem::set_blank_line_before(self, value)
70 }
71}
72
73impl TriviaTarget for BindingDecl {
74 fn span(&self) -> ExprSpan {
75 self.span
76 }
77 fn set_leading_comment(&mut self, comment: crate::ast::Comment) {
78 self.leading_comment = Some(comment);
79 }
80 fn set_trailing_line_comment(&mut self, comment: crate::ast::Comment) {
81 self.trailing_line_comment = Some(comment);
82 }
83 fn set_blank_line_before(&mut self, value: bool) {
84 self.blank_line_before = value;
85 }
86}
87
88impl TriviaTarget for RelationshipDecl {
89 fn span(&self) -> ExprSpan {
90 self.span
91 }
92 fn set_leading_comment(&mut self, comment: crate::ast::Comment) {
93 self.leading_comment = Some(comment);
94 }
95 fn set_trailing_line_comment(&mut self, comment: crate::ast::Comment) {
96 self.trailing_line_comment = Some(comment);
97 }
98 fn set_blank_line_before(&mut self, value: bool) {
99 self.blank_line_before = value;
100 }
101}
102
103impl TriviaTarget for ConditionalBranch {
104 fn span(&self) -> ExprSpan {
105 self.span
106 }
107 fn set_leading_comment(&mut self, comment: crate::ast::Comment) {
108 self.leading_comment = Some(comment);
109 }
110 fn set_trailing_line_comment(&mut self, comment: crate::ast::Comment) {
111 self.trailing_line_comment = Some(comment);
112 }
113 fn set_blank_line_before(&mut self, value: bool) {
114 self.blank_line_before = value;
115 }
116}
117
118impl TriviaTarget for RequirementDecl {
119 fn span(&self) -> ExprSpan {
120 self.span
121 }
122 fn set_leading_comment(&mut self, comment: crate::ast::Comment) {
123 self.leading_comment = Some(comment);
124 }
125 fn set_trailing_line_comment(&mut self, comment: crate::ast::Comment) {
126 self.trailing_line_comment = Some(comment);
127 }
128 fn set_blank_line_before(&mut self, value: bool) {
129 self.blank_line_before = value;
130 }
131}
132
133trait TrailingTriviaTarget {
137 fn open_brace_span(&self) -> proc_macro2::Span;
140 fn close_span(&self) -> proc_macro2::Span;
142 fn set_trailing_comment(&mut self, comment: crate::ast::Comment);
143 fn set_blank_line_before_close(&mut self, value: bool);
144}
145
146impl TrailingTriviaTarget for Sheet {
147 fn open_brace_span(&self) -> proc_macro2::Span {
148 self.open_brace_span.end
149 }
150 fn close_span(&self) -> proc_macro2::Span {
151 self.span.end
152 }
153 fn set_trailing_comment(&mut self, comment: crate::ast::Comment) {
154 self.trailing_comment = Some(comment);
155 }
156 fn set_blank_line_before_close(&mut self, value: bool) {
157 self.blank_line_before_close = value;
158 }
159}
160
161impl TrailingTriviaTarget for RelationshipDecl {
162 fn open_brace_span(&self) -> proc_macro2::Span {
163 self.open_brace_span.end
164 }
165 fn close_span(&self) -> proc_macro2::Span {
166 self.span.end
167 }
168 fn set_trailing_comment(&mut self, comment: crate::ast::Comment) {
169 self.trailing_comment = Some(comment);
170 }
171 fn set_blank_line_before_close(&mut self, value: bool) {
172 self.blank_line_before_close = value;
173 }
174}
175
176impl TrailingTriviaTarget for RequireBlock {
177 fn open_brace_span(&self) -> proc_macro2::Span {
178 self.open_brace_span.end
179 }
180 fn close_span(&self) -> proc_macro2::Span {
181 self.span.end
182 }
183 fn set_trailing_comment(&mut self, comment: crate::ast::Comment) {
184 self.trailing_comment = Some(comment);
185 }
186 fn set_blank_line_before_close(&mut self, value: bool) {
187 self.blank_line_before_close = value;
188 }
189}
190
191impl TrailingTriviaTarget for ConditionalBranch {
192 fn open_brace_span(&self) -> proc_macro2::Span {
193 self.open_brace_span.end
194 }
195 fn close_span(&self) -> proc_macro2::Span {
196 self.span.end
197 }
198 fn set_trailing_comment(&mut self, comment: crate::ast::Comment) {
199 self.trailing_comment = Some(comment);
200 }
201 fn set_blank_line_before_close(&mut self, value: bool) {
202 self.blank_line_before_close = value;
203 }
204}
205
206impl TrailingTriviaTarget for crate::ast::DefaultBranch {
207 fn open_brace_span(&self) -> proc_macro2::Span {
208 self.open_brace_span.end
209 }
210 fn close_span(&self) -> proc_macro2::Span {
211 self.span.end
212 }
213 fn set_trailing_comment(&mut self, comment: crate::ast::Comment) {
214 self.trailing_comment = Some(comment);
215 }
216 fn set_blank_line_before_close(&mut self, value: bool) {
217 self.blank_line_before_close = value;
218 }
219}
220
221fn after_open_brace(source: &str, line_starts: &[usize], open_brace: proc_macro2::Span) -> usize {
229 line_column_to_byte(source, line_starts, open_brace.start()) + 1
230}
231
232fn before_close_brace(
237 source: &str,
238 line_starts: &[usize],
239 close_brace: proc_macro2::Span,
240) -> usize {
241 line_column_to_byte(source, line_starts, close_brace.end()) - 1
242}
243
244fn attach_trailing<C: TrailingTriviaTarget>(
259 source: &str,
260 line_starts: &[usize],
261 last_child_end: Option<LineColumn>,
262 container: &mut C,
263) -> Option<crate::ast::Comment> {
264 let start = match last_child_end {
265 Some(pos) => line_column_to_byte(source, line_starts, pos),
266 None => after_open_brace(source, line_starts, container.open_brace_span()),
267 };
268 let end = before_close_brace(source, line_starts, container.close_span());
269 if start >= end {
270 return None;
271 }
272 let gap_text = &source[start..end];
273 let (comment, blank_line_before_close, same_line) = analyze_gap(gap_text);
274 container.set_blank_line_before_close(blank_line_before_close);
275 match comment {
276 Some(comment) if same_line && last_child_end.is_some() => Some(comment),
277 Some(comment) => {
278 container.set_trailing_comment(comment);
279 None
280 }
281 None => None,
282 }
283}
284
285fn attach_conditional_trailing(
294 source: &str,
295 line_starts: &[usize],
296 cond: &mut ConditionalDecl,
297) -> Option<crate::ast::Comment> {
298 let has_last_child = cond.default.is_some() || !cond.branches.is_empty();
299 let start = if let Some(default) = &cond.default {
300 line_column_to_byte(source, line_starts, default.span.end.end())
301 } else if let Some(last_branch) = cond.branches.last() {
302 line_column_to_byte(source, line_starts, last_branch.span.end.end())
303 } else {
304 after_open_brace(source, line_starts, cond.open_brace_span.end)
305 };
306 let end = before_close_brace(source, line_starts, cond.span.end);
307 if start >= end {
308 return None;
309 }
310 let gap_text = &source[start..end];
311 let (comment, blank_line_before_close, same_line) = analyze_gap(gap_text);
312 cond.blank_line_before_close = blank_line_before_close;
313 match comment {
314 Some(comment) if same_line && has_last_child => Some(comment),
315 Some(comment) => {
316 cond.trailing_comment = Some(comment);
317 None
318 }
319 None => None,
320 }
321}
322
323pub fn attach_trivia(source: &str, sheet: &mut Sheet) {
334 let line_starts = line_start_byte_offsets(source);
335 let sheet_start = line_column_to_byte(source, &line_starts, sheet.span.start.start());
336 let (leading_comment, _, _) = analyze_gap(&source[..sheet_start]);
337 sheet.leading_comment = leading_comment;
338 attach_gaps(source, &line_starts, &mut sheet.items);
339 let last_child_end = sheet.items.last().map(|item| item.span().end.end());
340 if let Some(comment) = attach_trailing(source, &line_starts, last_child_end, sheet) {
341 sheet
342 .items
343 .last_mut()
344 .expect("same-line routing only returns Some when a last child exists")
345 .set_trailing_line_comment(comment);
346 }
347 for item in &mut sheet.items {
348 match item {
349 crate::ast::SheetItem::Relationship(rel) => {
350 attach_relationship(source, &line_starts, rel)
351 }
352 crate::ast::SheetItem::Conditional(cond) => {
353 attach_conditional(source, &line_starts, cond)
354 }
355 crate::ast::SheetItem::Out(out_decl) => attach_out(source, &line_starts, out_decl),
356 crate::ast::SheetItem::Cell(cell) => attach_cell(source, &line_starts, cell),
357 crate::ast::SheetItem::Source(source_decl) => {
358 attach_source(source, &line_starts, source_decl)
359 }
360 crate::ast::SheetItem::Error { .. } => {}
361 }
362 }
363}
364
365fn attach_relationship(source: &str, line_starts: &[usize], rel: &mut RelationshipDecl) {
367 attach_gaps(source, line_starts, &mut rel.bindings);
368 let last_child_end = rel.bindings.last().map(|b| b.span().end.end());
369 if let Some(comment) = attach_trailing(source, line_starts, last_child_end, rel) {
370 rel.bindings
371 .last_mut()
372 .expect("same-line routing only returns Some when a last child exists")
373 .set_trailing_line_comment(comment);
374 }
375}
376
377fn attach_conditional(source: &str, line_starts: &[usize], cond: &mut ConditionalDecl) {
379 attach_gaps(source, line_starts, &mut cond.branches);
380 for branch in &mut cond.branches {
381 attach_gaps(source, line_starts, &mut branch.relationships);
382 let last_child_end = branch.relationships.last().map(|r| r.span().end.end());
383 if let Some(comment) = attach_trailing(source, line_starts, last_child_end, branch) {
384 branch
385 .relationships
386 .last_mut()
387 .expect("same-line routing only returns Some when a last child exists")
388 .set_trailing_line_comment(comment);
389 }
390 for rel in &mut branch.relationships {
391 attach_relationship(source, line_starts, rel);
392 }
393 }
394 if let Some(default) = &mut cond.default {
395 attach_gaps(source, line_starts, &mut default.relationships);
396 let last_child_end = default.relationships.last().map(|r| r.span().end.end());
397 if let Some(comment) = attach_trailing(source, line_starts, last_child_end, default) {
398 default
399 .relationships
400 .last_mut()
401 .expect("same-line routing only returns Some when a last child exists")
402 .set_trailing_line_comment(comment);
403 }
404 for rel in default.relationships.iter_mut() {
405 attach_relationship(source, line_starts, rel);
406 }
407 }
408 if let Some(comment) = attach_conditional_trailing(source, line_starts, cond) {
409 if let Some(default) = &mut cond.default {
410 default.trailing_line_comment = Some(comment);
411 } else if let Some(last_branch) = cond.branches.last_mut() {
412 last_branch.set_trailing_line_comment(comment);
413 }
414 }
415}
416
417fn attach_require_block(source: &str, line_starts: &[usize], require: Option<&mut RequireBlock>) {
424 let Some(require) = require else {
425 return;
426 };
427 attach_gaps(source, line_starts, &mut require.requirements);
428 let last_child_end = require.requirements.last().map(|r| r.span().end.end());
429 if let Some(comment) = attach_trailing(source, line_starts, last_child_end, require) {
430 require
431 .requirements
432 .last_mut()
433 .expect("same-line routing only returns Some when a last child exists")
434 .set_trailing_line_comment(comment);
435 }
436}
437
438fn attach_out(source: &str, line_starts: &[usize], out_decl: &mut OutDecl) {
441 attach_require_block(source, line_starts, out_decl.require.as_mut());
442}
443
444fn attach_cell(source: &str, line_starts: &[usize], cell: &mut CellDecl) {
447 attach_require_block(source, line_starts, cell.require.as_mut());
448}
449
450fn attach_source(source: &str, line_starts: &[usize], source_decl: &mut SourceDecl) {
453 attach_require_block(source, line_starts, source_decl.require.as_mut());
454}
455
456fn attach_gaps<T: TriviaTarget>(source: &str, line_starts: &[usize], items: &mut [T]) {
464 if items.len() < 2 {
465 return;
466 }
467 for i in 1..items.len() {
468 let start = line_column_to_byte(source, line_starts, items[i - 1].span().end.end());
469 let end = line_column_to_byte(source, line_starts, items[i].span().start.start());
470 let gap_text = &source[start..end];
471 let (comment, blank_line_before, same_line) = analyze_gap(gap_text);
472 items[i].set_blank_line_before(blank_line_before);
473 if let Some(comment) = comment {
474 if same_line {
475 items[i - 1].set_trailing_line_comment(comment);
476 } else {
477 items[i].set_leading_comment(comment);
478 }
479 }
480 }
481}
482
483fn analyze_gap(gap: &str) -> (Option<crate::ast::Comment>, bool, bool) {
494 use crate::ast::Comment;
495 let mut lines: Vec<&str> = gap.lines().collect();
496 if !gap.ends_with('\n') && lines.last().is_some_and(|line| line.trim().is_empty()) {
506 lines.pop();
507 }
508 let mut comment = None;
509 if let Some(last) = lines.last() {
510 let trimmed = last.trim();
511 if let Some(text) = trimmed.strip_prefix("//") {
512 let mut collected = vec![text.trim().to_string()];
513 lines.pop();
514 while let Some(line) = lines.last() {
515 let trimmed = line.trim();
516 if let Some(text) = trimmed.strip_prefix("//") {
517 collected.push(text.trim().to_string());
518 lines.pop();
519 } else {
520 break;
521 }
522 }
523 collected.reverse();
524 comment = Some(Comment::Line(collected.join("\n")));
525 } else if let Some(inner) = trimmed
526 .strip_prefix("/*")
527 .and_then(|s| s.strip_suffix("*/"))
528 {
529 comment = Some(Comment::Block(inner.trim().to_string()));
531 lines.pop();
532 } else if trimmed.ends_with("*/") {
533 let mut collected = Vec::new();
537 let closing_prefix = trimmed
538 .strip_suffix("*/")
539 .expect("checked ends_with(\"*/\") above")
540 .trim();
541 if !closing_prefix.is_empty() {
542 collected.push(closing_prefix.to_string());
543 }
544 lines.pop();
545 let mut found_open = false;
546 while let Some(line) = lines.last() {
547 let trimmed = line.trim();
548 lines.pop();
549 if let Some(text) = trimmed.strip_prefix("/*") {
550 let opening_suffix = text.trim();
551 if !opening_suffix.is_empty() {
552 collected.push(opening_suffix.to_string());
553 }
554 found_open = true;
555 break;
556 }
557 collected.push(trimmed.to_string());
558 }
559 if found_open {
562 collected.reverse();
563 comment = Some(Comment::Block(collected.join("\n")));
564 }
565 }
566 }
567 let same_line = comment.is_some() && lines.is_empty();
573 let blank_line_before = lines.len() > 1 && lines[1..].iter().any(|l| l.trim().is_empty());
578 (comment, blank_line_before, same_line)
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use crate::AdamAstParser;
585
586 #[test]
587 fn attaches_a_line_comment_immediately_before_a_cell_decl() {
588 let source = "sheet s {\n cell a: i32 = 1;\n // the total\n cell b: i32 = 2;\n}";
589 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
590 attach_trivia(source, &mut sheet);
591 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
592 panic!("expected Cell");
593 };
594 assert_eq!(
595 b.leading_comment,
596 Some(crate::ast::Comment::Line("the total".to_string()))
597 );
598 }
599
600 #[test]
601 fn attaches_a_multi_line_comment_block() {
602 let source = "sheet s {\n cell a: i32 = 1;\n // line one\n // line two\n cell b: i32 = 2;\n}";
603 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
604 attach_trivia(source, &mut sheet);
605 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
606 panic!("expected Cell");
607 };
608 assert_eq!(
609 b.leading_comment,
610 Some(crate::ast::Comment::Line("line one\nline two".to_string()))
611 );
612 }
613
614 #[test]
615 fn attaches_a_single_line_block_comment() {
616 let source =
617 "sheet s {\n cell a: i32 = 1;\n /* the total */\n cell b: i32 = 2;\n}";
618 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
619 attach_trivia(source, &mut sheet);
620 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
621 panic!("expected Cell");
622 };
623 assert_eq!(
624 b.leading_comment,
625 Some(crate::ast::Comment::Block("the total".to_string()))
626 );
627 }
628
629 #[test]
630 fn attaches_a_multi_line_block_comment() {
631 let source = "sheet s {\n cell a: i32 = 1;\n /*\n line one\n line two\n */\n cell b: i32 = 2;\n}";
632 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
633 attach_trivia(source, &mut sheet);
634 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
635 panic!("expected Cell");
636 };
637 assert_eq!(
638 b.leading_comment,
639 Some(crate::ast::Comment::Block("line one\nline two".to_string()))
640 );
641 }
642
643 #[test]
644 fn attaches_the_issue_105_license_header_repro_as_a_block_comment() {
645 let source =
646 "/*\n Copyright 2013 Adobe\n ...\n*/\nsheet s {\n cell a: i32 = 1;\n}";
647 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
648 attach_trivia(source, &mut sheet);
649 assert_eq!(
650 sheet.leading_comment,
651 Some(crate::ast::Comment::Block(
652 "Copyright 2013 Adobe\n...".to_string()
653 ))
654 );
655 }
656
657 #[test]
658 fn a_line_comment_is_recovered_as_comment_line() {
659 let source = "sheet s {\n cell a: i32 = 1;\n // the total\n cell b: i32 = 2;\n}";
660 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
661 attach_trivia(source, &mut sheet);
662 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
663 panic!("expected Cell");
664 };
665 assert_eq!(
666 b.leading_comment,
667 Some(crate::ast::Comment::Line("the total".to_string()))
668 );
669 }
670
671 #[test]
672 fn does_not_attach_a_comment_separated_by_a_blank_line() {
673 let source =
674 "sheet s {\n cell a: i32 = 1;\n // stale comment\n\n cell b: i32 = 2;\n}";
675 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
676 attach_trivia(source, &mut sheet);
677 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
678 panic!("expected Cell");
679 };
680 assert_eq!(b.leading_comment, None);
681 }
682
683 #[test]
684 fn no_comment_in_the_gap_leaves_leading_comment_none() {
685 let source = "sheet s {\n cell a: i32 = 1;\n cell b: i32 = 2;\n}";
686 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
687 attach_trivia(source, &mut sheet);
688 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
689 panic!("expected Cell");
690 };
691 assert_eq!(b.leading_comment, None);
692 }
693
694 #[test]
695 fn attaches_comments_correctly_across_more_than_one_gap() {
696 let source = "sheet s {\n cell a: i32 = 1;\n // first\n cell b: i32 = 2;\n // second\n cell c: i32 = 3;\n}";
697 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
698 attach_trivia(source, &mut sheet);
699 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
700 panic!("expected Cell");
701 };
702 assert_eq!(
703 b.leading_comment,
704 Some(crate::ast::Comment::Line("first".to_string()))
705 );
706 let crate::ast::SheetItem::Cell(c) = &sheet.items[2] else {
707 panic!("expected Cell");
708 };
709 assert_eq!(
710 c.leading_comment,
711 Some(crate::ast::Comment::Line("second".to_string()))
712 );
713 }
714
715 #[test]
716 fn attaches_a_comment_preceding_a_recovered_error_item() {
717 let source = "sheet s {\n cell a: i32 = 1;\n // fix me\n cell bad unknown_syntax\n cell c: i32 = 2;\n}";
718 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
719 attach_trivia(source, &mut sheet);
720 let crate::ast::SheetItem::Error {
721 leading_comment, ..
722 } = &sheet.items[1]
723 else {
724 panic!("expected Error");
725 };
726 assert_eq!(
727 leading_comment,
728 &Some(crate::ast::Comment::Line("fix me".to_string()))
729 );
730 }
731
732 #[test]
733 fn recovery_span_that_abuts_the_next_keyword_does_not_invert_the_gap() {
734 let source = "sheet s { cell bad relationship { y := x; } }";
735 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
736 attach_trivia(source, &mut sheet); assert!(matches!(
738 sheet.items[0],
739 crate::ast::SheetItem::Error { .. }
740 ));
741 assert!(matches!(
742 sheet.items[1],
743 crate::ast::SheetItem::Relationship(_)
744 ));
745 }
746
747 #[test]
748 fn sets_blank_line_before_true_when_a_blank_line_separates_two_items() {
749 let source = "sheet s {\n cell a: i32 = 1;\n\n cell b: i32 = 2;\n}";
750 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
751 attach_trivia(source, &mut sheet);
752 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
753 panic!("expected Cell");
754 };
755 assert!(b.blank_line_before);
756 }
757
758 #[test]
759 fn sets_blank_line_before_false_when_items_are_packed_tight() {
760 let source = "sheet s {\n cell a: i32 = 1;\n cell b: i32 = 2;\n}";
761 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
762 attach_trivia(source, &mut sheet);
763 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
764 panic!("expected Cell");
765 };
766 assert!(!b.blank_line_before);
767 }
768
769 #[test]
770 fn a_run_of_several_blank_lines_still_just_sets_the_flag_true() {
771 let source = "sheet s {\n cell a: i32 = 1;\n\n\n\n cell b: i32 = 2;\n}";
772 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
773 attach_trivia(source, &mut sheet);
774 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
775 panic!("expected Cell");
776 };
777 assert!(b.blank_line_before);
778 }
779
780 #[test]
781 fn blank_line_before_an_attached_comment_still_sets_the_flag_true() {
782 let source = "sheet s {\n cell a: i32 = 1;\n\n // c\n cell b: i32 = 2;\n}";
786 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
787 attach_trivia(source, &mut sheet);
788 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
789 panic!("expected Cell");
790 };
791 assert_eq!(
792 b.leading_comment,
793 Some(crate::ast::Comment::Line("c".to_string()))
794 );
795 assert!(b.blank_line_before);
796 }
797
798 #[test]
799 fn attaches_a_comment_and_blank_line_to_a_binding_inside_a_relationship() {
800 let source = "sheet s {\n relationship {\n b := a;\n\n // second\n a := b;\n }\n}";
801 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
802 attach_trivia(source, &mut sheet);
803 let crate::ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
804 panic!("expected Relationship");
805 };
806 assert_eq!(
807 rel.bindings[1].leading_comment,
808 Some(crate::ast::Comment::Line("second".to_string()))
809 );
810 assert!(rel.bindings[1].blank_line_before);
811 }
812
813 #[test]
814 fn attaches_a_comment_to_a_conditional_branch() {
815 let source = "sheet s {\n conditional m {\n 0i32 => { relationship { b := a; } }\n // one\n 1i32 => { relationship { b := a; } }\n }\n}";
816 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
817 attach_trivia(source, &mut sheet);
818 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
819 panic!("expected Conditional");
820 };
821 assert_eq!(
822 cond.branches[1].leading_comment,
823 Some(crate::ast::Comment::Line("one".to_string()))
824 );
825 }
826
827 #[test]
828 fn attaches_a_comment_to_a_relationship_nested_inside_a_conditional_branch() {
829 let source = "sheet s {\n conditional m {\n 0i32 => {\n relationship { b := a; }\n // second\n relationship { a := b; }\n }\n }\n}";
830 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
831 attach_trivia(source, &mut sheet);
832 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
833 panic!("expected Conditional");
834 };
835 assert_eq!(
836 cond.branches[0].relationships[1].leading_comment,
837 Some(crate::ast::Comment::Line("second".to_string()))
838 );
839 }
840
841 #[test]
842 fn attaches_a_comment_to_a_relationship_nested_inside_the_default_branch() {
843 let source = "sheet s {\n conditional m {\n _ => {\n relationship { b := a; }\n // second\n relationship { a := b; }\n }\n }\n}";
844 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
845 attach_trivia(source, &mut sheet);
846 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
847 panic!("expected Conditional");
848 };
849 let default = cond.default.as_ref().expect("default branch present");
850 assert_eq!(
851 default.relationships[1].leading_comment,
852 Some(crate::ast::Comment::Line("second".to_string()))
853 );
854 }
855
856 #[test]
857 fn attaches_a_leading_comment_before_the_sheet_itself() {
858 let source = "// file header\nsheet s {\n cell a: i32 = 1;\n}";
859 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
860 attach_trivia(source, &mut sheet);
861 assert_eq!(
862 sheet.leading_comment,
863 Some(crate::ast::Comment::Line("file header".to_string()))
864 );
865 }
866
867 #[test]
868 fn attaches_a_multi_line_leading_comment_before_the_sheet_itself() {
869 let source = "// line one\n// line two\nsheet s {\n cell a: i32 = 1;\n}";
870 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
871 attach_trivia(source, &mut sheet);
872 assert_eq!(
873 sheet.leading_comment,
874 Some(crate::ast::Comment::Line("line one\nline two".to_string()))
875 );
876 }
877
878 #[test]
879 fn no_leading_comment_before_the_sheet_leaves_it_none() {
880 let source = "sheet s {\n cell a: i32 = 1;\n}";
881 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
882 attach_trivia(source, &mut sheet);
883 assert_eq!(sheet.leading_comment, None);
884 }
885
886 #[test]
887 fn attaches_a_comment_to_a_requirement_inside_an_out_declaration() {
888 let source = "sheet s {\n out area: f64 := width * height require {\n @max_area width * height <= max_area;\n // second\n @c width <= 10.0;\n };\n}";
898 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
899 attach_trivia(source, &mut sheet);
900 let crate::ast::SheetItem::Out(out) = &sheet.items[0] else {
901 panic!("expected Out");
902 };
903 let require = out.require.as_ref().expect("require block present");
904 assert_eq!(
905 require.requirements[1].leading_comment,
906 Some(crate::ast::Comment::Line("second".to_string()))
907 );
908 }
909
910 #[test]
911 fn recovers_a_trailing_comment_before_a_sheets_closing_brace() {
912 let source = "sheet s {\n cell a: i32 = 1;\n // trailing\n}";
913 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
914 attach_trivia(source, &mut sheet);
915 assert_eq!(
916 sheet.trailing_comment,
917 Some(crate::ast::Comment::Line("trailing".to_string()))
918 );
919 }
920
921 #[test]
922 fn recovers_a_trailing_comment_in_an_empty_relationship_block() {
923 let source = "sheet s {\n relationship {\n // only this\n }\n}";
924 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
925 attach_trivia(source, &mut sheet);
926 let crate::ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
927 panic!("expected Relationship");
928 };
929 assert_eq!(
930 rel.trailing_comment,
931 Some(crate::ast::Comment::Line("only this".to_string()))
932 );
933 }
934
935 #[test]
936 fn recovers_a_trailing_comment_before_a_relationships_closing_brace() {
937 let source =
938 "sheet s {\n relationship {\n b := a;\n // trailing\n }\n}";
939 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
940 attach_trivia(source, &mut sheet);
941 let crate::ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
942 panic!("expected Relationship");
943 };
944 assert_eq!(
945 rel.trailing_comment,
946 Some(crate::ast::Comment::Line("trailing".to_string()))
947 );
948 }
949
950 #[test]
951 fn recovers_a_trailing_comment_before_a_conditional_branchs_closing_brace() {
952 let source = "sheet s {\n conditional m {\n 0i32 => {\n relationship { b := a; }\n // trailing\n }\n }\n}";
953 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
954 attach_trivia(source, &mut sheet);
955 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
956 panic!("expected Conditional");
957 };
958 assert_eq!(
959 cond.branches[0].trailing_comment,
960 Some(crate::ast::Comment::Line("trailing".to_string()))
961 );
962 }
963
964 #[test]
965 fn recovers_a_trailing_comment_in_a_default_arm() {
966 let source = "sheet s {\n conditional m {\n _ => {\n relationship { b := a; }\n // trailing\n }\n }\n}";
967 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
968 attach_trivia(source, &mut sheet);
969 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
970 panic!("expected Conditional");
971 };
972 let default = cond.default.as_ref().expect("default branch present");
973 assert_eq!(
974 default.trailing_comment,
975 Some(crate::ast::Comment::Line("trailing".to_string()))
976 );
977 }
978
979 #[test]
980 fn recovers_a_trailing_comment_before_a_conditionals_own_closing_brace() {
981 let source = "sheet s {\n conditional m {\n 0i32 => { relationship { b := a; } }\n // trailing\n }\n}";
982 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
983 attach_trivia(source, &mut sheet);
984 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
985 panic!("expected Conditional");
986 };
987 assert_eq!(
988 cond.trailing_comment,
989 Some(crate::ast::Comment::Line("trailing".to_string()))
990 );
991 }
992
993 #[test]
1000 fn recovers_a_trailing_comment_before_a_requires_closing_brace() {
1001 let source = "sheet s {\n out area: f64 := w require {\n @c w <= 10.0;\n // trailing\n };\n}";
1002 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1003 attach_trivia(source, &mut sheet);
1004 let crate::ast::SheetItem::Out(out) = &sheet.items[0] else {
1005 panic!("expected Out");
1006 };
1007 let require = out.require.as_ref().expect("require block present");
1008 assert_eq!(
1009 require.trailing_comment,
1010 Some(crate::ast::Comment::Line("trailing".to_string()))
1011 );
1012 }
1013
1014 #[test]
1015 fn attaches_a_comment_to_a_requirement_inside_a_cell_declaration() {
1016 let source = "sheet s {\n cell a: i32 = 1 require {\n @r1 a > 0;\n // second\n @r2 a < 10;\n };\n}";
1020 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1021 attach_trivia(source, &mut sheet);
1022 let crate::ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1023 panic!("expected Cell");
1024 };
1025 let require = cell.require.as_ref().expect("require block present");
1026 assert_eq!(
1027 require.requirements[1].leading_comment,
1028 Some(crate::ast::Comment::Line("second".to_string()))
1029 );
1030 }
1031
1032 #[test]
1033 fn recovers_a_trailing_comment_before_a_cells_requires_closing_brace() {
1034 let source = "sheet s {\n cell a: i32 = 1 require {\n @r a > 0;\n // trailing\n };\n}";
1037 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1038 attach_trivia(source, &mut sheet);
1039 let crate::ast::SheetItem::Cell(cell) = &sheet.items[0] else {
1040 panic!("expected Cell");
1041 };
1042 let require = cell.require.as_ref().expect("require block present");
1043 assert_eq!(
1044 require.trailing_comment,
1045 Some(crate::ast::Comment::Line("trailing".to_string()))
1046 );
1047 }
1048
1049 #[test]
1050 fn attaches_a_comment_to_a_requirement_inside_a_source_declaration() {
1051 let source = "sheet s {\n source a: i32 = 1 require {\n @r1 a > 0;\n // second\n @r2 a < 10;\n };\n}";
1054 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1055 attach_trivia(source, &mut sheet);
1056 let crate::ast::SheetItem::Source(source_decl) = &sheet.items[0] else {
1057 panic!("expected Source");
1058 };
1059 let require = source_decl.require.as_ref().expect("require block present");
1060 assert_eq!(
1061 require.requirements[1].leading_comment,
1062 Some(crate::ast::Comment::Line("second".to_string()))
1063 );
1064 }
1065
1066 #[test]
1067 fn recovers_a_trailing_comment_before_a_sources_requires_closing_brace() {
1068 let source = "sheet s {\n source a: i32 = 1 require {\n @r a > 0;\n // trailing\n };\n}";
1071 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1072 attach_trivia(source, &mut sheet);
1073 let crate::ast::SheetItem::Source(source_decl) = &sheet.items[0] else {
1074 panic!("expected Source");
1075 };
1076 let require = source_decl.require.as_ref().expect("require block present");
1077 assert_eq!(
1078 require.trailing_comment,
1079 Some(crate::ast::Comment::Line("trailing".to_string()))
1080 );
1081 }
1082
1083 #[test]
1084 fn sets_blank_line_before_close_when_a_blank_line_precedes_the_closing_brace() {
1085 let source = "sheet s {\n cell a: i32 = 1;\n\n}";
1086 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1087 attach_trivia(source, &mut sheet);
1088 assert!(sheet.blank_line_before_close);
1089 }
1090
1091 #[test]
1092 fn no_trailing_comment_leaves_trailing_comment_none() {
1093 let source = "sheet s {\n cell a: i32 = 1;\n}";
1094 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1095 attach_trivia(source, &mut sheet);
1096 assert_eq!(sheet.trailing_comment, None);
1097 }
1098
1099 #[test]
1103 fn a_same_line_trailing_line_comment_attaches_to_the_previous_cell_not_the_next() {
1104 let source =
1105 "sheet s {\n cell a: f64 = 2.0; // end of line comment\n cell b: f64 = 3.0;\n}";
1106 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1107 attach_trivia(source, &mut sheet);
1108 let crate::ast::SheetItem::Cell(a) = &sheet.items[0] else {
1109 panic!("expected Cell");
1110 };
1111 assert_eq!(
1112 a.trailing_line_comment,
1113 Some(crate::ast::Comment::Line("end of line comment".to_string()))
1114 );
1115 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
1116 panic!("expected Cell");
1117 };
1118 assert_eq!(b.leading_comment, None);
1119 assert!(!b.blank_line_before);
1120 }
1121
1122 #[test]
1123 fn a_same_line_trailing_block_comment_attaches_to_the_previous_cell_not_the_next() {
1124 let source = "sheet s {\n cell a: f64 = 2.0; /* note */\n cell b: f64 = 3.0;\n}";
1125 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1126 attach_trivia(source, &mut sheet);
1127 let crate::ast::SheetItem::Cell(a) = &sheet.items[0] else {
1128 panic!("expected Cell");
1129 };
1130 assert_eq!(
1131 a.trailing_line_comment,
1132 Some(crate::ast::Comment::Block("note".to_string()))
1133 );
1134 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
1135 panic!("expected Cell");
1136 };
1137 assert_eq!(b.leading_comment, None);
1138 }
1139
1140 #[test]
1141 fn a_comment_on_its_own_line_still_attaches_as_a_leading_comment() {
1142 let source = "sheet s {\n cell a: i32 = 1;\n // the total\n cell b: i32 = 2;\n}";
1145 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1146 attach_trivia(source, &mut sheet);
1147 let crate::ast::SheetItem::Cell(a) = &sheet.items[0] else {
1148 panic!("expected Cell");
1149 };
1150 assert_eq!(a.trailing_line_comment, None);
1151 let crate::ast::SheetItem::Cell(b) = &sheet.items[1] else {
1152 panic!("expected Cell");
1153 };
1154 assert_eq!(
1155 b.leading_comment,
1156 Some(crate::ast::Comment::Line("the total".to_string()))
1157 );
1158 }
1159
1160 #[test]
1161 fn a_same_line_trailing_comment_attaches_to_a_binding_inside_a_relationship() {
1162 let source =
1163 "sheet s {\n relationship {\n b := a; // note\n a := b;\n }\n}";
1164 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1165 attach_trivia(source, &mut sheet);
1166 let crate::ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1167 panic!("expected Relationship");
1168 };
1169 assert_eq!(
1170 rel.bindings[0].trailing_line_comment,
1171 Some(crate::ast::Comment::Line("note".to_string()))
1172 );
1173 assert_eq!(rel.bindings[1].leading_comment, None);
1174 }
1175
1176 #[test]
1177 fn a_same_line_trailing_comment_before_a_sheets_closing_brace_attaches_to_the_last_item() {
1178 let source = "sheet s {\n cell a: i32 = 1; // note\n}";
1179 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1180 attach_trivia(source, &mut sheet);
1181 let crate::ast::SheetItem::Cell(a) = &sheet.items[0] else {
1182 panic!("expected Cell");
1183 };
1184 assert_eq!(
1185 a.trailing_line_comment,
1186 Some(crate::ast::Comment::Line("note".to_string()))
1187 );
1188 assert_eq!(sheet.trailing_comment, None);
1189 }
1190
1191 #[test]
1192 fn same_line_gap_with_no_newline_still_recovers_a_block_comment() {
1193 let source = "sheet s {\n cell a: i32 = 1; /* note */ cell b: i32 = 2;\n}";
1197 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1198 attach_trivia(source, &mut sheet);
1199 let crate::ast::SheetItem::Cell(a) = &sheet.items[0] else {
1200 panic!("expected Cell");
1201 };
1202 assert_eq!(
1203 a.trailing_line_comment,
1204 Some(crate::ast::Comment::Block("note".to_string()))
1205 );
1206 }
1207
1208 #[test]
1209 fn a_same_line_trailing_comment_before_a_relationships_closing_brace_attaches_to_the_last_binding()
1210 {
1211 let source = "sheet s {\n relationship {\n b := a; // note\n }\n}";
1212 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1213 attach_trivia(source, &mut sheet);
1214 let crate::ast::SheetItem::Relationship(rel) = &sheet.items[0] else {
1215 panic!("expected Relationship");
1216 };
1217 assert_eq!(
1218 rel.bindings[0].trailing_line_comment,
1219 Some(crate::ast::Comment::Line("note".to_string()))
1220 );
1221 assert_eq!(rel.trailing_comment, None);
1222 }
1223
1224 #[test]
1225 fn a_same_line_trailing_comment_before_a_conditional_branchs_own_closing_brace_attaches_to_the_last_relationship()
1226 {
1227 let source = "sheet s {\n conditional m {\n 0i32 => {\n relationship { b := a; } // note\n }\n }\n}";
1228 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1229 attach_trivia(source, &mut sheet);
1230 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1231 panic!("expected Conditional");
1232 };
1233 assert_eq!(
1234 cond.branches[0].relationships[0].trailing_line_comment,
1235 Some(crate::ast::Comment::Line("note".to_string()))
1236 );
1237 assert_eq!(cond.branches[0].trailing_comment, None);
1238 }
1239
1240 #[test]
1241 fn a_same_line_trailing_comment_before_a_default_arms_own_closing_brace_attaches_to_the_last_relationship()
1242 {
1243 let source = "sheet s {\n conditional m {\n _ => {\n relationship { b := a; } // note\n }\n }\n}";
1244 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1245 attach_trivia(source, &mut sheet);
1246 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1247 panic!("expected Conditional");
1248 };
1249 let default = cond.default.as_ref().expect("default branch present");
1250 assert_eq!(
1251 default.relationships[0].trailing_line_comment,
1252 Some(crate::ast::Comment::Line("note".to_string()))
1253 );
1254 assert_eq!(default.trailing_comment, None);
1255 }
1256
1257 #[test]
1258 fn a_same_line_trailing_comment_before_a_conditionals_own_closing_brace_with_only_branches_attaches_to_the_last_branch()
1259 {
1260 let source = "sheet s {\n conditional m {\n 0i32 => { relationship { b := a; } } // note\n }\n}";
1261 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1262 attach_trivia(source, &mut sheet);
1263 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1264 panic!("expected Conditional");
1265 };
1266 assert_eq!(
1267 cond.branches[0].trailing_line_comment,
1268 Some(crate::ast::Comment::Line("note".to_string()))
1269 );
1270 assert_eq!(cond.trailing_comment, None);
1271 }
1272
1273 #[test]
1274 fn a_same_line_trailing_comment_before_a_conditionals_own_closing_brace_with_a_default_attaches_to_the_default()
1275 {
1276 let source = "sheet s {\n conditional m {\n _ => { relationship { b := a; } } // note\n }\n}";
1277 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1278 attach_trivia(source, &mut sheet);
1279 let crate::ast::SheetItem::Conditional(cond) = &sheet.items[0] else {
1280 panic!("expected Conditional");
1281 };
1282 let default = cond.default.as_ref().expect("default branch present");
1283 assert_eq!(
1284 default.trailing_line_comment,
1285 Some(crate::ast::Comment::Line("note".to_string()))
1286 );
1287 assert_eq!(cond.trailing_comment, None);
1288 }
1289
1290 #[test]
1291 fn a_same_line_trailing_comment_before_a_requires_closing_brace_attaches_to_the_last_requirement()
1292 {
1293 let source =
1294 "sheet s {\n out area: f64 := w require {\n @c w <= 10.0; // note\n };\n}";
1295 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
1296 attach_trivia(source, &mut sheet);
1297 let crate::ast::SheetItem::Out(out) = &sheet.items[0] else {
1298 panic!("expected Out");
1299 };
1300 let require = out.require.as_ref().expect("require block present");
1301 assert_eq!(
1302 require.requirements[0].trailing_line_comment,
1303 Some(crate::ast::Comment::Line("note".to_string()))
1304 );
1305 assert_eq!(require.trailing_comment, None);
1306 }
1307}