1use crate::as_yaml::YamlNode;
14use crate::lex::SyntaxKind;
15use crate::nodes::has_child_token;
16use crate::yaml::{Document, Mapping, Scalar, Sequence, SyntaxNode};
17use std::fmt;
18
19pub fn print_tree(node: &SyntaxNode) {
57 print_tree_indent(node, 0);
58}
59
60pub fn print_tree_indent(node: &SyntaxNode, indent: usize) {
62 let mut out = String::new();
63 tree_to_string_indent(node, indent, &mut out, false);
64 print!("{out}");
65}
66
67pub fn tree_to_string(node: &SyntaxNode) -> String {
85 let mut result = String::new();
86 tree_to_string_indent(node, 0, &mut result, false);
87 result
88}
89
90pub fn shape_to_string(node: &SyntaxNode) -> String {
98 let mut result = String::new();
99 tree_to_string_indent(node, 0, &mut result, true);
100 result
101}
102
103fn is_pending_block_marker(token: &rowan::SyntaxToken<crate::yaml::Lang>) -> bool {
104 token.kind() == SyntaxKind::WHITESPACE
105 && token.text().is_empty()
106 && token
107 .parent()
108 .is_some_and(|p| matches!(p.kind(), SyntaxKind::MAPPING | SyntaxKind::SEQUENCE))
109}
110
111fn tree_to_string_indent(node: &SyntaxNode, indent: usize, result: &mut String, shape: bool) {
114 let prefix = " ".repeat(indent);
115 for child in node.children_with_tokens() {
116 match child {
117 rowan::NodeOrToken::Node(n) => {
118 result.push_str(&format!("{}{:?}\n", prefix, n.kind()));
119 tree_to_string_indent(&n, indent + 1, result, shape);
120 }
121 rowan::NodeOrToken::Token(t) if shape && is_pending_block_marker(&t) => {}
122 rowan::NodeOrToken::Token(t) => {
123 let text = t.text().replace('\n', "\\n").replace('\r', "\\r");
124 result.push_str(&format!("{}{:?}: {:?}\n", prefix, t.kind(), text));
125 }
126 }
127 }
128}
129
130pub fn print_stats(node: &SyntaxNode) {
146 let mut node_counts = std::collections::HashMap::new();
147 let mut token_counts = std::collections::HashMap::new();
148
149 count_nodes(node, &mut node_counts, &mut token_counts);
150
151 println!("=== Node Counts ===");
152 let mut node_vec: Vec<_> = node_counts.iter().collect();
153 node_vec.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
154 for (kind, count) in node_vec {
155 println!(" {kind:?}: {count}");
156 }
157
158 println!("\n=== Token Counts ===");
159 let mut token_vec: Vec<_> = token_counts.iter().collect();
160 token_vec.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
161 for (kind, count) in token_vec {
162 println!(" {kind:?}: {count}");
163 }
164}
165
166fn count_nodes(
167 node: &SyntaxNode,
168 node_counts: &mut std::collections::HashMap<SyntaxKind, usize>,
169 token_counts: &mut std::collections::HashMap<SyntaxKind, usize>,
170) {
171 *node_counts.entry(node.kind()).or_insert(0) += 1;
172
173 for child in node.children_with_tokens() {
174 match child {
175 rowan::NodeOrToken::Node(n) => {
176 count_nodes(&n, node_counts, token_counts);
177 }
178 rowan::NodeOrToken::Token(t) => {
179 *token_counts.entry(t.kind()).or_insert(0) += 1;
180 }
181 }
182 }
183}
184
185pub fn validate_tree(node: &SyntaxNode) -> Result<(), String> {
216 let node = &crate::nodes::immutable_copy(node);
217 let mut in_flow = vec![crate::nodes::in_flow_collection(node)];
221 for event in node.preorder() {
222 match event {
223 rowan::WalkEvent::Enter(n) => {
224 let outer = *in_flow.last().expect("the walk's own entry stays");
225 validate_node(&n, outer)?;
226 let opens_flow = matches!(n.kind(), SyntaxKind::MAPPING | SyntaxKind::SEQUENCE)
227 && crate::nodes::starts_a_flow_collection(&n);
228 in_flow.push(outer || opens_flow);
229 }
230 rowan::WalkEvent::Leave(_) => {
231 in_flow.pop();
232 }
233 }
234 }
235 node.descendants()
236 .try_for_each(|n| validate_no_stacked_indents(&n))?;
237 node.descendants()
238 .try_for_each(|n| validate_no_double_trailing_newline(&n))
239}
240
241pub fn roundtrip_ok(node: &SyntaxNode) -> Result<(), String> {
254 let text = node.to_string();
255 let parse = crate::Parse::parse_yaml(&text);
256 if !parse.positioned_errors().is_empty() {
257 return Err(format!(
258 "re-parse produced {} error(s); first: {}",
259 parse.positioned_errors().len(),
260 parse.positioned_errors()[0].message,
261 ));
262 }
263 let round = parse.tree().to_string();
264 if round != text {
265 return Err(format!(
266 "roundtrip mismatch:\n original: {text:?}\n reparsed: {round:?}",
267 ));
268 }
269 Ok(())
270}
271
272pub fn key_string(key: &YamlNode) -> Option<String> {
286 let identity = crate::as_yaml::node_identity(key.syntax(), None).ok()??;
287 Some(identity.key_string())
288}
289
290#[cfg(test)]
295#[track_caller]
296pub(crate) fn assert_cst_ok(node: &impl rowan::ast::AstNode<Language = crate::yaml::Lang>) {
297 let syntax = node.syntax();
298 if let Err(e) = validate_tree(syntax) {
299 panic!("CST invariant violated: {e}\n---\n{syntax}\n---");
300 }
301 if let Err(e) = roundtrip_ok(syntax) {
302 panic!("roundtrip failed: {e}\n---\n{syntax}\n---");
303 }
304}
305
306#[cfg(test)]
309#[track_caller]
310pub(crate) fn assert_edited(yaml: &crate::yaml::YamlFile, expected: &str) {
311 use rowan::ast::AstNode;
312 use std::str::FromStr;
313 assert_eq!(yaml.to_string(), expected);
314 let reparsed = crate::yaml::YamlFile::from_str(expected).unwrap();
315 assert_eq!(
316 shape_to_string(yaml.syntax()),
317 shape_to_string(reparsed.syntax())
318 );
319 let violations: Vec<String> = yaml
320 .documents()
321 .flat_map(|doc| crate::validator::Validator::new().validate(&doc))
322 .map(|v| v.to_string())
323 .collect();
324 assert_eq!(violations, Vec::<String>::new());
325}
326
327fn validate_node(node: &SyntaxNode, in_flow: bool) -> Result<(), String> {
329 if matches!(
330 node.kind(),
331 SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
332 ) && in_flow
333 {
334 validate_flow_entry_end(node)?;
335 }
336 validate_children(node)?;
337 match node.kind() {
338 SyntaxKind::MAPPING_ENTRY => {
339 let keys: Vec<_> = node
340 .children()
341 .filter(|c| c.kind() == SyntaxKind::KEY)
342 .collect();
343 if keys.len() != 1 {
344 return Err(format!(
345 "MAPPING_ENTRY should have exactly 1 KEY, found {}",
346 keys.len()
347 ));
348 }
349
350 let colons: Vec<_> = node
356 .children_with_tokens()
357 .filter(|c| c.as_token().is_some_and(|t| t.kind() == SyntaxKind::COLON))
358 .collect();
359 let is_explicit_key = has_child_token(node, |k| k == SyntaxKind::QUESTION);
360 let expected_colons = if is_explicit_key || in_flow {
361 0..=1
362 } else {
363 1..=1
364 };
365 if !expected_colons.contains(&colons.len()) {
366 return Err(format!(
367 "MAPPING_ENTRY should have {} COLON(s), found {}",
368 if is_explicit_key || in_flow {
369 "0 or 1"
370 } else {
371 "exactly 1"
372 },
373 colons.len()
374 ));
375 }
376
377 let values: Vec<_> = node
378 .children()
379 .filter(|c| c.kind() == SyntaxKind::VALUE)
380 .collect();
381 if values.len() != 1 {
382 return Err(format!(
383 "MAPPING_ENTRY should have exactly 1 VALUE, found {}",
384 values.len()
385 ));
386 }
387
388 if !in_flow && !is_last_entry_in_parent(node) && !entry_is_terminated(node) {
395 return Err(format!(
396 "Block-style MAPPING_ENTRY not terminated by a NEWLINE (last token: {:?})",
397 node.last_token().map(|t| t.kind())
398 ));
399 }
400 }
401 SyntaxKind::MAPPING | SyntaxKind::SEQUENCE
405 if !crate::nodes::starts_a_flow_collection(node) =>
406 {
407 let stray = node
408 .children_with_tokens()
409 .filter_map(|c| c.into_token())
410 .filter(|t| t.kind() == SyntaxKind::COMMENT)
411 .find(|t| {
412 std::iter::successors(t.prev_token(), |p| p.prev_token())
413 .find(|p| !p.text().is_empty())
414 .is_some_and(|p| {
415 !matches!(p.kind(), SyntaxKind::NEWLINE | SyntaxKind::INDENT)
416 })
417 });
418 if let Some(comment) = stray {
419 return Err(format!(
420 "{:?} holds the comment {:?} ending an entry's line",
421 node.kind(),
422 comment.text()
423 ));
424 }
425 let nested = node
429 .ancestors()
430 .skip(1)
431 .any(|n| matches!(n.kind(), SyntaxKind::MAPPING | SyntaxKind::SEQUENCE));
432 let trailing_line = node
433 .last_child_or_token()
434 .and_then(|c| c.into_token())
435 .filter(|t| !t.text().is_empty());
436 if let Some(token) = trailing_line.filter(|_| nested) {
437 return Err(format!(
438 "nested {:?} ends with {:?} {:?} after its last entry",
439 node.kind(),
440 token.kind(),
441 token.text()
442 ));
443 }
444 let leading_line = node
446 .children_with_tokens()
447 .find(|c| !c.as_token().is_some_and(|t| t.text().is_empty()))
448 .and_then(|c| c.into_token());
449 if let Some(token) = leading_line {
450 return Err(format!(
451 "{:?} starts with {:?} {:?} before its first entry",
452 node.kind(),
453 token.kind(),
454 token.text()
455 ));
456 }
457 }
458 SyntaxKind::SEQUENCE_ENTRY
459 if !in_flow && !is_last_entry_in_parent(node) && !entry_is_terminated(node) =>
460 {
461 return Err(format!(
462 "Block-style SEQUENCE_ENTRY not terminated by a NEWLINE (last token: {:?})",
463 node.last_token().map(|t| t.kind())
464 ));
465 }
466 _ => {}
467 }
468
469 Ok(())
470}
471
472fn validate_children(node: &SyntaxNode) -> Result<(), String> {
477 match node.kind() {
478 SyntaxKind::KEY | SyntaxKind::VALUE | SyntaxKind::SEQUENCE_ENTRY
479 if node.first_child().is_none() =>
480 {
481 Err(format!("{:?} holds no node", node.kind()))
482 }
483 SyntaxKind::SCALAR if node.first_token().is_none() => Err("SCALAR holds no token".into()),
484 SyntaxKind::MAPPING | SyntaxKind::SEQUENCE => {
485 let entry = if node.kind() == SyntaxKind::MAPPING {
486 SyntaxKind::MAPPING_ENTRY
487 } else {
488 SyntaxKind::SEQUENCE_ENTRY
489 };
490 let flow = crate::nodes::starts_a_flow_collection(node);
491 let stray = node.children_with_tokens().find(|c| match c {
492 rowan::NodeOrToken::Node(n) => n.kind() != entry && n.kind() != SyntaxKind::ERROR,
493 rowan::NodeOrToken::Token(t) => match t.kind() {
494 SyntaxKind::WHITESPACE
495 | SyntaxKind::NEWLINE
496 | SyntaxKind::INDENT
497 | SyntaxKind::COMMENT => false,
498 SyntaxKind::LEFT_BRACE
499 | SyntaxKind::RIGHT_BRACE
500 | SyntaxKind::LEFT_BRACKET
501 | SyntaxKind::RIGHT_BRACKET => !flow,
502 _ => true,
503 },
504 });
505 match stray {
506 Some(c) => Err(format!(
507 "{:?} holds {:?} outside its entries",
508 node.kind(),
509 c.kind()
510 )),
511 None => Ok(()),
512 }
513 }
514 _ => Ok(()),
515 }
516}
517
518fn validate_flow_entry_end(entry: &SyntaxNode) -> Result<(), String> {
522 let children: Vec<_> = entry.children_with_tokens().collect();
523 let end = children
524 .iter()
525 .rposition(|c| c.as_node().is_some() || c.kind() == SyntaxKind::COMMA)
526 .map_or(0, |i| i + 1);
527 let newline = children[end..]
528 .iter()
529 .position(|c| c.kind() == SyntaxKind::NEWLINE);
530 match newline {
531 Some(i) if end + i + 1 < children.len() => Err(format!(
532 "flow {:?} holds {:?} after the end of its line",
533 entry.kind(),
534 children[end + i + 1].kind()
535 )),
536 _ => Ok(()),
537 }
538}
539
540fn entry_is_terminated(entry: &SyntaxNode) -> bool {
548 crate::nodes::trivia::ends_line(entry)
549}
550
551fn is_last_entry_in_parent(entry: &SyntaxNode) -> bool {
558 let kind = entry.kind();
562 let mut sib = entry.next_sibling();
563 while let Some(s) = sib {
564 if s.kind() == kind {
565 return false;
566 }
567 sib = s.next_sibling();
568 }
569 true
570}
571
572fn validate_no_stacked_indents(node: &SyntaxNode) -> Result<(), String> {
578 let mut prev_was_indent = false;
579 for child in node.children_with_tokens() {
580 match &child {
581 rowan::NodeOrToken::Token(t) if t.kind() == SyntaxKind::INDENT => {
582 if prev_was_indent {
583 return Err(format!(
584 "adjacent INDENT tokens inside {:?} -- would render as stacked indentation",
585 node.kind()
586 ));
587 }
588 prev_was_indent = true;
589 }
590 rowan::NodeOrToken::Token(_) => prev_was_indent = false,
591 rowan::NodeOrToken::Node(_) => prev_was_indent = false,
592 }
593 }
594 Ok(())
595}
596
597fn validate_no_double_trailing_newline(node: &SyntaxNode) -> Result<(), String> {
611 let checks_apply = matches!(
612 node.kind(),
613 SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
614 );
615 if checks_apply {
616 let kinds: Vec<_> = node.children_with_tokens().map(|c| c.kind()).collect();
617 if kinds.ends_with(&[SyntaxKind::NEWLINE, SyntaxKind::NEWLINE]) {
618 return Err(format!(
619 "{:?} ends with two NEWLINE tokens -- would render as a stray blank line: {:?}",
620 node.kind(),
621 node.text().to_string(),
622 ));
623 }
624 }
625 Ok(())
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 use crate::yaml::YamlFile;
632 use rowan::ast::AstNode;
633 use std::str::FromStr;
634
635 #[test]
636 fn test_validate_tree_fits_a_small_stack() {
637 let text = format!("{}{}", "[{".repeat(127), "}]".repeat(127));
639 let green = YamlFile::from_str(&text)
640 .unwrap()
641 .syntax()
642 .green()
643 .into_owned();
644 let tree = green.clone();
647 std::thread::Builder::new()
648 .stack_size(64 * 1024)
649 .spawn(move || validate_tree(&SyntaxNode::new_root_mut(tree)))
650 .unwrap()
651 .join()
652 .unwrap()
653 .unwrap();
654 drop(green);
655 }
656
657 #[test]
660 fn test_validate_tree_line_end_comment_outside_entry() {
661 let file = YamlFile::from_str("- a # c\n- b\n").unwrap();
662 assert_eq!(validate_tree(file.syntax()), Ok(()));
663 let entry = file
664 .syntax()
665 .descendants()
666 .find(|n| n.kind() == SyntaxKind::SEQUENCE_ENTRY)
667 .unwrap();
668 let line_end: Vec<_> = entry
669 .children_with_tokens()
670 .skip_while(|c| c.kind() != SyntaxKind::COMMENT)
671 .collect();
672 for c in &line_end {
673 c.detach();
674 }
675 let sequence = entry.parent().unwrap();
676 crate::nodes::insert_children(&sequence, entry.index() + 1, line_end);
677 assert_eq!(file.syntax().to_string(), "- a # c\n- b\n");
678 assert_eq!(
679 validate_tree(file.syntax()),
680 Err("SEQUENCE holds the comment \"# c\" ending an entry's line".to_string())
681 );
682 }
683
684 #[test]
688 fn test_validate_tree_comment_line_after_explicit_block_key() {
689 for yaml in ["? - a\n# c\n", "- ? - a\n# c\n"] {
690 let file = YamlFile::from_str(yaml).unwrap();
691 assert_eq!(validate_tree(file.syntax()), Ok(()), "{yaml:?}");
692 }
693 }
694
695 #[test]
698 fn test_validate_tree_nested_collection_ending_in_comment() {
699 let file = YamlFile::from_str("a:\n b: 1\n # c\n").unwrap();
700 assert_eq!(validate_tree(file.syntax()), Ok(()));
701 let root = file.syntax().first_child().unwrap().first_child().unwrap();
702 let nested = root
703 .descendants()
704 .find(|n| n.kind() == SyntaxKind::MAPPING && n != &root)
705 .unwrap();
706 let lines: Vec<_> = root
707 .children_with_tokens()
708 .skip_while(|c| c.kind() == SyntaxKind::MAPPING_ENTRY)
709 .collect();
710 for c in &lines {
711 match c {
712 rowan::NodeOrToken::Node(n) => n.detach(),
713 rowan::NodeOrToken::Token(t) => t.detach(),
714 }
715 }
716 crate::nodes::append_children(&nested, lines);
717 assert_eq!(file.syntax().to_string(), "a:\n b: 1\n # c\n");
718 assert_eq!(
719 validate_tree(file.syntax()),
720 Err("nested MAPPING ends with NEWLINE \"\\n\" after its last entry".to_string())
721 );
722 }
723
724 #[test]
725 fn test_validate_tree_implicit_null_ending_sequence_entry() {
726 for yaml in [
727 "- a:\n- c\n",
728 "-\n a: &b\n- c\n",
729 "- &c : &a\n- c\n",
730 "- &a\n- a\n-\n &a : a\n b: &b\n-\n &c : &a\n",
731 ] {
732 let file = YamlFile::from_str(yaml).unwrap();
733 assert_eq!(validate_tree(file.syntax()), Ok(()), "{yaml:?}");
734 }
735 let file = YamlFile::from_str("- a: 1\n- c\n").unwrap();
736 let entry = file
737 .syntax()
738 .descendants()
739 .find(|n| n.kind() == SyntaxKind::SEQUENCE_ENTRY)
740 .unwrap();
741 entry.last_token().unwrap().detach();
742 assert_eq!(
743 validate_tree(file.syntax()),
744 Err(
745 "Block-style SEQUENCE_ENTRY not terminated by a NEWLINE (last token: Some(INT))"
746 .to_string()
747 )
748 );
749 }
750
751 #[test]
755 fn test_validate_tree_newlines_around_value() {
756 for yaml in ["{:\nv\n}\n", "{a:\n\nv}\n", "-\n\n a\n"] {
757 let file = YamlFile::from_str(yaml).unwrap();
758 assert_eq!(validate_tree(file.syntax()), Ok(()), "{yaml:?}");
759 }
760 let file = YamlFile::from_str("- a\n- b\n").unwrap();
761 let entry = file
762 .syntax()
763 .descendants()
764 .find(|n| n.kind() == SyntaxKind::SEQUENCE_ENTRY)
765 .unwrap();
766 let blank = YamlFile::from_str("- a\n").unwrap();
767 let newline = blank
768 .syntax()
769 .descendants_with_tokens()
770 .find(|c| c.kind() == SyntaxKind::NEWLINE)
771 .unwrap();
772 newline.detach();
773 crate::nodes::append_children(&entry, vec![newline]);
774 assert_eq!(
775 validate_tree(file.syntax()),
776 Err(
777 "SEQUENCE_ENTRY ends with two NEWLINE tokens -- would render as a stray blank line: \"- a\\n\\n\""
778 .to_string()
779 )
780 );
781 }
782
783 #[test]
786 fn test_validate_tree_entry_without_value() {
787 let file = YamlFile::parse("a:\n<<\n").tree();
788 assert_eq!(validate_tree(file.syntax()), Ok(()));
789 for entry in file
790 .syntax()
791 .descendants()
792 .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
793 .collect::<Vec<_>>()
794 {
795 let value = entry
796 .children()
797 .find(|c| c.kind() == SyntaxKind::VALUE)
798 .unwrap();
799 let index = value.index();
800 value.detach();
801 assert_eq!(
802 validate_tree(file.syntax()),
803 Err("MAPPING_ENTRY should have exactly 1 VALUE, found 0".to_string())
804 );
805 crate::nodes::insert_children(&entry, index, vec![value.into()]);
806 assert_eq!(file.syntax().to_string(), "a:\n<<\n");
807 }
808 }
809
810 #[test]
811 fn test_validate_tree_empty_slots() {
812 let file = YamlFile::from_str("a: [b]\n").unwrap();
813 let find = |kind| {
814 file.syntax()
815 .descendants()
816 .find(|n| n.kind() == kind)
817 .unwrap()
818 };
819 for kind in [
820 SyntaxKind::KEY,
821 SyntaxKind::VALUE,
822 SyntaxKind::SEQUENCE_ENTRY,
823 ] {
824 let slot = find(kind);
825 let node = slot.first_child().unwrap();
826 let index = node.index();
827 node.detach();
828 assert_eq!(
829 validate_tree(file.syntax()),
830 Err(format!("{kind:?} holds no node"))
831 );
832 crate::nodes::insert_children(&slot, index, vec![node.into()]);
833 }
834 let token = find(SyntaxKind::SCALAR).first_token().unwrap();
835 token.detach();
836 assert_eq!(
837 validate_tree(file.syntax()),
838 Err("SCALAR holds no token".to_string())
839 );
840 }
841
842 #[test]
844 fn test_validate_tree_stray_token_in_collection() {
845 let file = YamlFile::parse("[]:\n]:").tree();
846 assert_eq!(validate_tree(file.syntax()), Ok(()));
847 let error = file
848 .syntax()
849 .descendants()
850 .find(|n| n.kind() == SyntaxKind::ERROR)
851 .unwrap();
852 let mapping = error.parent().unwrap();
853 let token = error.first_token().unwrap();
854 let index = error.index();
855 error.detach();
856 token.detach();
857 crate::nodes::insert_children(&mapping, index, vec![token.into()]);
858 assert_eq!(file.syntax().to_string(), "[]:\n]:");
859 assert_eq!(
860 validate_tree(file.syntax()),
861 Err("MAPPING holds RIGHT_BRACKET outside its entries".to_string())
862 );
863 }
864
865 #[test]
866 fn test_print_tree() {
867 let yaml = YamlFile::from_str("name: Alice").unwrap();
868 print_tree(yaml.syntax());
870 }
871
872 #[test]
873 fn test_tree_to_string() {
874 let yaml = YamlFile::from_str("name: Alice").unwrap();
875 let s = tree_to_string(yaml.syntax());
876 let expected = "DOCUMENT\n MAPPING\n MAPPING_ENTRY\n KEY\n SCALAR\n STRING: \"name\"\n COLON: \":\"\n WHITESPACE: \" \"\n VALUE\n SCALAR\n STRING: \"Alice\"\n";
877 assert_eq!(s, expected);
878 }
879
880 #[test]
881 fn test_tree_to_string_sequence() {
882 let yaml = YamlFile::from_str("- item1\n- item2\n").unwrap();
883 let s = tree_to_string(yaml.syntax());
884 assert_eq!(
885 s,
886 concat!(
887 "DOCUMENT\n",
888 " SEQUENCE\n",
889 " SEQUENCE_ENTRY\n",
890 " DASH: \"-\"\n",
891 " WHITESPACE: \" \"\n",
892 " SCALAR\n",
893 " STRING: \"item1\"\n",
894 " NEWLINE: \"\\\\n\"\n",
895 " SEQUENCE_ENTRY\n",
896 " DASH: \"-\"\n",
897 " WHITESPACE: \" \"\n",
898 " SCALAR\n",
899 " STRING: \"item2\"\n",
900 " NEWLINE: \"\\\\n\"\n",
901 )
902 );
903 }
904
905 #[test]
906 fn test_print_stats() {
907 let yaml = YamlFile::from_str("name: Alice\nage: 30\n").unwrap();
908 print_stats(yaml.syntax());
910 }
911
912 #[test]
913 fn test_validate_tree() {
914 let yaml = YamlFile::from_str("name: Alice\nage: 30\n").unwrap();
915 validate_tree(yaml.syntax()).expect("Tree should be valid");
916 }
917
918 fn build(kind: SyntaxKind, children: &[(SyntaxKind, Option<&str>)]) -> SyntaxNode {
919 let mut builder = rowan::GreenNodeBuilder::new();
921 builder.start_node(kind.into());
922 for (kind, text) in children {
923 match (kind, text) {
924 (SyntaxKind::ROOT, None) => builder.finish_node(),
925 (kind, None) => builder.start_node((*kind).into()),
926 (kind, Some(text)) => builder.token((*kind).into(), text),
927 }
928 }
929 builder.finish_node();
930 SyntaxNode::new_root(builder.finish())
931 }
932
933 const END: (SyntaxKind, Option<&str>) = (SyntaxKind::ROOT, None);
934
935 fn entry(key: &'static str, value: &'static str) -> Vec<(SyntaxKind, Option<&'static str>)> {
936 use SyntaxKind::*;
937 vec![
938 (MAPPING_ENTRY, None),
939 (KEY, None),
940 (SCALAR, None),
941 (STRING, Some(key)),
942 END,
943 END,
944 (COLON, Some(":")),
945 (WHITESPACE, Some(" ")),
946 (VALUE, None),
947 (SCALAR, None),
948 (STRING, Some(value)),
949 END,
950 END,
951 ]
952 }
953
954 #[test]
955 fn test_validate_tree_mapping_entry_errors() {
956 use SyntaxKind::*;
957 let mut two_keys = entry("a", "1");
958 two_keys.insert(1, (KEY, None));
959 two_keys.insert(2, END);
960 two_keys.extend([(NEWLINE, Some("\n")), END]);
961 assert_eq!(
962 validate_tree(&build(MAPPING, &two_keys)),
963 Err("MAPPING_ENTRY should have exactly 1 KEY, found 2".to_string())
964 );
965
966 let mut no_colon = entry("a", "1");
967 no_colon.remove(6);
968 no_colon.extend([(NEWLINE, Some("\n")), END]);
969 assert_eq!(
970 validate_tree(&build(MAPPING, &no_colon)),
971 Err("MAPPING_ENTRY should have exactly 1 COLON(s), found 0".to_string())
972 );
973
974 let mut two_values = entry("a", "1");
975 two_values.extend([(VALUE, None), END, (NEWLINE, Some("\n")), END]);
976 assert_eq!(
977 validate_tree(&build(MAPPING, &two_values)),
978 Err("MAPPING_ENTRY should have exactly 1 VALUE, found 2".to_string())
979 );
980
981 let mut unterminated = entry("a", "1");
982 unterminated.push(END);
983 unterminated.extend(entry("b", "2"));
984 unterminated.extend([(NEWLINE, Some("\n")), END]);
985 assert_eq!(
986 validate_tree(&build(MAPPING, &unterminated)),
987 Err(
988 "Block-style MAPPING_ENTRY not terminated by a NEWLINE (last token: Some(STRING))"
989 .to_string()
990 )
991 );
992 }
993
994 #[test]
995 fn test_validate_tree_flow_entry_may_lack_colon() {
996 use SyntaxKind::*;
997 let mut children = vec![(LEFT_BRACE, Some("{"))];
998 let mut flow_entry = entry("a", "1");
999 flow_entry.remove(6);
1000 children.extend(flow_entry);
1001 children.extend([END, (RIGHT_BRACE, Some("}"))]);
1002 assert_eq!(validate_tree(&build(MAPPING, &children)), Ok(()));
1003 }
1004
1005 #[test]
1006 fn test_validate_tree_stacked_indents_and_double_newline() {
1007 use SyntaxKind::*;
1008 assert_eq!(
1009 validate_tree(&build(
1010 SEQUENCE,
1011 &[
1012 (SEQUENCE_ENTRY, None),
1013 (DASH, Some("-")),
1014 (WHITESPACE, Some(" ")),
1015 (SCALAR, None),
1016 (STRING, Some("a")),
1017 END,
1018 (NEWLINE, Some("\n")),
1019 END,
1020 (INDENT, Some(" ")),
1021 (INDENT, Some(" ")),
1022 ]
1023 )),
1024 Err(
1025 "adjacent INDENT tokens inside SEQUENCE -- would render as stacked indentation"
1026 .to_string()
1027 )
1028 );
1029 let mut doubled = entry("a", "1");
1030 doubled.extend([(NEWLINE, Some("\n")), (NEWLINE, Some("\n")), END]);
1031 assert_eq!(
1032 validate_tree(&build(MAPPING, &doubled)),
1033 Err(
1034 "MAPPING_ENTRY ends with two NEWLINE tokens -- would render as a stray blank line: \"a: 1\\n\\n\""
1035 .to_string()
1036 )
1037 );
1038 }
1039
1040 #[test]
1041 fn test_roundtrip_ok_reports_parse_errors() {
1042 let node = build(SyntaxKind::ROOT, &[(SyntaxKind::STRING, Some("[a"))]);
1043 assert_eq!(
1044 roundtrip_ok(&node),
1045 Err(format!(
1046 "re-parse produced 1 error(s); first: {}",
1047 crate::Parse::parse_yaml("[a").positioned_errors()[0].message
1048 ))
1049 );
1050 let file = YamlFile::from_str("a: 1\n").unwrap();
1051 assert_eq!(roundtrip_ok(file.syntax()), Ok(()));
1052 }
1053}
1054
1055pub struct PrettyDebug<'a, T>(pub &'a T);
1073
1074impl<'a> fmt::Display for PrettyDebug<'a, Document> {
1075 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1076 writeln!(f, "Document {{")?;
1077
1078 if let Some(mapping) = self.0.as_mapping() {
1079 write_indented(f, &format!("{}", PrettyDebug(&mapping)), 1)?;
1080 } else if let Some(sequence) = self.0.as_sequence() {
1081 write_indented(f, &format!("{}", PrettyDebug(&sequence)), 1)?;
1082 } else if let Some(scalar) = self.0.as_scalar() {
1083 writeln!(f, " {}", PrettyDebug(&scalar))?;
1084 }
1085
1086 writeln!(f, "}}")
1087 }
1088}
1089
1090impl<'a> fmt::Display for PrettyDebug<'a, Mapping> {
1091 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1092 writeln!(f, "Mapping [")?;
1093
1094 for (key, value) in self.0.iter() {
1095 write!(f, " ")?;
1096 format_node(f, &key, 0)?;
1097 write!(f, ": ")?;
1098 format_node(f, &value, 1)?;
1099 writeln!(f)?;
1100 }
1101
1102 write!(f, "]")
1103 }
1104}
1105
1106impl<'a> fmt::Display for PrettyDebug<'a, Sequence> {
1107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1108 writeln!(f, "Sequence [")?;
1109
1110 for (i, value) in self.0.values().enumerate() {
1111 write!(f, " [{i}]: ")?;
1112 format_node(f, &value, 1)?;
1113 writeln!(f)?;
1114 }
1115
1116 write!(f, "]")
1117 }
1118}
1119
1120impl<'a> fmt::Display for PrettyDebug<'a, Scalar> {
1121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1122 write!(f, "Scalar({:?})", self.0.value())
1123 }
1124}
1125
1126fn write_indented(f: &mut fmt::Formatter<'_>, text: &str, indent: usize) -> fmt::Result {
1127 let indent_str = " ".repeat(indent);
1128 for line in text.lines() {
1129 writeln!(f, "{indent_str}{line}")?;
1130 }
1131 Ok(())
1132}
1133
1134fn format_node(f: &mut fmt::Formatter<'_>, node: &YamlNode, indent: usize) -> fmt::Result {
1135 let indent_str = " ".repeat(indent);
1136
1137 let write_scalar = |f: &mut fmt::Formatter<'_>, sv: crate::scalar::ScalarValue| {
1138 write!(f, "{:?} (type: {:?})", sv.value(), sv.scalar_type())
1139 };
1140 match node {
1141 YamlNode::Scalar(s) => write_scalar(f, crate::scalar::ScalarValue::from_scalar(s))?,
1142 YamlNode::Mapping(m) => {
1143 writeln!(f, "Mapping {{")?;
1144 for (k, v) in m.iter() {
1145 write!(f, "{indent_str} ")?;
1146 format_node(f, &k, 0)?;
1147 write!(f, ": ")?;
1148 format_node(f, &v, indent + 1)?;
1149 writeln!(f)?;
1150 }
1151 write!(f, "{indent_str}}}")?;
1152 }
1153 YamlNode::Sequence(s) => {
1154 writeln!(f, "Sequence [")?;
1155 for (i, v) in s.values().enumerate() {
1156 write!(f, "{indent_str} [{i}]: ")?;
1157 format_node(f, &v, indent + 1)?;
1158 writeln!(f)?;
1159 }
1160 write!(f, "{indent_str}]")?;
1161 }
1162 YamlNode::Alias(a) => {
1163 write!(f, "Alias(*{})", a.name())?;
1164 }
1165 YamlNode::TaggedNode(t) => {
1166 write!(f, "Tagged({:?}, ", t.tag().unwrap_or_default())?;
1167 let content = rowan::ast::AstNode::syntax(t)
1168 .children()
1169 .find_map(YamlNode::from_syntax);
1170 match content {
1171 Some(YamlNode::Scalar(s)) => {
1172 let sv = crate::scalar::ScalarValue::from_tagged(t);
1173 write_scalar(
1174 f,
1175 sv.unwrap_or_else(|| crate::scalar::ScalarValue::from_scalar(&s)),
1176 )?;
1177 }
1178 Some(content) => format_node(f, &content, indent)?,
1179 None => {}
1180 }
1181 write!(f, ")")?;
1182 }
1183 }
1184
1185 Ok(())
1186}
1187
1188pub struct ValueInspector<'a>(pub &'a crate::as_yaml::YamlNode);
1212
1213impl<'a> fmt::Display for ValueInspector<'a> {
1214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1215 writeln!(f, "Value Inspector:")?;
1216 writeln!(f, "===============")?;
1217
1218 match self.0 {
1219 crate::as_yaml::YamlNode::Scalar(s) => {
1220 writeln!(f, "Type: Scalar")?;
1221 writeln!(f, " Value: {:?}", s.as_string())?;
1222
1223 writeln!(f, "\nCoercion Capabilities:")?;
1224 writeln!(f, " to_i64: {:?}", self.0.to_i64())?;
1225 writeln!(f, " to_f64: {:?}", self.0.to_f64())?;
1226 writeln!(f, " to_bool: {:?}", self.0.to_bool())?;
1227 }
1228 crate::as_yaml::YamlNode::Mapping(m) => {
1229 writeln!(f, "Type: Mapping")?;
1230 writeln!(f, " Size: {} entries", m.len())?;
1231 }
1232 crate::as_yaml::YamlNode::Sequence(s) => {
1233 writeln!(f, "Type: Sequence")?;
1234 writeln!(f, " Length: {} items", s.len())?;
1235 }
1236 crate::as_yaml::YamlNode::Alias(a) => {
1237 writeln!(f, "Type: Alias")?;
1238 writeln!(f, " Anchor name: {}", a.name())?;
1239 }
1240 crate::as_yaml::YamlNode::TaggedNode(_) => {
1241 writeln!(f, "Type: TaggedNode")?;
1242 }
1243 }
1244
1245 Ok(())
1246 }
1247}
1248
1249pub struct VisualDiff<'a> {
1270 pub before: &'a Document,
1272 pub after: &'a Document,
1274}
1275
1276impl<'a> fmt::Display for VisualDiff<'a> {
1277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1278 writeln!(f, "Visual Diff:")?;
1279 writeln!(f, "============")?;
1280
1281 writeln!(f, "\nBefore:")?;
1282 writeln!(f, "-------")?;
1283 for line in self.before.to_string().lines() {
1284 writeln!(f, " {line}")?;
1285 }
1286
1287 writeln!(f, "\nAfter:")?;
1288 writeln!(f, "------")?;
1289 for line in self.after.to_string().lines() {
1290 writeln!(f, " {line}")?;
1291 }
1292
1293 writeln!(f, "\nChanges:")?;
1294 writeln!(f, "--------")?;
1295
1296 let before_str = self.before.to_string();
1298 let after_str = self.after.to_string();
1299 let before_lines: Vec<_> = before_str.lines().collect();
1300 let after_lines: Vec<_> = after_str.lines().collect();
1301
1302 for (i, (b, a)) in before_lines.iter().zip(after_lines.iter()).enumerate() {
1303 if b != a {
1304 writeln!(f, "Line {}: {:?} -> {:?}", i + 1, b, a)?;
1305 }
1306 }
1307
1308 if before_lines.len() > after_lines.len() {
1309 writeln!(
1310 f,
1311 "Removed {} lines",
1312 before_lines.len() - after_lines.len()
1313 )?;
1314 } else if after_lines.len() > before_lines.len() {
1315 writeln!(f, "Added {} lines", after_lines.len() - before_lines.len())?;
1316 }
1317
1318 Ok(())
1319 }
1320}
1321
1322pub fn graphviz_dot(node: &SyntaxNode) -> String {
1340 let mut result = String::from("digraph CST {\n");
1341 result.push_str(" node [shape=box, fontname=\"Courier\"];\n");
1342 result.push_str(" edge [fontsize=10];\n\n");
1343
1344 let mut counter = 0;
1345 graphviz_node(&mut result, node, None, &mut counter);
1346
1347 result.push_str("}\n");
1348 result
1349}
1350
1351fn graphviz_node(
1352 result: &mut String,
1353 node: &SyntaxNode,
1354 parent_id: Option<usize>,
1355 counter: &mut usize,
1356) {
1357 let node_id = *counter;
1358 *counter += 1;
1359
1360 let label = format!("{:?}", node.kind());
1362 result.push_str(&format!(" n{node_id} [label=\"{label}\"];\n"));
1363
1364 if let Some(pid) = parent_id {
1366 result.push_str(&format!(" n{pid} -> n{node_id};\n"));
1367 }
1368
1369 for child in node.children_with_tokens() {
1371 match child {
1372 rowan::NodeOrToken::Node(n) => {
1373 graphviz_node(result, &n, Some(node_id), counter);
1374 }
1375 rowan::NodeOrToken::Token(t) => {
1376 let token_id = *counter;
1377 *counter += 1;
1378
1379 let text = t
1380 .text()
1381 .replace('\\', "\\\\")
1382 .replace('"', "\\\"")
1383 .replace('\n', "\\\\n")
1384 .replace('\r', "\\\\r");
1385 let label = format!("{:?}\\n\\\"{}\\\"", t.kind(), text);
1386 result.push_str(&format!(
1387 " n{token_id} [label=\"{label}\", shape=ellipse, style=filled, fillcolor=lightgray];\n"
1388 ));
1389 result.push_str(&format!(" n{node_id} -> n{token_id};\n"));
1390 }
1391 }
1392 }
1393}
1394
1395#[cfg(test)]
1396mod new_debug_tests {
1397 use super::*;
1398 use crate::yaml::YamlFile;
1399 use rowan::ast::AstNode;
1400 use std::str::FromStr;
1401
1402 #[test]
1403 fn test_pretty_debug_document() {
1404 let doc = Document::from_str("name: Alice\nage: 30").unwrap();
1405 let output = format!("{}", PrettyDebug(&doc));
1406
1407 assert_eq!(output.lines().next(), Some("Document {"));
1409 assert_eq!(output.lines().last(), Some("}"));
1410 }
1411
1412 #[test]
1413 fn test_pretty_debug_shows_mapping_keys() {
1414 let doc = Document::from_str("a: {b: {c: 1}}\nl: [x, [y]]\n").unwrap();
1415 assert_eq!(
1416 PrettyDebug(&doc).to_string(),
1417 concat!(
1418 "Document {\n",
1419 " Mapping [\n",
1420 " \"a\" (type: String): Mapping {\n",
1421 " \"b\" (type: String): Mapping {\n",
1422 " \"c\" (type: String): \"1\" (type: Integer)\n",
1423 " }\n",
1424 " }\n",
1425 " \"l\" (type: String): Sequence [\n",
1426 " [0]: \"x\" (type: String)\n",
1427 " [1]: Sequence [\n",
1428 " [0]: \"y\" (type: String)\n",
1429 " ]\n",
1430 " ]\n",
1431 " ]\n",
1432 "}\n",
1433 )
1434 );
1435 }
1436
1437 #[test]
1438 fn test_pretty_debug_shows_tagged_content() {
1439 let doc = Document::from_str("a: !!int 5\nb: !t [x, *y]\n").unwrap();
1440 assert_eq!(
1441 PrettyDebug(&doc).to_string(),
1442 concat!(
1443 "Document {\n",
1444 " Mapping [\n",
1445 " \"a\" (type: String): Tagged(\"!!int\", \"5\" (type: Integer))\n",
1446 " \"b\" (type: String): Tagged(\"!t\", Sequence [\n",
1447 " [0]: \"x\" (type: String)\n",
1448 " [1]: Alias(*y)\n",
1449 " ])\n",
1450 " ]\n",
1451 "}\n",
1452 )
1453 );
1454 }
1455
1456 #[test]
1457 fn test_value_inspector_scalar() {
1458 let yaml = YamlFile::from_str("port: 8080").unwrap();
1459 let doc = yaml.document().unwrap();
1460 let mapping = doc.as_mapping().unwrap();
1461 let value = mapping.get("port").unwrap();
1462
1463 let output = format!("{}", ValueInspector(&value));
1464
1465 assert_eq!(output.lines().next(), Some("Value Inspector:"));
1466 assert_eq!(output.lines().nth(1), Some("==============="));
1467 assert_eq!(output.lines().nth(2), Some("Type: Scalar"));
1468 }
1469
1470 #[test]
1471 fn test_visual_diff_simple() {
1472 let before = Document::from_str("name: Alice").unwrap();
1473 let after = Document::from_str("name: Bob").unwrap();
1474
1475 let diff = VisualDiff {
1476 before: &before,
1477 after: &after,
1478 };
1479
1480 let output = format!("{}", diff);
1481
1482 assert_eq!(output.lines().next(), Some("Visual Diff:"));
1483 assert_eq!(output.lines().nth(1), Some("============"));
1484 }
1485
1486 #[test]
1487 fn test_graphviz_dot_structure() {
1488 let yaml = YamlFile::from_str("name: Alice").unwrap();
1489 let dot = graphviz_dot(yaml.syntax());
1490
1491 assert_eq!(dot.lines().next(), Some("digraph CST {"));
1492 assert_eq!(dot.lines().last(), Some("}"));
1493 }
1494
1495 #[test]
1496 fn test_graphviz_dot_escapes_token_text() {
1497 let yaml = YamlFile::from_str("k: \"a\\\"b\\\\c\"\n").unwrap();
1498 let dot = graphviz_dot(yaml.syntax());
1499 let token_labels: Vec<&str> = dot
1500 .lines()
1501 .filter(|l| l.contains("shape=ellipse"))
1502 .map(|l| l.split(", shape=").next().unwrap().trim())
1503 .collect();
1504 assert_eq!(
1505 token_labels,
1506 vec![
1507 r#"n6 [label="STRING\n\"k\"""#,
1508 r#"n7 [label="COLON\n\":\"""#,
1509 r#"n8 [label="WHITESPACE\n\" \"""#,
1510 r#"n11 [label="STRING\n\"\"a\\\"b\\\\c\"\"""#,
1511 r#"n12 [label="NEWLINE\n\"\\n\"""#,
1512 ]
1513 );
1514 }
1515
1516 #[test]
1517 fn test_graphviz_dot_contains_nodes() {
1518 let yaml = YamlFile::from_str("key: value").unwrap();
1519 let dot = graphviz_dot(yaml.syntax());
1520
1521 let node_count = dot.lines().filter(|l| l.trim().starts_with("node")).count();
1523 assert!(
1524 node_count >= 1,
1525 "expected at least one node declaration in dot output"
1526 );
1527 }
1528
1529 #[test]
1530 fn test_pretty_debug_sequence_and_scalar_roots() {
1531 let doc = Document::from_str("- 1\n- [2]\n").unwrap();
1532 assert_eq!(
1533 PrettyDebug(&doc).to_string(),
1534 concat!(
1535 "Document {\n",
1536 " Sequence [\n",
1537 " [0]: \"1\" (type: Integer)\n",
1538 " [1]: Sequence [\n",
1539 " [0]: \"2\" (type: Integer)\n",
1540 " ]\n",
1541 " ]\n",
1542 "}\n",
1543 )
1544 );
1545 let doc = Document::from_str("5\n").unwrap();
1546 assert_eq!(
1547 PrettyDebug(&doc).to_string(),
1548 "Document {\n Scalar(\"5\")\n}\n"
1549 );
1550 }
1551
1552 #[test]
1553 fn test_value_inspector_kinds() {
1554 let doc =
1555 Document::from_str("s: 'yes'\nm: {a: 1, b: 2}\nl: [1]\nx: *a\nt: !t 1\n").unwrap();
1556 let mapping = doc.as_mapping().unwrap();
1557 let inspect = |key: &str| ValueInspector(&mapping.get(key).unwrap()).to_string();
1558 let header = "Value Inspector:\n===============\n";
1559 assert_eq!(
1560 inspect("s"),
1561 format!("{header}Type: Scalar\n Value: \"yes\"\n\nCoercion Capabilities:\n to_i64: None\n to_f64: None\n to_bool: None\n")
1562 );
1563 assert_eq!(
1564 inspect("m"),
1565 format!("{header}Type: Mapping\n Size: 2 entries\n")
1566 );
1567 assert_eq!(
1568 inspect("l"),
1569 format!("{header}Type: Sequence\n Length: 1 items\n")
1570 );
1571 assert_eq!(
1572 inspect("x"),
1573 format!("{header}Type: Alias\n Anchor name: a\n")
1574 );
1575 assert_eq!(inspect("t"), format!("{header}Type: TaggedNode\n"));
1576 }
1577
1578 #[test]
1579 fn test_visual_diff_output() {
1580 let before = Document::from_str("name: Alice\nage: 30\n").unwrap();
1581 let after = Document::from_str("name: Bob\nage: 30\nx: 1\n").unwrap();
1582 let header = |a: &Document, b: &Document| {
1583 let mut out = "Visual Diff:\n============\n\nBefore:\n-------\n".to_string();
1584 for line in a.to_string().lines() {
1585 out.push_str(&format!(" {line}\n"));
1586 }
1587 out.push_str("\nAfter:\n------\n");
1588 for line in b.to_string().lines() {
1589 out.push_str(&format!(" {line}\n"));
1590 }
1591 out + "\nChanges:\n--------\n"
1592 };
1593 assert_eq!(
1594 VisualDiff {
1595 before: &before,
1596 after: &after
1597 }
1598 .to_string(),
1599 header(&before, &after) + "Line 1: \"name: Alice\" -> \"name: Bob\"\nAdded 1 lines\n"
1600 );
1601 assert_eq!(
1602 VisualDiff {
1603 before: &after,
1604 after: &before
1605 }
1606 .to_string(),
1607 header(&after, &before) + "Line 1: \"name: Bob\" -> \"name: Alice\"\nRemoved 1 lines\n"
1608 );
1609 }
1610
1611 #[test]
1612 fn test_visual_diff_same_length() {
1613 let before = Document::from_str("a: 1\n").unwrap();
1614 let after = Document::from_str("a: 2\n").unwrap();
1615 let diff = VisualDiff {
1616 before: &before,
1617 after: &after,
1618 }
1619 .to_string();
1620 assert!(
1621 diff.ends_with("Changes:\n--------\nLine 1: \"a: 1\" -> \"a: 2\"\n"),
1622 "{diff}"
1623 );
1624 }
1625
1626 #[test]
1627 fn test_graphviz_dot_full_output() {
1628 let yaml = YamlFile::from_str("a").unwrap();
1629 assert_eq!(
1630 graphviz_dot(yaml.syntax()),
1631 concat!(
1632 "digraph CST {\n",
1633 " node [shape=box, fontname=\"Courier\"];\n",
1634 " edge [fontsize=10];\n\n",
1635 " n0 [label=\"ROOT\"];\n",
1636 " n1 [label=\"DOCUMENT\"];\n",
1637 " n0 -> n1;\n",
1638 " n2 [label=\"SCALAR\"];\n",
1639 " n1 -> n2;\n",
1640 " n3 [label=\"STRING\\n\\\"a\\\"\", shape=ellipse, style=filled, fillcolor=lightgray];\n",
1641 " n2 -> n3;\n",
1642 "}\n",
1643 )
1644 );
1645 }
1646
1647 #[test]
1648 fn test_shape_to_string_with_leading_comments() {
1649 let file = YamlFile::from_str("a: 1\n# c\nb: 2\n").unwrap();
1650 file.document().unwrap().as_mapping().unwrap().remove("a");
1651 assert_eq!(file.to_string(), "# c\nb: 2\n");
1652 let expected = "DOCUMENT\n COMMENT: \"# c\"\n NEWLINE: \"\\\\n\"\n MAPPING\n MAPPING_ENTRY\n KEY\n SCALAR\n STRING: \"b\"\n COLON: \":\"\n WHITESPACE: \" \"\n VALUE\n SCALAR\n INT: \"2\"\n NEWLINE: \"\\\\n\"\n";
1653 assert_eq!(tree_to_string(file.syntax()), expected);
1654 assert_eq!(shape_to_string(file.syntax()), expected);
1655 let reparsed = YamlFile::from_str(&file.to_string()).unwrap();
1656 assert_eq!(shape_to_string(reparsed.syntax()), expected);
1657 }
1658
1659 #[test]
1660 fn test_shape_to_string_leaves_out_pending_block_marker() {
1661 let mapping = crate::Mapping::new_pending_block();
1662 assert_eq!(
1663 tree_to_string(mapping.syntax()),
1664 "LEFT_BRACE: \"{\"\nWHITESPACE: \"\"\nRIGHT_BRACE: \"}\"\n"
1665 );
1666 assert_eq!(
1667 shape_to_string(mapping.syntax()),
1668 "LEFT_BRACE: \"{\"\nRIGHT_BRACE: \"}\"\n"
1669 );
1670 }
1671
1672 #[test]
1673 fn test_key_string() {
1674 let doc = Document::from_str("0x10: a\n'x': b\n*a : c\n").unwrap();
1675 let keys: Vec<_> = doc.as_mapping().unwrap().keys().collect();
1676 assert_eq!(key_string(&keys[0]), Some("16".to_string()));
1677 assert_eq!(key_string(&keys[1]), Some("x".to_string()));
1678 assert_eq!(key_string(&keys[2]), None);
1679 }
1680}