1use crate::ast;
17
18fn output_column(out: &str) -> usize {
22 out.rsplit('\n').next().unwrap_or_default().chars().count()
23}
24
25fn write_expression(
29 out: &mut String,
30 source: &str,
31 expr: &cel_parser::Expr,
32 depth: usize,
33 suffix_columns: usize,
34) {
35 let text = cel_parser::format_expr_with_layout(
36 expr,
37 source,
38 depth,
39 output_column(out),
40 100,
41 suffix_columns,
42 );
43 append_expression_layout(out, &text);
44}
45
46fn append_expression_layout(out: &mut String, text: &str) {
50 if text.starts_with('\n') {
51 out.truncate(out.trim_end_matches(' ').len());
52 }
53 out.push_str(text);
54}
55
56fn cell_clause_suffix(
58 filter: Option<&ast::CellFilter>,
59 require: Option<&ast::RequireBlock>,
60) -> usize {
61 if filter.is_some() {
62 8
63 } else if require.is_some() {
64 10
65 } else {
66 1
67 }
68}
69
70fn indent(depth: usize) -> String {
72 " ".repeat(depth)
73}
74
75fn write_comment_inline(out: &mut String, comment: &ast::Comment, depth: usize) {
84 match comment {
85 ast::Comment::Line(text) => {
86 let mut lines = text.split('\n');
87 if let Some(first) = lines.next() {
88 out.push_str("// ");
89 out.push_str(first);
90 out.push('\n');
91 }
92 for line in lines {
93 out.push_str(&indent(depth));
94 out.push_str("// ");
95 out.push_str(line);
96 out.push('\n');
97 }
98 }
99 ast::Comment::Block(text) => {
100 if text.contains('\n') {
101 out.push_str("/*\n");
102 for line in text.split('\n') {
103 out.push_str(&indent(depth + 1));
104 out.push_str(line);
105 out.push('\n');
106 }
107 out.push_str(&indent(depth));
108 out.push_str("*/\n");
109 } else {
110 out.push_str("/* ");
111 out.push_str(text);
112 out.push_str(" */\n");
113 }
114 }
115 }
116}
117
118fn write_comment(out: &mut String, comment: &ast::Comment, depth: usize) {
121 out.push_str(&indent(depth));
122 write_comment_inline(out, comment, depth);
123}
124
125fn write_doc_comment(out: &mut String, marker: &str, doc_comment: Option<&str>, depth: usize) {
130 if let Some(doc) = doc_comment {
131 for line in doc.split('\n') {
132 out.push_str(&indent(depth));
133 out.push_str(marker);
134 out.push_str(line);
135 out.push('\n');
136 }
137 }
138}
139
140fn write_trivia(
142 out: &mut String,
143 blank_line_before: bool,
144 leading_comment: Option<&ast::Comment>,
145 depth: usize,
146) {
147 if blank_line_before {
148 out.push('\n');
149 }
150 if let Some(comment) = leading_comment {
151 write_comment(out, comment, depth);
152 }
153}
154
155fn write_trailing_trivia(
159 out: &mut String,
160 blank_line_before_close: bool,
161 trailing_comment: Option<&ast::Comment>,
162 depth: usize,
163) {
164 if blank_line_before_close {
165 out.push('\n');
166 }
167 if let Some(comment) = trailing_comment {
168 write_comment(out, comment, depth);
169 }
170}
171
172fn write_line_end(
177 out: &mut String,
178 terminator: &str,
179 trailing_line_comment: Option<&ast::Comment>,
180 depth: usize,
181) {
182 out.push_str(terminator);
183 match trailing_line_comment {
184 Some(comment) => {
185 out.push(' ');
186 write_comment_inline(out, comment, depth);
187 }
188 None => out.push('\n'),
189 }
190}
191
192fn source_text_or_empty(span: ast::ExprSpan) -> String {
196 span.start.source_text().unwrap_or_default()
197}
198
199fn point_span(span: proc_macro2::Span) -> ast::ExprSpan {
203 ast::ExprSpan {
204 start: span,
205 end: span,
206 }
207}
208
209fn emit_decl_gap(
230 source: &str,
231 prev_end: ast::ExprSpan,
232 next_start: ast::ExprSpan,
233 expected: &[&'static str],
234 depth: usize,
235) -> String {
236 use cel_parser::trivia::{GapPiece, line_column_to_byte, line_start_byte_offsets, scan_gap};
237 let pieces = if source.is_empty() {
240 scan_gap("", expected)
241 } else {
242 let line_starts = line_start_byte_offsets(source);
243 let start = line_column_to_byte(source, &line_starts, prev_end.end.end());
244 let end = line_column_to_byte(source, &line_starts, next_start.start.start());
245 match source.get(start..end) {
246 Some(gap) => scan_gap(gap, expected),
247 None => scan_gap("", expected),
248 }
249 };
250 let mut out = String::new();
251 for piece in pieces {
252 match piece {
253 GapPiece::Punct(tok) => {
254 if !out.ends_with(' ') {
255 out.push(' ');
256 }
257 out.push_str(tok);
258 out.push(' ');
259 }
260 GapPiece::Comment(cel_parser::Comment::Block(text)) => {
261 if !out.ends_with(' ') {
262 out.push(' ');
263 }
264 out.push_str("/* ");
265 out.push_str(&text);
266 out.push_str(" */");
267 }
268 GapPiece::Comment(cel_parser::Comment::Line(text)) => {
269 if !out.ends_with(' ') {
270 out.push(' ');
271 }
272 out.push_str("// ");
273 out.push_str(&text);
274 out.push('\n');
275 out.push_str(&indent(depth + 1));
276 }
277 }
278 }
279 if !expected.is_empty() && !out.is_empty() && !out.ends_with(' ') {
282 out.push(' ');
283 }
284 out
285}
286
287fn write_binding(out: &mut String, source: &str, binding: &ast::BindingDecl, depth: usize) {
292 write_trivia(
293 out,
294 binding.blank_line_before,
295 binding.leading_comment.as_ref(),
296 depth,
297 );
298 out.push_str(&indent(depth));
299 if binding.destructure {
300 out.push('(');
301 for (i, (name, _)) in binding.outputs.iter().enumerate() {
302 if i > 0 {
303 out.push_str(", ");
304 }
305 out.push_str(name);
306 }
307 if binding.outputs.len() == 1 {
308 out.push(',');
309 }
310 out.push(')');
311 } else {
312 debug_assert_eq!(
313 binding.outputs.len(),
314 1,
315 "a non-destructuring binding names exactly one output"
316 );
317 out.push_str(&binding.outputs[0].0);
318 }
319 let op_start = binding
321 .outputs
322 .last()
323 .map(|(_, span)| *span)
324 .unwrap_or(binding.span);
325 out.push_str(&emit_decl_gap(
326 source,
327 op_start,
328 binding.body.span(),
329 &[":="],
330 depth,
331 ));
332 write_expression(out, source, &binding.body, depth, 1);
333 out.push_str(&emit_decl_gap(
335 source,
336 binding.body.span(),
337 point_span(binding.span.end),
338 &[],
339 depth,
340 ));
341 write_line_end(out, ";", binding.trailing_line_comment.as_ref(), depth);
342}
343
344fn write_relationship(out: &mut String, source: &str, rel: &ast::RelationshipDecl, depth: usize) {
346 write_trivia(
347 out,
348 rel.blank_line_before,
349 rel.leading_comment.as_ref(),
350 depth,
351 );
352 write_doc_comment(out, "///", rel.doc_comment.as_deref(), depth);
353 out.push_str(&indent(depth));
354 out.push_str("relationship");
355 out.push_str(&emit_decl_gap(
358 source,
359 point_span(rel.span.start),
360 rel.open_brace_span,
361 &[],
362 depth,
363 ));
364 out.push_str(" {\n");
365 for binding in &rel.bindings {
366 write_binding(out, source, binding, depth + 1);
367 }
368 write_trailing_trivia(
369 out,
370 rel.blank_line_before_close,
371 rel.trailing_comment.as_ref(),
372 depth + 1,
373 );
374 out.push_str(&indent(depth));
375 write_line_end(out, "}", rel.trailing_line_comment.as_ref(), depth);
376}
377
378fn write_branch_relationships(
381 out: &mut String,
382 source: &str,
383 relationships: &[ast::RelationshipDecl],
384 trailing_comment: Option<&ast::Comment>,
385 blank_line_before_close: bool,
386 line_trailing_comment: Option<&ast::Comment>,
387 depth: usize,
388) {
389 out.push_str("{\n");
390 for rel in relationships {
391 write_relationship(out, source, rel, depth + 1);
392 }
393 write_trailing_trivia(out, blank_line_before_close, trailing_comment, depth + 1);
394 out.push_str(&indent(depth));
395 write_line_end(out, "}", line_trailing_comment, depth);
396}
397
398fn write_branch(out: &mut String, source: &str, branch: &ast::ConditionalBranch, depth: usize) {
402 write_trivia(
403 out,
404 branch.blank_line_before,
405 branch.leading_comment.as_ref(),
406 depth,
407 );
408 out.push_str(&indent(depth));
409 if branch.negated {
410 out.push('-');
411 }
412 out.push_str(&source_text_or_empty(branch.literal_span));
413 out.push_str(&emit_decl_gap(
416 source,
417 branch.literal_span,
418 branch.open_brace_span,
419 &["=>"],
420 depth,
421 ));
422 write_branch_relationships(
423 out,
424 source,
425 &branch.relationships,
426 branch.trailing_comment.as_ref(),
427 branch.blank_line_before_close,
428 branch.trailing_line_comment.as_ref(),
429 depth,
430 );
431}
432
433fn write_conditional(out: &mut String, source: &str, cond: &ast::ConditionalDecl, depth: usize) {
440 write_trivia(
441 out,
442 cond.blank_line_before,
443 cond.leading_comment.as_ref(),
444 depth,
445 );
446 write_doc_comment(out, "///", cond.doc_comment.as_deref(), depth);
447 out.push_str(&indent(depth));
448 out.push_str("conditional ");
449 let header = cel_parser::format_expr_header_with_layout(
450 &cond.match_expr,
451 source,
452 depth,
453 (cond.span.start.end(), cond.open_brace_span.start.start()),
454 output_column(out),
455 100,
456 2,
457 );
458 append_expression_layout(out, &header);
459 out.push_str(" {\n");
460 for branch in &cond.branches {
461 write_branch(out, source, branch, depth + 1);
462 }
463 if let Some(default) = &cond.default {
464 out.push_str(&indent(depth + 1));
465 out.push('_');
466 out.push_str(&emit_decl_gap(
468 source,
469 point_span(default.span.start),
470 default.open_brace_span,
471 &["=>"],
472 depth + 1,
473 ));
474 write_branch_relationships(
475 out,
476 source,
477 &default.relationships,
478 default.trailing_comment.as_ref(),
479 default.blank_line_before_close,
480 default.trailing_line_comment.as_ref(),
481 depth + 1,
482 );
483 }
484 write_trailing_trivia(
485 out,
486 cond.blank_line_before_close,
487 cond.trailing_comment.as_ref(),
488 depth + 1,
489 );
490 out.push_str(&indent(depth));
491 write_line_end(out, "}", cond.trailing_line_comment.as_ref(), depth);
492}
493
494fn write_cell(out: &mut String, source: &str, cell: &ast::CellDecl, depth: usize) {
500 write_trivia(
501 out,
502 cell.blank_line_before,
503 cell.leading_comment.as_ref(),
504 depth,
505 );
506 write_doc_comment(out, "///", cell.doc_comment.as_deref(), depth);
507 out.push_str(&indent(depth));
508 out.push_str("cell ");
509 out.push_str(&cell.name);
510 if let Some(type_expr) = &cell.type_name {
511 out.push_str(": ");
512 out.push_str(&source_text_or_empty(type_expr.span()));
513 }
514 if let Some(expr) = &cell.initializer {
516 let op_start = cell
517 .type_name
518 .as_ref()
519 .map(ast::TypeExpr::span)
520 .unwrap_or(cell.name_span);
521 out.push_str(&emit_decl_gap(source, op_start, expr.span(), &["="], depth));
522 write_expression(
523 out,
524 source,
525 expr,
526 depth,
527 cell_clause_suffix(cell.filter.as_ref(), cell.require.as_ref()),
528 );
529 }
530 write_cell_clauses(
531 out,
532 source,
533 depth,
534 cell.initializer.as_ref().map(cel_parser::Expr::span),
535 cell.type_name.as_ref().map(ast::TypeExpr::span),
536 cell.name_span,
537 cell.filter.as_ref(),
538 cell.require.as_ref(),
539 cell.span.end,
540 );
541 write_line_end(out, ";", cell.trailing_line_comment.as_ref(), depth);
542}
543
544#[allow(clippy::too_many_arguments)]
551fn write_cell_clauses(
552 out: &mut String,
553 source: &str,
554 depth: usize,
555 initializer_span: Option<ast::ExprSpan>,
556 type_span: Option<ast::ExprSpan>,
557 name_span: ast::ExprSpan,
558 filter: Option<&ast::CellFilter>,
559 require: Option<&ast::RequireBlock>,
560 terminator: proc_macro2::Span,
561) {
562 let mut prev = initializer_span.or(type_span).unwrap_or(name_span);
563 if let Some(filter) = filter {
564 out.push_str(&emit_decl_gap(
567 source,
568 prev,
569 filter.body.span(),
570 &["filter"],
571 depth,
572 ));
573 write_expression(
574 out,
575 source,
576 &filter.body,
577 depth,
578 if require.is_some() { 10 } else { 1 },
579 );
580 prev = filter.body.span();
581 }
582 if let Some(require) = require {
583 out.push_str(&emit_decl_gap(
587 source,
588 prev,
589 require.open_brace_span,
590 &["require"],
591 depth,
592 ));
593 write_require_block(out, source, require, depth);
594 prev = require.span;
595 }
596 out.push_str(&emit_decl_gap(
598 source,
599 prev,
600 point_span(terminator),
601 &[],
602 depth,
603 ));
604}
605
606fn write_requirement(out: &mut String, source: &str, req: &ast::RequirementDecl, depth: usize) {
610 write_trivia(
611 out,
612 req.blank_line_before,
613 req.leading_comment.as_ref(),
614 depth,
615 );
616 out.push_str(&indent(depth));
617 if let Some(name) = &req.name {
618 out.push('@');
619 out.push_str(name);
620 if let Some(name_span) = req.name_span {
622 out.push_str(&emit_decl_gap(
623 source,
624 name_span,
625 req.body.span(),
626 &[],
627 depth,
628 ));
629 }
630 out.push(' ');
631 }
632 write_expression(out, source, &req.body, depth, 1);
633 write_line_end(out, ";", req.trailing_line_comment.as_ref(), depth);
634}
635
636fn write_require_block(out: &mut String, source: &str, require: &ast::RequireBlock, depth: usize) {
641 out.push_str("{\n");
642 for req in &require.requirements {
643 write_requirement(out, source, req, depth + 1);
644 }
645 write_trailing_trivia(
646 out,
647 require.blank_line_before_close,
648 require.trailing_comment.as_ref(),
649 depth + 1,
650 );
651 out.push_str(&indent(depth));
652 out.push('}');
653}
654
655fn write_out(out: &mut String, source: &str, decl: &ast::OutDecl, depth: usize) {
659 write_trivia(
660 out,
661 decl.blank_line_before,
662 decl.leading_comment.as_ref(),
663 depth,
664 );
665 write_doc_comment(out, "///", decl.doc_comment.as_deref(), depth);
666 out.push_str(&indent(depth));
667 out.push_str("out ");
668 out.push_str(&decl.name);
669 if let Some(type_expr) = &decl.type_name {
670 out.push_str(": ");
671 out.push_str(&source_text_or_empty(type_expr.span()));
672 }
673 let op_start = decl
675 .type_name
676 .as_ref()
677 .map(ast::TypeExpr::span)
678 .unwrap_or(decl.name_span);
679 out.push_str(&emit_decl_gap(
680 source,
681 op_start,
682 decl.initializer.span(),
683 &[":="],
684 depth,
685 ));
686 write_expression(
687 out,
688 source,
689 &decl.initializer,
690 depth,
691 cell_clause_suffix(decl.filter.as_ref(), decl.require.as_ref()),
692 );
693 write_cell_clauses(
694 out,
695 source,
696 depth,
697 Some(decl.initializer.span()),
698 decl.type_name.as_ref().map(ast::TypeExpr::span),
699 decl.name_span,
700 decl.filter.as_ref(),
701 decl.require.as_ref(),
702 decl.span.end,
703 );
704 write_line_end(out, ";", decl.trailing_line_comment.as_ref(), depth);
705}
706
707fn write_source(out: &mut String, source: &str, decl: &ast::SourceDecl, depth: usize) {
712 write_trivia(
713 out,
714 decl.blank_line_before,
715 decl.leading_comment.as_ref(),
716 depth,
717 );
718 write_doc_comment(out, "///", decl.doc_comment.as_deref(), depth);
719 out.push_str(&indent(depth));
720 out.push_str("source ");
721 out.push_str(&decl.name);
722 if let Some(type_expr) = &decl.type_name {
723 out.push_str(": ");
724 out.push_str(&source_text_or_empty(type_expr.span()));
725 }
726 if let Some(expr) = &decl.initializer {
728 let op_start = decl
729 .type_name
730 .as_ref()
731 .map(ast::TypeExpr::span)
732 .unwrap_or(decl.name_span);
733 out.push_str(&emit_decl_gap(source, op_start, expr.span(), &["="], depth));
734 write_expression(
735 out,
736 source,
737 expr,
738 depth,
739 cell_clause_suffix(decl.filter.as_ref(), decl.require.as_ref()),
740 );
741 }
742 write_cell_clauses(
743 out,
744 source,
745 depth,
746 decl.initializer.as_ref().map(cel_parser::Expr::span),
747 decl.type_name.as_ref().map(ast::TypeExpr::span),
748 decl.name_span,
749 decl.filter.as_ref(),
750 decl.require.as_ref(),
751 decl.span.end,
752 );
753 write_line_end(out, ";", decl.trailing_line_comment.as_ref(), depth);
754}
755
756fn write_sheet_item(out: &mut String, source: &str, item: &ast::SheetItem, depth: usize) {
761 match item {
762 ast::SheetItem::Cell(cell) => write_cell(out, source, cell, depth),
763 ast::SheetItem::Relationship(rel) => write_relationship(out, source, rel, depth),
764 ast::SheetItem::Conditional(cond) => write_conditional(out, source, cond, depth),
765 ast::SheetItem::Out(out_decl) => write_out(out, source, out_decl, depth),
766 ast::SheetItem::Source(decl) => write_source(out, source, decl, depth),
767 ast::SheetItem::Error { .. } => {
768 unreachable!("format_sheet is only called on a sheet with no recorded syntax errors")
769 }
770 }
771}
772
773pub fn format_sheet(sheet: &ast::Sheet, source: &str) -> String {
793 debug_assert!(
794 sheet.errors.is_empty(),
795 "format_sheet's precondition: no recorded syntax errors"
796 );
797 let mut out = String::new();
798 write_trivia(&mut out, false, sheet.leading_comment.as_ref(), 0);
799 write_doc_comment(&mut out, "//!", sheet.doc_comment.as_deref(), 0);
800 out.push_str(&format!("sheet {} {{\n", sheet.name));
801 for item in &sheet.items {
802 write_sheet_item(&mut out, source, item, 1);
803 }
804 write_trailing_trivia(
805 &mut out,
806 sheet.blank_line_before_close,
807 sheet.trailing_comment.as_ref(),
808 1,
809 );
810 out.push_str("}\n");
811 out
812}
813
814#[derive(Debug)]
816pub enum FormatSourceError {
817 Parse(cel_parser::ParseError),
819 Recovered(Vec<cel_parser::ParseError>),
821}
822
823impl std::fmt::Display for FormatSourceError {
824 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
825 match self {
826 FormatSourceError::Parse(error) => write!(f, "{error}"),
827 FormatSourceError::Recovered(errors) => {
828 for (i, error) in errors.iter().enumerate() {
829 if i > 0 {
830 writeln!(f)?;
831 }
832 write!(f, "{error}")?;
833 }
834 Ok(())
835 }
836 }
837 }
838}
839
840impl std::error::Error for FormatSourceError {}
841
842pub fn format_source(source: &str) -> Result<String, FormatSourceError> {
864 let mut sheet = crate::AdamAstParser::new()
865 .parse_str(source)
866 .map_err(FormatSourceError::Parse)?;
867 if !sheet.errors.is_empty() {
868 return Err(FormatSourceError::Recovered(sheet.errors));
869 }
870 crate::attach_trivia(source, &mut sheet);
871 Ok(format_sheet(&sheet, source))
872}
873
874#[cfg(test)]
875mod tests {
876 #[test]
878 fn output_column_tracks_the_active_line() {
879 for (text, expected) in [
880 ("", 0),
881 ("cell é = ", 9),
882 ("old prefix\n := ", 7),
883 ("line\n", 0),
884 ] {
885 assert_eq!(super::output_column(text), expected);
886 }
887 }
888
889 #[test]
891 fn grouped_conditional_headers_preserve_comments() {
892 for header in [
893 "(Sample { value: 1.0 }) /* first */",
894 "/* first */ (Sample { value: 1.0 })",
895 "(/* first */ Sample { value: 1.0 } /* second */) /* third */",
896 "(/* first */ (/* second */ Sample { value: 1.0 }) /* third */) /* fourth */",
897 "(Sample { value: 1.0 }) /* first /* nested ) */ end */",
898 "(/* first /* nested ( */ end */ Sample { value: 1.0 }) /* second */",
899 "((Sample { value: 1.0 }) /* first */) /* second */",
900 "(// first\n Sample { value: 1.0 } // second\n) // third\n",
901 "// first\n (/* second */ Sample { value: 1.0 }) // third\n",
902 "(/* first */ p) /* second */",
903 "(/* first */ (Sample { value: 1.0 }).value) /* second */",
904 ] {
905 let source = format!("sheet s {{ conditional {header} {{ _ => {{}} }} }}");
906 let formatted = super::format_source(&source).unwrap();
907 let mut previous = 0;
908 for label in ["first", "second", "third", "fourth"] {
909 if source.contains(label) {
910 assert_eq!(
911 formatted.matches(label).count(),
912 1,
913 "{source} -> {formatted}"
914 );
915 let position = formatted.find(label).unwrap();
916 assert!(position >= previous, "{formatted}");
917 previous = position;
918 }
919 }
920 assert_eq!(super::format_source(&formatted).unwrap(), formatted);
921 }
922 }
923
924 use super::*;
925 use crate::AdamAstParser;
926
927 fn format(source: &str) -> String {
928 let mut sheet = AdamAstParser::new().parse_str(source).unwrap();
929 crate::attach_trivia(source, &mut sheet);
930 format_sheet(&sheet, source)
931 }
932
933 #[test]
934 fn formats_an_empty_sheet() {
935 assert_eq!(format("sheet s {}"), "sheet s {\n}\n");
936 }
937
938 #[test]
939 fn preserves_a_leading_comment_before_the_sheet_itself() {
940 assert_eq!(
941 format("// file header\nsheet s { cell x: i32 = 1; }"),
942 "// file header\nsheet s {\n cell x: i32 = 1;\n}\n"
943 );
944 }
945
946 #[test]
947 fn no_leading_comment_before_the_sheet_emits_nothing_extra() {
948 assert_eq!(
949 format("sheet s { cell x: i32 = 1; }"),
950 "sheet s {\n cell x: i32 = 1;\n}\n"
951 );
952 }
953
954 #[test]
955 fn formats_a_cell_with_type_and_initializer() {
956 assert_eq!(
957 format("sheet s { cell width: f64 = 1920.0; }"),
958 "sheet s {\n cell width: f64 = 1920.0;\n}\n"
959 );
960 }
961
962 #[test]
963 fn formats_a_source_decl() {
964 assert_eq!(
965 format("sheet s { source width: i32 = 4; }"),
966 "sheet s {\n source width: i32 = 4;\n}\n"
967 );
968 }
969
970 #[test]
971 fn formats_a_cell_with_only_a_type_annotation() {
972 assert_eq!(
973 format("sheet s { cell area: f64; }"),
974 "sheet s {\n cell area: f64;\n}\n"
975 );
976 }
977
978 #[test]
979 fn formats_a_cell_with_only_an_initializer() {
980 assert_eq!(
981 format("sheet s { cell mode = 0i32; }"),
982 "sheet s {\n cell mode = 0i32;\n}\n"
983 );
984 }
985
986 #[test]
987 fn packed_cells_stay_packed_and_a_blank_line_before_a_relationship_is_preserved() {
988 let source = "sheet s {\n cell a: i32 = 1;\n cell b: i32 = 2;\n\n relationship { b := a; }\n}";
989 let expected = "sheet s {\n cell a: i32 = 1;\n cell b: i32 = 2;\n\n relationship {\n b := a;\n }\n}\n";
990 assert_eq!(format(source), expected);
991 }
992
993 #[test]
994 fn a_run_of_blank_lines_collapses_to_one() {
995 let source = "sheet s {\n cell a: i32 = 1;\n\n\n\n cell b: i32 = 2;\n}";
996 let expected = "sheet s {\n cell a: i32 = 1;\n\n cell b: i32 = 2;\n}\n";
997 assert_eq!(format(source), expected);
998 }
999
1000 #[test]
1001 fn formats_a_relationship_with_multiple_bindings() {
1002 let source = "sheet s {\n relationship {\n area := width * height;\n width := area / height;\n }\n}";
1003 let expected = "sheet s {\n relationship {\n area := width * height;\n width := area / height;\n }\n}\n";
1004 assert_eq!(format(source), expected);
1005 }
1006
1007 #[test]
1008 fn formats_a_multi_output_destructuring_binding_with_parens() {
1009 let source =
1010 "sheet s {\n relationship {\n (sum, diff) := (a + b, a - b);\n }\n}";
1011 let expected =
1012 "sheet s {\n relationship {\n (sum, diff) := (a + b, a - b);\n }\n}\n";
1013 assert_eq!(format(source), expected);
1014 }
1015
1016 #[test]
1017 fn formats_a_single_element_tuple_destructuring_binding_with_a_trailing_comma() {
1018 let source = "sheet s {\n relationship {\n (x,) := (a,);\n }\n}";
1019 let expected = "sheet s {\n relationship {\n (x,) := (a,);\n }\n}\n";
1020 assert_eq!(format(source), expected);
1021 }
1022
1023 #[test]
1024 fn drops_redundant_grouping_parens_from_a_non_destructuring_binding() {
1025 let source = "sheet s {\n relationship {\n (x) := a;\n }\n}";
1026 let expected = "sheet s {\n relationship {\n x := a;\n }\n}\n";
1027 assert_eq!(format(source), expected);
1028 }
1029
1030 #[test]
1031 fn preserves_a_comment_on_a_nested_binding() {
1032 let source = "sheet s {\n relationship {\n b := a;\n\n // second\n a := b;\n }\n}";
1033 let expected = "sheet s {\n relationship {\n b := a;\n\n // second\n a := b;\n }\n}\n";
1034 assert_eq!(format(source), expected);
1035 }
1036
1037 #[test]
1038 fn formats_a_conditional_with_branches_and_a_default_and_no_trailing_commas() {
1039 let source = "sheet s {\n conditional p {\n 0i32 => { relationship { b := a; } },\n _ => { relationship { a := b; } },\n }\n}";
1040 let expected = "sheet s {\n conditional p {\n 0i32 => {\n relationship {\n b := a;\n }\n }\n _ => {\n relationship {\n a := b;\n }\n }\n }\n}\n";
1041 assert_eq!(format(source), expected);
1042 }
1043
1044 #[test]
1045 fn formats_a_conditional_branch_with_a_negated_literal_key() {
1046 let source = "sheet s {\n conditional p {\n -1i32 => { relationship { b := a; } },\n _ => { relationship { a := b; } },\n }\n}";
1047 let expected = "sheet s {\n conditional p {\n -1i32 => {\n relationship {\n b := a;\n }\n }\n _ => {\n relationship {\n a := b;\n }\n }\n }\n}\n";
1048 assert_eq!(format(source), expected);
1049 }
1050
1051 #[test]
1052 fn formats_a_conditional_with_an_expression_match_subject() {
1053 let source = "sheet s {\n conditional a && b {\n _ => { relationship { d := c; } },\n }\n}";
1054 let expected = "sheet s {\n conditional a && b {\n _ => {\n relationship {\n d := c;\n }\n }\n }\n}\n";
1055 assert_eq!(format(source), expected);
1056 }
1057
1058 #[test]
1059 fn preserves_a_comment_on_a_conditional_branch() {
1060 let source = "sheet s {\n conditional m {\n 0i32 => { relationship { b := a; } }\n // one\n 1i32 => { relationship { b := a; } }\n }\n}";
1061 let expected = "sheet s {\n conditional m {\n 0i32 => {\n relationship {\n b := a;\n }\n }\n // one\n 1i32 => {\n relationship {\n b := a;\n }\n }\n }\n}\n";
1062 assert_eq!(format(source), expected);
1063 }
1064
1065 #[test]
1066 fn binding_body_delegates_precedence_aware_parenthesization_to_cel_parser() {
1067 let source = "sheet s { relationship { c := (a + b) * 2i32; } }";
1068 let expected = "sheet s {\n relationship {\n c := (a + b) * 2i32;\n }\n}\n";
1069 assert_eq!(format(source), expected);
1070 }
1071
1072 #[test]
1073 fn format_is_idempotent_through_a_reparse() {
1074 let source = "sheet demo {\n cell a: f64 = 2.0;\n cell b: f64 = 3.0;\n\n relationship {\n c := a * b;\n }\n}";
1075 let once = format(source);
1076 let twice = format(&once);
1077 assert_eq!(once, twice);
1078 }
1079
1080 #[test]
1081 fn format_is_idempotent_through_a_reparse_with_a_conditional() {
1082 let source = "sheet demo {\n cell p: i32 = 0;\n cell c: f64;\n\n conditional p {\n 0i32 => {\n relationship {\n c := c;\n }\n }\n _ => {\n relationship {\n c := c;\n }\n }\n }\n}";
1083 let once = format(source);
1084 let twice = format(&once);
1085 assert_eq!(once, twice);
1086 }
1087
1088 #[test]
1089 fn formats_an_out_with_explicit_type_and_no_requirements() {
1090 let source = "sheet s {\n out area: f64 := width * height;\n}";
1091 let expected = "sheet s {\n out area: f64 := width * height;\n}\n";
1092 assert_eq!(format(source), expected);
1093 }
1094
1095 #[test]
1096 fn formats_an_out_with_no_type_annotation() {
1097 let source = "sheet s {\n out area := width;\n}";
1098 let expected = "sheet s {\n out area := width;\n}\n";
1099 assert_eq!(format(source), expected);
1100 }
1101
1102 #[test]
1103 fn formats_an_out_with_requirements_in_declaration_order() {
1104 let source = "sheet s {\n out area: f64 := width * height require {\n @max_area width * height <= max_area;\n };\n}";
1105 let expected = "sheet s {\n out area: f64 := width * height require {\n @max_area width * height <= max_area;\n };\n}\n";
1106 assert_eq!(format(source), expected);
1107 }
1108
1109 #[test]
1110 fn formats_a_cell_with_a_require_block() {
1111 let source =
1112 "sheet s {\n cell x: i32 = 5 require {\n @positive x > 0;\n };\n}";
1113 assert_eq!(format(source), format!("{source}\n"));
1114 }
1115
1116 #[test]
1117 fn formats_a_source_with_a_require_block() {
1118 let source =
1119 "sheet s {\n source x: i32 = 5 require {\n @positive x > 0;\n };\n}";
1120 assert_eq!(format(source), format!("{source}\n"));
1121 }
1122
1123 #[test]
1124 fn formats_a_source_with_a_filter_clause() {
1125 let source = "sheet s {\n source x: i32 = 5 filter 0..=10;\n}";
1126 assert_eq!(format(source), format!("{source}\n"));
1127 }
1128
1129 #[test]
1130 fn formats_an_out_with_a_filter_clause() {
1131 let source =
1132 "sheet s {\n cell width: i32 = 4;\n out area := width filter 0..=100;\n}";
1133 assert_eq!(format(source), format!("{source}\n"));
1134 }
1135
1136 #[test]
1137 fn formats_a_cell_with_an_explicit_tuple_type() {
1138 assert_eq!(
1139 format("sheet s { cell a: (i32, f64) = (1, 2.5); }"),
1140 "sheet s {\n cell a: (i32, f64) = (1, 2.5);\n}\n"
1141 );
1142 }
1143
1144 #[test]
1145 fn formats_a_cell_with_a_nested_tuple_type() {
1146 assert_eq!(
1147 format("sheet s { cell a: (i32, (f64, String)); }"),
1148 "sheet s {\n cell a: (i32, (f64, String));\n}\n"
1149 );
1150 }
1151
1152 #[test]
1153 fn formats_an_out_with_an_explicit_tuple_type() {
1154 assert_eq!(
1155 format("sheet s { out a: (i32, i32) := (x, x); }"),
1156 "sheet s {\n out a: (i32, i32) := (x, x);\n}\n"
1157 );
1158 }
1159
1160 #[test]
1161 fn format_is_idempotent_through_a_reparse_with_a_tuple_cell() {
1162 let source = "sheet s {\n cell a: (i32, f64) = (1, 2.5);\n}";
1163 let once = format(source);
1164 let twice = format(&once);
1165 assert_eq!(once, twice);
1166 }
1167
1168 #[test]
1169 fn formats_a_single_line_block_comment_preserving_its_style() {
1170 let source =
1177 "sheet s {\n cell a: i32 = 1;\n /* the total */\n cell x: i32 = 1;\n}";
1178 let expected =
1179 "sheet s {\n cell a: i32 = 1;\n /* the total */\n cell x: i32 = 1;\n}\n";
1180 assert_eq!(format(source), expected);
1181 }
1182
1183 #[test]
1184 fn formats_a_multi_line_block_comment_preserving_its_style() {
1185 let source = "sheet s {\n cell a: i32 = 1;\n /*\n line one\n line two\n */\n cell x: i32 = 1;\n}";
1188 let expected = "sheet s {\n cell a: i32 = 1;\n /*\n line one\n line two\n */\n cell x: i32 = 1;\n}\n";
1189 assert_eq!(format(source), expected);
1190 }
1191
1192 #[test]
1193 fn formats_the_issue_105_license_header_repro_without_dropping_it() {
1194 let source =
1195 "/*\n Copyright 2013 Adobe\n ...\n*/\nsheet s {\n cell a: i32 = 1;\n}";
1196 let expected =
1197 "/*\n Copyright 2013 Adobe\n ...\n*/\nsheet s {\n cell a: i32 = 1;\n}\n";
1198 assert_eq!(format(source), expected);
1199 }
1200
1201 #[test]
1202 fn block_comment_formatting_is_idempotent_through_a_reparse() {
1203 let source = "sheet s {\n cell a: i32 = 1;\n /*\n line one\n line two\n */\n cell x: i32 = 1;\n}";
1208 let once = format(source);
1209 let twice = format(&once);
1210 assert_eq!(once, twice);
1211 }
1212
1213 #[test]
1214 fn formats_a_cell_with_a_doc_comment() {
1215 assert_eq!(
1216 format("sheet s { /// the total\n cell x: i32 = 1; }"),
1217 "sheet s {\n /// the total\n cell x: i32 = 1;\n}\n"
1218 );
1219 }
1220
1221 #[test]
1222 fn formats_a_sheet_level_doc_comment() {
1223 assert_eq!(
1224 format("//! module docs\nsheet s { cell x: i32 = 1; }"),
1225 "//! module docs\nsheet s {\n cell x: i32 = 1;\n}\n"
1226 );
1227 }
1228
1229 #[test]
1230 fn formats_a_plain_comment_and_doc_comment_together_in_source_order() {
1231 let source =
1236 "sheet s {\n cell w: i32 = 0;\n // TODO\n /// docs\n cell x: i32 = 1;\n}";
1237 let expected =
1238 "sheet s {\n cell w: i32 = 0;\n // TODO\n /// docs\n cell x: i32 = 1;\n}\n";
1239 assert_eq!(format(source), expected);
1240 }
1241
1242 #[test]
1243 fn formats_doc_comments_on_a_relationship_conditional_and_out() {
1244 let source = "sheet s {\n /// r\n relationship { b := a; }\n\n /// o\n out area: f64 := w;\n}";
1245 let expected = "sheet s {\n /// r\n relationship {\n b := a;\n }\n\n /// o\n out area: f64 := w;\n}\n";
1246 assert_eq!(format(source), expected);
1247 }
1248
1249 #[test]
1250 fn doc_comment_formatting_is_idempotent_through_a_reparse() {
1251 let source = "sheet s {\n /// the total\n cell x: i32 = 1;\n}";
1252 let once = format(source);
1253 let twice = format(&once);
1254 assert_eq!(once, twice);
1255 }
1256
1257 #[test]
1258 fn formats_a_trailing_comment_before_the_sheets_closing_brace() {
1259 assert_eq!(
1260 format("sheet s {\n cell x: i32 = 1;\n // trailing\n}"),
1261 "sheet s {\n cell x: i32 = 1;\n // trailing\n}\n"
1262 );
1263 }
1264
1265 #[test]
1266 fn formats_a_trailing_comment_in_an_empty_relationship() {
1267 assert_eq!(
1268 format("sheet s {\n relationship {\n // only this\n }\n}"),
1269 "sheet s {\n relationship {\n // only this\n }\n}\n"
1270 );
1271 }
1272
1273 #[test]
1274 fn formats_a_trailing_comment_before_a_relationships_closing_brace() {
1275 let source =
1276 "sheet s {\n relationship {\n b := a;\n // trailing\n }\n}";
1277 let expected =
1278 "sheet s {\n relationship {\n b := a;\n // trailing\n }\n}\n";
1279 assert_eq!(format(source), expected);
1280 }
1281
1282 #[test]
1283 fn formats_a_trailing_comment_before_a_conditionals_own_closing_brace() {
1284 let source = "sheet s {\n conditional m {\n 0i32 => { relationship { b := a; } }\n // trailing\n }\n}";
1285 let expected = "sheet s {\n conditional m {\n 0i32 => {\n relationship {\n b := a;\n }\n }\n // trailing\n }\n}\n";
1286 assert_eq!(format(source), expected);
1287 }
1288
1289 #[test]
1290 fn formats_a_trailing_comment_in_a_default_arm() {
1291 let source = "sheet s {\n conditional m {\n _ => {\n relationship { b := a; }\n // trailing\n }\n }\n}";
1292 let expected = "sheet s {\n conditional m {\n _ => {\n relationship {\n b := a;\n }\n // trailing\n }\n }\n}\n";
1293 assert_eq!(format(source), expected);
1294 }
1295
1296 #[test]
1297 fn formats_a_trailing_comment_before_a_requires_closing_brace() {
1298 let source = "sheet s {\n out area: f64 := w require {\n @c w <= 10.0;\n // trailing\n };\n}";
1299 let expected = "sheet s {\n out area: f64 := w require {\n @c w <= 10.0;\n // trailing\n };\n}\n";
1300 assert_eq!(format(source), expected);
1301 }
1302
1303 #[test]
1304 fn formats_a_trailing_comment_before_a_cells_requires_closing_brace() {
1305 let source = "sheet s {\n cell a: i32 = 1 require {\n @r a > 0;\n // trailing\n };\n}";
1310 let expected = "sheet s {\n cell a: i32 = 1 require {\n @r a > 0;\n // trailing\n };\n}\n";
1311 assert_eq!(format(source), expected);
1312 }
1313
1314 #[test]
1315 fn formats_a_trailing_comment_before_a_sources_requires_closing_brace() {
1316 let source = "sheet s {\n source a: i32 = 1 require {\n @r a > 0;\n // trailing\n };\n}";
1319 let expected = "sheet s {\n source a: i32 = 1 require {\n @r a > 0;\n // trailing\n };\n}\n";
1320 assert_eq!(format(source), expected);
1321 }
1322
1323 #[test]
1324 fn trailing_trivia_formatting_is_idempotent_through_a_reparse() {
1325 let source = "sheet s {\n cell a: i32 = 1;\n // trailing\n}";
1326 let once = format(source);
1327 let twice = format(&once);
1328 assert_eq!(once, twice);
1329 }
1330
1331 #[test]
1332 fn formats_a_cell_with_a_filter() {
1333 assert_eq!(
1334 format("sheet s { cell a: i32 = 1 filter _; }"),
1335 "sheet s {\n cell a: i32 = 1 filter _;\n}\n"
1336 );
1337 }
1338
1339 #[test]
1340 fn formats_a_cell_with_a_filter_referencing_a_cell() {
1341 assert_eq!(
1342 format("sheet s { cell hi: i32 = 100; cell a: i32 = 1 filter min(_, hi); }"),
1343 "sheet s {\n cell hi: i32 = 100;\n cell a: i32 = 1 filter min(_, hi);\n}\n"
1344 );
1345 }
1346
1347 #[test]
1348 fn format_is_idempotent_through_a_reparse_with_a_filter() {
1349 let source = "sheet s {\n cell a: i32 = 1 filter _;\n}";
1350 let once = format(source);
1351 let twice = format(&once);
1352 assert_eq!(once, twice);
1353 }
1354
1355 #[test]
1356 fn formats_a_filter() {
1357 let source = "sheet s { cell x: i32 = 0 filter 0..=10; }";
1358 let sheet = AdamAstParser::new().parse_str(source).unwrap();
1359 assert_eq!(
1360 format_sheet(&sheet, source),
1361 "sheet s {\n cell x: i32 = 0 filter 0..=10;\n}\n"
1362 );
1363 }
1364
1365 #[test]
1366 fn formats_a_requirement_with_no_label() {
1367 let source = "sheet s {\n cell x: i32 = 5 require {\n x > 0;\n };\n}";
1368 assert_eq!(format(source), format!("{source}\n"));
1369 }
1370
1371 #[test]
1372 fn format_source_formats_valid_source() {
1373 assert_eq!(
1374 format_source("sheet s{cell x:i32=1;}").unwrap(),
1375 "sheet s {\n cell x: i32 = 1;\n}\n"
1376 );
1377 }
1378
1379 #[test]
1380 fn format_source_errors_on_source_that_fails_to_parse() {
1381 assert!(matches!(
1382 format_source("not a sheet at all"),
1383 Err(FormatSourceError::Parse(_))
1384 ));
1385 }
1386
1387 #[test]
1388 fn format_source_errors_on_a_recovered_syntax_error() {
1389 assert!(matches!(
1390 format_source("sheet s { cell x unknown_syntax }"),
1391 Err(FormatSourceError::Recovered(_))
1392 ));
1393 }
1394
1395 #[test]
1399 fn formats_a_same_line_trailing_line_comment_on_a_cell() {
1400 let source =
1401 "sheet s {\n cell a: f64 = 2.0; // end of line comment\n cell b: f64 = 3.0;\n}";
1402 let expected =
1403 "sheet s {\n cell a: f64 = 2.0; // end of line comment\n cell b: f64 = 3.0;\n}\n";
1404 assert_eq!(format(source), expected);
1405 }
1406
1407 #[test]
1408 fn formats_a_same_line_trailing_block_comment_on_a_cell() {
1409 let source = "sheet s {\n cell a: f64 = 2.0; /* note */\n cell b: f64 = 3.0;\n}";
1410 let expected = "sheet s {\n cell a: f64 = 2.0; /* note */\n cell b: f64 = 3.0;\n}\n";
1411 assert_eq!(format(source), expected);
1412 }
1413
1414 #[test]
1415 fn formats_a_same_line_trailing_comment_on_the_last_item_before_the_sheets_closing_brace() {
1416 let source = "sheet s {\n cell a: i32 = 1; // note\n}";
1417 let expected = "sheet s {\n cell a: i32 = 1; // note\n}\n";
1418 assert_eq!(format(source), expected);
1419 }
1420
1421 #[test]
1422 fn formats_a_same_line_trailing_comment_on_a_binding() {
1423 let source =
1424 "sheet s {\n relationship {\n b := a; // note\n a := b;\n }\n}";
1425 let expected =
1426 "sheet s {\n relationship {\n b := a; // note\n a := b;\n }\n}\n";
1427 assert_eq!(format(source), expected);
1428 }
1429
1430 #[test]
1431 fn formats_a_same_line_block_comment_when_the_gap_has_no_newline_at_all() {
1432 let source = "sheet s {\n cell a: i32 = 1; /* note */ cell b: i32 = 2;\n}";
1436 let expected = "sheet s {\n cell a: i32 = 1; /* note */\n cell b: i32 = 2;\n}\n";
1437 assert_eq!(format(source), expected);
1438 }
1439
1440 #[test]
1441 fn formats_a_same_line_trailing_comment_before_a_conditional_branchs_own_closing_brace() {
1442 let source = "sheet s {\n conditional m {\n 0i32 => {\n relationship { b := a; } // note\n }\n }\n}";
1443 let expected = "sheet s {\n conditional m {\n 0i32 => {\n relationship {\n b := a;\n } // note\n }\n }\n}\n";
1444 assert_eq!(format(source), expected);
1445 }
1446
1447 #[test]
1448 fn formats_a_same_line_trailing_comment_before_a_default_arms_own_closing_brace() {
1449 let source = "sheet s {\n conditional m {\n _ => {\n relationship { b := a; } // note\n }\n }\n}";
1450 let expected = "sheet s {\n conditional m {\n _ => {\n relationship {\n b := a;\n } // note\n }\n }\n}\n";
1451 assert_eq!(format(source), expected);
1452 }
1453
1454 #[test]
1455 fn formats_a_same_line_trailing_comment_before_a_conditionals_own_closing_brace_with_only_branches()
1456 {
1457 let source = "sheet s {\n conditional m {\n 0i32 => { relationship { b := a; } } // note\n }\n}";
1458 let expected = "sheet s {\n conditional m {\n 0i32 => {\n relationship {\n b := a;\n }\n } // note\n }\n}\n";
1459 assert_eq!(format(source), expected);
1460 }
1461
1462 #[test]
1463 fn formats_a_same_line_trailing_comment_before_a_conditionals_own_closing_brace_with_a_default()
1464 {
1465 let source = "sheet s {\n conditional m {\n _ => { relationship { b := a; } } // note\n }\n}";
1466 let expected = "sheet s {\n conditional m {\n _ => {\n relationship {\n b := a;\n }\n } // note\n }\n}\n";
1467 assert_eq!(format(source), expected);
1468 }
1469
1470 #[test]
1471 fn formats_a_same_line_trailing_comment_before_a_requires_closing_brace() {
1472 let source =
1473 "sheet s {\n out area: f64 := w require {\n @c w <= 10.0; // note\n };\n}";
1474 let expected = "sheet s {\n out area: f64 := w require {\n @c w <= 10.0; // note\n };\n}\n";
1475 assert_eq!(format(source), expected);
1476 }
1477
1478 #[test]
1479 fn same_line_trailing_comment_formatting_is_idempotent_through_a_reparse() {
1480 let source =
1481 "sheet s {\n cell a: f64 = 2.0; // end of line comment\n cell b: f64 = 3.0;\n}";
1482 let once = format(source);
1483 let twice = format(&once);
1484 assert_eq!(once, twice);
1485 }
1486
1487 #[test]
1492 fn preserves_a_block_comment_between_a_cell_initializer_and_its_semicolon() {
1493 let source = "sheet s {\n cell a: i32 = 1 /* trailing */;\n}";
1494 assert_eq!(
1495 format(source),
1496 "sheet s {\n cell a: i32 = 1 /* trailing */;\n}\n"
1497 );
1498 }
1499
1500 #[test]
1501 fn preserves_a_block_comment_between_a_type_and_the_equals() {
1502 let source = "sheet s {\n cell a: i32 /* t */ = 1;\n}";
1503 assert_eq!(
1504 format(source),
1505 "sheet s {\n cell a: i32 /* t */ = 1;\n}\n"
1506 );
1507 }
1508
1509 #[test]
1510 fn a_declaration_with_no_intra_token_comments_is_unchanged() {
1511 assert_eq!(
1512 format("sheet s { cell a: i32 = 1; }"),
1513 "sheet s {\n cell a: i32 = 1;\n}\n"
1514 );
1515 }
1516
1517 #[test]
1518 fn a_cell_initializer_with_leading_inline_and_trailing_comments_round_trips_losslessly() {
1519 let source = "sheet s {\n cell a: i32 = /* p */ 1 /* q */ + 2 /* r */;\n}";
1520 let once = format_source(source).unwrap();
1521 for needle in ["/* p */", "/* q */", "/* r */"] {
1523 assert!(once.contains(needle), "comment {needle} lost: {once:?}");
1524 }
1525 let twice = format_source(&once).unwrap();
1527 assert_eq!(once, twice);
1528 }
1529
1530 #[test]
1531 fn preserves_a_line_comment_between_a_cell_initializer_and_its_semicolon() {
1532 let source = "sheet s {\n cell a: i32 = 1 // c\n ;\n}";
1536 let once = format_source(source).unwrap();
1537 assert!(once.contains("// c"), "comment must survive: {once:?}");
1538 assert!(
1539 once.trim_end().ends_with('}'),
1540 "the sheet's own closing brace must not have been commented out: {once:?}"
1541 );
1542 let twice = format_source(&once).unwrap();
1544 assert_eq!(once, twice, "formatting must be idempotent");
1545 }
1546
1547 #[test]
1548 fn preserves_a_line_comment_between_a_type_and_the_equals() {
1549 let source = "sheet s {\n cell a: i32 // c\n = 1;\n}";
1550 let once = format_source(source).unwrap();
1551 assert!(once.contains("// c"), "comment must survive: {once:?}");
1552 let twice = format_source(&once).unwrap();
1553 assert_eq!(once, twice, "formatting must be idempotent");
1554 }
1555
1556 #[test]
1557 fn preserves_comment_order_after_the_equals() {
1558 let source = "sheet s {\n cell a: i32 = /* after */ 1;\n}";
1559 let once = format_source(source).unwrap();
1560 let eq = once.find('=').expect("has =");
1561 let c = once.find("/* after */").expect("comment survives");
1562 assert!(c > eq, "comment must stay AFTER the =, got: {once:?}");
1563 }
1564
1565 #[test]
1566 fn preserves_comment_order_around_the_equals() {
1567 let source = "sheet s {\n cell a: i32 /* before */ = /* after */ 1;\n}";
1568 let once = format_source(source).unwrap();
1569 let before = once.find("/* before */").expect("before survives");
1570 let eq = once.find('=').expect("has =");
1571 let after = once.find("/* after */").expect("after survives");
1572 assert!(before < eq && eq < after, "order lost: {once:?}");
1573 }
1574
1575 #[test]
1576 fn preserves_a_comment_before_the_filter_keyword() {
1577 let source = "sheet s {\n cell a: i32 = 1 /* keep */ filter 0..=10;\n}";
1578 let once = format_source(source).unwrap();
1579 assert!(once.contains("/* keep */"), "comment dropped: {once:?}");
1580 }
1581
1582 #[test]
1583 fn preserves_a_comment_after_the_filter_body() {
1584 let source = "sheet s {\n cell a: i32 = 1 filter 0..=10 /* keep */;\n}";
1585 let once = format_source(source).unwrap();
1586 assert!(once.contains("/* keep */"), "comment dropped: {once:?}");
1587 }
1588
1589 #[test]
1590 fn preserves_a_comment_before_the_require_keyword() {
1591 let source = "sheet s {\n out area: f64 := w /* keep */ require {\n @c w <= 10.0;\n };\n}";
1592 let once = format_source(source).unwrap();
1593 assert!(once.contains("/* keep */"), "comment dropped: {once:?}");
1594 }
1595
1596 fn assert_comment_survives_and_is_idempotent(source: &str) {
1600 let once = format_source(source).unwrap();
1601 assert!(once.contains("/* keep */"), "comment dropped: {once:?}");
1602 let twice = format_source(&once).unwrap();
1603 assert_eq!(once, twice, "formatting must be idempotent: {once:?}");
1604 }
1605
1606 #[test]
1607 fn preserves_a_comment_between_the_filter_keyword_and_body() {
1608 assert_comment_survives_and_is_idempotent(
1609 "sheet s {\n cell a: i32 = 1 filter /* keep */ 0..=10;\n}",
1610 );
1611 }
1612
1613 #[test]
1614 fn preserves_a_comment_between_the_require_keyword_and_brace() {
1615 assert_comment_survives_and_is_idempotent(
1616 "sheet s {\n out area: f64 := w require /* keep */ {\n @c w <= 10.0;\n };\n}",
1617 );
1618 }
1619
1620 #[test]
1621 fn preserves_a_comment_between_a_branch_literal_and_the_fat_arrow() {
1622 assert_comment_survives_and_is_idempotent(
1623 "sheet s {\n conditional p {\n 0i32 /* keep */ => { relationship { b := a; } }\n }\n}",
1624 );
1625 }
1626
1627 #[test]
1628 fn preserves_a_comment_between_the_fat_arrow_and_a_branch_body() {
1629 assert_comment_survives_and_is_idempotent(
1630 "sheet s {\n conditional p {\n 0i32 => /* keep */ { relationship { b := a; } }\n }\n}",
1631 );
1632 }
1633
1634 #[test]
1635 fn preserves_a_comment_between_a_requirement_label_and_body() {
1636 assert_comment_survives_and_is_idempotent(
1637 "sheet s {\n out area: f64 := w require {\n @c /* keep */ w <= 10.0;\n };\n}",
1638 );
1639 }
1640
1641 #[test]
1642 fn preserves_a_comment_between_a_conditional_subject_and_its_brace() {
1643 assert_comment_survives_and_is_idempotent(
1644 "sheet s {\n conditional p /* keep */ {\n 0i32 => { relationship { b := a; } }\n }\n}",
1645 );
1646 }
1647
1648 #[test]
1649 fn preserves_a_comment_between_the_relationship_keyword_and_its_brace() {
1650 assert_comment_survives_and_is_idempotent(
1651 "sheet s {\n relationship /* keep */ {\n b := a;\n }\n}",
1652 );
1653 }
1654}