Skip to main content

rlsp_yaml_parser/
node.rs

1// SPDX-License-Identifier: MIT
2
3//! YAML AST node types.
4//!
5//! [`Node<Loc>`] is the core type — a YAML value parameterized by its
6//! location type.  For most uses `Loc = Span`.  The loader produces
7//! `Vec<Document<Span>>`.
8
9use crate::event::{CollectionStyle, ScalarStyle};
10use crate::pos::Span;
11
12// ---------------------------------------------------------------------------
13// Public types
14// ---------------------------------------------------------------------------
15
16/// A YAML document: a root node plus directive metadata.
17#[derive(Debug, Clone, PartialEq)]
18pub struct Document<Loc = Span> {
19    /// The root node of the document.
20    pub root: Node<Loc>,
21    /// YAML version declared by a `%YAML` directive, if present (e.g. `(1, 2)`).
22    pub version: Option<(u8, u8)>,
23    /// Tag handle/prefix pairs declared by `%TAG` directives (handle, prefix).
24    pub tags: Vec<(String, String)>,
25    /// Comments that appear at document level (before or between nodes).
26    pub comments: Vec<String>,
27    /// Whether the document was introduced with an explicit `---` marker.
28    pub explicit_start: bool,
29    /// Whether the document was closed with an explicit `...` marker.
30    pub explicit_end: bool,
31}
32
33/// A YAML node parameterized by its location type.
34#[derive(Debug, Clone, PartialEq)]
35pub enum Node<Loc = Span> {
36    /// A scalar value.
37    Scalar {
38        /// The scalar content as a UTF-8 string (after block/flow unfolding).
39        value: String,
40        /// The presentation style used in the source (plain, single-quoted, etc.).
41        style: ScalarStyle,
42        /// Anchor name defined on this node (e.g. `&anchor`), if any.
43        anchor: Option<String>,
44        /// Tag applied to this node (e.g. `!!str`), if any.
45        tag: Option<String>,
46        /// Source span covering this scalar in the input.
47        loc: Loc,
48        /// Comment lines that appear before this node (e.g. `# note`).
49        /// Populated only for non-first entries in a mapping or sequence.
50        /// Document-prefix leading comments are discarded by the tokenizer
51        /// per YAML §9.2 and cannot be recovered here.
52        leading_comments: Vec<String>,
53        /// Inline comment on the same line as this node (e.g. `# note`).
54        trailing_comment: Option<String>,
55    },
56    /// A mapping (sequence of key–value pairs preserving declaration order).
57    Mapping {
58        /// Key–value pairs in declaration order.
59        entries: Vec<(Self, Self)>,
60        /// The presentation style used in the source (block or flow).
61        style: CollectionStyle,
62        /// Anchor name defined on this mapping (e.g. `&anchor`), if any.
63        anchor: Option<String>,
64        /// Tag applied to this mapping (e.g. `!!map`), if any.
65        tag: Option<String>,
66        /// Source span from the opening indicator to the last entry.
67        loc: Loc,
68        /// Comment lines that appear before this node.
69        leading_comments: Vec<String>,
70        /// Inline comment on the same line as this node.
71        trailing_comment: Option<String>,
72    },
73    /// A sequence (ordered list of nodes).
74    Sequence {
75        /// Ordered list of child nodes.
76        items: Vec<Self>,
77        /// The presentation style used in the source (block or flow).
78        style: CollectionStyle,
79        /// Anchor name defined on this sequence (e.g. `&anchor`), if any.
80        anchor: Option<String>,
81        /// Tag applied to this sequence (e.g. `!!seq`), if any.
82        tag: Option<String>,
83        /// Source span from the opening indicator to the last item.
84        loc: Loc,
85        /// Comment lines that appear before this node.
86        leading_comments: Vec<String>,
87        /// Inline comment on the same line as this node.
88        trailing_comment: Option<String>,
89    },
90    /// An alias reference (lossless mode only — resolved mode expands these).
91    Alias {
92        /// The anchor name this alias refers to (without the `*` sigil).
93        name: String,
94        /// Source span covering the `*name` alias token.
95        loc: Loc,
96        /// Comment lines that appear before this node.
97        leading_comments: Vec<String>,
98        /// Inline comment on the same line as this node.
99        trailing_comment: Option<String>,
100    },
101}
102
103impl<Loc> Node<Loc> {
104    /// Returns the anchor name if this node defines one.
105    pub fn anchor(&self) -> Option<&str> {
106        match self {
107            Self::Scalar { anchor, .. }
108            | Self::Mapping { anchor, .. }
109            | Self::Sequence { anchor, .. } => anchor.as_deref(),
110            Self::Alias { .. } => None,
111        }
112    }
113
114    /// Returns the leading comments for this node.
115    pub fn leading_comments(&self) -> &[String] {
116        match self {
117            Self::Scalar {
118                leading_comments, ..
119            }
120            | Self::Mapping {
121                leading_comments, ..
122            }
123            | Self::Sequence {
124                leading_comments, ..
125            }
126            | Self::Alias {
127                leading_comments, ..
128            } => leading_comments,
129        }
130    }
131
132    /// Returns the trailing comment for this node, if any.
133    pub fn trailing_comment(&self) -> Option<&str> {
134        match self {
135            Self::Scalar {
136                trailing_comment, ..
137            }
138            | Self::Mapping {
139                trailing_comment, ..
140            }
141            | Self::Sequence {
142                trailing_comment, ..
143            }
144            | Self::Alias {
145                trailing_comment, ..
146            } => trailing_comment.as_deref(),
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::event::ScalarStyle;
155    use crate::pos::{Pos, Span};
156
157    fn zero_span() -> Span {
158        Span {
159            start: Pos::ORIGIN,
160            end: Pos::ORIGIN,
161        }
162    }
163
164    fn plain_scalar(value: &str) -> Node<Span> {
165        Node::Scalar {
166            value: value.to_owned(),
167            style: ScalarStyle::Plain,
168            anchor: None,
169            tag: None,
170            loc: zero_span(),
171            leading_comments: Vec::new(),
172            trailing_comment: None,
173        }
174    }
175
176    // NF-1: node_debug_includes_leading_comments
177    #[test]
178    fn node_debug_includes_leading_comments() {
179        let node = Node::Scalar {
180            value: "val".to_owned(),
181            style: ScalarStyle::Plain,
182            anchor: None,
183            tag: None,
184            loc: zero_span(),
185            leading_comments: vec!["# note".to_owned()],
186            trailing_comment: None,
187        };
188        let debug = format!("{node:?}");
189        assert!(debug.contains("# note"), "debug output: {debug}");
190    }
191
192    // NF-2: node_partial_eq_considers_leading_comments
193    #[test]
194    fn node_partial_eq_considers_leading_comments() {
195        let a = Node::Scalar {
196            value: "val".to_owned(),
197            style: ScalarStyle::Plain,
198            anchor: None,
199            tag: None,
200            loc: zero_span(),
201            leading_comments: vec!["# a".to_owned()],
202            trailing_comment: None,
203        };
204        let b = Node::Scalar {
205            value: "val".to_owned(),
206            style: ScalarStyle::Plain,
207            anchor: None,
208            tag: None,
209            loc: zero_span(),
210            leading_comments: vec!["# b".to_owned()],
211            trailing_comment: None,
212        };
213        assert_ne!(a, b);
214    }
215
216    // NF-3: node_clone_preserves_comments
217    #[test]
218    fn node_clone_preserves_comments() {
219        let node = Node::Scalar {
220            value: "val".to_owned(),
221            style: ScalarStyle::Plain,
222            anchor: None,
223            tag: None,
224            loc: zero_span(),
225            leading_comments: vec!["# x".to_owned()],
226            trailing_comment: Some("# y".to_owned()),
227        };
228        let cloned = node.clone();
229        assert_eq!(node, cloned);
230        assert_eq!(cloned.leading_comments(), &["# x"]);
231        assert_eq!(cloned.trailing_comment(), Some("# y"));
232    }
233
234    // Sanity: plain_scalar helper produces empty comment fields.
235    #[test]
236    fn plain_scalar_has_empty_comments() {
237        let n = plain_scalar("hello");
238        assert!(n.leading_comments().is_empty());
239        assert!(n.trailing_comment().is_none());
240    }
241
242    fn bare_document(explicit_start: bool, explicit_end: bool) -> Document<Span> {
243        Document {
244            root: plain_scalar("val"),
245            version: None,
246            tags: Vec::new(),
247            comments: Vec::new(),
248            explicit_start,
249            explicit_end,
250        }
251    }
252
253    // NF-DOC-1: explicit_start and explicit_end default to false
254    #[test]
255    fn document_explicit_flags_in_equality() {
256        let a = bare_document(false, false);
257        let b = bare_document(false, false);
258        assert_eq!(a, b);
259    }
260
261    // NF-DOC-2: PartialEq distinguishes differing explicit_start
262    #[test]
263    fn document_partial_eq_distinguishes_explicit_start() {
264        let a = bare_document(true, false);
265        let b = bare_document(false, false);
266        assert_ne!(a, b);
267    }
268
269    // NF-DOC-3: PartialEq distinguishes differing explicit_end
270    #[test]
271    fn document_partial_eq_distinguishes_explicit_end() {
272        let a = bare_document(false, true);
273        let b = bare_document(false, false);
274        assert_ne!(a, b);
275    }
276
277    // NF-DOC-4: Clone preserves both flags
278    #[test]
279    fn document_clone_preserves_explicit_flags() {
280        let doc = bare_document(true, true);
281        let cloned = doc.clone();
282        assert_eq!(doc, cloned);
283        assert!(cloned.explicit_start);
284        assert!(cloned.explicit_end);
285    }
286}