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