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::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 from `%YAML` directive, if present.
22    pub version: Option<(u8, u8)>,
23    /// Tag handle/prefix pairs from `%TAG` directives.
24    pub tags: Vec<(String, String)>,
25    /// Comments that appear at document level (before or between nodes).
26    pub comments: Vec<String>,
27}
28
29/// A YAML node parameterized by its location type.
30#[derive(Debug, Clone, PartialEq)]
31pub enum Node<Loc = Span> {
32    /// A scalar value.
33    Scalar {
34        value: String,
35        style: ScalarStyle,
36        anchor: Option<String>,
37        tag: Option<String>,
38        loc: Loc,
39        /// Comment lines that appear before this node (e.g. `# note`).
40        /// Populated only for non-first entries in a mapping or sequence.
41        /// Document-prefix leading comments are discarded by the tokenizer
42        /// per YAML §9.2 and cannot be recovered here.
43        leading_comments: Vec<String>,
44        /// Inline comment on the same line as this node (e.g. `# note`).
45        trailing_comment: Option<String>,
46    },
47    /// A mapping (sequence of key–value pairs preserving declaration order).
48    Mapping {
49        entries: Vec<(Self, Self)>,
50        anchor: Option<String>,
51        tag: Option<String>,
52        loc: Loc,
53        /// Comment lines that appear before this node.
54        leading_comments: Vec<String>,
55        /// Inline comment on the same line as this node.
56        trailing_comment: Option<String>,
57    },
58    /// A sequence (ordered list of nodes).
59    Sequence {
60        items: Vec<Self>,
61        anchor: Option<String>,
62        tag: Option<String>,
63        loc: Loc,
64        /// Comment lines that appear before this node.
65        leading_comments: Vec<String>,
66        /// Inline comment on the same line as this node.
67        trailing_comment: Option<String>,
68    },
69    /// An alias reference (lossless mode only — resolved mode expands these).
70    Alias {
71        name: String,
72        loc: Loc,
73        /// Comment lines that appear before this node.
74        leading_comments: Vec<String>,
75        /// Inline comment on the same line as this node.
76        trailing_comment: Option<String>,
77    },
78}
79
80impl<Loc> Node<Loc> {
81    /// Returns the anchor name if this node defines one.
82    pub fn anchor(&self) -> Option<&str> {
83        match self {
84            Self::Scalar { anchor, .. }
85            | Self::Mapping { anchor, .. }
86            | Self::Sequence { anchor, .. } => anchor.as_deref(),
87            Self::Alias { .. } => None,
88        }
89    }
90
91    /// Returns the leading comments for this node.
92    pub fn leading_comments(&self) -> &[String] {
93        match self {
94            Self::Scalar {
95                leading_comments, ..
96            }
97            | Self::Mapping {
98                leading_comments, ..
99            }
100            | Self::Sequence {
101                leading_comments, ..
102            }
103            | Self::Alias {
104                leading_comments, ..
105            } => leading_comments,
106        }
107    }
108
109    /// Returns the trailing comment for this node, if any.
110    pub fn trailing_comment(&self) -> Option<&str> {
111        match self {
112            Self::Scalar {
113                trailing_comment, ..
114            }
115            | Self::Mapping {
116                trailing_comment, ..
117            }
118            | Self::Sequence {
119                trailing_comment, ..
120            }
121            | Self::Alias {
122                trailing_comment, ..
123            } => trailing_comment.as_deref(),
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::event::ScalarStyle;
132    use crate::pos::{Pos, Span};
133
134    fn zero_span() -> Span {
135        Span {
136            start: Pos::ORIGIN,
137            end: Pos::ORIGIN,
138        }
139    }
140
141    fn plain_scalar(value: &str) -> Node<Span> {
142        Node::Scalar {
143            value: value.to_owned(),
144            style: ScalarStyle::Plain,
145            anchor: None,
146            tag: None,
147            loc: zero_span(),
148            leading_comments: Vec::new(),
149            trailing_comment: None,
150        }
151    }
152
153    // NF-1: node_debug_includes_leading_comments
154    #[test]
155    fn node_debug_includes_leading_comments() {
156        let node = Node::Scalar {
157            value: "val".to_owned(),
158            style: ScalarStyle::Plain,
159            anchor: None,
160            tag: None,
161            loc: zero_span(),
162            leading_comments: vec!["# note".to_owned()],
163            trailing_comment: None,
164        };
165        let debug = format!("{node:?}");
166        assert!(debug.contains("# note"), "debug output: {debug}");
167    }
168
169    // NF-2: node_partial_eq_considers_leading_comments
170    #[test]
171    fn node_partial_eq_considers_leading_comments() {
172        let a = Node::Scalar {
173            value: "val".to_owned(),
174            style: ScalarStyle::Plain,
175            anchor: None,
176            tag: None,
177            loc: zero_span(),
178            leading_comments: vec!["# a".to_owned()],
179            trailing_comment: None,
180        };
181        let b = Node::Scalar {
182            value: "val".to_owned(),
183            style: ScalarStyle::Plain,
184            anchor: None,
185            tag: None,
186            loc: zero_span(),
187            leading_comments: vec!["# b".to_owned()],
188            trailing_comment: None,
189        };
190        assert_ne!(a, b);
191    }
192
193    // NF-3: node_clone_preserves_comments
194    #[test]
195    fn node_clone_preserves_comments() {
196        let node = Node::Scalar {
197            value: "val".to_owned(),
198            style: ScalarStyle::Plain,
199            anchor: None,
200            tag: None,
201            loc: zero_span(),
202            leading_comments: vec!["# x".to_owned()],
203            trailing_comment: Some("# y".to_owned()),
204        };
205        let cloned = node.clone();
206        assert_eq!(node, cloned);
207        assert_eq!(cloned.leading_comments(), &["# x"]);
208        assert_eq!(cloned.trailing_comment(), Some("# y"));
209    }
210
211    // Sanity: plain_scalar helper produces empty comment fields.
212    #[test]
213    fn plain_scalar_has_empty_comments() {
214        let n = plain_scalar("hello");
215        assert!(n.leading_comments().is_empty());
216        assert!(n.trailing_comment().is_none());
217    }
218}