Skip to main content

yaml_edit/
debug.rs

1//! Debug utilities for inspecting and understanding YAML structures.
2//!
3//! This module provides debugging tools including:
4//! - CST tree visualization
5//! - Pretty Debug formatting showing actual values
6//! - Visual diffs between documents
7//! - Deep value inspection
8//! - AST visualization (GraphViz output)
9//!
10//! These functions are useful for both contributors working on yaml-edit
11//! and users debugging their YAML documents.
12
13use 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
19/// Prints the CST (Concrete Syntax Tree) structure to stdout.
20///
21/// This is invaluable for understanding how YAML is parsed into the tree structure,
22/// debugging formatting issues, and verifying that mutations produce the expected tree.
23///
24/// # Example
25///
26/// ```
27/// use yaml_edit::{YamlFile, debug};
28/// use rowan::ast::AstNode;
29/// use std::str::FromStr;
30///
31/// let yaml = YamlFile::from_str("team:\n  - Alice\n  - Bob").unwrap();
32/// debug::print_tree(yaml.syntax());
33/// ```
34///
35/// Output:
36/// ```text
37/// DOCUMENT
38///   MAPPING
39///     MAPPING_ENTRY
40///       KEY
41///         SCALAR
42///           STRING: "team"
43///       COLON: ":"
44///       VALUE
45///         NEWLINE: "\\n"
46///         INDENT: "  "
47///         SEQUENCE
48///           SEQUENCE_ENTRY
49///             DASH: "-"
50///             WHITESPACE: " "
51///             SCALAR
52///               STRING: "Alice"
53///             NEWLINE: "\\n"
54/// ...
55/// ```
56pub fn print_tree(node: &SyntaxNode) {
57    print_tree_indent(node, 0);
58}
59
60/// Prints the CST structure with a custom starting indentation.
61pub 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
67/// Returns a string representation of the CST structure.
68///
69/// Like `print_tree` but returns a string instead of printing to stdout.
70///
71/// # Example
72///
73/// ```
74/// use yaml_edit::{YamlFile, debug};
75/// use rowan::ast::AstNode;
76/// use std::str::FromStr;
77///
78/// let yaml = YamlFile::from_str("name: Alice").unwrap();
79/// let tree_str = debug::tree_to_string(yaml.syntax());
80///
81/// 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";
82/// assert_eq!(tree_str, expected);
83/// ```
84pub 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
90/// Like [`tree_to_string`], but without the zero-width marker that an
91/// empty `{}` or `[]` carries when it is to become a block collection once
92/// it has an entry (see [`Mapping::new_pending_block`]). Re-parsing the
93/// text cannot recover the marker, so this is what to compare between an
94/// edited tree and a re-parse of its text.
95///
96/// [`Mapping::new_pending_block`]: crate::Mapping::new_pending_block
97pub 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
111/// Write `node`'s children to `result`, leaving out pending-block markers
112/// for a shape.
113fn 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
130/// Prints statistics about a YAML document's CST.
131///
132/// Shows counts of different node and token types, which can be useful
133/// for understanding document complexity and debugging issues.
134///
135/// # Example
136///
137/// ```
138/// use yaml_edit::{YamlFile, debug};
139/// use rowan::ast::AstNode;
140/// use std::str::FromStr;
141///
142/// let yaml = YamlFile::from_str("team:\n  - Alice\n  - Bob").unwrap();
143/// debug::print_stats(yaml.syntax());
144/// ```
145pub 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
185/// Validates that the CST follows expected structural invariants.
186///
187/// This is useful for testing that mutations don't break tree structure.
188/// Returns `Ok(())` if the tree is valid, or an error message if issues are found.
189///
190/// Current checks:
191/// - Every MAPPING_ENTRY has exactly one KEY
192/// - Every MAPPING_ENTRY has exactly one COLON (zero allowed for
193///   explicit-key entries with implicit-null values)
194/// - Every MAPPING_ENTRY has at most one VALUE
195/// - Block-style MAPPING_ENTRY and SEQUENCE_ENTRY nodes are terminated
196///   by a NEWLINE -- either as their own last token, or via a trailing
197///   comment/newline pair at the parent level, or (for MAPPING_ENTRYs)
198///   an implicit-null value
199/// - A block MAPPING or SEQUENCE starts with its first entry: the
200///   comment and blank lines before it belong to the parent
201/// - No two adjacent INDENT tokens (would render as doubled indentation)
202/// - No two adjacent NEWLINE tokens at the tail of a MAPPING_ENTRY (would
203///   render as a stray blank line)
204///
205/// # Example
206///
207/// ```
208/// use yaml_edit::{YamlFile, debug};
209/// use rowan::ast::AstNode;
210/// use std::str::FromStr;
211///
212/// let yaml = YamlFile::from_str("name: Alice\n").unwrap();
213/// debug::validate_tree(yaml.syntax()).expect("Tree should be valid");
214/// ```
215pub fn validate_tree(node: &SyntaxNode) -> Result<(), String> {
216    let node = &crate::nodes::immutable_copy(node);
217    // Each pass walks the tree iteratively, so deep nesting cannot overflow
218    // the stack. Whether each node being walked is in a flow collection is
219    // passed down, as asking each one would walk all its ancestors.
220    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
241/// Roundtrip check: `parse(node.to_string()).to_string()` must equal
242/// `node.to_string()`, and the re-parse must have no errors.
243///
244/// This catches classes of bugs where a mutation produces text that
245/// looks acceptable but re-parses into a different structure (e.g.
246/// missing/extra indentation that changes nesting).
247///
248/// Note: this shares our parser's blindspots. If a mutation produces
249/// a text that our parser reads back into the same shape as the
250/// pre-mutation CST, the check passes even when a different YAML
251/// parser would disagree. It is *not* a differential test against an
252/// oracle parser.
253pub 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
272/// The string under which [`YamlValue`](crate::YamlValue) holds `key` as a
273/// mapping key, or `None` if `key` holds an alias, which cannot be
274/// followed here.
275///
276/// ```
277/// use yaml_edit::{debug, Document};
278/// use std::str::FromStr;
279///
280/// let doc = Document::from_str("0x10: a\n{y: 1, x: 2}: b\n").unwrap();
281/// let keys: Vec<_> = doc.as_mapping().unwrap().keys().collect();
282/// assert_eq!(debug::key_string(&keys[0]), Some("16".to_string()));
283/// assert_eq!(debug::key_string(&keys[1]), Some("{x: 2, y: 1}".to_string()));
284/// ```
285pub 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/// Panic unless `node` passes [`validate_tree`] and [`roundtrip_ok`].
291///
292/// Unit-test counterpart of the `assert_cst_ok` helper in `tests/common`;
293/// call it on the root after every mutation.
294#[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/// Panic unless `yaml` reads `expected`, has the tree a parse of that text
307/// gives, and passes the validator.
308#[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
327/// Check `node`, which is in a flow collection if `in_flow`.
328fn 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            // A MAPPING_ENTRY normally has exactly one COLON. Two forms
351            // may lack it:
352            //   - Explicit-key entries (`? key\n`) with implicit-null value.
353            //   - Flow shorthand entries (`{key}`, `{,}`) where the entire
354            //     entry is implicit key: implicit-value with no COLON.
355            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            // Block MAPPING_ENTRYs must be terminated by a NEWLINE of
389            // their own, or one in the block node they end with.
390            //
391            // The last entry of a mapping is allowed to lack a
392            // terminator: an unterminated source doc (`a: 1\nb: 2`)
393            // is legitimate YAML and must roundtrip.
394            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        // A comment directly in a block collection is a line of its own:
402        // one ending an entry's line belongs to that entry. The entry may
403        // end in an implicit null after its line break, as in `? - a\n`.
404        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            // The lines after a nested collection's last entry belong to the
426            // collection holding the next one, or to the root collection at
427            // the end of the input.
428            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            // The lines before its first entry belong to its parent.
445            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
472/// Every KEY, VALUE and SEQUENCE_ENTRY holds a node, an omitted one being
473/// the implicit null, and every SCALAR a token. A collection holds its
474/// entries, with the trivia between them and its delimiters; a token no
475/// entry could start goes in an ERROR node.
476fn 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
518/// A flow entry ends with the line of its comma, or of its content if it
519/// has none: the lines after that, and the next line's indentation, are
520/// the collection's.
521fn 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
540/// Is this entry terminated by a NEWLINE of its own, along with the
541/// comment ending its line, if any?
542///
543/// A NEWLINE followed only by an implicit null's zero-width token counts
544/// too: in `? - a\n` the line break ends the key's sequence, and the
545/// entry's empty value comes after it. So does one followed by the spaces
546/// of a block scalar's last line, which belong to its body.
547fn entry_is_terminated(entry: &SyntaxNode) -> bool {
548    crate::nodes::trivia::ends_line(entry)
549}
550
551/// Is this entry the last MAPPING_ENTRY / SEQUENCE_ENTRY child of its
552/// parent MAPPING / SEQUENCE?
553///
554/// The last entry of a block collection is allowed to lack a trailing
555/// NEWLINE (an unterminated source doc, `a: 1\nb: 2`, is legitimate
556/// YAML and must roundtrip).
557fn is_last_entry_in_parent(entry: &SyntaxNode) -> bool {
558    // Walk siblings after `entry` looking for another of the same
559    // kind. O(distance-to-next-same-kind-sibling) per call, so the
560    // total cost across all entries in one parent is O(n), not O(n^2).
561    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
572/// Reject adjacent INDENT tokens among the children of `node`.
573///
574/// Two INDENTs concatenate at render time -- the sequence-under-empty-key
575/// bug (fixed for #38) manifested exactly this way (parent VALUE's INDENT
576/// followed by SEQUENCE's own INDENT rendered as 4 spaces).
577fn 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
597/// An entry should not end with two adjacent NEWLINE tokens as direct
598/// children. Line breaks with a node between them, as around a flow
599/// entry's value on a line of its own, are not a blank line.
600///
601/// That shape shows up when a placeholder NEWLINE isn't stripped after a
602/// child collection gains content (see issue #18 and the sequence-under-
603/// empty-key bug). Renders as a spurious blank line.
604///
605/// Only checked for MAPPING_ENTRY and SEQUENCE_ENTRY. Not applied to
606/// MAPPING / SEQUENCE containers themselves: those legitimately carry
607/// bare NEWLINE tokens between entries (blank-line separators are
608/// valid formatting) and at the tail (trailing blank lines are valid
609/// YAML input).
610fn 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        // The walk used to recurse once per node.
638        let text = format!("{}{}", "[{".repeat(127), "}]".repeat(127));
639        let green = YamlFile::from_str(&text)
640            .unwrap()
641            .syntax()
642            .green()
643            .into_owned();
644        // Dropping the last reference to a tree recurses through it, so
645        // keep one out here.
646        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    /// A comment ending an entry's line belongs to the entry, not to the
658    /// collection beside it.
659    #[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    /// An explicit key without a value that ends in a block node ends its
685    /// line before the zero-width null of its value, so a comment after
686    /// that is a line of its own.
687    #[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    /// A nested block collection ends in an entry: the lines after it are
696    /// its parent's.
697    #[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    /// Line breaks with a node between them are not a blank line: in a
752    /// flow entry the value may sit on its own line, between the line break
753    /// after the `:` and the one ending the entry.
754    #[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    /// The parser gives an entry whose value was omitted, or whose `:` is
784    /// missing, an implicit null VALUE, so one without is malformed.
785    #[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    /// A token no entry starts with goes in an ERROR node.
843    #[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        // Should not panic
869        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        // Should not panic
909        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        // Each child without text opens a node, `END` closes one.
920        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
1055/// Pretty-print a YAML document showing its structure and values.
1056///
1057/// This provides a human-readable view of the document structure,
1058/// showing both the logical YAML structure and the actual values.
1059///
1060/// # Example
1061///
1062/// ```
1063/// use yaml_edit::{Document, debug::PrettyDebug};
1064/// use std::str::FromStr;
1065///
1066/// let doc = Document::from_str("name: Alice\nage: 30").unwrap();
1067/// assert_eq!(
1068///     PrettyDebug(&doc).to_string(),
1069///     "Document {\n  Mapping [\n    \"name\" (type: String): \"Alice\" (type: String)\n    \"age\" (type: String): \"30\" (type: Integer)\n  ]\n}\n"
1070/// );
1071/// ```
1072pub 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
1188/// Value inspector for deep type analysis.
1189///
1190/// Provides detailed information about a YAML value including:
1191/// - Parsed type
1192/// - Raw text representation
1193/// - Coercion possibilities
1194///
1195/// # Example
1196///
1197/// ```
1198/// use yaml_edit::{YamlFile, debug::ValueInspector};
1199/// use std::str::FromStr;
1200///
1201/// let yaml = YamlFile::from_str("port: 8080").unwrap();
1202/// let doc = yaml.document().unwrap();
1203/// let mapping = doc.as_mapping().unwrap();
1204/// let value = mapping.get("port").unwrap();
1205///
1206/// assert_eq!(
1207///     ValueInspector(&value).to_string(),
1208///     "Value Inspector:\n===============\nType: Scalar\n  Value: \"8080\"\n\nCoercion Capabilities:\n  to_i64: Some(8080)\n  to_f64: None\n  to_bool: None\n"
1209/// );
1210/// ```
1211pub 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
1249/// Visual diff between two YAML documents.
1250///
1251/// Shows additions, deletions, and modifications between two versions.
1252///
1253/// # Example
1254///
1255/// ```
1256/// use yaml_edit::{Document, debug::VisualDiff};
1257/// use std::str::FromStr;
1258///
1259/// let before = Document::from_str("name: Alice\nage: 30").unwrap();
1260/// let after = Document::from_str("name: Bob\nage: 30").unwrap();
1261///
1262/// let diff = VisualDiff {
1263///     before: &before,
1264///     after: &after,
1265/// }
1266/// .to_string();
1267/// assert!(diff.ends_with("Changes:\n--------\nLine 1: \"name: Alice\" -> \"name: Bob\"\n"));
1268/// ```
1269pub struct VisualDiff<'a> {
1270    /// The original document
1271    pub before: &'a Document,
1272    /// The modified document
1273    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        // Simple line-based diff
1297        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
1322/// Generate GraphViz DOT format visualization of the CST.
1323///
1324/// This creates a visual graph representation suitable for rendering
1325/// with GraphViz tools like `dot`.
1326///
1327/// # Example
1328///
1329/// ```
1330/// use yaml_edit::{YamlFile, debug::graphviz_dot};
1331/// use rowan::ast::AstNode;
1332/// use std::str::FromStr;
1333///
1334/// let yaml = YamlFile::from_str("name: Alice").unwrap();
1335/// let dot = graphviz_dot(yaml.syntax());
1336/// assert!(dot.starts_with("digraph CST {\n"));
1337/// // Save to file and render with: dot -Tpng output.dot -o output.png
1338/// ```
1339pub 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    // Create node
1361    let label = format!("{:?}", node.kind());
1362    result.push_str(&format!("  n{node_id} [label=\"{label}\"];\n"));
1363
1364    // Link to parent
1365    if let Some(pid) = parent_id {
1366        result.push_str(&format!("  n{pid} -> n{node_id};\n"));
1367    }
1368
1369    // Process children
1370    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        // Check structure exists (exact formatting may vary)
1408        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        // Should have node declarations (at least one "node" line in the DOT output)
1522        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}