1use styx_cst::{
7 AstNode, Document, Entry, NodeOrToken, Object, Separator, Sequence, SyntaxKind, SyntaxNode,
8};
9
10use crate::FormatOptions;
11
12pub fn format_cst(node: &SyntaxNode, options: FormatOptions) -> String {
16 let mut formatter = CstFormatter::new(options);
17 formatter.format_node(node);
18 formatter.finish()
19}
20
21pub fn format_source(source: &str, options: FormatOptions) -> String {
26 let parsed = styx_cst::parse(source);
27 if !parsed.is_ok() {
28 return source.to_string();
30 }
31 format_cst(&parsed.syntax(), options)
32}
33
34struct CstFormatter {
35 out: String,
36 options: FormatOptions,
37 indent_level: usize,
38 at_line_start: bool,
40 after_newline: bool,
42}
43
44impl CstFormatter {
45 fn new(options: FormatOptions) -> Self {
46 Self {
47 out: String::new(),
48 options,
49 indent_level: 0,
50 at_line_start: true,
51 after_newline: false,
52 }
53 }
54
55 fn finish(mut self) -> String {
56 if !self.out.ends_with('\n') && !self.out.is_empty() {
58 self.out.push('\n');
59 }
60 self.out
61 }
62
63 fn write_indent(&mut self) {
64 if self.at_line_start && self.indent_level > 0 {
65 for _ in 0..self.indent_level {
66 self.out.push_str(self.options.indent);
67 }
68 }
69 self.at_line_start = false;
70 }
71
72 fn write(&mut self, s: &str) {
73 if s.is_empty() {
74 return;
75 }
76 self.write_indent();
77 self.out.push_str(s);
78 self.after_newline = false;
79 }
80
81 fn write_newline(&mut self) {
82 self.out.push('\n');
83 self.at_line_start = true;
84 self.after_newline = true;
85 }
86
87 fn format_node(&mut self, node: &SyntaxNode) {
88 match node.kind() {
89 SyntaxKind::DOCUMENT => self.format_document(node),
91 SyntaxKind::ENTRY => self.format_entry(node),
92 SyntaxKind::OBJECT => self.format_object(node),
93 SyntaxKind::SEQUENCE => self.format_sequence(node),
94 SyntaxKind::KEY => self.format_key(node),
95 SyntaxKind::VALUE => self.format_value(node),
96 SyntaxKind::SCALAR => self.format_scalar(node),
97 SyntaxKind::TAG => self.format_tag(node),
98 SyntaxKind::TAG_PAYLOAD => self.format_tag_payload(node),
99 SyntaxKind::UNIT => self.write("@"),
100 SyntaxKind::HEREDOC => self.format_heredoc(node),
101 SyntaxKind::ATTRIBUTES => self.format_attributes(node),
102 SyntaxKind::ATTRIBUTE => self.format_attribute(node),
103
104 SyntaxKind::L_BRACE
106 | SyntaxKind::R_BRACE
107 | SyntaxKind::L_PAREN
108 | SyntaxKind::R_PAREN
109 | SyntaxKind::COMMA
110 | SyntaxKind::GT
111 | SyntaxKind::AT
112 | SyntaxKind::TAG_TOKEN
113 | SyntaxKind::BARE_SCALAR
114 | SyntaxKind::QUOTED_SCALAR
115 | SyntaxKind::RAW_SCALAR
116 | SyntaxKind::HEREDOC_START
117 | SyntaxKind::HEREDOC_CONTENT
118 | SyntaxKind::HEREDOC_END
119 | SyntaxKind::LINE_COMMENT
120 | SyntaxKind::DOC_COMMENT
121 | SyntaxKind::WHITESPACE
122 | SyntaxKind::NEWLINE
123 | SyntaxKind::EOF
124 | SyntaxKind::ERROR
125 | SyntaxKind::__LAST_TOKEN
126 | SyntaxKind::TAG_NAME => {
127 }
130 }
131 }
132
133 fn format_document(&mut self, node: &SyntaxNode) {
134 let doc = Document::cast(node.clone()).unwrap();
135 let entries: Vec<_> = doc.entries().collect();
136
137 let mut consecutive_newlines = 0;
139 let mut entry_index = 0;
140 let mut wrote_content = false;
141 let mut just_wrote_doc_comment = false;
143
144 for el in node.children_with_tokens() {
145 match el.kind() {
146 SyntaxKind::NEWLINE => {
147 consecutive_newlines += 1;
148 }
149 SyntaxKind::WHITESPACE => {
150 }
152 SyntaxKind::LINE_COMMENT => {
153 if let Some(token) = el.into_token() {
154 if wrote_content {
155 self.write_newline();
156 if consecutive_newlines >= 2 {
158 self.write_newline();
159 }
160 }
161 self.write(token.text());
162 wrote_content = true;
163 consecutive_newlines = 0;
164 just_wrote_doc_comment = false;
165 }
166 }
167 SyntaxKind::DOC_COMMENT => {
168 if let Some(token) = el.into_token() {
169 if wrote_content {
170 self.write_newline();
171
172 if !just_wrote_doc_comment {
180 let had_blank_line = consecutive_newlines >= 2;
181 let prev_was_schema =
182 entry_index == 1 && is_schema_declaration(&entries[0]);
183 let prev_had_doc = entry_index > 0
184 && entries[entry_index - 1].doc_comments().next().is_some();
185 let prev_is_block =
186 entry_index > 0 && is_block_entry(&entries[entry_index - 1]);
187
188 if had_blank_line
189 || prev_was_schema
190 || prev_had_doc
191 || prev_is_block
192 {
193 self.write_newline();
194 }
195 }
196 }
197 self.write(token.text());
198 wrote_content = true;
199 consecutive_newlines = 0;
200 just_wrote_doc_comment = true;
201 }
202 }
203 SyntaxKind::ENTRY => {
204 if let Some(entry_node) = el.into_node() {
205 let entry = &entries[entry_index];
206
207 if wrote_content {
208 self.write_newline();
209
210 if !just_wrote_doc_comment {
216 let had_blank_line = consecutive_newlines >= 2;
217 let prev_was_schema =
218 entry_index == 1 && is_schema_declaration(&entries[0]);
219 let prev_had_doc = entry_index > 0
220 && entries[entry_index - 1].doc_comments().next().is_some();
221 let prev_is_block =
222 entry_index > 0 && is_block_entry(&entries[entry_index - 1]);
223 let current_is_block = is_block_entry(entry);
224
225 if had_blank_line
226 || prev_was_schema
227 || prev_had_doc
228 || prev_is_block
229 || current_is_block
230 {
231 self.write_newline();
232 }
233 }
234 }
235
236 self.format_node(&entry_node);
237 wrote_content = true;
238 consecutive_newlines = 0;
239 entry_index += 1;
240 just_wrote_doc_comment = false;
241 }
242 }
243 _ => {
244 }
246 }
247 }
248 }
249
250 fn format_entry(&mut self, node: &SyntaxNode) {
251 let entry = Entry::cast(node.clone()).unwrap();
252
253 if let Some(key) = entry.key() {
254 self.format_node(key.syntax());
255 }
256
257 if entry.value().is_some() {
259 self.write(" ");
260 }
261
262 if let Some(value) = entry.value() {
263 self.format_node(value.syntax());
264 }
265 }
266
267 fn format_object(&mut self, node: &SyntaxNode) {
268 let obj = Object::cast(node.clone()).unwrap();
269 let entries: Vec<_> = obj.entries().collect();
270 let separator = obj.separator();
271
272 self.write("{");
273
274 let has_comments = node.children_with_tokens().any(|el| {
276 matches!(
277 el.kind(),
278 SyntaxKind::LINE_COMMENT | SyntaxKind::DOC_COMMENT
279 )
280 });
281
282 if entries.is_empty() && !has_comments {
284 self.write("}");
285 return;
286 }
287
288 let has_block_child = entries.iter().any(|e| contains_block_object(e.syntax()));
290
291 let is_multiline = matches!(separator, Separator::Newline | Separator::Mixed)
293 || has_comments
294 || has_block_child
295 || entries.is_empty(); if is_multiline {
298 self.write_newline();
300 self.indent_level += 1;
301
302 let mut wrote_content = false;
305 let mut consecutive_newlines = 0;
306 for el in node.children_with_tokens() {
307 match el.kind() {
308 SyntaxKind::NEWLINE => {
309 consecutive_newlines += 1;
310 }
311 SyntaxKind::LINE_COMMENT | SyntaxKind::DOC_COMMENT => {
312 if let Some(token) = el.into_token() {
313 if wrote_content {
314 self.write_newline();
315 if consecutive_newlines >= 2 {
317 self.write_newline();
318 }
319 }
320 self.write(token.text());
321 wrote_content = true;
322 consecutive_newlines = 0;
323 }
324 }
325 SyntaxKind::ENTRY => {
326 if let Some(entry_node) = el.into_node() {
327 if wrote_content {
328 self.write_newline();
329 if consecutive_newlines >= 2 {
331 self.write_newline();
332 }
333 }
334 self.format_node(&entry_node);
335 wrote_content = true;
336 consecutive_newlines = 0;
337 }
338 }
339 SyntaxKind::WHITESPACE | SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
342 _ => {
343 consecutive_newlines = 0;
344 }
345 }
346 }
347
348 self.write_newline();
349 self.indent_level -= 1;
350 self.write("}");
351 } else {
352 for (i, entry) in entries.iter().enumerate() {
354 self.format_node(entry.syntax());
355
356 if i < entries.len() - 1 {
357 self.write(", ");
358 }
359 }
360 self.write("}");
361 }
362 }
363
364 fn format_sequence(&mut self, node: &SyntaxNode) {
365 let seq = Sequence::cast(node.clone()).unwrap();
366 let entries: Vec<_> = seq.entries().collect();
367
368 self.write("(");
369
370 let has_comments = node.children_with_tokens().any(|el| {
372 matches!(
373 el.kind(),
374 SyntaxKind::LINE_COMMENT | SyntaxKind::DOC_COMMENT
375 )
376 });
377
378 if entries.is_empty() && !has_comments {
380 self.write(")");
381 return;
382 }
383
384 let should_collapse =
387 !has_comments && entries.len() == 1 && !contains_block_object(entries[0].syntax());
388
389 let single_tag_with_block =
392 !has_comments && entries.len() == 1 && is_tag_with_block_payload(entries[0].syntax());
393
394 let is_multiline = !should_collapse
395 && !single_tag_with_block
396 && (seq.is_multiline() || has_comments || entries.is_empty());
397
398 if single_tag_with_block {
399 if let Some(key) = entries[0]
401 .syntax()
402 .children()
403 .find(|n| n.kind() == SyntaxKind::KEY)
404 {
405 for child in key.children() {
406 self.format_node(&child);
407 }
408 }
409 self.write(")");
410 } else if is_multiline {
411 self.write_newline();
413 self.indent_level += 1;
414
415 let mut wrote_content = false;
417 let mut consecutive_newlines = 0;
418 for el in node.children_with_tokens() {
419 match el.kind() {
420 SyntaxKind::NEWLINE => {
421 consecutive_newlines += 1;
422 }
423 SyntaxKind::LINE_COMMENT | SyntaxKind::DOC_COMMENT => {
424 if let Some(token) = el.into_token() {
425 if wrote_content {
426 self.write_newline();
427 if consecutive_newlines >= 2 {
429 self.write_newline();
430 }
431 }
432 self.write(token.text());
433 wrote_content = true;
434 consecutive_newlines = 0;
435 }
436 }
437 SyntaxKind::ENTRY => {
438 if let Some(entry_node) = el.into_node() {
439 if wrote_content {
440 self.write_newline();
441 if consecutive_newlines >= 2 {
443 self.write_newline();
444 }
445 }
446 if let Some(key) =
448 entry_node.children().find(|n| n.kind() == SyntaxKind::KEY)
449 {
450 for child in key.children() {
451 self.format_node(&child);
452 }
453 }
454 wrote_content = true;
455 consecutive_newlines = 0;
456 }
457 }
458 SyntaxKind::WHITESPACE | SyntaxKind::L_PAREN | SyntaxKind::R_PAREN => {}
460 _ => {
461 consecutive_newlines = 0;
462 }
463 }
464 }
465
466 self.write_newline();
467 self.indent_level -= 1;
468 self.write(")");
469 } else {
470 for (i, entry) in entries.iter().enumerate() {
472 if let Some(key) = entry
474 .syntax()
475 .children()
476 .find(|n| n.kind() == SyntaxKind::KEY)
477 {
478 for child in key.children() {
479 self.format_node(&child);
480 }
481 }
482
483 if i < entries.len() - 1 {
484 self.write(" ");
485 }
486 }
487 self.write(")");
488 }
489 }
490
491 fn format_key(&mut self, node: &SyntaxNode) {
492 for child in node.children() {
494 self.format_node(&child);
495 }
496
497 for token in node.children_with_tokens().filter_map(|el| el.into_token()) {
499 match token.kind() {
500 SyntaxKind::BARE_SCALAR | SyntaxKind::QUOTED_SCALAR | SyntaxKind::RAW_SCALAR => {
501 self.write(token.text());
502 }
503 _ => {}
504 }
505 }
506 }
507
508 fn format_value(&mut self, node: &SyntaxNode) {
509 for child in node.children() {
510 self.format_node(&child);
511 }
512 }
513
514 fn format_scalar(&mut self, node: &SyntaxNode) {
515 for token in node.children_with_tokens().filter_map(|el| el.into_token()) {
517 match token.kind() {
518 SyntaxKind::BARE_SCALAR | SyntaxKind::QUOTED_SCALAR | SyntaxKind::RAW_SCALAR => {
519 self.write(token.text());
520 }
521 _ => {}
522 }
523 }
524 }
525
526 fn format_tag(&mut self, node: &SyntaxNode) {
527 for el in node.children_with_tokens() {
531 match el {
532 NodeOrToken::Token(token) if token.kind() == SyntaxKind::TAG_TOKEN => {
533 self.write(token.text());
535 }
536 NodeOrToken::Node(child) if child.kind() == SyntaxKind::TAG_PAYLOAD => {
537 self.format_tag_payload(&child);
538 }
539 _ => {}
540 }
541 }
542 }
543
544 fn format_tag_payload(&mut self, node: &SyntaxNode) {
545 for child in node.children() {
546 match child.kind() {
547 SyntaxKind::SEQUENCE => {
548 self.format_sequence(&child);
550 }
551 SyntaxKind::OBJECT => {
552 self.format_object(&child);
554 }
555 _ => self.format_node(&child),
556 }
557 }
558 }
559
560 fn format_heredoc(&mut self, node: &SyntaxNode) {
561 self.write(&node.to_string());
563 }
564
565 fn format_attributes(&mut self, node: &SyntaxNode) {
566 let attrs: Vec<_> = node
567 .children()
568 .filter(|n| n.kind() == SyntaxKind::ATTRIBUTE)
569 .collect();
570
571 for (i, attr) in attrs.iter().enumerate() {
572 self.format_attribute(attr);
573 if i < attrs.len() - 1 {
574 self.write(" ");
575 }
576 }
577 }
578
579 fn format_attribute(&mut self, node: &SyntaxNode) {
580 for el in node.children_with_tokens() {
582 match el {
583 NodeOrToken::Token(token) => match token.kind() {
584 SyntaxKind::BARE_SCALAR => self.write(token.text()),
585 SyntaxKind::GT => self.write(">"),
586 _ => {}
587 },
588 NodeOrToken::Node(child) => {
589 self.format_node(&child);
590 }
591 }
592 }
593 }
594}
595
596fn is_block_entry(entry: &Entry) -> bool {
599 if let Some(value) = entry.value() {
600 contains_block_object(value.syntax())
602 } else {
603 false
604 }
605}
606
607fn is_tag_with_block_payload(entry_node: &SyntaxNode) -> bool {
610 let key = match entry_node.children().find(|n| n.kind() == SyntaxKind::KEY) {
612 Some(k) => k,
613 None => return false,
614 };
615
616 for child in key.children() {
618 if child.kind() == SyntaxKind::TAG {
619 for tag_child in child.children() {
621 if tag_child.kind() == SyntaxKind::TAG_PAYLOAD {
622 return contains_block_object(&tag_child);
624 }
625 }
626 }
627 }
628
629 false
630}
631
632fn contains_block_object(node: &SyntaxNode) -> bool {
635 if node.kind() == SyntaxKind::OBJECT
637 && let Some(obj) = Object::cast(node.clone())
638 {
639 let sep = obj.separator();
640 if matches!(sep, Separator::Newline | Separator::Mixed) {
641 return true;
642 }
643 if node
645 .children_with_tokens()
646 .any(|el| el.kind() == SyntaxKind::DOC_COMMENT)
647 {
648 return true;
649 }
650 }
651
652 for child in node.children() {
654 if contains_block_object(&child) {
655 return true;
656 }
657 }
658
659 false
660}
661
662fn is_schema_declaration(entry: &Entry) -> bool {
664 if let Some(key) = entry.key() {
665 key.syntax().children().any(|n| {
667 if n.kind() == SyntaxKind::TAG {
668 n.children_with_tokens().any(|el| {
670 if let NodeOrToken::Token(token) = el {
671 token.kind() == SyntaxKind::TAG_TOKEN && token.text() == "@schema"
672 } else {
673 false
674 }
675 })
676 } else {
677 false
678 }
679 })
680 } else {
681 false
682 }
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 fn format(source: &str) -> String {
690 format_source(source, FormatOptions::default())
691 }
692
693 #[test]
694 fn test_parse_errors_detected() {
695 let input = "config {a 1 b 2}";
697 let parsed = styx_cst::parse(input);
698 assert!(
699 !parsed.is_ok(),
700 "Expected parse errors for '{}', but got none. Errors: {:?}",
701 input,
702 parsed.errors()
703 );
704 let output = format(input);
706 assert_eq!(
707 output, input,
708 "Formatter should return original source for documents with parse errors"
709 );
710 }
711
712 #[test]
713 fn test_simple_document() {
714 let input = "name Alice\nage 30";
715 let output = format(input);
716 insta::assert_snapshot!(output);
717 }
718
719 #[test]
720 fn test_preserves_comments() {
721 let input = r#"// This is a comment
722name Alice
723/// Doc comment
724age 30"#;
725 let output = format(input);
726 insta::assert_snapshot!(output);
727 }
728
729 #[test]
730 fn test_inline_object() {
731 let input = "point {x 1, y 2}";
732 let output = format(input);
733 insta::assert_snapshot!(output);
734 }
735
736 #[test]
737 fn test_multiline_object() {
738 let input = "server {\n host localhost\n port 8080\n}";
739 let output = format(input);
740 insta::assert_snapshot!(output);
741 }
742
743 #[test]
744 fn test_nested_objects() {
745 let input = "config {\n server {\n host localhost\n }\n}";
746 let output = format(input);
747 insta::assert_snapshot!(output);
748 }
749
750 #[test]
751 fn test_sequence() {
752 let input = "items (a b c)";
753 let output = format(input);
754 insta::assert_snapshot!(output);
755 }
756
757 #[test]
758 fn test_tagged_value() {
759 let input = "type @string";
760 let output = format(input);
761 insta::assert_snapshot!(output);
762 }
763
764 #[test]
765 fn test_schema_declaration() {
766 let input = "@schema schema.styx\n\nname test";
767 let output = format(input);
768 insta::assert_snapshot!(output);
769 }
770
771 #[test]
772 fn test_tag_with_nested_tag_payload() {
773 let input = "@seq(@string @Schema)";
776 let output = format(input);
777 assert_eq!(output.trim(), "@seq(@string @Schema)");
779 }
780
781 #[test]
782 fn test_sequence_with_multiple_scalars() {
783 let input = "(a b c)";
784 let output = format(input);
785 assert_eq!(output.trim(), "(a b c)");
786 }
787
788 #[test]
789 fn test_complex_schema() {
790 let input = r#"meta {
791 id https://example.com/schema
792 version 1.0
793}
794schema {
795 @ @object{
796 name @string
797 port @int
798 }
799}"#;
800 let output = format(input);
801 insta::assert_snapshot!(output);
802 }
803
804 #[test]
805 fn test_path_syntax_in_object() {
806 let input = r#"resources {
807 limits cpu>500m memory>256Mi
808 requests cpu>100m memory>128Mi
809}"#;
810 let output = format(input);
811 insta::assert_snapshot!(output);
812 }
813
814 #[test]
815 fn test_syntax_error_space_after_gt() {
816 let input = "limits cpu> 500m";
818 let parsed = styx_cst::parse(input);
819 assert!(!parsed.is_ok(), "should have parse error");
820 let output = format(input);
821 assert_eq!(output, input);
822 }
823
824 #[test]
825 fn test_syntax_error_space_before_gt() {
826 let input = "limits cpu >500m";
828 let parsed = styx_cst::parse(input);
829 assert!(!parsed.is_ok(), "should have parse error");
830 let output = format(input);
831 assert_eq!(output, input);
832 }
833
834 #[test]
835 fn test_tag_with_separate_sequence() {
836 let input = "@a ()";
839 let output = format(input);
840 assert_eq!(output.trim(), "@a ()");
841 }
842
843 #[test]
844 fn test_tag_with_attached_sequence() {
845 let input = "@a()";
847 let output = format(input);
848 assert_eq!(output.trim(), "@a()");
849 }
850
851 #[test]
854 fn test_multiline_sequence_preserves_structure() {
855 let input = r#"items (
856 a
857 b
858 c
859)"#;
860 let output = format(input);
861 insta::assert_snapshot!(output);
862 }
863
864 #[test]
865 fn test_sequence_with_trailing_comment() {
866 let input = r#"extends (
867 "@eslint/js:recommended"
868 typescript-eslint:strictTypeChecked
869 // don't fold
870)"#;
871 let output = format(input);
872 insta::assert_snapshot!(output);
873 }
874
875 #[test]
876 fn test_sequence_with_inline_comments() {
877 let input = r#"items (
878 // first item
879 a
880 // second item
881 b
882)"#;
883 let output = format(input);
884 insta::assert_snapshot!(output);
885 }
886
887 #[test]
888 fn test_sequence_comment_idempotent() {
889 let input = r#"extends (
890 "@eslint/js:recommended"
891 typescript-eslint:strictTypeChecked
892 // don't fold
893)"#;
894 let once = format(input);
895 let twice = format(&once);
896 assert_eq!(once, twice, "formatting should be idempotent");
897 }
898
899 #[test]
900 fn test_inline_sequence_stays_inline() {
901 let input = "items (a b c)";
903 let output = format(input);
904 assert_eq!(output.trim(), "items (a b c)");
905 }
906
907 #[test]
908 fn test_sequence_with_doc_comment() {
909 let input = r#"items (
910 /// Documentation for first
911 a
912 b
913)"#;
914 let output = format(input);
915 insta::assert_snapshot!(output);
916 }
917
918 #[test]
919 fn test_nested_multiline_sequence() {
920 let input = r#"outer (
921 (a b)
922 // between
923 (c d)
924)"#;
925 let output = format(input);
926 insta::assert_snapshot!(output);
927 }
928
929 #[test]
930 fn test_sequence_in_object_with_comment() {
931 let input = r#"config {
932 items (
933 a
934 // comment
935 b
936 )
937}"#;
938 let output = format(input);
939 insta::assert_snapshot!(output);
940 }
941
942 #[test]
943 fn test_object_with_only_comments() {
944 let input = r#"pre-commit {
946 // generate-readmes false
947 // rustfmt false
948 // cargo-lock false
949}"#;
950 let output = format(input);
951 insta::assert_snapshot!(output);
952 }
953
954 #[test]
955 fn test_object_comments_with_blank_line() {
956 let input = r#"config {
958 // first group
959 // still first group
960
961 // second group after blank line
962 // still second group
963}"#;
964 let output = format(input);
965 insta::assert_snapshot!(output);
966 }
967
968 #[test]
969 fn test_object_mixed_entries_and_comments() {
970 let input = r#"settings {
972 enabled true
973 // disabled-option false
974 name "test"
975 // another-disabled option
976}"#;
977 let output = format(input);
978 insta::assert_snapshot!(output);
979 }
980
981 #[test]
982 fn test_schema_with_doc_comments_in_inline_object() {
983 let input = include_str!("fixtures/before-format.styx");
986 let output = format(input);
987
988 assert!(
990 output.contains("/// Features to use for clippy"),
991 "Doc comment for clippy-features was lost!\nOutput:\n{}",
992 output
993 );
994 assert!(
995 output.contains("/// Features to use for docs"),
996 "Doc comment for docs-features was lost!\nOutput:\n{}",
997 output
998 );
999 assert!(
1000 output.contains("/// Features to use for doc tests"),
1001 "Doc comment for doc-test-features was lost!\nOutput:\n{}",
1002 output
1003 );
1004
1005 insta::assert_snapshot!(output);
1006 }
1007
1008 #[test]
1009 fn test_dibs_extracted_schema() {
1010 let input = include_str!("fixtures/dibs-extracted.styx");
1012 let output = format(input);
1013 insta::assert_snapshot!(output);
1014 }
1015
1016 #[test]
1023 fn fmt_001_bare_scalar() {
1024 insta::assert_snapshot!(format("foo bar"));
1025 }
1026
1027 #[test]
1028 fn fmt_002_quoted_scalar() {
1029 insta::assert_snapshot!(format(r#"foo "hello world""#));
1030 }
1031
1032 #[test]
1033 fn fmt_003_raw_scalar() {
1034 insta::assert_snapshot!(format(r#"path r"/usr/bin""#));
1035 }
1036
1037 #[test]
1038 fn fmt_004_multiple_entries() {
1039 insta::assert_snapshot!(format("foo bar\nbaz qux"));
1040 }
1041
1042 #[test]
1043 fn fmt_005_unit_tag() {
1044 insta::assert_snapshot!(format("empty @"));
1045 }
1046
1047 #[test]
1048 fn fmt_006_simple_tag() {
1049 insta::assert_snapshot!(format("type @string"));
1050 }
1051
1052 #[test]
1053 fn fmt_007_tag_with_scalar_payload() {
1054 insta::assert_snapshot!(format(r#"default @default("hello")"#));
1055 }
1056
1057 #[test]
1058 fn fmt_008_nested_tags() {
1059 insta::assert_snapshot!(format("type @optional(@string)"));
1060 }
1061
1062 #[test]
1063 fn fmt_009_deeply_nested_tags() {
1064 insta::assert_snapshot!(format("type @seq(@optional(@string))"));
1065 }
1066
1067 #[test]
1068 fn fmt_010_path_syntax() {
1069 insta::assert_snapshot!(format("limits cpu>500m memory>256Mi"));
1070 }
1071
1072 #[test]
1075 fn fmt_011_empty_inline_object() {
1076 insta::assert_snapshot!(format("config {}"));
1077 }
1078
1079 #[test]
1080 fn fmt_012_single_entry_inline_object() {
1081 insta::assert_snapshot!(format("config {name foo}"));
1082 }
1083
1084 #[test]
1085 fn fmt_013_multi_entry_inline_object() {
1086 insta::assert_snapshot!(format("point {x 1, y 2, z 3}"));
1087 }
1088
1089 #[test]
1090 fn fmt_014_nested_inline_objects() {
1091 insta::assert_snapshot!(format("outer {inner {value 42}}"));
1092 }
1093
1094 #[test]
1095 fn fmt_015_inline_object_with_tags() {
1096 insta::assert_snapshot!(format("schema {name @string, age @int}"));
1097 }
1098
1099 #[test]
1100 fn fmt_016_tag_with_inline_object_payload() {
1101 insta::assert_snapshot!(format("type @object{name @string}"));
1102 }
1103
1104 #[test]
1105 fn fmt_017_inline_object_no_commas() {
1106 insta::assert_snapshot!(format("config {a 1 b 2}"));
1108 }
1109
1110 #[test]
1111 fn fmt_018_inline_object_mixed_separators() {
1112 insta::assert_snapshot!(format("config {a 1, b 2 c 3}"));
1113 }
1114
1115 #[test]
1116 fn fmt_019_deeply_nested_inline() {
1117 insta::assert_snapshot!(format("a {b {c {d {e 1}}}}"));
1118 }
1119
1120 #[test]
1121 fn fmt_020_inline_with_unit_values() {
1122 insta::assert_snapshot!(format("flags {debug @, verbose @}"));
1123 }
1124
1125 #[test]
1128 fn fmt_021_simple_block_object() {
1129 insta::assert_snapshot!(format("config {\n name foo\n value bar\n}"));
1130 }
1131
1132 #[test]
1133 fn fmt_022_block_object_irregular_indent() {
1134 insta::assert_snapshot!(format("config {\n name foo\n value bar\n}"));
1135 }
1136
1137 #[test]
1138 fn fmt_023_nested_block_objects() {
1139 insta::assert_snapshot!(format("outer {\n inner {\n value 42\n }\n}"));
1140 }
1141
1142 #[test]
1143 fn fmt_024_block_with_inline_child() {
1144 insta::assert_snapshot!(format("config {\n point {x 1, y 2}\n name foo\n}"));
1145 }
1146
1147 #[test]
1148 fn fmt_025_inline_with_block_child() {
1149 insta::assert_snapshot!(format("config {nested {\n a 1\n}}"));
1151 }
1152
1153 #[test]
1154 fn fmt_026_block_object_blank_lines() {
1155 insta::assert_snapshot!(format("config {\n a 1\n\n b 2\n}"));
1156 }
1157
1158 #[test]
1159 fn fmt_027_block_object_multiple_blank_lines() {
1160 insta::assert_snapshot!(format("config {\n a 1\n\n\n\n b 2\n}"));
1161 }
1162
1163 #[test]
1164 fn fmt_028_empty_block_object() {
1165 insta::assert_snapshot!(format("config {\n}"));
1166 }
1167
1168 #[test]
1169 fn fmt_029_block_single_entry() {
1170 insta::assert_snapshot!(format("config {\n only_one value\n}"));
1171 }
1172
1173 #[test]
1174 fn fmt_030_mixed_block_inline_siblings() {
1175 insta::assert_snapshot!(format("a {x 1}\nb {\n y 2\n}"));
1176 }
1177
1178 #[test]
1181 fn fmt_031_empty_sequence() {
1182 insta::assert_snapshot!(format("items ()"));
1183 }
1184
1185 #[test]
1186 fn fmt_032_single_item_sequence() {
1187 insta::assert_snapshot!(format("items (one)"));
1188 }
1189
1190 #[test]
1191 fn fmt_033_multi_item_sequence() {
1192 insta::assert_snapshot!(format("items (a b c d e)"));
1193 }
1194
1195 #[test]
1196 fn fmt_034_nested_sequences() {
1197 insta::assert_snapshot!(format("matrix ((1 2) (3 4))"));
1198 }
1199
1200 #[test]
1201 fn fmt_035_sequence_of_objects() {
1202 insta::assert_snapshot!(format("points ({x 1} {x 2})"));
1203 }
1204
1205 #[test]
1206 fn fmt_036_block_sequence() {
1207 insta::assert_snapshot!(format("items (\n a\n b\n c\n)"));
1208 }
1209
1210 #[test]
1211 fn fmt_037_sequence_with_trailing_newline() {
1212 insta::assert_snapshot!(format("items (a b c\n)"));
1213 }
1214
1215 #[test]
1216 fn fmt_038_tag_with_sequence_payload() {
1217 insta::assert_snapshot!(format("type @seq(a b c)"));
1218 }
1219
1220 #[test]
1221 fn fmt_039_tag_sequence_attached() {
1222 insta::assert_snapshot!(format("type @seq()"));
1223 }
1224
1225 #[test]
1226 fn fmt_040_tag_sequence_detached() {
1227 insta::assert_snapshot!(format("type @seq ()"));
1228 }
1229
1230 #[test]
1233 fn fmt_041_line_comment_before_entry() {
1234 insta::assert_snapshot!(format("// comment\nfoo bar"));
1235 }
1236
1237 #[test]
1238 fn fmt_042_doc_comment_before_entry() {
1239 insta::assert_snapshot!(format("/// doc comment\nfoo bar"));
1240 }
1241
1242 #[test]
1243 fn fmt_043_comment_inside_block_object() {
1244 insta::assert_snapshot!(format("config {\n // comment\n foo bar\n}"));
1245 }
1246
1247 #[test]
1248 fn fmt_044_doc_comment_inside_block_object() {
1249 insta::assert_snapshot!(format("config {\n /// doc\n foo bar\n}"));
1250 }
1251
1252 #[test]
1253 fn fmt_045_comment_between_entries() {
1254 insta::assert_snapshot!(format("config {\n a 1\n // middle\n b 2\n}"));
1255 }
1256
1257 #[test]
1258 fn fmt_046_comment_at_end_of_object() {
1259 insta::assert_snapshot!(format("config {\n a 1\n // trailing\n}"));
1260 }
1261
1262 #[test]
1263 fn fmt_047_inline_object_with_doc_comment() {
1264 insta::assert_snapshot!(format("config {/// doc\na 1, b 2}"));
1266 }
1267
1268 #[test]
1269 fn fmt_048_comment_in_sequence() {
1270 insta::assert_snapshot!(format("items (\n // comment\n a\n b\n)"));
1271 }
1272
1273 #[test]
1274 fn fmt_049_multiple_comments_grouped() {
1275 insta::assert_snapshot!(format("config {\n // first\n // second\n a 1\n}"));
1276 }
1277
1278 #[test]
1279 fn fmt_050_comments_with_blank_line_between() {
1280 insta::assert_snapshot!(format("config {\n // group 1\n\n // group 2\n a 1\n}"));
1281 }
1282
1283 #[test]
1286 fn fmt_051_optional_with_newline_before_close() {
1287 insta::assert_snapshot!(format("foo @optional(@string\n)"));
1289 }
1290
1291 #[test]
1292 fn fmt_052_seq_with_newline_before_close() {
1293 insta::assert_snapshot!(format("foo @seq(@string\n)"));
1294 }
1295
1296 #[test]
1297 fn fmt_053_object_with_newline_before_close() {
1298 insta::assert_snapshot!(format("foo @object{a @string\n}"));
1299 }
1300
1301 #[test]
1302 fn fmt_054_deeply_nested_with_weird_breaks() {
1303 insta::assert_snapshot!(format("foo @optional(@object{a @seq(@string\n)\n})"));
1304 }
1305
1306 #[test]
1307 fn fmt_055_closing_delimiters_on_own_lines() {
1308 insta::assert_snapshot!(format("foo @a(@b{x 1\n}\n)"));
1309 }
1310
1311 #[test]
1312 fn fmt_056_inline_entries_one_has_doc_comment() {
1313 insta::assert_snapshot!(format("config {a @unit, /// doc\nb @unit, c @unit}"));
1315 }
1316
1317 #[test]
1318 fn fmt_057_mixed_inline_block_with_doc() {
1319 insta::assert_snapshot!(format("schema {@ @object{a @unit, /// doc\nb @string}}"));
1320 }
1321
1322 #[test]
1323 fn fmt_058_tag_map_with_doc_comments() {
1324 insta::assert_snapshot!(format(
1325 "fields @map(@string@enum{/// variant a\na @unit, /// variant b\nb @unit})"
1326 ));
1327 }
1328
1329 #[test]
1330 fn fmt_059_nested_enums_with_docs() {
1331 insta::assert_snapshot!(format(
1332 "type @enum{/// first\na @object{/// inner\nx @int}, b @unit}"
1333 ));
1334 }
1335
1336 #[test]
1337 fn fmt_060_the_dibs_pattern() {
1338 insta::assert_snapshot!(format(
1340 r#"schema {@ @object{decls @map(@string@enum{
1341 /// A query
1342 query @object{
1343 params @optional(@object{params @map(@string@enum{uuid @unit, /// doc
1344 optional @seq(@type{name T})
1345 })})
1346 }
1347})}}"#
1348 ));
1349 }
1350
1351 #[test]
1354 fn fmt_061_two_inline_entries() {
1355 insta::assert_snapshot!(format("a 1\nb 2"));
1356 }
1357
1358 #[test]
1359 fn fmt_062_two_block_entries() {
1360 insta::assert_snapshot!(format("a {\n x 1\n}\nb {\n y 2\n}"));
1361 }
1362
1363 #[test]
1364 fn fmt_063_inline_then_block() {
1365 insta::assert_snapshot!(format("a 1\nb {\n y 2\n}"));
1366 }
1367
1368 #[test]
1369 fn fmt_064_block_then_inline() {
1370 insta::assert_snapshot!(format("a {\n x 1\n}\nb 2"));
1371 }
1372
1373 #[test]
1374 fn fmt_065_inline_inline_with_existing_blank() {
1375 insta::assert_snapshot!(format("a 1\n\nb 2"));
1376 }
1377
1378 #[test]
1379 fn fmt_066_three_entries_mixed() {
1380 insta::assert_snapshot!(format("a 1\nb {\n x 1\n}\nc 3"));
1381 }
1382
1383 #[test]
1384 fn fmt_067_meta_then_schema_blocks() {
1385 insta::assert_snapshot!(format("meta {\n id test\n}\nschema {\n @ @string\n}"));
1386 }
1387
1388 #[test]
1389 fn fmt_068_doc_comment_entry_spacing() {
1390 insta::assert_snapshot!(format("/// doc for a\na 1\n/// doc for b\nb 2"));
1391 }
1392
1393 #[test]
1394 fn fmt_069_multiple_blocks_no_blanks() {
1395 insta::assert_snapshot!(format("a {\nx 1\n}\nb {\ny 2\n}\nc {\nz 3\n}"));
1396 }
1397
1398 #[test]
1399 fn fmt_070_schema_declaration_spacing() {
1400 insta::assert_snapshot!(format("@schema foo.styx\nname test"));
1401 }
1402
1403 #[test]
1406 fn fmt_071_tag_chain() {
1407 insta::assert_snapshot!(format("type @optional @string"));
1408 }
1409
1410 #[test]
1411 fn fmt_072_tag_with_object_then_scalar() {
1412 insta::assert_snapshot!(format("type @default({x 1} @object{x @int})"));
1413 }
1414
1415 #[test]
1416 fn fmt_073_multiple_tags_same_entry() {
1417 insta::assert_snapshot!(format("field @deprecated @optional(@string)"));
1418 }
1419
1420 #[test]
1421 fn fmt_074_tag_payload_is_unit() {
1422 insta::assert_snapshot!(format("empty @some(@)"));
1423 }
1424
1425 #[test]
1426 fn fmt_075_tag_with_heredoc() {
1427 insta::assert_snapshot!(format("sql @raw(<<EOF\nSELECT *\nEOF)"));
1428 }
1429
1430 #[test]
1431 fn fmt_076_tag_payload_sequence_of_tags() {
1432 insta::assert_snapshot!(format("types @union(@string @int @bool)"));
1433 }
1434
1435 #[test]
1436 fn fmt_077_tag_map_compact() {
1437 insta::assert_snapshot!(format("fields @map(@string@int)"));
1438 }
1439
1440 #[test]
1441 fn fmt_078_tag_map_with_complex_value() {
1442 insta::assert_snapshot!(format("fields @map(@string@object{x @int, y @int})"));
1443 }
1444
1445 #[test]
1446 fn fmt_079_tag_type_reference() {
1447 insta::assert_snapshot!(format("field @type{name MyType}"));
1448 }
1449
1450 #[test]
1451 fn fmt_080_tag_default_with_at() {
1452 insta::assert_snapshot!(format("opt @default(@ @optional(@string))"));
1453 }
1454
1455 #[test]
1458 fn fmt_081_simple_heredoc() {
1459 insta::assert_snapshot!(format("text <<EOF\nhello\nworld\nEOF"));
1460 }
1461
1462 #[test]
1463 fn fmt_082_heredoc_in_object() {
1464 insta::assert_snapshot!(format("config {\n sql <<SQL\nSELECT *\nSQL\n}"));
1465 }
1466
1467 #[test]
1468 fn fmt_083_heredoc_indented_content() {
1469 insta::assert_snapshot!(format("code <<END\n indented\n more\nEND"));
1470 }
1471
1472 #[test]
1473 fn fmt_084_multiple_heredocs() {
1474 insta::assert_snapshot!(format("a <<A\nfirst\nA\nb <<B\nsecond\nB"));
1475 }
1476
1477 #[test]
1478 fn fmt_085_heredoc_empty() {
1479 insta::assert_snapshot!(format("empty <<EOF\nEOF"));
1480 }
1481
1482 #[test]
1485 fn fmt_086_quoted_with_escapes() {
1486 insta::assert_snapshot!(format(r#"msg "hello\nworld\ttab""#));
1487 }
1488
1489 #[test]
1490 fn fmt_087_quoted_with_quotes() {
1491 insta::assert_snapshot!(format(r#"msg "say \"hello\"""#));
1492 }
1493
1494 #[test]
1495 fn fmt_088_raw_string_with_hashes() {
1496 insta::assert_snapshot!(format(r##"pattern r#"foo"bar"#"##));
1497 }
1498
1499 #[test]
1500 fn fmt_089_quoted_empty() {
1501 insta::assert_snapshot!(format(r#"empty """#));
1502 }
1503
1504 #[test]
1505 fn fmt_090_mixed_scalar_types() {
1506 insta::assert_snapshot!(format(r#"config {bare word, quoted "str", raw r"path"}"#));
1507 }
1508
1509 #[test]
1512 fn fmt_091_schema_with_meta() {
1513 insta::assert_snapshot!(format(
1514 r#"meta {id "app:config@1", cli myapp}
1515schema {@ @object{
1516 name @string
1517 port @default(8080 @int)
1518}}"#
1519 ));
1520 }
1521
1522 #[test]
1523 fn fmt_092_enum_with_object_variants() {
1524 insta::assert_snapshot!(format(
1525 r#"type @enum{
1526 /// A simple variant
1527 simple @unit
1528 /// Complex variant
1529 complex @object{x @int, y @int}
1530}"#
1531 ));
1532 }
1533
1534 #[test]
1535 fn fmt_093_nested_optionals() {
1536 insta::assert_snapshot!(format("type @optional(@optional(@optional(@string)))"));
1537 }
1538
1539 #[test]
1540 fn fmt_094_map_of_maps() {
1541 insta::assert_snapshot!(format("data @map(@string@map(@string@int))"));
1542 }
1543
1544 #[test]
1545 fn fmt_095_sequence_of_enums() {
1546 insta::assert_snapshot!(format("items @seq(@enum{a @unit, b @unit, c @unit})"));
1547 }
1548
1549 #[test]
1550 fn fmt_096_all_builtin_types() {
1551 insta::assert_snapshot!(format(
1552 "types {s @string, i @int, b @bool, f @float, u @unit}"
1553 ));
1554 }
1555
1556 #[test]
1557 fn fmt_097_deep_nesting_mixed() {
1558 insta::assert_snapshot!(format(
1559 "a @object{b @seq(@enum{c @object{d @optional(@map(@string@int))}})}"
1560 ));
1561 }
1562
1563 #[test]
1564 fn fmt_098_realistic_config_schema() {
1565 insta::assert_snapshot!(format(
1566 r#"meta {id "crate:myapp@1", cli myapp, description "My application config"}
1567schema {@ @object{
1568 /// Server configuration
1569 server @object{
1570 /// Hostname to bind
1571 host @default("localhost" @string)
1572 /// Port number
1573 port @default(8080 @int)
1574 }
1575 /// Database settings
1576 database @optional(@object{
1577 url @string
1578 pool_size @default(10 @int)
1579 })
1580}}"#
1581 ));
1582 }
1583
1584 #[test]
1585 fn fmt_099_attributes_syntax() {
1586 insta::assert_snapshot!(format("resource limits>cpu>500m limits>memory>256Mi"));
1587 }
1588
1589 #[test]
1590 fn fmt_100_everything_combined() {
1591 insta::assert_snapshot!(format(
1592 r#"// Top level comment
1593meta {id "test@1"}
1594
1595/// Schema documentation
1596schema {@ @object{
1597 /// A string field
1598 name @string
1599
1600 /// An enum with variants
1601 kind @enum{
1602 /// Simple kind
1603 simple @unit
1604 /// Complex kind
1605 complex @object{
1606 /// Nested value
1607 value @optional(@int)
1608 }
1609 }
1610
1611 /// A sequence
1612 items @seq(@string)
1613
1614 /// A map
1615 data @map(@string@object{x @int, y @int})
1616}}"#
1617 ));
1618 }
1619}
1620
1621#[cfg(test)]
1622mod proptests {
1623 use super::*;
1624 use proptest::prelude::*;
1625
1626 fn bare_scalar() -> impl Strategy<Value = String> {
1628 prop::string::string_regex("[a-zA-Z][a-zA-Z0-9_-]{0,10}")
1630 .unwrap()
1631 .prop_filter("non-empty", |s| !s.is_empty())
1632 }
1633
1634 fn quoted_scalar() -> impl Strategy<Value = String> {
1636 prop_oneof![
1637 prop::string::string_regex(r#"[a-zA-Z0-9 _-]{0,20}"#)
1639 .unwrap()
1640 .prop_map(|s| format!("\"{}\"", s)),
1641 prop::string::string_regex(r#"[a-zA-Z0-9 ]{0,10}"#)
1643 .unwrap()
1644 .prop_map(|s| format!("\"hello\\n{}\\t\"", s)),
1645 ]
1646 }
1647
1648 fn raw_scalar() -> impl Strategy<Value = String> {
1650 prop_oneof![
1651 prop::string::string_regex(r#"[a-zA-Z0-9/_\\.-]{0,15}"#)
1653 .unwrap()
1654 .prop_map(|s| format!("r\"{}\"", s)),
1655 prop::string::string_regex(r#"[a-zA-Z0-9 "/_\\.-]{0,15}"#)
1657 .unwrap()
1658 .prop_map(|s| format!("r#\"{}\"#", s)),
1659 ]
1660 }
1661
1662 fn scalar() -> impl Strategy<Value = String> {
1664 prop_oneof![
1665 4 => bare_scalar(),
1666 3 => quoted_scalar(),
1667 1 => raw_scalar(),
1668 ]
1669 }
1670
1671 fn tag_name() -> impl Strategy<Value = String> {
1673 prop::string::string_regex("[a-zA-Z][a-zA-Z0-9_-]{0,8}")
1674 .unwrap()
1675 .prop_filter("non-empty", |s| !s.is_empty())
1676 }
1677
1678 fn tag() -> impl Strategy<Value = String> {
1683 prop_oneof![
1684 Just("@".to_string()),
1686 tag_name().prop_map(|n| format!("@{n}")),
1688 (tag_name(), flat_sequence()).prop_map(|(n, s)| format!("@{n}{s}")),
1690 (tag_name(), inline_object()).prop_map(|(n, o)| format!("@{n}{o}")),
1692 (tag_name(), quoted_scalar()).prop_map(|(n, q)| format!("@{n}{q}")),
1694 (tag_name()).prop_map(|n| format!("@{n} @")),
1696 ]
1697 }
1698
1699 fn attribute() -> impl Strategy<Value = String> {
1701 (bare_scalar(), scalar()).prop_map(|(k, v)| format!("{k}>{v}"))
1702 }
1703
1704 fn flat_sequence() -> impl Strategy<Value = String> {
1706 prop::collection::vec(scalar(), 0..5).prop_map(|items| {
1707 if items.is_empty() {
1708 "()".to_string()
1709 } else {
1710 format!("({})", items.join(" "))
1711 }
1712 })
1713 }
1714
1715 fn nested_sequence() -> impl Strategy<Value = String> {
1717 prop::collection::vec(flat_sequence(), 1..4)
1718 .prop_map(|seqs| format!("({})", seqs.join(" ")))
1719 }
1720
1721 fn sequence() -> impl Strategy<Value = String> {
1723 prop_oneof![
1724 3 => flat_sequence(),
1725 1 => nested_sequence(),
1726 ]
1727 }
1728
1729 fn inline_object() -> impl Strategy<Value = String> {
1731 prop::collection::vec((bare_scalar(), scalar()), 0..4).prop_map(|entries| {
1732 if entries.is_empty() {
1733 "{}".to_string()
1734 } else {
1735 let inner: Vec<String> = entries
1736 .into_iter()
1737 .map(|(k, v)| format!("{k} {v}"))
1738 .collect();
1739 format!("{{{}}}", inner.join(", "))
1740 }
1741 })
1742 }
1743
1744 fn multiline_object() -> impl Strategy<Value = String> {
1746 prop::collection::vec((bare_scalar(), scalar()), 1..4).prop_map(|entries| {
1747 let inner: Vec<String> = entries
1748 .into_iter()
1749 .map(|(k, v)| format!(" {k} {v}"))
1750 .collect();
1751 format!("{{\n{}\n}}", inner.join("\n"))
1752 })
1753 }
1754
1755 fn line_comment() -> impl Strategy<Value = String> {
1757 prop::string::string_regex("[a-zA-Z0-9 _-]{0,30}")
1758 .unwrap()
1759 .prop_map(|s| format!("// {}", s.trim()))
1760 }
1761
1762 fn doc_comment() -> impl Strategy<Value = String> {
1764 prop::string::string_regex("[a-zA-Z0-9 _-]{0,30}")
1765 .unwrap()
1766 .prop_map(|s| format!("/// {}", s.trim()))
1767 }
1768
1769 fn heredoc() -> impl Strategy<Value = String> {
1771 let delimiters = prop_oneof![
1772 Just("EOF".to_string()),
1773 Just("END".to_string()),
1774 Just("TEXT".to_string()),
1775 Just("CODE".to_string()),
1776 ];
1777 let content = prop::string::string_regex("[a-zA-Z0-9 \n_.-]{0,50}").unwrap();
1778 let lang_hint = prop_oneof![
1779 Just("".to_string()),
1780 Just(",txt".to_string()),
1781 Just(",rust".to_string()),
1782 ];
1783 (delimiters, content, lang_hint)
1784 .prop_map(|(delim, content, hint)| format!("<<{delim}{hint}\n{content}\n{delim}"))
1785 }
1786
1787 fn simple_value() -> impl Strategy<Value = String> {
1789 prop_oneof![
1790 3 => scalar(),
1791 2 => sequence(),
1792 2 => tag(),
1793 1 => inline_object(),
1794 1 => multiline_object(),
1795 1 => heredoc(),
1796 1 => prop::collection::vec(attribute(), 1..4).prop_map(|attrs| attrs.join(" ")),
1798 ]
1799 }
1800
1801 fn entry() -> impl Strategy<Value = String> {
1803 prop_oneof![
1804 (bare_scalar(), simple_value()).prop_map(|(k, v)| format!("{k} {v}")),
1806 (tag(), simple_value()).prop_map(|(t, v)| format!("{t} {v}")),
1808 ]
1809 }
1810
1811 fn commented_entry() -> impl Strategy<Value = String> {
1813 prop_oneof![
1814 3 => entry(),
1815 1 => (doc_comment(), entry()).prop_map(|(c, e)| format!("{c}\n{e}")),
1816 1 => (line_comment(), entry()).prop_map(|(c, e)| format!("{c}\n{e}")),
1817 ]
1818 }
1819
1820 fn document() -> impl Strategy<Value = String> {
1822 prop::collection::vec(commented_entry(), 1..5).prop_map(|entries| entries.join("\n"))
1823 }
1824
1825 fn deep_object(depth: usize) -> BoxedStrategy<String> {
1827 if depth == 0 {
1828 scalar().boxed()
1829 } else {
1830 prop_oneof![
1831 2 => scalar(),
1833 1 => prop::collection::vec(
1835 (bare_scalar(), deep_object(depth - 1)),
1836 1..3
1837 ).prop_map(|entries| {
1838 let inner: Vec<String> = entries.into_iter()
1839 .map(|(k, v)| format!(" {k} {v}"))
1840 .collect();
1841 format!("{{\n{}\n}}", inner.join("\n"))
1842 }),
1843 ]
1844 .boxed()
1845 }
1846 }
1847
1848 fn sequence_of_tags() -> impl Strategy<Value = String> {
1850 prop::collection::vec(tag(), 1..5).prop_map(|tags| format!("({})", tags.join(" ")))
1851 }
1852
1853 fn object_with_sequences() -> impl Strategy<Value = String> {
1855 prop::collection::vec((bare_scalar(), flat_sequence()), 1..4).prop_map(|entries| {
1856 let inner: Vec<String> = entries
1857 .into_iter()
1858 .map(|(k, v)| format!(" {k} {v}"))
1859 .collect();
1860 format!("{{\n{}\n}}", inner.join("\n"))
1861 })
1862 }
1863
1864 fn strip_spans(value: &mut styx_tree::Value) {
1866 value.span = None;
1867 if let Some(ref mut tag) = value.tag {
1868 tag.span = None;
1869 }
1870 if let Some(ref mut payload) = value.payload {
1871 match payload {
1872 styx_tree::Payload::Scalar(s) => s.span = None,
1873 styx_tree::Payload::Sequence(seq) => {
1874 seq.span = None;
1875 for item in &mut seq.items {
1876 strip_spans(item);
1877 }
1878 }
1879 styx_tree::Payload::Object(obj) => {
1880 obj.span = None;
1881 for entry in &mut obj.entries {
1882 strip_spans(&mut entry.key);
1883 strip_spans(&mut entry.value);
1884 }
1885 }
1886 }
1887 }
1888 }
1889
1890 fn parse_to_tree(source: &str) -> Option<styx_tree::Value> {
1892 let mut value = styx_tree::parse(source).ok()?;
1893 strip_spans(&mut value);
1894 Some(value)
1895 }
1896
1897 proptest! {
1898 #[test]
1900 fn format_preserves_semantics(input in document()) {
1901 let tree1 = parse_to_tree(&input);
1902
1903 if tree1.is_none() {
1905 return Ok(());
1906 }
1907 let tree1 = tree1.unwrap();
1908
1909 let formatted = format_source(&input, FormatOptions::default());
1910 let tree2 = parse_to_tree(&formatted);
1911
1912 prop_assert!(
1913 tree2.is_some(),
1914 "Formatted output should parse. Input:\n{}\nFormatted:\n{}",
1915 input,
1916 formatted
1917 );
1918 let tree2 = tree2.unwrap();
1919
1920 prop_assert_eq!(
1921 tree1,
1922 tree2,
1923 "Formatting changed semantics!\nInput:\n{}\nFormatted:\n{}",
1924 input,
1925 formatted
1926 );
1927 }
1928
1929 #[test]
1931 fn format_is_idempotent(input in document()) {
1932 let once = format_source(&input, FormatOptions::default());
1933 let twice = format_source(&once, FormatOptions::default());
1934
1935 prop_assert_eq!(
1936 &once,
1937 &twice,
1938 "Formatting is not idempotent!\nInput:\n{}\nOnce:\n{}\nTwice:\n{}",
1939 input,
1940 &once,
1941 &twice
1942 );
1943 }
1944
1945 #[test]
1947 fn format_deep_objects(key in bare_scalar(), value in deep_object(4)) {
1948 let input = format!("{key} {value}");
1949 let tree1 = parse_to_tree(&input);
1950
1951 if tree1.is_none() {
1952 return Ok(());
1953 }
1954 let tree1 = tree1.unwrap();
1955
1956 let formatted = format_source(&input, FormatOptions::default());
1957 let tree2 = parse_to_tree(&formatted);
1958
1959 prop_assert!(
1960 tree2.is_some(),
1961 "Deep object should parse after formatting. Input:\n{}\nFormatted:\n{}",
1962 input,
1963 formatted
1964 );
1965
1966 prop_assert_eq!(
1967 tree1,
1968 tree2.unwrap(),
1969 "Deep object semantics changed!\nInput:\n{}\nFormatted:\n{}",
1970 input,
1971 formatted
1972 );
1973 }
1974
1975 #[test]
1977 fn format_sequence_of_tags(key in bare_scalar(), seq in sequence_of_tags()) {
1978 let input = format!("{key} {seq}");
1979 let tree1 = parse_to_tree(&input);
1980
1981 if tree1.is_none() {
1982 return Ok(());
1983 }
1984 let tree1 = tree1.unwrap();
1985
1986 let formatted = format_source(&input, FormatOptions::default());
1987 let tree2 = parse_to_tree(&formatted);
1988
1989 prop_assert!(
1990 tree2.is_some(),
1991 "Tag sequence should parse. Input:\n{}\nFormatted:\n{}",
1992 input,
1993 formatted
1994 );
1995
1996 prop_assert_eq!(
1997 tree1,
1998 tree2.unwrap(),
1999 "Tag sequence semantics changed!\nInput:\n{}\nFormatted:\n{}",
2000 input,
2001 formatted
2002 );
2003 }
2004
2005 #[test]
2007 fn format_objects_with_sequences(key in bare_scalar(), obj in object_with_sequences()) {
2008 let input = format!("{key} {obj}");
2009 let tree1 = parse_to_tree(&input);
2010
2011 if tree1.is_none() {
2012 return Ok(());
2013 }
2014 let tree1 = tree1.unwrap();
2015
2016 let formatted = format_source(&input, FormatOptions::default());
2017 let tree2 = parse_to_tree(&formatted);
2018
2019 prop_assert!(
2020 tree2.is_some(),
2021 "Object with sequences should parse. Input:\n{}\nFormatted:\n{}",
2022 input,
2023 formatted
2024 );
2025
2026 prop_assert_eq!(
2027 tree1,
2028 tree2.unwrap(),
2029 "Object with sequences semantics changed!\nInput:\n{}\nFormatted:\n{}",
2030 input,
2031 formatted
2032 );
2033 }
2034
2035 #[test]
2037 fn format_preserves_comments(input in document_with_comments()) {
2038 let original_comments = extract_comments(&input);
2039
2040 if original_comments.is_empty() {
2042 return Ok(());
2043 }
2044
2045 let formatted = format_source(&input, FormatOptions::default());
2046 let formatted_comments = extract_comments(&formatted);
2047
2048 prop_assert_eq!(
2049 original_comments.len(),
2050 formatted_comments.len(),
2051 "Comment count changed!\nInput ({} comments):\n{}\nFormatted ({} comments):\n{}\nOriginal comments: {:?}\nFormatted comments: {:?}",
2052 original_comments.len(),
2053 input,
2054 formatted_comments.len(),
2055 formatted,
2056 original_comments,
2057 formatted_comments
2058 );
2059
2060 for comment in &original_comments {
2062 prop_assert!(
2063 formatted_comments.contains(comment),
2064 "Comment lost during formatting!\nMissing: {:?}\nInput:\n{}\nFormatted:\n{}\nOriginal comments: {:?}\nFormatted comments: {:?}",
2065 comment,
2066 input,
2067 formatted,
2068 original_comments,
2069 formatted_comments
2070 );
2071 }
2072 }
2073
2074 #[test]
2076 fn format_preserves_comments_in_empty_objects(
2077 key in bare_scalar(),
2078 comments in prop::collection::vec(line_comment(), 1..5)
2079 ) {
2080 let inner = comments.iter()
2081 .map(|c| format!(" {c}"))
2082 .collect::<Vec<_>>()
2083 .join("\n");
2084 let input = format!("{key} {{\n{inner}\n}}");
2085
2086 let original_comments = extract_comments(&input);
2087 let formatted = format_source(&input, FormatOptions::default());
2088 let formatted_comments = extract_comments(&formatted);
2089
2090 prop_assert_eq!(
2091 original_comments.len(),
2092 formatted_comments.len(),
2093 "Comments in empty object lost!\nInput:\n{}\nFormatted:\n{}",
2094 input,
2095 formatted
2096 );
2097 }
2098
2099 #[test]
2101 fn format_preserves_comments_mixed_with_entries(
2102 key in bare_scalar(),
2103 items in prop::collection::vec(
2104 prop_oneof![
2105 (bare_scalar(), scalar()).prop_map(|(k, v)| format!("{k} {v}")),
2107 line_comment(),
2109 ],
2110 2..6
2111 )
2112 ) {
2113 let inner = items.iter()
2114 .map(|item| format!(" {item}"))
2115 .collect::<Vec<_>>()
2116 .join("\n");
2117 let input = format!("{key} {{\n{inner}\n}}");
2118
2119 let original_comments = extract_comments(&input);
2120 let formatted = format_source(&input, FormatOptions::default());
2121 let formatted_comments = extract_comments(&formatted);
2122
2123 prop_assert_eq!(
2124 original_comments.len(),
2125 formatted_comments.len(),
2126 "Comments mixed with entries lost!\nInput:\n{}\nFormatted:\n{}\nOriginal: {:?}\nFormatted: {:?}",
2127 input,
2128 formatted,
2129 original_comments,
2130 formatted_comments
2131 );
2132 }
2133
2134 #[test]
2136 fn format_preserves_comments_in_sequences(
2137 key in bare_scalar(),
2138 items in prop::collection::vec(
2139 prop_oneof![
2140 2 => scalar(),
2142 1 => line_comment(),
2144 ],
2145 2..6
2146 )
2147 ) {
2148 let has_comment = items.iter().any(|i| i.starts_with("//"));
2150 if !has_comment {
2151 return Ok(());
2152 }
2153
2154 let inner = items.iter()
2155 .map(|item| format!(" {item}"))
2156 .collect::<Vec<_>>()
2157 .join("\n");
2158 let input = format!("{key} (\n{inner}\n)");
2159
2160 let original_comments = extract_comments(&input);
2161 let formatted = format_source(&input, FormatOptions::default());
2162 let formatted_comments = extract_comments(&formatted);
2163
2164 prop_assert_eq!(
2165 original_comments.len(),
2166 formatted_comments.len(),
2167 "Comments in sequence lost!\nInput:\n{}\nFormatted:\n{}\nOriginal: {:?}\nFormatted: {:?}",
2168 input,
2169 formatted,
2170 original_comments,
2171 formatted_comments
2172 );
2173 }
2174 }
2175
2176 fn document_with_comments() -> impl Strategy<Value = String> {
2178 prop::collection::vec(
2179 prop_oneof![
2180 2 => entry(),
2182 2 => (line_comment(), entry()).prop_map(|(c, e)| format!("{c}\n{e}")),
2184 1 => (doc_comment(), entry()).prop_map(|(c, e)| format!("{c}\n{e}")),
2186 1 => object_with_internal_comments(),
2188 ],
2189 1..5,
2190 )
2191 .prop_map(|entries| entries.join("\n"))
2192 }
2193
2194 fn object_with_internal_comments() -> impl Strategy<Value = String> {
2196 (
2197 bare_scalar(),
2198 prop::collection::vec(
2199 prop_oneof![
2200 2 => (bare_scalar(), scalar()).prop_map(|(k, v)| format!("{k} {v}")),
2202 1 => line_comment(),
2204 ],
2205 1..5,
2206 ),
2207 )
2208 .prop_map(|(key, items)| {
2209 let inner = items
2210 .iter()
2211 .map(|item| format!(" {item}"))
2212 .collect::<Vec<_>>()
2213 .join("\n");
2214 format!("{key} {{\n{inner}\n}}")
2215 })
2216 }
2217
2218 fn extract_comments(source: &str) -> Vec<String> {
2220 let mut comments = Vec::new();
2221 for line in source.lines() {
2222 let trimmed = line.trim();
2223 if trimmed.starts_with("///") || trimmed.starts_with("//") {
2224 comments.push(trimmed.to_string());
2225 }
2226 }
2227 comments
2228 }
2229}
2230
2231#[cfg(test)]
2232mod consecutive_doc_comment_tests {
2233 use super::*;
2234
2235 fn format(source: &str) -> String {
2236 format_source(source, FormatOptions::default())
2237 }
2238
2239 #[test]
2240 fn test_consecutive_doc_comments_no_blank_line() {
2241 let input = r#"/// First line of doc
2243/// Second line of doc
2244entry value
2245"#;
2246 let output = format(input);
2247 assert!(
2249 !output.contains("/// First line of doc\n\n/// Second line of doc"),
2250 "Consecutive doc comments should not have a blank line between them!\nOutput:\n{}",
2251 output
2252 );
2253 assert!(
2255 output.contains("/// First line of doc\n/// Second line of doc"),
2256 "Consecutive doc comments should be on consecutive lines!\nOutput:\n{}",
2257 output
2258 );
2259 }
2260
2261 #[test]
2262 fn test_consecutive_doc_comments_after_block_entry() {
2263 let input = r#"/// Create something
2265CreateThing @insert{
2266 params {name @string}
2267 into things
2268 values {name $name}
2269}
2270
2271/// First line of doc for next entry
2272/// Second line of doc for next entry
2273NextEntry @query{
2274 from things
2275 select {id}
2276}
2277"#;
2278 let output = format(input);
2279 assert!(
2281 !output.contains(
2282 "/// First line of doc for next entry\n\n/// Second line of doc for next entry"
2283 ),
2284 "Consecutive doc comments should not have a blank line between them!\nOutput:\n{}",
2285 output
2286 );
2287 assert!(
2289 output.contains(
2290 "/// First line of doc for next entry\n/// Second line of doc for next entry"
2291 ),
2292 "Consecutive doc comments should be on consecutive lines!\nOutput:\n{}",
2293 output
2294 );
2295 }
2296
2297 #[test]
2298 fn test_consecutive_doc_comments_after_line_comment() {
2299 let input = r#"// Section header
2301
2302/// First line of doc
2303/// Second line of doc
2304entry value
2305"#;
2306 let output = format(input);
2307 assert!(
2309 !output.contains("/// First line of doc\n\n/// Second line of doc"),
2310 "Consecutive doc comments should not have a blank line between them!\nOutput:\n{}",
2311 output
2312 );
2313 }
2314}