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}