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::{CollectionStyle, 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        let span = zero_span();
434        let node = Node::Mapping {
435            entries: vec![],
436            style: CollectionStyle::Block,
437            anchor: Some("m".to_owned()),
438            anchor_loc: Some(span),
439            tag: None,
440            tag_loc: None,
441            loc: zero_span(),
442            leading_comments: None,
443            trailing_comment: None,
444        };
445        assert_eq!(node.anchor_loc(), Some(span));
446    }
447
448    // AL-NODE-5: anchor_loc_accessor_returns_some_for_anchored_sequence
449    #[test]
450    fn anchor_loc_accessor_returns_some_for_anchored_sequence() {
451        let span = zero_span();
452        let node = Node::Sequence {
453            items: vec![],
454            style: CollectionStyle::Block,
455            anchor: Some("s".to_owned()),
456            anchor_loc: Some(span),
457            tag: None,
458            tag_loc: None,
459            loc: zero_span(),
460            leading_comments: None,
461            trailing_comment: None,
462        };
463        assert_eq!(node.anchor_loc(), Some(span));
464    }
465
466    // -----------------------------------------------------------------------
467    // TL-NODE: tag_loc() accessor
468    // -----------------------------------------------------------------------
469
470    // TL-NODE-1: tag_loc_accessor_returns_some_for_tagged_scalar
471    #[test]
472    fn tag_loc_accessor_returns_some_for_tagged_scalar() {
473        let span = zero_span();
474        let node = Node::Scalar {
475            value: "v".to_owned(),
476            style: ScalarStyle::Plain,
477            anchor: None,
478            anchor_loc: None,
479            tag: Some("!t".to_owned()),
480            tag_loc: Some(span),
481            loc: zero_span(),
482            leading_comments: None,
483            trailing_comment: None,
484        };
485        assert_eq!(node.tag_loc(), Some(span));
486    }
487
488    // TL-NODE-2: tag_loc_accessor_returns_none_for_untagged_scalar
489    #[test]
490    fn tag_loc_accessor_returns_none_for_untagged_scalar() {
491        let node = Node::Scalar {
492            value: "v".to_owned(),
493            style: ScalarStyle::Plain,
494            anchor: None,
495            anchor_loc: None,
496            tag: None,
497            tag_loc: None,
498            loc: zero_span(),
499            leading_comments: None,
500            trailing_comment: None,
501        };
502        assert_eq!(node.tag_loc(), None);
503    }
504
505    // TL-NODE-3: tag_loc_accessor_returns_none_for_alias
506    #[test]
507    fn tag_loc_accessor_returns_none_for_alias() {
508        let node = Node::Alias {
509            name: "x".to_owned(),
510            loc: zero_span(),
511            leading_comments: None,
512            trailing_comment: None,
513        };
514        assert_eq!(node.tag_loc(), None);
515    }
516
517    // TL-NODE-4: tag_loc_accessor_returns_some_for_tagged_mapping
518    #[test]
519    fn tag_loc_accessor_returns_some_for_tagged_mapping() {
520        let span = zero_span();
521        let node = Node::Mapping {
522            entries: vec![],
523            style: CollectionStyle::Block,
524            anchor: None,
525            anchor_loc: None,
526            tag: Some("!!map".to_owned()),
527            tag_loc: Some(span),
528            loc: zero_span(),
529            leading_comments: None,
530            trailing_comment: None,
531        };
532        assert_eq!(node.tag_loc(), Some(span));
533    }
534
535    // TL-NODE-5: tag_loc_accessor_returns_some_for_tagged_sequence
536    #[test]
537    fn tag_loc_accessor_returns_some_for_tagged_sequence() {
538        let span = zero_span();
539        let node = Node::Sequence {
540            items: vec![],
541            style: CollectionStyle::Block,
542            anchor: None,
543            anchor_loc: None,
544            tag: Some("!!seq".to_owned()),
545            tag_loc: Some(span),
546            loc: zero_span(),
547            leading_comments: None,
548            trailing_comment: None,
549        };
550        assert_eq!(node.tag_loc(), Some(span));
551    }
552}