yaml_edit/nodes/mod.rs
1//! AST node types for YAML.
2//!
3//! # CST invariants
4//!
5//! yaml-edit is a lossless editor: every mutation produces text that
6//! re-parses into the same tree shape (see [`crate::debug::roundtrip_ok`]
7//! and [`crate::debug::validate_tree`]). To keep that property, all
8//! mutation helpers in this module must uphold the following invariants
9//! on the concrete syntax tree.
10//!
11//! ## Where whitespace lives
12//!
13//! For a block-style mapping entry `key:\n - a`, the NEWLINE after
14//! `key:` and the INDENT before the SEQUENCE both live inside the parent
15//! VALUE, *not* inside the child SEQUENCE. The SEQUENCE_ENTRYs
16//! themselves carry only the DASH, WHITESPACE, and content:
17//!
18//! ```text
19//! MAPPING_ENTRY
20//! KEY ...
21//! COLON ":"
22//! VALUE
23//! NEWLINE "\n"
24//! INDENT " "
25//! SEQUENCE
26//! SEQUENCE_ENTRY
27//! DASH "-"
28//! WHITESPACE " "
29//! SCALAR ...
30//! ```
31//!
32//! Consequence: when constructing a fresh block SEQUENCE or MAPPING to
33//! splice under a key, do **not** prepend its own leading INDENT for the
34//! first entry - the parent VALUE already carries one. Duplicating it
35//! renders as doubled indentation and violates the no-stacked-INDENT
36//! check in [`crate::debug::validate_tree`].
37//!
38//! For flow-style values (`{a: 1}`, `[1, 2]`) all separators live
39//! *inside* the flow SEQUENCE / MAPPING: the parent VALUE has no direct
40//! NEWLINE child. `value_is_block` in `mapping.rs` distinguishes the two
41//! by looking for a NEWLINE token as a direct child of VALUE.
42//!
43//! ## Entry termination
44//!
45//! Block-style MAPPING_ENTRY and SEQUENCE_ENTRY nodes are terminated by
46//! a NEWLINE. That terminator normally lives as the entry's own last
47//! token, but the parser sometimes lifts it out when a trailing comment
48//! separates it from the next sibling:
49//!
50//! ```text
51//! # SEQUENCE_ENTRY ends with STRING; the NEWLINE lives at the SEQUENCE
52//! # level after the following COMMENT.
53//! SEQUENCE
54//! SEQUENCE_ENTRY
55//! DASH "-"
56//! SCALAR ("a")
57//! WHITESPACE " "
58//! COMMENT "# note"
59//! NEWLINE "\n"
60//! SEQUENCE_ENTRY ...
61//! ```
62//!
63//! Two carve-outs:
64//! - The **last** entry of a block collection may lack a terminator
65//! (unterminated sources like `a: 1\nb: 2` are legitimate YAML and
66//! must roundtrip).
67//! - An entry ending in an **implicit-null** scalar (see below) has a
68//! zero-width `SCALAR { NULL "" }` as its last leaf, not a NEWLINE.
69//! That's a well-formed terminated entry; the NEWLINE lives elsewhere
70//! in the tree (inside the KEY subtree for `? key\n`, inside the VALUE
71//! between COLON and SCALAR for `key:\n`).
72//!
73//! When inserting a new sibling entry after an existing one, mutation
74//! helpers must first ensure the predecessor is terminated
75//! (`ensure_trailing_newline` in this module).
76//!
77//! ## Implicit-null values
78//!
79//! Every `MAPPING_ENTRY` and `SEQUENCE_ENTRY` holds a value node, even
80//! when the source omits it. The parser emits a zero-width
81//! `SCALAR { NULL "" }` at every "missing value" position:
82//!
83//! ```text
84//! Source form CST shape
85//! ------------------------------- ----------------------------------------
86//! key:\n (block mapping) VALUE { NEWLINE, SCALAR { NULL "" } }
87//! key: (block mapping, EOF) VALUE { SCALAR { NULL "" } }
88//! {key} (flow mapping) MAPPING_ENTRY { KEY { SCALAR "key" },
89//! VALUE { SCALAR { NULL "" } } }
90//! {key:} (flow mapping) MAPPING_ENTRY { KEY { SCALAR "key" },
91//! COLON,
92//! VALUE { SCALAR { NULL "" } } }
93//! {,} (flow mapping) MAPPING_ENTRY { KEY { SCALAR { NULL "" } },
94//! VALUE { SCALAR { NULL "" } },
95//! COMMA }
96//! {: v} (flow mapping) KEY { SCALAR { NULL "" } }, COLON, VALUE ...
97//! ? key\n (explicit key) MAPPING_ENTRY { QUESTION, KEY ..., NEWLINE,
98//! VALUE { SCALAR { NULL "" } } }
99//! - \n (block sequence) SEQUENCE_ENTRY { DASH, WHITESPACE,
100//! SCALAR { NULL "" }, NEWLINE }
101//! [a, , c] (flow sequence) SEQUENCE_ENTRY { SCALAR { NULL "" }, COMMA, ... }
102//! ```
103//!
104//! The invariant: **every KEY, every VALUE, every SEQUENCE_ENTRY holds
105//! exactly one scalar or collection node.** Mappings wrap the value in a
106//! `VALUE` node; sequences hold the scalar directly as a
107//! `SEQUENCE_ENTRY` child (matching how each shape wraps its non-null
108//! values -- sequences never had a wrapper, mappings always did).
109//!
110//! The zero-width `NULL ""` token renders as nothing, so round-trips are
111//! lossless. This is distinct from a programmatically-written null,
112//! which uses `SCALAR { NULL "null" }` (textual, three characters) so
113//! the value is visible in the output.
114//!
115//! Consequences for mutation helpers:
116//!
117//! - Iterating `SEQUENCE_ENTRY` children with `matches!(kind, SCALAR |
118//! MAPPING | SEQUENCE | ALIAS | TAGGED_NODE)` finds the value in
119//! every entry, including implicit-null ones. `len` / `get` /
120//! `set` / `remove` therefore share the same set of indexes.
121//! - `Scalar::is_null()` returns `true` for the zero-width form.
122//! - `set(i, real_value)` on an implicit-null entry may need to insert
123//! a separating WHITESPACE that the original didn't have (the value
124//! was zero-width so no separator was needed). See `Sequence::set`
125//! for the `after_dash` pattern used in block sequences.
126//!
127//! ## No stacked INDENTs, no double trailing NEWLINEs
128//!
129//! Two adjacent INDENT tokens as direct children of the same node
130//! concatenate at render time and produce visibly-wrong indentation
131//! (this was the shape of issue #38). Two adjacent NEWLINE tokens at the
132//! tail of a MAPPING_ENTRY or SEQUENCE_ENTRY render as a stray blank
133//! line (issue #18).
134//!
135//! Both are checked by [`crate::debug::validate_tree`]; mutation helpers
136//! must not produce either. Note the check for double NEWLINE is scoped
137//! to *entries*, not their parent MAPPING / SEQUENCE containers: bare
138//! NEWLINEs between entries (blank-line separators) are valid formatting
139//! and appear at the container level.
140//!
141//! ## Flow separators live on the previous entry
142//!
143//! Inside a flow mapping or sequence, the COMMA (and any following
144//! WHITESPACE / NEWLINE / INDENT) between two entries is stored as
145//! trailing siblings of the *previous* entry's KEY / VALUE / SCALAR,
146//! not inside the entry it precedes. Inserters that append a new entry
147//! must add a `, ` suffix to the entry they follow, not a `,` prefix on
148//! themselves.
149//!
150//! ## Prefer targeted splices over whole-node rebuilds
151//!
152//! Reconstructing an entire MAPPING or SEQUENCE from a builder discards
153//! anchors, tags, comments, quoting styles, and any tokens the fix
154//! didn't know about. Mutation helpers should locate the specific
155//! `NodeOrToken` range they need to change and use `splice_children`,
156//! preserving the surrounding tokens.
157
158use crate::lex::SyntaxKind;
159
160/// YAML language type for rowan.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
162pub enum Lang {}
163
164impl rowan::Language for Lang {
165 type Kind = SyntaxKind;
166
167 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
168 // SAFETY: SyntaxKind is #[repr(u16)] with contiguous discriminants
169 // from ROOT = 0 through EOF, so every value in that range is a valid
170 // variant. The bound is checked in every build, not just debug: a
171 // stray value here would otherwise be undefined behaviour rather
172 // than a panic.
173 assert!(
174 raw.0 <= SyntaxKind::EOF as u16,
175 "raw SyntaxKind value {} is out of range (max {})",
176 raw.0,
177 SyntaxKind::EOF as u16,
178 );
179 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
180 }
181
182 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
183 kind.into()
184 }
185}
186
187pub type SyntaxNode = rowan::SyntaxNode<Lang>;
188pub type SyntaxToken = rowan::SyntaxToken<Lang>;
189
190/// Build a standalone `SyntaxToken` of `kind` with `text`, ready to
191/// splice into a parent's child list via `splice_children`.
192pub(crate) fn fresh_token(kind: SyntaxKind, text: &str) -> SyntaxToken {
193 let mut builder = rowan::GreenNodeBuilder::new();
194 builder.start_node(SyntaxKind::ROOT.into());
195 builder.token(kind.into(), text);
196 builder.finish_node();
197 SyntaxNode::new_root_mut(builder.finish())
198 .first_token()
199 .expect("just built a token")
200}
201
202/// The first direct child node of `parent` with the given `kind`, if any.
203pub(crate) fn child_of_kind(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxNode> {
204 parent.children().find(|n| n.kind() == kind)
205}
206
207/// Does `parent` have a direct child token whose kind satisfies `pred`?
208///
209/// Flow/block style and several syntactic properties are decided by the
210/// presence of a marker token (`{`, `[`, `|`, `:`) directly under a node,
211/// never a nested one, so the search deliberately stays one level deep.
212pub(crate) fn has_child_token(parent: &SyntaxNode, pred: impl Fn(SyntaxKind) -> bool) -> bool {
213 parent
214 .children_with_tokens()
215 .any(|el| el.as_token().is_some_and(|t| pred(t.kind())))
216}
217
218/// Append `children` at the end of `parent`'s child list.
219pub(crate) fn append_children(
220 parent: &SyntaxNode,
221 children: Vec<rowan::NodeOrToken<SyntaxNode, SyntaxToken>>,
222) {
223 let end = parent.children_with_tokens().count();
224 parent.splice_children(end..end, children);
225}
226
227/// Does this subtree end with a NEWLINE leaf, or with any run of
228/// zero-width tokens (like an implicit-null NULL "") whose last
229/// non-empty predecessor is a NEWLINE?
230///
231/// `last_token()` alone can't answer this: it returns the deepest tail
232/// leaf, which may be a zero-width NULL sitting after a NEWLINE inside
233/// the same KEY subtree (`? b\n` under a tagged mapping). Walk `node`'s
234/// own token stream backwards, skipping empty tokens, and check what's
235/// there. The walk is strictly bounded to `node`'s subtree; callers
236/// don't need to worry about escaping into siblings.
237pub(crate) fn trailing_newline_reachable(node: &SyntaxNode) -> bool {
238 // rowan's descendants_with_tokens isn't DoubleEndedIterator, so
239 // materialise the (usually short) tail slice; the walk is still
240 // bounded to `node`'s subtree, avoiding prev_token()'s tree-wide
241 // reach.
242 let mut last_non_empty = None;
243 for el in node.descendants_with_tokens() {
244 if let Some(t) = el.into_token() {
245 if !t.text().is_empty() {
246 last_non_empty = Some(t);
247 }
248 }
249 }
250 last_non_empty.is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
251}
252
253/// Does `entry`'s line already end in a NEWLINE, counting the sibling one
254/// that explicit-key entries carry instead of owning it themselves?
255pub(crate) fn entry_line_terminated(entry: &SyntaxNode) -> bool {
256 trailing_newline_reachable(entry)
257 || entry
258 .next_sibling_or_token()
259 .is_some_and(|s| s.kind() == SyntaxKind::NEWLINE)
260}
261
262/// Append a trailing NEWLINE token to `entry` if its line doesn't already end
263/// with one. Used when a block-style entry is about to have a new sibling
264/// appended after it (its trailing newline separates the two entries
265/// visually).
266///
267/// The new sibling must be going in immediately after `entry`: an explicit-key
268/// entry is treated as terminated by a NEWLINE that follows it as a sibling,
269/// which only separates the two if nothing is spliced in between. Callers
270/// inserting at an independently computed position want
271/// [`ensure_own_trailing_newline`] instead.
272pub(crate) fn ensure_trailing_newline(entry: &SyntaxNode) {
273 if entry_line_terminated(entry) {
274 return;
275 }
276 append_children(entry, vec![fresh_token(SyntaxKind::NEWLINE, "\n").into()]);
277}
278
279/// Append a trailing NEWLINE token to `entry` unless the entry itself already
280/// ends in one, ignoring any NEWLINE that merely follows it as a sibling.
281pub(crate) fn ensure_own_trailing_newline(entry: &SyntaxNode) {
282 if trailing_newline_reachable(entry) {
283 return;
284 }
285 append_children(entry, vec![fresh_token(SyntaxKind::NEWLINE, "\n").into()]);
286}
287
288/// The `children_with_tokens()` indices of `parent`'s direct child nodes of
289/// the given kind, in order.
290///
291/// `splice_children` indexes by `children_with_tokens()`, so mutation helpers
292/// need entry positions in that numbering rather than `children()`'s.
293pub(crate) fn entry_indices(parent: &SyntaxNode, kind: SyntaxKind) -> Vec<usize> {
294 parent
295 .children_with_tokens()
296 .enumerate()
297 .filter(|(_, c)| c.as_node().is_some_and(|n| n.kind() == kind))
298 .map(|(i, _)| i)
299 .collect()
300}
301
302/// The `children_with_tokens()` index of `parent`'s `index`-th child node of
303/// the given kind.
304pub(crate) fn nth_entry_index(
305 parent: &SyntaxNode,
306 kind: SyntaxKind,
307 index: usize,
308) -> Option<usize> {
309 parent
310 .children_with_tokens()
311 .enumerate()
312 .filter(|(_, c)| c.as_node().is_some_and(|n| n.kind() == kind))
313 .map(|(i, _)| i)
314 .nth(index)
315}
316
317/// Emit `key`'s content for use inside a `KEY` node.
318///
319/// Identical to `key.build_content` except that a key which would render as
320/// a block scalar (`|` / `>`) is emitted as one double-quoted scalar
321/// instead. A block scalar is a valid *value* but cannot be a key: it does
322/// not read back as the same key, and the entry's `:` lands inside the
323/// block.
324pub(crate) fn build_key_content(builder: &mut rowan::GreenNodeBuilder, key: &impl crate::AsYaml) {
325 if key.as_node().is_none() {
326 // A raw value (&str, String, ...). Render it once on its own: the
327 // block form is emitted as a single STRING token whose text starts
328 // with the indicator, so check the text rather than the token kinds.
329 let mut probe = rowan::GreenNodeBuilder::new();
330 probe.start_node(SyntaxKind::ROOT.into());
331 key.build_content(&mut probe, 0, false);
332 probe.finish_node();
333 let rendered = SyntaxNode::new_root(probe.finish()).text().to_string();
334
335 if rendered.starts_with('|') || rendered.starts_with('>') {
336 // Re-render in flow context, where the same value is written as
337 // a double-quoted scalar that fits on one line.
338 let mut flow = rowan::GreenNodeBuilder::new();
339 flow.start_node(SyntaxKind::ROOT.into());
340 key.build_content(&mut flow, 0, true);
341 flow.finish_node();
342 let quoted = SyntaxNode::new_root(flow.finish()).text().to_string();
343
344 builder.start_node(SyntaxKind::SCALAR.into());
345 builder.token(SyntaxKind::STRING.into(), "ed);
346 builder.finish_node();
347 return;
348 }
349 }
350 key.build_content(builder, 0, false);
351}
352
353/// The `KEY` child of a `MAPPING_ENTRY`.
354pub(crate) fn entry_key(entry: &SyntaxNode) -> Option<SyntaxNode> {
355 child_of_kind(entry, SyntaxKind::KEY)
356}
357
358/// The `VALUE` child of a `MAPPING_ENTRY` or `SEQUENCE_ENTRY`.
359pub(crate) fn entry_value(entry: &SyntaxNode) -> Option<SyntaxNode> {
360 child_of_kind(entry, SyntaxKind::VALUE)
361}
362
363/// A macro to create AST node wrappers.
364macro_rules! ast_node {
365 ($ast:ident, $kind:ident, $doc:expr) => {
366 #[doc = $doc]
367 #[doc = ""]
368 #[doc = "**Note:** This type uses interior mutability through the rowan library."]
369 #[doc = "Mutation methods work even when called through `&self`. See the crate-level"]
370 #[doc = "documentation for details on the mutability model."]
371 #[derive(Clone, PartialEq, Eq, Hash)]
372 pub struct $ast(pub(crate) SyntaxNode);
373
374 impl std::fmt::Debug for $ast {
375 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376 f.debug_struct(stringify!($ast))
377 .field("syntax", &self.0)
378 .finish()
379 }
380 }
381
382 impl AstNode for $ast {
383 type Language = Lang;
384
385 fn can_cast(kind: SyntaxKind) -> bool {
386 kind == SyntaxKind::$kind
387 }
388
389 fn cast(syntax: SyntaxNode) -> Option<Self> {
390 if Self::can_cast(syntax.kind()) {
391 Some(Self(syntax))
392 } else {
393 None
394 }
395 }
396
397 fn syntax(&self) -> &SyntaxNode {
398 &self.0
399 }
400 }
401
402 impl std::fmt::Display for $ast {
403 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404 write!(f, "{}", self.0.text())
405 }
406 }
407 };
408}
409
410pub(crate) use ast_node;
411/// Emit the `byte_range` / `start_position` / `end_position` trio for a
412/// wrapper whose `self.0` exposes `text_range()`. `$what` names the node
413/// in the generated docs ("sequence", "comment", ...).
414macro_rules! ast_node_spans {
415 ($what:expr) => {
416 #[doc = concat!("Get the byte offset range of this ", $what, " in the source text.")]
417 #[doc = ""]
418 #[doc = "Returns the start and end byte offsets as a `TextPosition`."]
419 pub fn byte_range(&self) -> crate::TextPosition {
420 self.0.text_range().into()
421 }
422
423 #[doc = concat!("Get the line and column where this ", $what, " starts.")]
424 #[doc = ""]
425 #[doc = "Requires the original source text to calculate line/column from byte"]
426 #[doc = "offsets. Line and column numbers are 1-indexed."]
427 #[doc = ""]
428 #[doc = "# Arguments"]
429 #[doc = ""]
430 #[doc = "* `source_text` - The original YAML source text"]
431 pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
432 crate::byte_offset_to_line_column(source_text, self.byte_range().start as usize)
433 }
434
435 #[doc = concat!("Get the line and column where this ", $what, " ends.")]
436 #[doc = ""]
437 #[doc = "Requires the original source text to calculate line/column from byte"]
438 #[doc = "offsets. Line and column numbers are 1-indexed."]
439 #[doc = ""]
440 #[doc = "# Arguments"]
441 #[doc = ""]
442 #[doc = "* `source_text` - The original YAML source text"]
443 pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
444 crate::byte_offset_to_line_column(source_text, self.byte_range().end as usize)
445 }
446 };
447}
448
449pub(crate) use ast_node_spans;
450
451// Node modules
452pub mod alias_node;
453pub mod comment;
454pub mod directive;
455pub mod document;
456pub mod entry;
457pub mod mapping;
458pub mod scalar_node;
459pub mod sequence;
460pub mod tagged_node;
461
462// Re-exports
463pub use alias_node::Alias;
464pub use comment::Comment;
465pub use directive::Directive;
466pub use document::Document;
467pub use entry::{Entry, OccupiedEntry, VacantEntry};
468pub use mapping::{Mapping, MappingEntry};
469pub use scalar_node::{Scalar, ScalarConversionError};
470pub use sequence::Sequence;
471pub use tagged_node::TaggedNode;