Skip to main content

adam_lang/
ast.rs

1//! A span-carrying adam-lang structural AST, built by [`crate::AdamAstParser`] as an alternative to
2//! [`crate::AdamParser`]'s direct `adam_rs::Sheet` construction. Method bodies and cell
3//! initializers reference [`cel_parser::Expr`]/[`cel_parser::lex_lexer::Literal`] directly.
4//! Carries no resolved types, no `TypeRegistry` lookups, and never fails on semantic grounds
5//! (unknown type name, literal/type mismatch, undeclared cell, arity mismatch) — those checks
6//! are deferred to a later, separate compile-to-`Sheet` phase, mirroring
7//! [`cel_parser::AstContext`]'s design.
8
9pub use cel_parser::ExprSpan;
10use cel_parser::lex_lexer::Literal;
11
12pub use cel_parser::Comment;
13
14/// A parsed adam-lang sheet declaration, with source spans on every node.
15///
16/// Built by [`crate::AdamAstParser`]; consumed by the language server, the formatter, and the
17/// (separate, future) compile-to-`Sheet` phase.
18#[derive(Debug, Clone)]
19pub struct Sheet {
20    /// The sheet's declared name.
21    pub name: String,
22    /// The name token's span.
23    pub name_span: ExprSpan,
24    /// The sheet's items, in declaration order.
25    pub items: Vec<SheetItem>,
26    /// A leading `//`/`/* */` comment immediately preceding the `sheet` keyword (e.g. a file
27    /// header), if recovered by [`crate::trivia::attach_trivia`]. Unlike every other node's
28    /// `leading_comment`, this one has no enclosing sibling list to attach via — it covers the
29    /// gap between the start of the source and the sheet's own span.
30    pub leading_comment: Option<Comment>,
31    /// A leading `//!` doc comment immediately preceding the `sheet` keyword, if recovered by
32    /// [`crate::AdamAstParser`].
33    pub doc_comment: Option<String>,
34    /// A trailing comment immediately preceding the sheet's own closing `}`, if recovered. See
35    /// <https://github.com/stlab/cel-rs/issues/52>.
36    pub trailing_comment: Option<Comment>,
37    /// Whether a blank line preceded the sheet's own closing `}`, if recovered. See
38    /// <https://github.com/stlab/cel-rs/issues/52>.
39    pub blank_line_before_close: bool,
40    /// The span of the sheet's own opening `{`, used to recover trailing trivia when `items` is
41    /// empty. See <https://github.com/stlab/cel-rs/issues/52>.
42    pub open_brace_span: ExprSpan,
43    /// The span of the whole `sheet ... { ... }` construct.
44    pub span: ExprSpan,
45    /// Syntax errors recovered while parsing, in source order. Empty for a syntactically clean
46    /// sheet.
47    pub errors: Vec<cel_parser::ParseError>,
48}
49
50/// One top-level item inside a `sheet { ... }` body.
51#[derive(Debug, Clone)]
52pub enum SheetItem {
53    /// A `cell` declaration.
54    Cell(CellDecl),
55    /// A `relationship` declaration.
56    Relationship(RelationshipDecl),
57    /// A `conditional` declaration.
58    Conditional(ConditionalDecl),
59    /// An `out` declaration.
60    Out(OutDecl),
61    /// A `source` declaration.
62    Source(SourceDecl),
63    /// A syntax error recovered at declaration granularity; `span` covers the skipped tokens.
64    Error {
65        /// The span of the skipped, malformed item.
66        span: ExprSpan,
67        /// A leading `//`/`/* */` comment immediately preceding this item, if recovered by
68        /// [`crate::trivia::attach_trivia`]. Preserved even though the item failed to parse, so
69        /// a comment explaining a broken declaration (e.g. `// TODO: fix this`) isn't silently
70        /// dropped.
71        leading_comment: Option<Comment>,
72        /// A leading `///` doc comment immediately preceding this item, if recovered by
73        /// [`crate::AdamAstParser`] before parsing failed.
74        doc_comment: Option<String>,
75        /// A `//`/`/* */` comment on the same source line as this item's own last token, if
76        /// recovered by [`crate::trivia::attach_trivia`]. Distinct from `leading_comment`, which
77        /// precedes an item on its own line. See <https://github.com/stlab/cel-rs/issues/59>.
78        trailing_line_comment: Option<Comment>,
79        /// Whether the gap before this item contained a blank line, if recovered by
80        /// [`crate::trivia::attach_trivia`].
81        blank_line_before: bool,
82    },
83}
84
85impl SheetItem {
86    /// Returns this item's source span.
87    pub fn span(&self) -> ExprSpan {
88        match self {
89            SheetItem::Cell(c) => c.span,
90            SheetItem::Relationship(r) => r.span,
91            SheetItem::Conditional(c) => c.span,
92            SheetItem::Out(o) => o.span,
93            SheetItem::Source(s) => s.span,
94            SheetItem::Error { span, .. } => *span,
95        }
96    }
97
98    /// Sets this item's leading comment.
99    pub(crate) fn set_leading_comment(&mut self, comment: Comment) {
100        match self {
101            SheetItem::Cell(c) => c.leading_comment = Some(comment),
102            SheetItem::Relationship(r) => r.leading_comment = Some(comment),
103            SheetItem::Conditional(c) => c.leading_comment = Some(comment),
104            SheetItem::Out(o) => o.leading_comment = Some(comment),
105            SheetItem::Source(s) => s.leading_comment = Some(comment),
106            SheetItem::Error {
107                leading_comment, ..
108            } => *leading_comment = Some(comment),
109        }
110    }
111
112    /// Sets a `//`/`/* */` comment recovered on the same source line as this item's own last
113    /// token. See <https://github.com/stlab/cel-rs/issues/59>.
114    pub(crate) fn set_trailing_line_comment(&mut self, comment: Comment) {
115        match self {
116            SheetItem::Cell(c) => c.trailing_line_comment = Some(comment),
117            SheetItem::Relationship(r) => r.trailing_line_comment = Some(comment),
118            SheetItem::Conditional(c) => c.trailing_line_comment = Some(comment),
119            SheetItem::Out(o) => o.trailing_line_comment = Some(comment),
120            SheetItem::Source(s) => s.trailing_line_comment = Some(comment),
121            SheetItem::Error {
122                trailing_line_comment,
123                ..
124            } => *trailing_line_comment = Some(comment),
125        }
126    }
127
128    /// Sets whether a blank line preceded this item.
129    pub(crate) fn set_blank_line_before(&mut self, value: bool) {
130        match self {
131            SheetItem::Cell(c) => c.blank_line_before = value,
132            SheetItem::Relationship(r) => r.blank_line_before = value,
133            SheetItem::Conditional(c) => c.blank_line_before = value,
134            SheetItem::Out(o) => o.blank_line_before = value,
135            SheetItem::Source(s) => s.blank_line_before = value,
136            SheetItem::Error {
137                blank_line_before, ..
138            } => *blank_line_before = value,
139        }
140    }
141
142    /// Sets this item's doc comment and widens its span to start at `start` (the doc comment's
143    /// own first token), so `trivia::attach_trivia`'s gap scan stops before the doc comment's
144    /// source text instead of misparsing it as a plain `//` comment.
145    pub(crate) fn set_doc_comment(&mut self, text: String, start: proc_macro2::Span) {
146        match self {
147            SheetItem::Cell(c) => {
148                c.doc_comment = Some(text);
149                c.span.start = start;
150            }
151            SheetItem::Relationship(r) => {
152                r.doc_comment = Some(text);
153                r.span.start = start;
154            }
155            SheetItem::Conditional(c) => {
156                c.doc_comment = Some(text);
157                c.span.start = start;
158            }
159            SheetItem::Out(o) => {
160                o.doc_comment = Some(text);
161                o.span.start = start;
162            }
163            SheetItem::Source(s) => {
164                s.doc_comment = Some(text);
165                s.span.start = start;
166            }
167            SheetItem::Error {
168                doc_comment, span, ..
169            } => {
170                *doc_comment = Some(text);
171                span.start = start;
172            }
173        }
174    }
175}
176
177/// `type_expr = identifier | "(" [ type_expr ["," [ type_expr { "," type_expr } ]] ] ")".`
178///
179/// `()` is the empty tuple type (0 elements); `(T)` is grouping (same as bare `T` — types have
180/// no precedence to disambiguate, but staying symmetric with `cel_parser`'s expression grammar
181/// costs nothing); `(T,)` is a 1-element tuple; `(T, U, ...)` is n-element, no trailing comma.
182/// Shared verbatim with `cel_parser` (which additionally supports `[T]` array types and
183/// `Name(args)` parameterized/generic types) so both crates resolve, format, and error on
184/// exactly one type-expression grammar.
185pub use cel_parser::TypeExpr;
186
187/// `cell_decl = "cell" identifier cell_type_init [ cell_filter ] [ "require" "{" { requirement }
188/// "}" ] ";".`
189///
190/// `type_name`/`initializer` are unresolved — no `TypeRegistry` lookup, no literal validation.
191/// Exactly one of `type_name`, `initializer` may be absent, per the grammar's two
192/// `cell_type_init` forms, but this is not enforced here (an all-`None` `CellDecl` cannot be
193/// produced by [`crate::AdamAstParser`], which requires at least one of `:`/`=`).
194#[derive(Debug, Clone)]
195pub struct CellDecl {
196    /// The cell's declared name.
197    pub name: String,
198    /// The name token's span.
199    pub name_span: ExprSpan,
200    /// The `: type_expr` annotation, if present.
201    pub type_name: Option<TypeExpr>,
202    /// The `= expression` initializer, if present. Unresolved and unevaluated here — see
203    /// `crate::parser::AdamParser` for the compile-to-`Sheet` phase, which parses this with no
204    /// cell scope pushed and evaluates it eagerly, once, at parse time.
205    pub initializer: Option<cel_parser::Expr>,
206    /// The `filter` clause, if present.
207    pub filter: Option<CellFilter>,
208    /// The `require { ... }` validation block, if present.
209    pub require: Option<RequireBlock>,
210    /// A leading `//`/`/* */` comment immediately preceding this declaration, if recovered by
211    /// [`crate::trivia::attach_trivia`].
212    pub leading_comment: Option<Comment>,
213    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
214    /// [`crate::AdamAstParser`].
215    pub doc_comment: Option<String>,
216    /// A `//`/`/* */` comment on the same source line as this declaration's own last token, if
217    /// recovered by [`crate::trivia::attach_trivia`]. See
218    /// <https://github.com/stlab/cel-rs/issues/59>.
219    pub trailing_line_comment: Option<Comment>,
220    /// Whether a blank line preceded this declaration, if recovered by
221    /// [`crate::trivia::attach_trivia`].
222    pub blank_line_before: bool,
223    /// The span of the whole `cell ...;` declaration.
224    pub span: ExprSpan,
225}
226
227/// `source_decl = "source" identifier cell_type_init [ cell_filter ] [ "require" "{" {
228/// requirement } "}" ] ";".`
229///
230/// Same shape as [`CellDecl`]: a `source` cell's initializer is a one-time literal exactly like a
231/// plain `cell`'s, and it supports the same `filter` clause and `require` block.
232#[derive(Debug, Clone)]
233pub struct SourceDecl {
234    /// The source cell's declared name.
235    pub name: String,
236    /// The name token's span.
237    pub name_span: ExprSpan,
238    /// The `: type_expr` annotation, if present.
239    pub type_name: Option<TypeExpr>,
240    /// The `= expression` initializer, if present. Unresolved and unevaluated here — see
241    /// `crate::parser::AdamParser` for the compile-to-`Sheet` phase, which parses this with no
242    /// cell scope pushed and evaluates it eagerly, once, at parse time.
243    pub initializer: Option<cel_parser::Expr>,
244    /// The `filter` clause, if present.
245    pub filter: Option<CellFilter>,
246    /// The `require { ... }` validation block, if present.
247    pub require: Option<RequireBlock>,
248    /// A leading `//`/`/* */` comment immediately preceding this declaration, if recovered by
249    /// [`crate::trivia::attach_trivia`].
250    pub leading_comment: Option<Comment>,
251    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
252    /// [`crate::AdamAstParser`].
253    pub doc_comment: Option<String>,
254    /// A `//`/`/* */` comment on the same source line as this declaration's own last token, if
255    /// recovered by [`crate::trivia::attach_trivia`]. See
256    /// <https://github.com/stlab/cel-rs/issues/59>.
257    pub trailing_line_comment: Option<Comment>,
258    /// Whether a blank line preceded this declaration, if recovered by
259    /// [`crate::trivia::attach_trivia`].
260    pub blank_line_before: bool,
261    /// The span of the whole `source ...;` declaration.
262    pub span: ExprSpan,
263}
264
265/// `cell_filter = "filter" expression.`
266#[derive(Debug, Clone)]
267pub struct CellFilter {
268    /// The filter's body expression. `_` inside it denotes the candidate value being conformed;
269    /// every other identifier that names an already-declared cell is a deduced dependency.
270    pub body: cel_parser::Expr,
271    /// The span of the whole `filter ...` clause.
272    pub span: ExprSpan,
273}
274
275/// `relationship_decl = "relationship" "{" { binding } "}".`
276#[derive(Debug, Clone)]
277pub struct RelationshipDecl {
278    /// The relationship block's bindings, in declaration order.
279    pub bindings: Vec<BindingDecl>,
280    /// A leading comment immediately preceding this declaration, if recovered.
281    pub leading_comment: Option<Comment>,
282    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
283    /// [`crate::AdamAstParser`].
284    pub doc_comment: Option<String>,
285    /// A `//`/`/* */` comment on the same source line as this declaration's own closing `}`, if
286    /// recovered. Distinct from `trailing_comment`, which precedes this declaration's own
287    /// closing `}` on an earlier line. See <https://github.com/stlab/cel-rs/issues/59>.
288    pub trailing_line_comment: Option<Comment>,
289    /// Whether a blank line preceded this declaration, if recovered.
290    pub blank_line_before: bool,
291    /// A trailing comment immediately preceding this declaration's own closing `}`, if
292    /// recovered. See <https://github.com/stlab/cel-rs/issues/52>.
293    pub trailing_comment: Option<Comment>,
294    /// Whether a blank line preceded this declaration's own closing `}`, if recovered. See
295    /// <https://github.com/stlab/cel-rs/issues/52>.
296    pub blank_line_before_close: bool,
297    /// The span of this declaration's own opening `{`, used to recover trailing trivia when
298    /// `bindings` is empty. See <https://github.com/stlab/cel-rs/issues/52>.
299    pub open_brace_span: ExprSpan,
300    /// The span of the whole `relationship { ... }` declaration.
301    pub span: ExprSpan,
302}
303
304/// `binding = binding_target ":=" expression ";".`
305/// `binding_target = identifier | "(" identifier { "," identifier } [ "," ] ")".`
306///
307/// Unlike the old `method_decl` this replaces, a binding names no explicit input cell list —
308/// its inputs are whichever already-declared cells `body` references, deduced at compile time
309/// (see `crate::parser::AdamParser::parse_deduced_expr`); this untyped CST parser has no cell
310/// declarations to resolve against, so it records no input list at all, only the outputs.
311///
312/// Parenthesizing the left-hand side requests tuple destructuring, matching Rust's tuple-pattern
313/// syntax: `(a, b) := ...` and the single-element `(a,) := ...` (trailing comma mandatory,
314/// exactly as in a Rust 1-tuple pattern) both destructure `body`'s tuple result element-wise into
315/// `outputs`. A bare identifier, or a single parenthesized identifier with no comma (`(a) :=
316/// ...`, mere grouping), binds `body`'s whole result directly to that one output instead — see
317/// [`Self::destructure`].
318#[derive(Debug, Clone)]
319pub struct BindingDecl {
320    /// The binding's output cell names (the left-hand side), in declaration order.
321    pub outputs: Vec<(String, ExprSpan)>,
322    /// Whether the left-hand side requests destructuring (`(a, b) := ...` or `(a,) := ...`) as
323    /// opposed to a direct bind (`a := ...` or the equivalent grouping `(a) := ...`). Always
324    /// `true` when `outputs.len() > 1`; when `outputs.len() == 1`, distinguishes "destructure
325    /// this single-element tuple" from "bind this whole value directly" — a distinction the
326    /// parenthesized-or-not left-hand side is the only way to express, since both forms name
327    /// exactly one output.
328    pub destructure: bool,
329    /// The parsed right-hand-side expression.
330    pub body: cel_parser::Expr,
331    /// A leading comment immediately preceding this binding, if recovered by
332    /// [`crate::trivia::attach_trivia`].
333    pub leading_comment: Option<Comment>,
334    /// A `//`/`/* */` comment on the same source line as this binding's own last token, if
335    /// recovered by [`crate::trivia::attach_trivia`]. See
336    /// <https://github.com/stlab/cel-rs/issues/59>.
337    pub trailing_line_comment: Option<Comment>,
338    /// Whether a blank line preceded this binding, if recovered by
339    /// [`crate::trivia::attach_trivia`].
340    pub blank_line_before: bool,
341    /// The span of the whole `a := ...;` / `(a, b) := ...;` declaration.
342    pub span: ExprSpan,
343}
344
345/// `out_decl = "out" identifier [ ":" type_expr ] ":=" expression [ cell_filter ] [ "require"
346/// "{" { requirement } "}" ] ";".`
347///
348/// `type_expr` is unresolved here (no `TypeRegistry` lookup), matching `CellDecl`. When
349/// absent, the cell's type is inferred from `initializer`'s result type by the compile phase
350/// (`crate::parser::AdamParser`) — never here.
351#[derive(Debug, Clone)]
352pub struct OutDecl {
353    /// The declared cell's name.
354    pub name: String,
355    /// The name token's span.
356    pub name_span: ExprSpan,
357    /// The `: type_expr` annotation, if present.
358    pub type_name: Option<TypeExpr>,
359    /// The parsed initializer expression that computes this cell's value.
360    pub initializer: cel_parser::Expr,
361    /// The `filter` clause, if present.
362    pub filter: Option<CellFilter>,
363    /// The `require { ... }` validation block, if present.
364    pub require: Option<RequireBlock>,
365    /// A leading `//`/`/* */` comment immediately preceding this declaration, if recovered by
366    /// [`crate::trivia::attach_trivia`].
367    pub leading_comment: Option<Comment>,
368    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
369    /// [`crate::AdamAstParser`].
370    pub doc_comment: Option<String>,
371    /// A `//`/`/* */` comment on the same source line as this declaration's own last token, if
372    /// recovered by [`crate::trivia::attach_trivia`]. See
373    /// <https://github.com/stlab/cel-rs/issues/59>.
374    pub trailing_line_comment: Option<Comment>,
375    /// Whether a blank line preceded this declaration, if recovered by
376    /// [`crate::trivia::attach_trivia`].
377    pub blank_line_before: bool,
378    /// The span of the whole `out ... ;` declaration.
379    pub span: ExprSpan,
380}
381
382/// The optional `require { ... }` block trailing an `out` declaration's initializer.
383#[derive(Debug, Clone)]
384pub struct RequireBlock {
385    /// The block's requirements, in declaration order.
386    pub requirements: Vec<RequirementDecl>,
387    /// A trailing comment immediately preceding this block's own closing `}`, if recovered.
388    /// See <https://github.com/stlab/cel-rs/issues/52>.
389    pub trailing_comment: Option<Comment>,
390    /// Whether a blank line preceded this block's own closing `}`, if recovered. See
391    /// <https://github.com/stlab/cel-rs/issues/52>.
392    pub blank_line_before_close: bool,
393    /// The span of this block's own opening `{`, used to recover trailing trivia when
394    /// `requirements` is empty.
395    pub open_brace_span: ExprSpan,
396    /// The span of the whole `require { ... }` block.
397    pub span: ExprSpan,
398}
399
400/// `requirement = [ "@" identifier ] expression ";".`
401///
402/// `name`, when present, is a plain string label passed to `adam_rs::Sheet::add_requirement`,
403/// not a cell reference — it may coincide with a cell name declared elsewhere in the sheet but
404/// doesn't have to.
405#[derive(Debug, Clone)]
406pub struct RequirementDecl {
407    /// The requirement's declared label, if the `@identifier` marker was present.
408    pub name: Option<String>,
409    /// The `@identifier` marker's span, if present.
410    pub name_span: Option<ExprSpan>,
411    /// The parsed requirement body expression; must type-check as `bool`.
412    pub body: cel_parser::Expr,
413    /// A leading comment immediately preceding this requirement, if recovered by
414    /// [`crate::trivia::attach_trivia`].
415    pub leading_comment: Option<Comment>,
416    /// A `//`/`/* */` comment on the same source line as this requirement's own last token, if
417    /// recovered by [`crate::trivia::attach_trivia`]. See
418    /// <https://github.com/stlab/cel-rs/issues/59>.
419    pub trailing_line_comment: Option<Comment>,
420    /// Whether a blank line preceded this requirement, if recovered by
421    /// [`crate::trivia::attach_trivia`].
422    pub blank_line_before: bool,
423    /// The span of the whole `[ "@" identifier ] expr;` declaration.
424    pub span: ExprSpan,
425}
426
427/// `conditional_decl = "conditional" expression "{" { conditional_branch } "}".`
428#[derive(Debug, Clone)]
429pub struct ConditionalDecl {
430    /// The match subject: an arbitrary expression over already-declared cells (a bare
431    /// identifier, e.g. `mode`, is the degenerate single-cell case).
432    pub match_expr: cel_parser::Expr,
433    /// The named (literal `=>`) branches, in declaration order.
434    pub branches: Vec<ConditionalBranch>,
435    /// The `_ => { ... }` default branch, if present.
436    pub default: Option<DefaultBranch>,
437    /// A leading comment immediately preceding this declaration, if recovered.
438    pub leading_comment: Option<Comment>,
439    /// A leading `///` doc comment immediately preceding this declaration, if recovered by
440    /// [`crate::AdamAstParser`].
441    pub doc_comment: Option<String>,
442    /// A `//`/`/* */` comment on the same source line as this declaration's own closing `}`, if
443    /// recovered. Distinct from `trailing_comment`, which precedes this declaration's own
444    /// closing `}` on an earlier line. See <https://github.com/stlab/cel-rs/issues/59>.
445    pub trailing_line_comment: Option<Comment>,
446    /// Whether a blank line preceded this declaration, if recovered.
447    pub blank_line_before: bool,
448    /// A trailing comment immediately preceding this declaration's own closing `}`, if
449    /// recovered. See <https://github.com/stlab/cel-rs/issues/52>.
450    pub trailing_comment: Option<Comment>,
451    /// Whether a blank line preceded this declaration's own closing `}`, if recovered. See
452    /// <https://github.com/stlab/cel-rs/issues/52>.
453    pub blank_line_before_close: bool,
454    /// The span of this declaration's own opening `{`, used to recover trailing trivia when
455    /// there are no `branches`/`default`. See <https://github.com/stlab/cel-rs/issues/52>.
456    pub open_brace_span: ExprSpan,
457    /// The span of the whole `conditional ... { ... }` declaration.
458    pub span: ExprSpan,
459}
460
461/// The `_ => { ... }` default branch of a `conditional_decl`, mirroring `ConditionalBranch`'s
462/// shape (it has no match literal of its own).
463#[derive(Debug, Clone)]
464pub struct DefaultBranch {
465    /// The default branch's relationships, in declaration order.
466    pub relationships: Vec<RelationshipDecl>,
467    /// A trailing comment immediately preceding this branch's own closing `}`, if recovered.
468    /// See <https://github.com/stlab/cel-rs/issues/52>.
469    pub trailing_comment: Option<Comment>,
470    /// A `//`/`/* */` comment on the same source line as this branch's own closing `}` (between
471    /// it and the enclosing conditional's own closing `}`), if recovered. See
472    /// <https://github.com/stlab/cel-rs/issues/59>.
473    pub trailing_line_comment: Option<Comment>,
474    /// Whether a blank line preceded this branch's own closing `}`, if recovered.
475    pub blank_line_before_close: bool,
476    /// The span of this branch's own opening `{`, used to recover trailing trivia when
477    /// `relationships` is empty.
478    pub open_brace_span: ExprSpan,
479    /// The span from the `_` token through this branch's own closing `}`.
480    pub span: ExprSpan,
481}
482
483/// `conditional_branch = literal_pattern "=>" "{" { relationship_decl } "}" [ "," ].`
484/// `literal_pattern = ["-"] literal.`
485#[derive(Debug, Clone)]
486pub struct ConditionalBranch {
487    /// The branch's unresolved match literal, always stored unsigned; see [`Self::negated`]
488    /// for whether a leading `-` applies to it.
489    pub literal: Literal,
490    /// Whether the branch key is negated by a leading `-` (Rust's own `LiteralPattern` rule —
491    /// see <https://doc.rust-lang.org/reference/patterns.html#literal-patterns>). A `-`, if
492    /// present, precedes [`Self::literal_span`] and is covered by [`Self::span`]'s start, but
493    /// not by `literal_span` itself.
494    pub negated: bool,
495    /// The literal token's own span (never includes a leading `-`; see [`Self::negated`]).
496    pub literal_span: ExprSpan,
497    /// The branch's relationships, in declaration order.
498    pub relationships: Vec<RelationshipDecl>,
499    /// A leading comment immediately preceding this branch, if recovered by
500    /// [`crate::trivia::attach_trivia`].
501    pub leading_comment: Option<Comment>,
502    /// A `//`/`/* */` comment on the same source line as this branch's own closing `}`, if
503    /// recovered. Distinct from `trailing_comment`, which precedes this branch's own closing
504    /// `}` on an earlier line. See <https://github.com/stlab/cel-rs/issues/59>.
505    pub trailing_line_comment: Option<Comment>,
506    /// Whether a blank line preceded this branch, if recovered by
507    /// [`crate::trivia::attach_trivia`].
508    pub blank_line_before: bool,
509    /// A trailing comment immediately preceding this branch's own closing `}`, if recovered.
510    /// See <https://github.com/stlab/cel-rs/issues/52>.
511    pub trailing_comment: Option<Comment>,
512    /// Whether a blank line preceded this branch's own closing `}`, if recovered. See
513    /// <https://github.com/stlab/cel-rs/issues/52>.
514    pub blank_line_before_close: bool,
515    /// The span of this branch's own opening `{`, used to recover trailing trivia when
516    /// `relationships` is empty. See <https://github.com/stlab/cel-rs/issues/52>.
517    pub open_brace_span: ExprSpan,
518    /// The span from the branch's literal through its closing `}`.
519    pub span: ExprSpan,
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525    use proc_macro2::Span;
526
527    fn point(span: Span) -> ExprSpan {
528        ExprSpan {
529            start: span,
530            end: span,
531        }
532    }
533
534    #[test]
535    fn sheet_item_span_reads_the_cell_variant() {
536        let span = point(Span::call_site());
537        let item = SheetItem::Cell(CellDecl {
538            name: "x".to_string(),
539            name_span: span,
540            type_name: None,
541            initializer: None,
542            filter: None,
543            require: None,
544            leading_comment: None,
545            doc_comment: None,
546            trailing_line_comment: None,
547            blank_line_before: false,
548            span,
549        });
550        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
551    }
552
553    #[test]
554    fn sheet_item_span_reads_the_relationship_variant() {
555        let span = point(Span::call_site());
556        let item = SheetItem::Relationship(RelationshipDecl {
557            bindings: Vec::new(),
558            leading_comment: None,
559            doc_comment: None,
560            trailing_line_comment: None,
561            blank_line_before: false,
562            trailing_comment: None,
563            blank_line_before_close: false,
564            open_brace_span: span,
565            span,
566        });
567        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
568    }
569
570    #[test]
571    fn sheet_item_span_reads_the_conditional_variant() {
572        let span = point(Span::call_site());
573        let item = SheetItem::Conditional(ConditionalDecl {
574            match_expr: cel_parser::Expr::Ident {
575                name: "m".to_string(),
576                span,
577            },
578            branches: Vec::new(),
579            default: None,
580            leading_comment: None,
581            doc_comment: None,
582            trailing_line_comment: None,
583            blank_line_before: false,
584            trailing_comment: None,
585            blank_line_before_close: false,
586            open_brace_span: span,
587            span,
588        });
589        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
590    }
591
592    #[test]
593    fn sheet_item_span_reads_the_error_variant() {
594        let span = point(Span::call_site());
595        let item = SheetItem::Error {
596            span,
597            leading_comment: None,
598            doc_comment: None,
599            trailing_line_comment: None,
600            blank_line_before: false,
601        };
602        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
603    }
604
605    #[test]
606    fn set_leading_comment_sets_the_cell_variant() {
607        let span = point(Span::call_site());
608        let mut item = SheetItem::Cell(CellDecl {
609            name: "x".to_string(),
610            name_span: span,
611            type_name: None,
612            initializer: None,
613            filter: None,
614            require: None,
615            leading_comment: None,
616            doc_comment: None,
617            trailing_line_comment: None,
618            blank_line_before: false,
619            span,
620        });
621        item.set_leading_comment(Comment::Line("hi".to_string()));
622        match item {
623            SheetItem::Cell(c) => {
624                assert_eq!(c.leading_comment, Some(Comment::Line("hi".to_string())))
625            }
626            other => panic!("expected Cell, got {other:?}"),
627        }
628    }
629
630    #[test]
631    fn set_leading_comment_sets_the_error_variant() {
632        let span = point(Span::call_site());
633        let mut item = SheetItem::Error {
634            span,
635            leading_comment: None,
636            doc_comment: None,
637            trailing_line_comment: None,
638            blank_line_before: false,
639        };
640        item.set_leading_comment(Comment::Line("hi".to_string()));
641        match item {
642            SheetItem::Error {
643                leading_comment, ..
644            } => {
645                assert_eq!(leading_comment, Some(Comment::Line("hi".to_string())))
646            }
647            other => panic!("expected Error, got {other:?}"),
648        }
649    }
650
651    #[test]
652    fn set_blank_line_before_sets_the_cell_variant() {
653        let span = point(Span::call_site());
654        let mut item = SheetItem::Cell(CellDecl {
655            name: "x".to_string(),
656            name_span: span,
657            type_name: None,
658            initializer: None,
659            filter: None,
660            require: None,
661            leading_comment: None,
662            doc_comment: None,
663            trailing_line_comment: None,
664            blank_line_before: false,
665            span,
666        });
667        item.set_blank_line_before(true);
668        match item {
669            SheetItem::Cell(c) => assert!(c.blank_line_before),
670            other => panic!("expected Cell, got {other:?}"),
671        }
672    }
673
674    #[test]
675    fn sheet_item_span_reads_the_out_variant() {
676        let span = point(Span::call_site());
677        let item = SheetItem::Out(OutDecl {
678            name: "o".to_string(),
679            name_span: span,
680            type_name: None,
681            initializer: cel_parser::Expr::Ident {
682                name: "x".to_string(),
683                span,
684            },
685            filter: None,
686            require: None,
687            leading_comment: None,
688            doc_comment: None,
689            trailing_line_comment: None,
690            blank_line_before: false,
691            span,
692        });
693        assert_eq!(format!("{:?}", item.span()), format!("{span:?}"));
694    }
695
696    #[test]
697    fn set_leading_comment_sets_the_out_variant() {
698        let span = point(Span::call_site());
699        let mut item = SheetItem::Out(OutDecl {
700            name: "o".to_string(),
701            name_span: span,
702            type_name: None,
703            initializer: cel_parser::Expr::Ident {
704                name: "x".to_string(),
705                span,
706            },
707            filter: None,
708            require: None,
709            leading_comment: None,
710            doc_comment: None,
711            trailing_line_comment: None,
712            blank_line_before: false,
713            span,
714        });
715        item.set_leading_comment(Comment::Line("hi".to_string()));
716        match item {
717            SheetItem::Out(o) => {
718                assert_eq!(o.leading_comment, Some(Comment::Line("hi".to_string())))
719            }
720            other => panic!("expected Out, got {other:?}"),
721        }
722    }
723
724    #[test]
725    fn ast_type_expr_is_the_shared_cel_parser_type_expr() {
726        fn assert_same_type<T>(_: &T) {}
727        let span = cel_parser::ExprSpan {
728            start: Span::call_site(),
729            end: Span::call_site(),
730        };
731        let expr: TypeExpr = cel_parser::TypeExpr::Named {
732            name: "i32".to_string(),
733            args: Vec::new(),
734            span,
735        };
736        assert_same_type::<cel_parser::TypeExpr>(&expr);
737    }
738
739    #[test]
740    fn type_expr_named_span_is_its_own_span() {
741        let span = point(Span::call_site());
742        let expr = TypeExpr::Named {
743            name: "i32".to_string(),
744            args: Vec::new(),
745            span,
746        };
747        assert_eq!(format!("{:?}", expr.span()), format!("{span:?}"));
748    }
749
750    #[test]
751    fn type_expr_tuple_span_is_the_whole_parenthesized_span() {
752        let span = point(Span::call_site());
753        let expr = TypeExpr::Tuple {
754            elements: Vec::new(),
755            span,
756        };
757        assert_eq!(format!("{:?}", expr.span()), format!("{span:?}"));
758    }
759
760    #[test]
761    fn cell_decl_type_name_holds_a_nested_tuple_type_expr() {
762        let span = point(Span::call_site());
763        let cell = CellDecl {
764            name: "a".to_string(),
765            name_span: span,
766            type_name: Some(TypeExpr::Tuple {
767                elements: vec![
768                    TypeExpr::Named {
769                        name: "i32".to_string(),
770                        args: Vec::new(),
771                        span,
772                    },
773                    TypeExpr::Named {
774                        name: "f64".to_string(),
775                        args: Vec::new(),
776                        span,
777                    },
778                ],
779                span,
780            }),
781            initializer: None,
782            filter: None,
783            require: None,
784            leading_comment: None,
785            doc_comment: None,
786            trailing_line_comment: None,
787            blank_line_before: false,
788            span,
789        };
790        match cell.type_name {
791            Some(TypeExpr::Tuple { elements, .. }) => assert_eq!(elements.len(), 2),
792            other => panic!("expected Tuple, got {other:?}"),
793        }
794    }
795
796    #[test]
797    fn cell_decl_initializer_holds_a_parsed_expr() {
798        let span = point(Span::call_site());
799        let cell = CellDecl {
800            name: "a".to_string(),
801            name_span: span,
802            type_name: None,
803            initializer: Some(cel_parser::Expr::Ident {
804                name: "x".to_string(),
805                span,
806            }),
807            filter: None,
808            require: None,
809            leading_comment: None,
810            doc_comment: None,
811            trailing_line_comment: None,
812            blank_line_before: false,
813            span,
814        };
815        assert!(matches!(
816            cell.initializer,
817            Some(cel_parser::Expr::Ident { .. })
818        ));
819    }
820
821    #[test]
822    fn cell_decl_filter_field_holds_a_cell_filter() {
823        let span = point(Span::call_site());
824        let cell = CellDecl {
825            name: "a".to_string(),
826            name_span: span,
827            type_name: None,
828            initializer: None,
829            filter: Some(CellFilter {
830                body: cel_parser::Expr::Ident {
831                    name: "_".to_string(),
832                    span,
833                },
834                span,
835            }),
836            require: None,
837            leading_comment: None,
838            doc_comment: None,
839            trailing_line_comment: None,
840            blank_line_before: false,
841            span,
842        };
843        let filter = cell.filter.as_ref().expect("filter present");
844        assert!(matches!(
845            &filter.body,
846            cel_parser::Expr::Ident { name, .. } if name == "_"
847        ));
848    }
849}