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