use crate::as_yaml::YamlNode;
use crate::lex::SyntaxKind;
use crate::yaml::{Document, Mapping, Scalar, Sequence, SyntaxNode};
use std::fmt;
pub fn print_tree(node: &SyntaxNode) {
print_tree_indent(node, 0);
}
pub fn print_tree_indent(node: &SyntaxNode, indent: usize) {
for child in node.children_with_tokens() {
let prefix = " ".repeat(indent);
match child {
rowan::NodeOrToken::Node(n) => {
println!("{}{:?}", prefix, n.kind());
print_tree_indent(&n, indent + 1);
}
rowan::NodeOrToken::Token(t) => {
let text = t.text().replace('\n', "\\n").replace('\r', "\\r");
println!("{}{:?}: {:?}", prefix, t.kind(), text);
}
}
}
}
pub fn tree_to_string(node: &SyntaxNode) -> String {
let mut result = String::new();
tree_to_string_indent(node, 0, &mut result);
result
}
fn tree_to_string_indent(node: &SyntaxNode, indent: usize, result: &mut String) {
for child in node.children_with_tokens() {
let prefix = " ".repeat(indent);
match child {
rowan::NodeOrToken::Node(n) => {
result.push_str(&format!("{}{:?}\n", prefix, n.kind()));
tree_to_string_indent(&n, indent + 1, result);
}
rowan::NodeOrToken::Token(t) => {
let text = t.text().replace('\n', "\\n").replace('\r', "\\r");
result.push_str(&format!("{}{:?}: {:?}\n", prefix, t.kind(), text));
}
}
}
}
pub fn print_stats(node: &SyntaxNode) {
let mut node_counts = std::collections::HashMap::new();
let mut token_counts = std::collections::HashMap::new();
count_nodes(node, &mut node_counts, &mut token_counts);
println!("=== Node Counts ===");
let mut node_vec: Vec<_> = node_counts.iter().collect();
node_vec.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
for (kind, count) in node_vec {
println!(" {:?}: {}", kind, count);
}
println!("\n=== Token Counts ===");
let mut token_vec: Vec<_> = token_counts.iter().collect();
token_vec.sort_by_key(|(_, count)| std::cmp::Reverse(**count));
for (kind, count) in token_vec {
println!(" {:?}: {}", kind, count);
}
}
fn count_nodes(
node: &SyntaxNode,
node_counts: &mut std::collections::HashMap<SyntaxKind, usize>,
token_counts: &mut std::collections::HashMap<SyntaxKind, usize>,
) {
*node_counts.entry(node.kind()).or_insert(0) += 1;
for child in node.children_with_tokens() {
match child {
rowan::NodeOrToken::Node(n) => {
count_nodes(&n, node_counts, token_counts);
}
rowan::NodeOrToken::Token(t) => {
*token_counts.entry(t.kind()).or_insert(0) += 1;
}
}
}
}
pub fn validate_tree(node: &SyntaxNode) -> Result<(), String> {
validate_node(node)
}
fn validate_node(node: &SyntaxNode) -> Result<(), String> {
match node.kind() {
SyntaxKind::MAPPING_ENTRY => {
let keys: Vec<_> = node
.children()
.filter(|c| c.kind() == SyntaxKind::KEY)
.collect();
if keys.len() != 1 {
return Err(format!(
"MAPPING_ENTRY should have exactly 1 KEY, found {}",
keys.len()
));
}
let colons: Vec<_> = node
.children_with_tokens()
.filter(|c| {
c.as_token()
.map(|t| t.kind() == SyntaxKind::COLON)
.unwrap_or(false)
})
.collect();
if colons.len() != 1 {
return Err(format!(
"MAPPING_ENTRY should have exactly 1 COLON, found {}",
colons.len()
));
}
let values: Vec<_> = node
.children()
.filter(|c| c.kind() == SyntaxKind::VALUE)
.collect();
if values.len() > 1 {
return Err(format!(
"MAPPING_ENTRY should have at most 1 VALUE, found {}",
values.len()
));
}
if !is_in_flow_collection(node) {
if let Some(last_token) = node.last_token() {
if last_token.kind() != SyntaxKind::NEWLINE {
return Err(format!(
"Block-style MAPPING_ENTRY should end with NEWLINE, ends with {:?}",
last_token.kind()
));
}
}
}
}
SyntaxKind::SEQUENCE_ENTRY => {
if !is_in_flow_collection(node) {
if let Some(last_token) = node.last_token() {
if last_token.kind() != SyntaxKind::NEWLINE {
return Err(format!(
"Block-style SEQUENCE_ENTRY should end with NEWLINE, ends with {:?}",
last_token.kind()
));
}
}
}
}
_ => {}
}
for child in node.children() {
validate_node(&child)?;
}
Ok(())
}
fn is_in_flow_collection(node: &SyntaxNode) -> bool {
let mut current = node.parent();
while let Some(parent) = current {
match parent.kind() {
SyntaxKind::MAPPING => {
for child in parent.children_with_tokens() {
if let Some(token) = child.as_token() {
if token.kind() == SyntaxKind::LEFT_BRACE {
return true;
}
}
}
return false;
}
SyntaxKind::SEQUENCE => {
for child in parent.children_with_tokens() {
if let Some(token) = child.as_token() {
if token.kind() == SyntaxKind::LEFT_BRACKET {
return true;
}
}
}
return false;
}
_ => current = parent.parent(),
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::yaml::YamlFile;
use rowan::ast::AstNode;
use std::str::FromStr;
#[test]
fn test_print_tree() {
let yaml = YamlFile::from_str("name: Alice").unwrap();
print_tree(yaml.syntax());
}
#[test]
fn test_tree_to_string() {
let yaml = YamlFile::from_str("name: Alice").unwrap();
let s = tree_to_string(yaml.syntax());
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";
assert_eq!(s, expected);
}
#[test]
fn test_tree_to_string_sequence() {
let yaml = YamlFile::from_str("- item1\n- item2\n").unwrap();
let s = tree_to_string(yaml.syntax());
assert_eq!(
s,
concat!(
"DOCUMENT\n",
" SEQUENCE\n",
" SEQUENCE_ENTRY\n",
" DASH: \"-\"\n",
" WHITESPACE: \" \"\n",
" SCALAR\n",
" STRING: \"item1\"\n",
" NEWLINE: \"\\\\n\"\n",
" SEQUENCE_ENTRY\n",
" DASH: \"-\"\n",
" WHITESPACE: \" \"\n",
" SCALAR\n",
" STRING: \"item2\"\n",
" NEWLINE: \"\\\\n\"\n",
)
);
}
#[test]
fn test_print_stats() {
let yaml = YamlFile::from_str("name: Alice\nage: 30\n").unwrap();
print_stats(yaml.syntax());
}
#[test]
fn test_validate_tree() {
let yaml = YamlFile::from_str("name: Alice\nage: 30\n").unwrap();
validate_tree(yaml.syntax()).expect("Tree should be valid");
}
}
pub struct PrettyDebug<'a, T>(pub &'a T);
impl<'a> fmt::Display for PrettyDebug<'a, Document> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Document {{")?;
if let Some(mapping) = self.0.as_mapping() {
write_indented(f, &format!("{}", PrettyDebug(&mapping)), 1)?;
} else if let Some(sequence) = self.0.as_sequence() {
write_indented(f, &format!("{}", PrettyDebug(&sequence)), 1)?;
} else if let Some(scalar) = self.0.as_scalar() {
writeln!(f, " {}", PrettyDebug(&scalar))?;
}
writeln!(f, "}}")
}
}
impl<'a> fmt::Display for PrettyDebug<'a, Mapping> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Mapping [")?;
for (key, value) in self.0.iter() {
write!(f, " {:?}: ", key)?;
format_node(f, &value, 1)?;
writeln!(f)?;
}
write!(f, "]")
}
}
impl<'a> fmt::Display for PrettyDebug<'a, Sequence> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Sequence [")?;
for (i, value) in self.0.values().enumerate() {
write!(f, " [{}]: ", i)?;
format_node(f, &value, 1)?;
writeln!(f)?;
}
write!(f, "]")
}
}
impl<'a> fmt::Display for PrettyDebug<'a, Scalar> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Scalar({:?})", self.0.value())
}
}
fn write_indented(f: &mut fmt::Formatter<'_>, text: &str, indent: usize) -> fmt::Result {
let indent_str = " ".repeat(indent);
for line in text.lines() {
writeln!(f, "{}{}", indent_str, line)?;
}
Ok(())
}
fn format_node(f: &mut fmt::Formatter<'_>, node: &YamlNode, indent: usize) -> fmt::Result {
let indent_str = " ".repeat(indent);
match node {
YamlNode::Scalar(s) => {
let sv = crate::scalar::ScalarValue::from_scalar(s);
write!(f, "{:?} (type: {:?})", sv.value(), sv.scalar_type())?;
}
YamlNode::Mapping(m) => {
writeln!(f, "Mapping {{")?;
for (k, v) in m.iter() {
write!(f, "{} ", indent_str)?;
format_node(f, &k, 0)?;
write!(f, ": ")?;
format_node(f, &v, indent + 1)?;
writeln!(f)?;
}
write!(f, "{}}}", indent_str)?;
}
YamlNode::Sequence(s) => {
writeln!(f, "Sequence [")?;
for (i, v) in s.values().enumerate() {
write!(f, "{} [{}]: ", indent_str, i)?;
format_node(f, &v, indent + 1)?;
writeln!(f)?;
}
write!(f, "{}]", indent_str)?;
}
YamlNode::Alias(a) => {
write!(f, "Alias(*{})", a.name())?;
}
YamlNode::TaggedNode(t) => {
write!(f, "Tagged({:?})", t.tag().unwrap_or_default())?;
}
}
Ok(())
}
pub struct ValueInspector<'a>(pub &'a crate::as_yaml::YamlNode);
impl<'a> fmt::Display for ValueInspector<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Value Inspector:")?;
writeln!(f, "===============")?;
match self.0 {
crate::as_yaml::YamlNode::Scalar(s) => {
writeln!(f, "Type: Scalar")?;
writeln!(f, " Value: {:?}", s.as_string())?;
writeln!(f, "\nCoercion Capabilities:")?;
writeln!(f, " to_i64: {:?}", self.0.to_i64())?;
writeln!(f, " to_f64: {:?}", self.0.to_f64())?;
writeln!(f, " to_bool: {:?}", self.0.to_bool())?;
}
crate::as_yaml::YamlNode::Mapping(m) => {
writeln!(f, "Type: Mapping")?;
writeln!(f, " Size: {} entries", m.len())?;
}
crate::as_yaml::YamlNode::Sequence(s) => {
writeln!(f, "Type: Sequence")?;
writeln!(f, " Length: {} items", s.len())?;
}
crate::as_yaml::YamlNode::Alias(a) => {
writeln!(f, "Type: Alias")?;
writeln!(f, " Anchor name: {}", a.name())?;
}
crate::as_yaml::YamlNode::TaggedNode(_) => {
writeln!(f, "Type: TaggedNode")?;
}
}
Ok(())
}
}
pub struct VisualDiff<'a> {
pub before: &'a Document,
pub after: &'a Document,
}
impl<'a> fmt::Display for VisualDiff<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Visual Diff:")?;
writeln!(f, "============")?;
writeln!(f, "\nBefore:")?;
writeln!(f, "-------")?;
for line in self.before.to_string().lines() {
writeln!(f, " {}", line)?;
}
writeln!(f, "\nAfter:")?;
writeln!(f, "------")?;
for line in self.after.to_string().lines() {
writeln!(f, " {}", line)?;
}
writeln!(f, "\nChanges:")?;
writeln!(f, "--------")?;
let before_str = self.before.to_string();
let after_str = self.after.to_string();
let before_lines: Vec<_> = before_str.lines().collect();
let after_lines: Vec<_> = after_str.lines().collect();
for (i, (b, a)) in before_lines.iter().zip(after_lines.iter()).enumerate() {
if b != a {
writeln!(f, "Line {}: {:?} -> {:?}", i + 1, b, a)?;
}
}
if before_lines.len() > after_lines.len() {
writeln!(
f,
"Removed {} lines",
before_lines.len() - after_lines.len()
)?;
} else if after_lines.len() > before_lines.len() {
writeln!(f, "Added {} lines", after_lines.len() - before_lines.len())?;
}
Ok(())
}
}
pub fn graphviz_dot(node: &SyntaxNode) -> String {
let mut result = String::from("digraph CST {\n");
result.push_str(" node [shape=box, fontname=\"Courier\"];\n");
result.push_str(" edge [fontsize=10];\n\n");
let mut counter = 0;
graphviz_node(&mut result, node, None, &mut counter);
result.push_str("}\n");
result
}
fn graphviz_node(
result: &mut String,
node: &SyntaxNode,
parent_id: Option<usize>,
counter: &mut usize,
) {
let node_id = *counter;
*counter += 1;
let label = format!("{:?}", node.kind());
result.push_str(&format!(" n{} [label=\"{}\"];\n", node_id, label));
if let Some(pid) = parent_id {
result.push_str(&format!(" n{} -> n{};\n", pid, node_id));
}
for child in node.children_with_tokens() {
match child {
rowan::NodeOrToken::Node(n) => {
graphviz_node(result, &n, Some(node_id), counter);
}
rowan::NodeOrToken::Token(t) => {
let token_id = *counter;
*counter += 1;
let text = t
.text()
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\\\n")
.replace('\r', "\\\\r");
let label = format!("{:?}\\n{:?}", t.kind(), text);
result.push_str(&format!(
" n{} [label=\"{}\", shape=ellipse, style=filled, fillcolor=lightgray];\n",
token_id, label
));
result.push_str(&format!(" n{} -> n{};\n", node_id, token_id));
}
}
}
}
#[cfg(test)]
mod new_debug_tests {
use super::*;
use crate::yaml::YamlFile;
use rowan::ast::AstNode;
use std::str::FromStr;
#[test]
fn test_pretty_debug_document() {
let doc = Document::from_str("name: Alice\nage: 30").unwrap();
let output = format!("{}", PrettyDebug(&doc));
assert_eq!(output.lines().next(), Some("Document {"));
assert_eq!(output.lines().last(), Some("}"));
}
#[test]
fn test_value_inspector_scalar() {
let yaml = YamlFile::from_str("port: 8080").unwrap();
let doc = yaml.document().unwrap();
let mapping = doc.as_mapping().unwrap();
let value = mapping.get("port").unwrap();
let output = format!("{}", ValueInspector(&value));
assert_eq!(output.lines().next(), Some("Value Inspector:"));
assert_eq!(output.lines().nth(1), Some("==============="));
assert_eq!(output.lines().nth(2), Some("Type: Scalar"));
}
#[test]
fn test_visual_diff_simple() {
let before = Document::from_str("name: Alice").unwrap();
let after = Document::from_str("name: Bob").unwrap();
let diff = VisualDiff {
before: &before,
after: &after,
};
let output = format!("{}", diff);
assert_eq!(output.lines().next(), Some("Visual Diff:"));
assert_eq!(output.lines().nth(1), Some("============"));
}
#[test]
fn test_graphviz_dot_structure() {
let yaml = YamlFile::from_str("name: Alice").unwrap();
let dot = graphviz_dot(yaml.syntax());
assert_eq!(dot.lines().next(), Some("digraph CST {"));
assert_eq!(dot.lines().last(), Some("}"));
}
#[test]
fn test_graphviz_dot_contains_nodes() {
let yaml = YamlFile::from_str("key: value").unwrap();
let dot = graphviz_dot(yaml.syntax());
let node_count = dot.lines().filter(|l| l.trim().starts_with("node")).count();
assert!(
node_count >= 1,
"expected at least one node declaration in dot output"
);
}
}