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