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        /// Source span of the `&name` anchor token — from `&` through the last byte of the
45        /// name.  `Some` when `anchor` is `Some`; `None` otherwise.
46        anchor_loc: Option<Loc>,
47        /// Tag applied to this node (e.g. `!!str`), if any.
48        tag: Option<String>,
49        /// Source span of the tag token — from `!` through the last byte of the tag.
50        /// `Some` when `tag` is `Some`; `None` otherwise.
51        tag_loc: Option<Loc>,
52        /// Source span covering this scalar in the input.
53        loc: Loc,
54        /// Comment lines that appear before this node (e.g. `# note`).
55        /// Populated only for non-first entries in a mapping or sequence.
56        /// Document-prefix leading comments are discarded by the tokenizer
57        /// per YAML §9.2 and cannot be recovered here.
58        leading_comments: Option<Vec<String>>,
59        /// Inline comment on the same line as this node (e.g. `# note`).
60        trailing_comment: Option<String>,
61    },
62    /// A mapping (sequence of key–value pairs preserving declaration order).
63    Mapping {
64        /// Key–value pairs in declaration order.
65        entries: Vec<(Self, Self)>,
66        /// The presentation style used in the source (block or flow).
67        style: CollectionStyle,
68        /// Anchor name defined on this mapping (e.g. `&anchor`), if any.
69        anchor: Option<String>,
70        /// Source span of the `&name` anchor token — from `&` through the last byte of the
71        /// name.  `Some` when `anchor` is `Some`; `None` otherwise.
72        anchor_loc: Option<Loc>,
73        /// Tag applied to this mapping (e.g. `!!map`), if any.
74        tag: Option<String>,
75        /// Source span of the tag token — from `!` through the last byte of the tag.
76        /// `Some` when `tag` is `Some`; `None` otherwise.
77        tag_loc: Option<Loc>,
78        /// Source span from the opening indicator to the last entry.
79        loc: Loc,
80        /// Comment lines that appear before this node.
81        leading_comments: Option<Vec<String>>,
82        /// Inline comment on the same line as this node.
83        trailing_comment: Option<String>,
84    },
85    /// A sequence (ordered list of nodes).
86    Sequence {
87        /// Ordered list of child nodes.
88        items: Vec<Self>,
89        /// The presentation style used in the source (block or flow).
90        style: CollectionStyle,
91        /// Anchor name defined on this sequence (e.g. `&anchor`), if any.
92        anchor: Option<String>,
93        /// Source span of the `&name` anchor token — from `&` through the last byte of the
94        /// name.  `Some` when `anchor` is `Some`; `None` otherwise.
95        anchor_loc: Option<Loc>,
96        /// Tag applied to this sequence (e.g. `!!seq`), if any.
97        tag: Option<String>,
98        /// Source span of the tag token — from `!` through the last byte of the tag.
99        /// `Some` when `tag` is `Some`; `None` otherwise.
100        tag_loc: Option<Loc>,
101        /// Source span from the opening indicator to the last item.
102        loc: Loc,
103        /// Comment lines that appear before this node.
104        leading_comments: Option<Vec<String>>,
105        /// Inline comment on the same line as this node.
106        trailing_comment: Option<String>,
107    },
108    /// An alias reference (lossless mode only — resolved mode expands these).
109    Alias {
110        /// The anchor name this alias refers to (without the `*` sigil).
111        name: String,
112        /// Source span covering the `*name` alias token.
113        loc: Loc,
114        /// Comment lines that appear before this node.
115        leading_comments: Option<Vec<String>>,
116        /// Inline comment on the same line as this node.
117        trailing_comment: Option<String>,
118    },
119}
120
121impl<Loc> Node<Loc> {
122    /// Returns the anchor name if this node defines one.
123    pub fn anchor(&self) -> Option<&str> {
124        match self {
125            Self::Scalar { anchor, .. }
126            | Self::Mapping { anchor, .. }
127            | Self::Sequence { anchor, .. } => anchor.as_deref(),
128            Self::Alias { .. } => None,
129        }
130    }
131
132    /// Returns the source span of the `&name` anchor token, if any.
133    ///
134    /// `Some(span)` when `anchor()` is `Some`; `None` otherwise.
135    /// Always `None` for [`Node::Alias`] — the alias span is in `loc`.
136    pub const fn anchor_loc(&self) -> Option<Loc>
137    where
138        Loc: Copy,
139    {
140        match self {
141            Self::Scalar { anchor_loc, .. }
142            | Self::Mapping { anchor_loc, .. }
143            | Self::Sequence { anchor_loc, .. } => *anchor_loc,
144            Self::Alias { .. } => None,
145        }
146    }
147
148    /// Returns the source span of the tag token, if any.
149    ///
150    /// `Some(span)` when `tag()` is `Some`; `None` otherwise.
151    /// Always `None` for [`Node::Alias`].
152    pub const fn tag_loc(&self) -> Option<Loc>
153    where
154        Loc: Copy,
155    {
156        match self {
157            Self::Scalar { tag_loc, .. }
158            | Self::Mapping { tag_loc, .. }
159            | Self::Sequence { tag_loc, .. } => *tag_loc,
160            Self::Alias { .. } => None,
161        }
162    }
163
164    /// Returns the leading comments for this node.
165    pub fn leading_comments(&self) -> &[String] {
166        match self {
167            Self::Scalar {
168                leading_comments, ..
169            }
170            | Self::Mapping {
171                leading_comments, ..
172            }
173            | Self::Sequence {
174                leading_comments, ..
175            }
176            | Self::Alias {
177                leading_comments, ..
178            } => leading_comments.as_deref().unwrap_or(&[]),
179        }
180    }
181
182    /// Returns the trailing comment for this node, if any.
183    pub fn trailing_comment(&self) -> Option<&str> {
184        match self {
185            Self::Scalar {
186                trailing_comment, ..
187            }
188            | Self::Mapping {
189                trailing_comment, ..
190            }
191            | Self::Sequence {
192                trailing_comment, ..
193            }
194            | Self::Alias {
195                trailing_comment, ..
196            } => trailing_comment.as_deref(),
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::event::ScalarStyle;
205    use crate::pos::{Pos, Span};
206
207    fn zero_span() -> Span {
208        Span {
209            start: Pos::ORIGIN,
210            end: Pos::ORIGIN,
211        }
212    }
213
214    fn plain_scalar(value: &str) -> Node<Span> {
215        Node::Scalar {
216            value: value.to_owned(),
217            style: ScalarStyle::Plain,
218            anchor: None,
219            anchor_loc: None,
220            tag: None,
221            tag_loc: None,
222            loc: zero_span(),
223            leading_comments: None,
224            trailing_comment: None,
225        }
226    }
227
228    // NF-1: node_debug_includes_leading_comments
229    #[test]
230    fn node_debug_includes_leading_comments() {
231        let node = Node::Scalar {
232            value: "val".to_owned(),
233            style: ScalarStyle::Plain,
234            anchor: None,
235            anchor_loc: None,
236            tag: None,
237            tag_loc: None,
238            loc: zero_span(),
239            leading_comments: Some(vec!["# note".to_owned()]),
240            trailing_comment: None,
241        };
242        let debug = format!("{node:?}");
243        assert!(debug.contains("# note"), "debug output: {debug}");
244    }
245
246    // NF-2: node_partial_eq_considers_leading_comments
247    #[test]
248    fn node_partial_eq_considers_leading_comments() {
249        let a = Node::Scalar {
250            value: "val".to_owned(),
251            style: ScalarStyle::Plain,
252            anchor: None,
253            anchor_loc: None,
254            tag: None,
255            tag_loc: None,
256            loc: zero_span(),
257            leading_comments: Some(vec!["# a".to_owned()]),
258            trailing_comment: None,
259        };
260        let b = Node::Scalar {
261            value: "val".to_owned(),
262            style: ScalarStyle::Plain,
263            anchor: None,
264            anchor_loc: None,
265            tag: None,
266            tag_loc: None,
267            loc: zero_span(),
268            leading_comments: Some(vec!["# b".to_owned()]),
269            trailing_comment: None,
270        };
271        assert_ne!(a, b);
272    }
273
274    // NF-3: node_clone_preserves_comments
275    #[test]
276    fn node_clone_preserves_comments() {
277        let node = Node::Scalar {
278            value: "val".to_owned(),
279            style: ScalarStyle::Plain,
280            anchor: None,
281            anchor_loc: None,
282            tag: None,
283            tag_loc: None,
284            loc: zero_span(),
285            leading_comments: Some(vec!["# x".to_owned()]),
286            trailing_comment: Some("# y".to_owned()),
287        };
288        let cloned = node.clone();
289        assert_eq!(node, cloned);
290        assert_eq!(cloned.leading_comments(), &["# x"]);
291        assert_eq!(cloned.trailing_comment(), Some("# y"));
292    }
293
294    // Sanity: plain_scalar helper produces empty comment fields.
295    #[test]
296    fn plain_scalar_has_empty_comments() {
297        let n = plain_scalar("hello");
298        assert!(n.leading_comments().is_empty());
299        assert!(n.trailing_comment().is_none());
300    }
301
302    #[test]
303    fn node_accessor_returns_empty_slice_for_none() {
304        let node = Node::Scalar {
305            value: "v".to_owned(),
306            style: ScalarStyle::Plain,
307            anchor: None,
308            anchor_loc: None,
309            tag: None,
310            tag_loc: None,
311            loc: zero_span(),
312            leading_comments: None,
313            trailing_comment: None,
314        };
315        assert_eq!(node.leading_comments(), &[] as &[String]);
316    }
317
318    #[test]
319    fn node_accessor_returns_slice_for_some() {
320        let node = Node::Scalar {
321            value: "v".to_owned(),
322            style: ScalarStyle::Plain,
323            anchor: None,
324            anchor_loc: None,
325            tag: None,
326            tag_loc: None,
327            loc: zero_span(),
328            leading_comments: Some(vec!["# x".to_owned()]),
329            trailing_comment: None,
330        };
331        assert_eq!(node.leading_comments(), &["# x"]);
332    }
333
334    fn bare_document(explicit_start: bool, explicit_end: bool) -> Document<Span> {
335        Document {
336            root: plain_scalar("val"),
337            version: None,
338            tags: Vec::new(),
339            comments: Vec::new(),
340            explicit_start,
341            explicit_end,
342        }
343    }
344
345    // NF-DOC-1: explicit_start and explicit_end default to false
346    #[test]
347    fn document_explicit_flags_in_equality() {
348        let a = bare_document(false, false);
349        let b = bare_document(false, false);
350        assert_eq!(a, b);
351    }
352
353    // NF-DOC-2: PartialEq distinguishes differing explicit_start
354    #[test]
355    fn document_partial_eq_distinguishes_explicit_start() {
356        let a = bare_document(true, false);
357        let b = bare_document(false, false);
358        assert_ne!(a, b);
359    }
360
361    // NF-DOC-3: PartialEq distinguishes differing explicit_end
362    #[test]
363    fn document_partial_eq_distinguishes_explicit_end() {
364        let a = bare_document(false, true);
365        let b = bare_document(false, false);
366        assert_ne!(a, b);
367    }
368
369    // NF-DOC-4: Clone preserves both flags
370    #[test]
371    fn document_clone_preserves_explicit_flags() {
372        let doc = bare_document(true, true);
373        let cloned = doc.clone();
374        assert_eq!(doc, cloned);
375        assert!(cloned.explicit_start);
376        assert!(cloned.explicit_end);
377    }
378
379    // -----------------------------------------------------------------------
380    // AL-NODE: anchor_loc() accessor
381    // -----------------------------------------------------------------------
382
383    // AL-NODE-1: anchor_loc_accessor_returns_some_for_anchored_scalar
384    #[test]
385    fn anchor_loc_accessor_returns_some_for_anchored_scalar() {
386        let span = zero_span();
387        let node = Node::Scalar {
388            value: "v".to_owned(),
389            style: ScalarStyle::Plain,
390            anchor: Some("a".to_owned()),
391            anchor_loc: Some(span),
392            tag: None,
393            tag_loc: None,
394            loc: zero_span(),
395            leading_comments: None,
396            trailing_comment: None,
397        };
398        assert_eq!(node.anchor_loc(), Some(span));
399    }
400
401    // AL-NODE-2: anchor_loc_accessor_returns_none_for_unanchored_scalar
402    #[test]
403    fn anchor_loc_accessor_returns_none_for_unanchored_scalar() {
404        let node = Node::Scalar {
405            value: "v".to_owned(),
406            style: ScalarStyle::Plain,
407            anchor: None,
408            anchor_loc: None,
409            tag: None,
410            tag_loc: None,
411            loc: zero_span(),
412            leading_comments: None,
413            trailing_comment: None,
414        };
415        assert_eq!(node.anchor_loc(), None);
416    }
417
418    // AL-NODE-3: anchor_loc_accessor_returns_none_for_alias
419    #[test]
420    fn anchor_loc_accessor_returns_none_for_alias() {
421        let node = Node::Alias {
422            name: "x".to_owned(),
423            loc: zero_span(),
424            leading_comments: None,
425            trailing_comment: None,
426        };
427        assert_eq!(node.anchor_loc(), None);
428    }
429
430    // AL-NODE-4: anchor_loc_accessor_returns_some_for_anchored_mapping
431    #[test]
432    fn anchor_loc_accessor_returns_some_for_anchored_mapping() {
433        use crate::event::CollectionStyle;
434        let span = zero_span();
435        let node = Node::Mapping {
436            entries: vec![],
437            style: CollectionStyle::Block,
438            anchor: Some("m".to_owned()),
439            anchor_loc: Some(span),
440            tag: None,
441            tag_loc: None,
442            loc: zero_span(),
443            leading_comments: None,
444            trailing_comment: None,
445        };
446        assert_eq!(node.anchor_loc(), Some(span));
447    }
448
449    // AL-NODE-5: anchor_loc_accessor_returns_some_for_anchored_sequence
450    #[test]
451    fn anchor_loc_accessor_returns_some_for_anchored_sequence() {
452        use crate::event::CollectionStyle;
453        let span = zero_span();
454        let node = Node::Sequence {
455            items: vec![],
456            style: CollectionStyle::Block,
457            anchor: Some("s".to_owned()),
458            anchor_loc: Some(span),
459            tag: None,
460            tag_loc: None,
461            loc: zero_span(),
462            leading_comments: None,
463            trailing_comment: None,
464        };
465        assert_eq!(node.anchor_loc(), Some(span));
466    }
467
468    // -----------------------------------------------------------------------
469    // TL-NODE: tag_loc() accessor
470    // -----------------------------------------------------------------------
471
472    // TL-NODE-1: tag_loc_accessor_returns_some_for_tagged_scalar
473    #[test]
474    fn tag_loc_accessor_returns_some_for_tagged_scalar() {
475        let span = zero_span();
476        let node = Node::Scalar {
477            value: "v".to_owned(),
478            style: ScalarStyle::Plain,
479            anchor: None,
480            anchor_loc: None,
481            tag: Some("!t".to_owned()),
482            tag_loc: Some(span),
483            loc: zero_span(),
484            leading_comments: None,
485            trailing_comment: None,
486        };
487        assert_eq!(node.tag_loc(), Some(span));
488    }
489
490    // TL-NODE-2: tag_loc_accessor_returns_none_for_untagged_scalar
491    #[test]
492    fn tag_loc_accessor_returns_none_for_untagged_scalar() {
493        let node = Node::Scalar {
494            value: "v".to_owned(),
495            style: ScalarStyle::Plain,
496            anchor: None,
497            anchor_loc: None,
498            tag: None,
499            tag_loc: None,
500            loc: zero_span(),
501            leading_comments: None,
502            trailing_comment: None,
503        };
504        assert_eq!(node.tag_loc(), None);
505    }
506
507    // TL-NODE-3: tag_loc_accessor_returns_none_for_alias
508    #[test]
509    fn tag_loc_accessor_returns_none_for_alias() {
510        let node = Node::Alias {
511            name: "x".to_owned(),
512            loc: zero_span(),
513            leading_comments: None,
514            trailing_comment: None,
515        };
516        assert_eq!(node.tag_loc(), None);
517    }
518
519    // TL-NODE-4: tag_loc_accessor_returns_some_for_tagged_mapping
520    #[test]
521    fn tag_loc_accessor_returns_some_for_tagged_mapping() {
522        use crate::event::CollectionStyle;
523        let span = zero_span();
524        let node = Node::Mapping {
525            entries: vec![],
526            style: CollectionStyle::Block,
527            anchor: None,
528            anchor_loc: None,
529            tag: Some("!!map".to_owned()),
530            tag_loc: Some(span),
531            loc: zero_span(),
532            leading_comments: None,
533            trailing_comment: None,
534        };
535        assert_eq!(node.tag_loc(), Some(span));
536    }
537
538    // TL-NODE-5: tag_loc_accessor_returns_some_for_tagged_sequence
539    #[test]
540    fn tag_loc_accessor_returns_some_for_tagged_sequence() {
541        use crate::event::CollectionStyle;
542        let span = zero_span();
543        let node = Node::Sequence {
544            items: vec![],
545            style: CollectionStyle::Block,
546            anchor: None,
547            anchor_loc: None,
548            tag: Some("!!seq".to_owned()),
549            tag_loc: Some(span),
550            loc: zero_span(),
551            leading_comments: None,
552            trailing_comment: None,
553        };
554        assert_eq!(node.tag_loc(), Some(span));
555    }
556}