Skip to main content

helm_schema_syntax/
actions.rs

1//! Template action tokens: a linear, byte-ordered stream of `{{ … }}`
2//! action spans extracted from the tree-sitter Go-template parse. The layout
3//! parser overlays these tokens on the line structure; they classify holes
4//! and control regions but never decide YAML container structure.
5
6use crate::cst::{ControlKind, Span};
7
8/// Parse `source` with the tree-sitter Go-template grammar. This crate is
9/// the single owner of the raw Go-template tree parse; `helm-schema-ast`
10/// re-exports this function and layers the typed expression AST on top.
11#[tracing::instrument(skip_all, fields(bytes = source.len()))]
12#[must_use]
13pub fn parse_go_template(source: &str) -> Option<tree_sitter::Tree> {
14    let language =
15        tree_sitter::Language::new(helm_schema_template_grammar::go_template::language());
16    let mut parser = tree_sitter::Parser::new();
17    parser.set_language(&language).ok()?;
18    parser.parse(source, None)
19}
20
21#[derive(Clone, Copy, Debug)]
22pub(crate) struct ActionToken {
23    pub(crate) span: Span,
24    pub(crate) kind: TokenKind,
25}
26
27#[derive(Clone, Copy, Debug)]
28pub(crate) enum TokenKind {
29    /// `{{ pipeline }}` — renders output at this position.
30    Output { expr_span: Span },
31    /// `{{ $x := … }}` / `{{ $x = … }}` — renders nothing.
32    Assign,
33    /// `{{/* … */}}`.
34    TemplateComment,
35    /// `{{ break }}`.
36    Break,
37    /// `{{ continue }}`.
38    Continue,
39    /// Header of a control region; `region_end` is the byte end of the whole
40    /// `{{ … }}…{{ end }}` construct.
41    RegionOpen {
42        region: usize,
43        kind: ControlKind,
44        region_end: usize,
45    },
46    /// An `{{ else }}` / `{{ else if … }}` / `{{ else with … }}` boundary.
47    RegionBranch { region: usize },
48    /// The `{{ end }}` closer.
49    RegionEnd { region: usize },
50    /// Unparsable action content.
51    Error,
52}
53
54pub(crate) fn collect_action_tokens(root: tree_sitter::Node<'_>) -> Vec<ActionToken> {
55    let mut out = Vec::new();
56    let mut next_region = 0usize;
57    walk_body(root, &mut next_region, &mut out);
58    out.sort_by_key(|token| (token.span.start, token.span.end));
59    out
60}
61
62fn is_left_delimiter(kind: &str) -> bool {
63    matches!(kind, "{{" | "{{-")
64}
65
66fn is_right_delimiter(kind: &str) -> bool {
67    matches!(kind, "}}" | "-}}")
68}
69
70fn control_kind(node_kind: &str) -> Option<ControlKind> {
71    match node_kind {
72        "if_action" => Some(ControlKind::If),
73        "with_action" => Some(ControlKind::With),
74        "range_action" => Some(ControlKind::Range),
75        "define_action" => Some(ControlKind::Define),
76        "block_action" => Some(ControlKind::Block),
77        _ => None,
78    }
79}
80
81/// Walk a body-level node (the template root, an `ERROR` recovery node):
82/// inline `{{ expr }}` actions arrive as flat delimiter/content sibling runs
83/// because `_pipeline_action` is inlined in the grammar.
84fn walk_body(node: tree_sitter::Node<'_>, next_region: &mut usize, out: &mut Vec<ActionToken>) {
85    let mut group = GroupState::default();
86    let mut cursor = node.walk();
87    if !cursor.goto_first_child() {
88        return;
89    }
90    loop {
91        let child = cursor.node();
92        dispatch_body_child(child, next_region, &mut group, out);
93        if !cursor.goto_next_sibling() {
94            break;
95        }
96    }
97    group.finish(node.end_byte(), out);
98}
99
100fn dispatch_body_child<'tree>(
101    child: tree_sitter::Node<'tree>,
102    next_region: &mut usize,
103    group: &mut GroupState<'tree>,
104    out: &mut Vec<ActionToken>,
105) {
106    let kind = child.kind();
107    if !child.is_named() {
108        if is_left_delimiter(kind) {
109            group.open(child, out);
110        } else if is_right_delimiter(kind) {
111            group.close(child.end_byte(), out);
112        }
113        return;
114    }
115    if group.collect_named(child) {
116        return;
117    }
118    match kind {
119        "text" | "yaml_no_injection_text" | "comment" => {
120            // A comment outside a delimiter group only occurs in recovery
121            // trees; the grouped path handles the normal case.
122        }
123        "template_action" => out.push(ActionToken {
124            span: node_span(child),
125            kind: TokenKind::Output {
126                expr_span: node_span(child),
127            },
128        }),
129        "break_action" => out.push(ActionToken {
130            span: node_span(child),
131            kind: TokenKind::Break,
132        }),
133        "continue_action" => out.push(ActionToken {
134            span: node_span(child),
135            kind: TokenKind::Continue,
136        }),
137        "ERROR" => walk_body(child, next_region, out),
138        _ => {
139            if let Some(control) = control_kind(kind) {
140                walk_control(child, control, next_region, out);
141            } else {
142                out.push(ActionToken {
143                    span: node_span(child),
144                    kind: TokenKind::Error,
145                });
146            }
147        }
148    }
149}
150
151/// Walk a control node. Its own bracket actions (`{{ if … }}`, `{{ else }}`,
152/// `{{ end }}`) are the delimiter runs WITHOUT a field name; branch-body
153/// children (and inner action delimiters) carry the branch field.
154fn walk_control(
155    node: tree_sitter::Node<'_>,
156    kind: ControlKind,
157    next_region: &mut usize,
158    out: &mut Vec<ActionToken>,
159) {
160    let region = *next_region;
161    *next_region += 1;
162    let region_end = node.end_byte();
163
164    let mut group = GroupState::default();
165    let mut bracket: Option<Bracket> = None;
166    let mut opened = false;
167
168    let mut cursor = node.walk();
169    if !cursor.goto_first_child() {
170        return;
171    }
172    loop {
173        let child = cursor.node();
174        let field = cursor.field_name();
175        if let Some(state) = bracket.as_mut() {
176            if !child.is_named() && field.is_none() && is_right_delimiter(child.kind()) {
177                let closed = Bracket {
178                    end: child.end_byte(),
179                    ..*state
180                };
181                bracket = None;
182                emit_bracket(closed, region, kind, region_end, &mut opened, out);
183            } else if !child.is_named() {
184                match child.kind() {
185                    "else" => state.has_else = true,
186                    "end" => state.has_end = true,
187                    _ => {}
188                }
189            }
190        } else if field.is_none() && !child.is_named() && is_left_delimiter(child.kind()) {
191            bracket = Some(Bracket {
192                start: child.start_byte(),
193                end: child.end_byte(),
194                has_else: false,
195                has_end: false,
196            });
197        } else {
198            dispatch_body_child(child, next_region, &mut group, out);
199        }
200        if !cursor.goto_next_sibling() {
201            break;
202        }
203    }
204    group.finish(node.end_byte(), out);
205}
206
207#[derive(Clone, Copy)]
208struct Bracket {
209    start: usize,
210    end: usize,
211    has_else: bool,
212    has_end: bool,
213}
214
215fn emit_bracket(
216    bracket: Bracket,
217    region: usize,
218    kind: ControlKind,
219    region_end: usize,
220    opened: &mut bool,
221    out: &mut Vec<ActionToken>,
222) {
223    let span = Span::new(bracket.start, bracket.end);
224    let token_kind = if !*opened {
225        *opened = true;
226        TokenKind::RegionOpen {
227            region,
228            kind,
229            region_end,
230        }
231    } else if bracket.has_end {
232        TokenKind::RegionEnd { region }
233    } else if bracket.has_else {
234        TokenKind::RegionBranch { region }
235    } else {
236        TokenKind::Error
237    };
238    out.push(ActionToken {
239        span,
240        kind: token_kind,
241    });
242}
243
244/// State machine grouping a flat `{{`, content…, `}}` sibling run into one
245/// action token. Defensive against recovery trees: unbalanced delimiters
246/// surface as [`TokenKind::Error`] tokens instead of being dropped.
247#[derive(Default)]
248struct GroupState<'tree> {
249    start: Option<usize>,
250    named: Vec<tree_sitter::Node<'tree>>,
251}
252
253impl<'tree> GroupState<'tree> {
254    fn open(&mut self, child: tree_sitter::Node<'tree>, out: &mut Vec<ActionToken>) {
255        if let Some(start) = self.start.take() {
256            out.push(ActionToken {
257                span: Span::new(start, child.start_byte()),
258                kind: TokenKind::Error,
259            });
260            self.named.clear();
261        }
262        self.start = Some(child.start_byte());
263    }
264
265    fn close(&mut self, end: usize, out: &mut Vec<ActionToken>) {
266        let Some(start) = self.start.take() else {
267            return;
268        };
269        let span = Span::new(start, end);
270        let kind = classify_group(&self.named);
271        self.named.clear();
272        out.push(ActionToken { span, kind });
273    }
274
275    /// Returns `true` when the child was consumed as group content.
276    fn collect_named(&mut self, child: tree_sitter::Node<'tree>) -> bool {
277        if self.start.is_some() {
278            self.named.push(child);
279            return true;
280        }
281        false
282    }
283
284    fn finish(&mut self, node_end: usize, out: &mut Vec<ActionToken>) {
285        if let Some(start) = self.start.take() {
286            out.push(ActionToken {
287                span: Span::new(start, node_end),
288                kind: TokenKind::Error,
289            });
290            self.named.clear();
291        }
292    }
293}
294
295fn classify_group(named: &[tree_sitter::Node<'_>]) -> TokenKind {
296    let Some(first) = named.first() else {
297        return TokenKind::Error;
298    };
299    match first.kind() {
300        "comment" => TokenKind::TemplateComment,
301        "variable_definition" | "assignment" => TokenKind::Assign,
302        "ERROR" => TokenKind::Error,
303        _ => {
304            let last = named.last().unwrap_or(first);
305            TokenKind::Output {
306                expr_span: Span::new(first.start_byte(), last.end_byte()),
307            }
308        }
309    }
310}
311
312fn node_span(node: tree_sitter::Node<'_>) -> Span {
313    Span::new(node.start_byte(), node.end_byte())
314}