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)]
129#[allow(clippy::indexing_slicing, clippy::expect_used, clippy::unwrap_used)]
130mod tests {
131    use super::*;
132    use crate::event::ScalarStyle;
133    use crate::pos::{Pos, Span};
134
135    fn zero_span() -> Span {
136        Span {
137            start: Pos::ORIGIN,
138            end: Pos::ORIGIN,
139        }
140    }
141
142    fn plain_scalar(value: &str) -> Node<Span> {
143        Node::Scalar {
144            value: value.to_owned(),
145            style: ScalarStyle::Plain,
146            anchor: None,
147            tag: None,
148            loc: zero_span(),
149            leading_comments: Vec::new(),
150            trailing_comment: None,
151        }
152    }
153
154    // NF-1: node_debug_includes_leading_comments
155    #[test]
156    fn node_debug_includes_leading_comments() {
157        let node = Node::Scalar {
158            value: "val".to_owned(),
159            style: ScalarStyle::Plain,
160            anchor: None,
161            tag: None,
162            loc: zero_span(),
163            leading_comments: vec!["# note".to_owned()],
164            trailing_comment: None,
165        };
166        let debug = format!("{node:?}");
167        assert!(debug.contains("# note"), "debug output: {debug}");
168    }
169
170    // NF-2: node_partial_eq_considers_leading_comments
171    #[test]
172    fn node_partial_eq_considers_leading_comments() {
173        let a = Node::Scalar {
174            value: "val".to_owned(),
175            style: ScalarStyle::Plain,
176            anchor: None,
177            tag: None,
178            loc: zero_span(),
179            leading_comments: vec!["# a".to_owned()],
180            trailing_comment: None,
181        };
182        let b = Node::Scalar {
183            value: "val".to_owned(),
184            style: ScalarStyle::Plain,
185            anchor: None,
186            tag: None,
187            loc: zero_span(),
188            leading_comments: vec!["# b".to_owned()],
189            trailing_comment: None,
190        };
191        assert_ne!(a, b);
192    }
193
194    // NF-3: node_clone_preserves_comments
195    #[test]
196    fn node_clone_preserves_comments() {
197        let node = Node::Scalar {
198            value: "val".to_owned(),
199            style: ScalarStyle::Plain,
200            anchor: None,
201            tag: None,
202            loc: zero_span(),
203            leading_comments: vec!["# x".to_owned()],
204            trailing_comment: Some("# y".to_owned()),
205        };
206        let cloned = node.clone();
207        assert_eq!(node, cloned);
208        assert_eq!(cloned.leading_comments(), &["# x"]);
209        assert_eq!(cloned.trailing_comment(), Some("# y"));
210    }
211
212    // Sanity: plain_scalar helper produces empty comment fields.
213    #[test]
214    fn plain_scalar_has_empty_comments() {
215        let n = plain_scalar("hello");
216        assert!(n.leading_comments().is_empty());
217        assert!(n.trailing_comment().is_none());
218    }
219}