Skip to main content

helm_schema_syntax/
dump.rs

1//! A compact, deterministic text rendering of the CST for golden tests.
2//! One node per line, children indented; every node shows its byte span so
3//! fixtures pin exact attribution geometry.
4
5use std::fmt::Write as _;
6
7use crate::cst::{
8    BlockScalar, ControlKind, Node, OpaqueKind, ScalarPart, ScalarParts, Span, TemplatedDocument,
9};
10
11impl TemplatedDocument<'_> {
12    /// Render the parsed document as a deterministic dump.
13    #[must_use]
14    pub fn dump(&self) -> String {
15        let mut out = String::new();
16        for (index, span) in self.document_spans.iter().enumerate() {
17            let _ = writeln!(out, "document {index} {}", fmt_span(*span));
18        }
19        for node in &self.roots {
20            dump_node(node, self.source, 0, &mut out);
21        }
22        out
23    }
24}
25
26#[expect(
27    clippy::too_many_lines,
28    reason = "keeping the exhaustive node rendering in one match makes dump coverage auditable"
29)]
30fn dump_node(node: &Node, source: &str, depth: usize, out: &mut String) {
31    let pad = "  ".repeat(depth);
32    match node {
33        Node::Mapping(entry) => {
34            let shape = if entry.block.is_some() {
35                "block"
36            } else if entry.opens_scope {
37                "open"
38            } else {
39                "closed"
40            };
41            let _ = write!(
42                out,
43                "{pad}entry {} {} indent={} key={}",
44                fmt_span(entry.span),
45                shape,
46                entry.indent,
47                fmt_parts_text(&entry.key, source),
48            );
49            if let Some(value) = &entry.value {
50                let _ = write!(out, " value={}", fmt_parts(value, source));
51            }
52            if let Some(block) = &entry.block {
53                let _ = write!(out, " {}", fmt_block(block));
54            }
55            out.push('\n');
56            for child in &entry.children {
57                dump_node(child, source, depth + 1, out);
58            }
59        }
60        Node::Sequence(item) => {
61            let _ = write!(
62                out,
63                "{pad}item {} indent={}",
64                fmt_span(item.span),
65                item.indent
66            );
67            if let Some(value) = &item.value {
68                let _ = write!(out, " value={}", fmt_parts(value, source));
69            }
70            if let Some(block) = &item.block {
71                let _ = write!(out, " {}", fmt_block(block));
72            }
73            out.push('\n');
74            for child in &item.children {
75                dump_node(child, source, depth + 1, out);
76            }
77        }
78        Node::Control(region) => {
79            let kind = match region.kind {
80                ControlKind::If => "if",
81                ControlKind::With => "with",
82                ControlKind::Range => "range",
83                ControlKind::Define => "define",
84                ControlKind::Block => "block",
85            };
86            let nested = if region.well_nested {
87                ""
88            } else {
89                " ill-nested"
90            };
91            let _ = writeln!(out, "{pad}control {kind} {}{nested}", fmt_span(region.span));
92            for branch in &region.branches {
93                let _ = writeln!(
94                    out,
95                    "{pad}  branch {} {}",
96                    fmt_span(branch.header),
97                    fmt_text(branch.header, source),
98                );
99                for child in &branch.body {
100                    dump_node(child, source, depth + 2, out);
101                }
102            }
103        }
104        Node::Output(output) => {
105            let _ = writeln!(
106                out,
107                "{pad}output {} {}",
108                fmt_span(output.span),
109                fmt_text(output.span, source),
110            );
111        }
112        Node::Comment(comment) => {
113            let _ = writeln!(
114                out,
115                "{pad}comment {} {}",
116                fmt_span(comment.span),
117                fmt_parts(&comment.content, source),
118            );
119        }
120        Node::Scalar(scalar) => {
121            let _ = writeln!(
122                out,
123                "{pad}scalar {} indent={} {}",
124                fmt_span(scalar.span),
125                scalar.indent,
126                fmt_parts(&scalar.content, source),
127            );
128        }
129        Node::Opaque(opaque) => {
130            let kind = match opaque.kind {
131                OpaqueKind::TemplateComment => "template-comment",
132                OpaqueKind::Assignment => "assign",
133                OpaqueKind::Break => "break",
134                OpaqueKind::Continue => "continue",
135                OpaqueKind::InlineRegion => "inline-region",
136                OpaqueKind::ActionLineText => "action-line-text",
137                OpaqueKind::ParseError => "parse-error",
138            };
139            let _ = writeln!(
140                out,
141                "{pad}opaque {kind} {} {}",
142                fmt_span(opaque.span),
143                fmt_text(opaque.span, source),
144            );
145        }
146    }
147}
148
149fn fmt_span(span: Span) -> String {
150    format!("[{}..{})", span.start, span.end)
151}
152
153fn fmt_text(span: Span, source: &str) -> String {
154    let text = source.get(span.start..span.end).unwrap_or("");
155    format!("{text:?}")
156}
157
158/// Key text: literal source of the whole run (holes visible as raw actions).
159fn fmt_parts_text(parts: &ScalarParts, source: &str) -> String {
160    fmt_text(parts.span, source)
161}
162
163/// Value rendering: text runs verbatim, holes marked with ⟦…⟧ so partial
164/// scalars show their exact split.
165fn fmt_parts(parts: &ScalarParts, source: &str) -> String {
166    let mut rendered = String::new();
167    for part in &parts.parts {
168        match part {
169            ScalarPart::Text(span) => {
170                rendered.push_str(source.get(span.start..span.end).unwrap_or(""));
171            }
172            ScalarPart::Hole(span) => {
173                rendered.push('⟦');
174                rendered.push_str(source.get(span.start..span.end).unwrap_or(""));
175                rendered.push('⟧');
176            }
177        }
178    }
179    format!("{rendered:?}")
180}
181
182fn fmt_block(block: &BlockScalar) -> String {
183    format!(
184        "block-header={} body={} suppressed-holes={}",
185        fmt_span(block.header),
186        fmt_span(block.body),
187        block.holes.len()
188    )
189}