Skip to main content

helm_schema_syntax/
cst.rs

1//! The `TemplatedDocument` CST: YAML layout nodes, template control regions,
2//! output holes, and comments, all carrying byte spans into the source.
3
4/// A half-open byte range `[start, end)` into the parsed source.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub struct Span {
7    /// Inclusive byte offset where the range begins.
8    pub start: usize,
9    /// Exclusive byte offset where the range ends.
10    pub end: usize,
11}
12
13impl Span {
14    /// Creates a half-open byte range.
15    #[must_use]
16    pub fn new(start: usize, end: usize) -> Self {
17        Self { start, end }
18    }
19}
20
21/// A parsed templated-YAML source: the layout node forest with document
22/// boundaries.
23pub struct TemplatedDocument<'src> {
24    pub(crate) source: &'src str,
25    pub(crate) roots: Vec<Node>,
26    pub(crate) document_spans: Vec<Span>,
27}
28
29impl<'src> TemplatedDocument<'src> {
30    /// Parse a template source, running the Go-template parse internally.
31    /// Returns an empty document when tree-sitter fails entirely (layout
32    /// still parses; only action holes would be missing, so failure is
33    /// modeled as a document with no action tokens).
34    #[must_use]
35    pub fn parse(source: &'src str) -> Self {
36        match crate::actions::parse_go_template(source) {
37            Some(tree) => Self::parse_with_root(source, tree.root_node()),
38            None => crate::parse::parse_document(source, Vec::new()),
39        }
40    }
41
42    /// Parse a template source reusing an existing Go-template parse of the
43    /// same source (avoids a second tree-sitter pass).
44    #[must_use]
45    pub fn parse_with_root(source: &'src str, root: tree_sitter::Node<'_>) -> Self {
46        let tokens = crate::actions::collect_action_tokens(root);
47        crate::parse::parse_document(source, tokens)
48    }
49
50    /// Returns the source text parsed into this document.
51    #[must_use]
52    pub fn source(&self) -> &'src str {
53        self.source
54    }
55
56    /// Returns the top-level CST nodes in source order.
57    #[must_use]
58    pub fn roots(&self) -> &[Node] {
59        &self.roots
60    }
61
62    /// Top-level document spans, split at `---` separator lines (nonempty
63    /// spans only, mirroring the resource-identity document splitter).
64    #[must_use]
65    pub fn document_spans(&self) -> &[Span] {
66        &self.document_spans
67    }
68}
69
70/// A CST node. Containers own the nodes that structurally nest below them;
71/// control regions and action nodes are overlay nodes attached where they
72/// appear, without affecting container structure.
73#[derive(Debug)]
74pub enum Node {
75    /// A YAML mapping entry.
76    Mapping(MappingEntry),
77    /// A YAML sequence item.
78    Sequence(SequenceItem),
79    /// A structured Go-template control region.
80    Control(ControlRegion),
81    /// A standalone output action.
82    Output(OutputAction),
83    /// A YAML comment line.
84    Comment(CommentLine),
85    /// A scalar content line.
86    Scalar(ScalarLine),
87    /// Source retained without a more precise structural interpretation.
88    Opaque(OpaqueNode),
89}
90
91/// A scalar run split into literal text and template-action holes.
92#[derive(Debug)]
93pub struct ScalarParts {
94    /// Byte range covered by the complete scalar run.
95    pub span: Span,
96    /// Literal and templated pieces in source order.
97    pub parts: Vec<ScalarPart>,
98}
99
100/// One literal or templated piece of a scalar run.
101#[derive(Debug)]
102pub enum ScalarPart {
103    /// Literal source text.
104    Text(Span),
105    /// A Go-template output action embedded in the scalar.
106    Hole(Span),
107}
108
109/// One `key: …` mapping entry line and, when the entry opens a scope, the
110/// nodes nested below it.
111#[derive(Debug)]
112pub struct MappingEntry {
113    /// The entry's own line content (key start through line end).
114    pub span: Span,
115    /// Effective indent. Entries nested inline after a sequence dash use the
116    /// line model's `dash + 2` convention rather than the literal column.
117    pub indent: usize,
118    /// Parsed mapping key, including any embedded template holes.
119    pub key: ScalarParts,
120    /// Inline (non-block) value text, when present.
121    pub value: Option<ScalarParts>,
122    /// Block-scalar header and suppressed body, for `key: |`-style entries.
123    pub block: Option<BlockScalar>,
124    /// Whether the entry opened a container scope (empty, template, or
125    /// block-scalar value with a plain key). Closed or invalid-key entries
126    /// never adopt children.
127    pub opens_scope: bool,
128    /// Nodes structurally nested below the mapping entry.
129    pub children: Vec<Node>,
130}
131
132impl MappingEntry {
133    /// The sequence items nested below this entry in document order, looking
134    /// through control regions: an item closed while a branch was active is
135    /// owned by that branch, while an item still open when the region ended
136    /// escapes to the entry itself. Guard structure is dropped; use
137    /// `children` to keep it.
138    #[must_use]
139    pub fn sequence_items(&self) -> Vec<&SequenceItem> {
140        let mut items = Vec::new();
141        collect_sequence_items(&self.children, &mut items);
142        items.sort_by_key(|item| item.span.start);
143        items
144    }
145}
146
147fn collect_sequence_items<'nodes>(nodes: &'nodes [Node], items: &mut Vec<&'nodes SequenceItem>) {
148    for node in nodes {
149        match node {
150            Node::Sequence(item) => items.push(item),
151            Node::Control(region) => {
152                for branch in &region.branches {
153                    collect_sequence_items(&branch.body, items);
154                }
155            }
156            _ => {}
157        }
158    }
159}
160
161/// One `- …` sequence item line and the nodes nested below it. An inline
162/// `- key: …` entry appears as the first child with effective indent
163/// `dash + 2`.
164#[derive(Debug)]
165pub struct SequenceItem {
166    /// The sequence item's own source line.
167    pub span: Span,
168    /// The dash column.
169    pub indent: usize,
170    /// Inline scalar item content (`- foo`), when present.
171    pub value: Option<ScalarParts>,
172    /// Block-scalar header and suppressed body, for `- |` items.
173    pub block: Option<BlockScalar>,
174    /// Nodes structurally nested below the sequence item.
175    pub children: Vec<Node>,
176}
177
178impl SequenceItem {
179    /// The item's content span: the first content after the dash through the
180    /// end of the deepest node nested below the item (a bare dash with no
181    /// content spans its own line).
182    #[must_use]
183    pub fn content_span(&self) -> Span {
184        let start = if let Some(value) = &self.value {
185            value.span.start
186        } else if let Some(block) = &self.block {
187            block.header.start
188        } else if let Some(first) = self.children.first() {
189            first.span_start()
190        } else {
191            self.span.start
192        };
193        let end = subtree_end(
194            self.span.end,
195            self.value.as_ref(),
196            self.block.as_ref(),
197            &self.children,
198        );
199        Span::new(start, end.max(start))
200    }
201}
202
203impl Node {
204    /// The byte where this node's own content starts.
205    #[must_use]
206    pub fn span_start(&self) -> usize {
207        match self {
208            Node::Mapping(entry) => entry.span.start,
209            Node::Sequence(item) => item.span.start,
210            Node::Control(region) => region.span.start,
211            Node::Output(action) => action.span.start,
212            Node::Comment(comment) => comment.span.start,
213            Node::Scalar(line) => line.span.start,
214            Node::Opaque(opaque) => opaque.span.start,
215        }
216    }
217
218    /// The end of the deepest content in this node's subtree: nested nodes,
219    /// block-scalar bodies, and inline-value holes that run past the line
220    /// end for multi-line actions.
221    #[must_use]
222    pub fn subtree_end(&self) -> usize {
223        match self {
224            Node::Mapping(entry) => subtree_end(
225                entry.span.end,
226                entry.value.as_ref(),
227                entry.block.as_ref(),
228                &entry.children,
229            ),
230            Node::Sequence(item) => subtree_end(
231                item.span.end,
232                item.value.as_ref(),
233                item.block.as_ref(),
234                &item.children,
235            ),
236            Node::Control(region) => region
237                .branches
238                .iter()
239                .flat_map(|branch| &branch.body)
240                .map(Node::subtree_end)
241                .fold(region.span.end, usize::max),
242            Node::Output(action) => action.span.end,
243            Node::Comment(comment) => comment.span.end,
244            Node::Scalar(line) => scalar_parts_end(&line.content).max(line.span.end),
245            Node::Opaque(opaque) => opaque.span.end,
246        }
247    }
248}
249
250fn subtree_end(
251    own_end: usize,
252    value: Option<&ScalarParts>,
253    block: Option<&BlockScalar>,
254    children: &[Node],
255) -> usize {
256    let mut end = own_end;
257    if let Some(value) = value {
258        end = end.max(scalar_parts_end(value));
259    }
260    if let Some(block) = block {
261        end = end.max(block.header.end).max(block.body.end);
262    }
263    for child in children {
264        end = end.max(child.subtree_end());
265    }
266    end
267}
268
269fn scalar_parts_end(parts: &ScalarParts) -> usize {
270    let mut end = parts.span.end;
271    for part in &parts.parts {
272        let (ScalarPart::Text(span) | ScalarPart::Hole(span)) = part;
273        end = end.max(span.end);
274    }
275    end
276}
277
278/// A literal block scalar (`|` / `>` families): its header token and the
279/// suppressed body span, with any template actions inside the body kept as
280/// suppressed holes.
281#[derive(Debug)]
282pub struct BlockScalar {
283    /// Block-scalar header token, including chomping and indentation markers.
284    pub header: Span,
285    /// Full body lines; empty (`start == end`) when the block has no body.
286    pub body: Span,
287    /// Template actions suppressed from normal YAML layout parsing.
288    pub holes: Vec<Span>,
289}
290
291/// Go-template action that owns a structured body.
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
293pub enum ControlKind {
294    /// Conditional `if` action.
295    If,
296    /// Context-selecting `with` action.
297    With,
298    /// Iterating `range` action.
299    Range,
300    /// Named-template `define` action.
301    Define,
302    /// Overridable named-template `block` action.
303    Block,
304}
305
306/// A template control region (`{{ if }}…{{ end }}` and friends) with its
307/// branch bodies. Container structure is decided by the visible YAML lines
308/// alone, so a region's branches hold exactly the nodes that opened and
309/// closed while the branch was active.
310#[derive(Debug)]
311pub struct ControlRegion {
312    /// Kind of action that opened the region.
313    pub kind: ControlKind,
314    /// Full source range from opener through closing action.
315    pub span: Span,
316    /// Conditional or fallback branches in source order.
317    pub branches: Vec<ControlBranch>,
318    /// `false` when the region provably violates the well-nested assumption:
319    /// a container opened inside a branch was still open when the branch
320    /// ended (its children escape the region), or a branch boundary sat
321    /// mid-line inside YAML content. Downstream must treat such regions
322    /// conservatively.
323    pub well_nested: bool,
324}
325
326/// One branch of a control region: its header action (`{{ if … }}`,
327/// `{{ else }}`, …) and the nodes emitted while the branch was active.
328#[derive(Debug)]
329pub struct ControlBranch {
330    /// Template action that opens this branch.
331    pub header: Span,
332    /// CST nodes emitted while this branch is active.
333    pub body: Vec<Node>,
334}
335
336/// A standalone-line output action (`{{ include "x" . }}` on its own line).
337/// Inline actions inside scalars are represented as [`ScalarPart::Hole`]s
338/// instead.
339#[derive(Debug)]
340pub struct OutputAction {
341    /// The full action span including delimiters.
342    pub span: Span,
343    /// The span of the expression inside the delimiters.
344    pub expr_span: Span,
345}
346
347/// A YAML `#` comment line (which may itself contain template actions).
348#[derive(Debug)]
349pub struct CommentLine {
350    /// Full source range of the comment line.
351    pub span: Span,
352    /// Comment content split around any template holes.
353    pub content: ScalarParts,
354}
355
356/// A plain scalar content line that is not a mapping entry or sequence item:
357/// flow-collection continuations, `---` markers, malformed keys, and other
358/// text the layout keeps only for its popping effect.
359#[derive(Debug)]
360pub struct ScalarLine {
361    /// Full source range of the scalar line.
362    pub span: Span,
363    /// YAML indentation column.
364    pub indent: usize,
365    /// Scalar content split around any template holes.
366    pub content: ScalarParts,
367}
368
369/// A span kept without further interpretation. Opaque nodes never guess:
370/// they preserve the raw span so downstream can attribute conservatively.
371#[derive(Debug)]
372pub struct OpaqueNode {
373    /// Source range retained for attribution.
374    pub span: Span,
375    /// Reason the source remains opaque.
376    pub kind: OpaqueKind,
377}
378
379/// Classification of source retained without deeper CST structure.
380#[derive(Clone, Copy, Debug, PartialEq, Eq)]
381pub enum OpaqueKind {
382    /// A `{{/* … */}}` template comment.
383    TemplateComment,
384    /// A `{{ $x := … }}` / `{{ $x = … }}` assignment action.
385    Assignment,
386    /// A `{{ break }}` atom.
387    Break,
388    /// A `{{ continue }}` atom.
389    Continue,
390    /// A control region that opened mid-line inside YAML content; the whole
391    /// region is preserved as one raw span.
392    InlineRegion,
393    /// Literal YAML text sharing a line with a standalone action.
394    ActionLineText,
395    /// Unparsable template content (tree-sitter `ERROR` output).
396    ParseError,
397}