Skip to main content

yaml_edit/
anchor_resolution.rs

1//! Anchor and alias resolution for semantic YAML operations
2//!
3//! This module provides functionality to resolve YAML anchors and aliases,
4//! enabling semantic lookups while preserving the lossless nature of the
5//! syntax tree.
6//!
7//! # Design Philosophy
8//!
9//! - **Opt-in**: Resolution is explicit via `get_resolved()` methods
10//! - **Preserves structure**: Original syntax tree remains unchanged
11//! - **Lazy evaluation**: Anchors are resolved on-demand, not eagerly
12//! - **Error handling**: An undefined or recursive alias, a merge key
13//!   whose operand is not a mapping, a key repeated within a mapping,
14//!   distinct keys that would collide in a resolved mapping, or `!!set`, `!!omap` and `!!pairs` content
15//!   that [`YamlValue`] cannot hold are reported as a [`ResolveError`]
16//!
17//! # Examples
18//!
19//! ```
20//! use yaml_edit::Document;
21//! use yaml_edit::anchor_resolution::DocumentResolvedExt;
22//! use std::str::FromStr;
23//!
24//! let yaml = r#"
25//! defaults: &defaults
26//!   timeout: 30
27//!   retries: 3
28//!
29//! production:
30//!   <<: *defaults
31//!   host: prod.example.com
32//! "#;
33//!
34//! let doc = Document::from_str(yaml).unwrap();
35//!
36//! // Regular get() doesn't resolve - returns the alias syntax
37//! // get_resolved() expands aliases and merge keys
38//! if let Some(resolved_value) = doc.get_resolved("production").unwrap() {
39//!     if let Some(prod) = resolved_value.as_mapping() {
40//!         // This will find 'timeout: 30' from the merged defaults
41//!         if let Some(timeout) = prod.get("timeout") {
42//!             assert_eq!(timeout.to_i64(), Some(30));
43//!         }
44//!         // This will find the overridden value
45//!         if let Some(host) = prod.get("host") {
46//!             assert_eq!(host.as_scalar().map(|s| s.to_string()).as_deref(), Some("prod.example.com"));
47//!         }
48//!     }
49//! }
50//! ```
51
52use crate::as_yaml::{key_identity, AsYaml, NodeIdentity};
53use crate::lex::SyntaxKind;
54use crate::value::YamlValue;
55use crate::yaml::SyntaxNode;
56use rowan::ast::AstNode;
57use std::collections::HashMap;
58
59/// An error resolving aliases or merge keys, or converting a node to a
60/// [`YamlValue`] that cannot hold it.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ResolveError {
63    /// An alias refers to an anchor that is not defined before it in the
64    /// document.
65    UndefinedAlias {
66        /// The anchor name the alias references, without the `*`.
67        alias: String,
68    },
69    /// An alias refers to an anchor it is nested in, so expanding it
70    /// would not terminate.
71    RecursiveAlias {
72        /// The anchor name the alias references, without the `*`.
73        alias: String,
74    },
75    /// A merge key (`<<`) operand is neither a mapping nor an alias to
76    /// one.
77    InvalidMergeOperand {
78        /// The source text of the offending operand.
79        operand: String,
80    },
81    /// A mapping has two equal keys, such as `a` and `'a'`, or a key and
82    /// an alias to an equal node, so it cannot keep both values. A direct
83    /// key shadowing a key from a merge key (`<<`) is not a duplicate. The
84    /// members of a `!!set` and the keys of a `!!omap` have to be distinct
85    /// too.
86    DuplicateKey {
87        /// The source text of the key that was seen first.
88        first: String,
89        /// The source text of the key that repeats it.
90        second: String,
91    },
92    /// Two distinct keys of a mapping have the same string form, such as
93    /// the int `1` and the string `'1'`, so they cannot both be kept in a
94    /// [`YamlValue::Mapping`]. The same goes for the members of a
95    /// [`YamlValue::Set`] and the keys of a [`YamlValue::OrderedMapping`].
96    KeyCollision {
97        /// The source text of the key that was seen first.
98        first: String,
99        /// The source text of the key that collides with it.
100        second: String,
101    },
102    /// The content of a `!!set` is not a mapping, so it has no members.
103    InvalidSetContent {
104        /// The source text of the content.
105        content: String,
106    },
107    /// A `!!set` member is not a scalar, so it cannot be kept in a
108    /// [`YamlValue::Set`].
109    InvalidSetMember {
110        /// The source text of the member.
111        member: String,
112    },
113    /// A `!!set` member has a value other than null, which a
114    /// [`YamlValue::Set`] cannot keep.
115    NonNullSetValue {
116        /// The source text of the member.
117        member: String,
118        /// The source text of its value.
119        value: String,
120    },
121    /// An item of a `!!omap` or `!!pairs` sequence is not a mapping with a
122    /// single entry.
123    InvalidPairsItem {
124        /// The tag of the sequence, `!!omap` or `!!pairs`.
125        tag: String,
126        /// The source text of the item.
127        item: String,
128    },
129    /// The content of a `!!omap` or `!!pairs` is not a sequence, so it has
130    /// no entries.
131    InvalidPairsContent {
132        /// The tag, `!!omap` or `!!pairs`.
133        tag: String,
134        /// The source text of the content.
135        content: String,
136    },
137    /// [`YamlValue::cast`] met an alias, which it cannot resolve without
138    /// the document's anchors. [`YamlValue::from_document`] and
139    /// [`DocumentResolvedExt::get_resolved`] resolve aliases.
140    UnresolvedAlias {
141        /// The anchor name the alias references, without the `*`.
142        alias: String,
143    },
144    /// A document, key, value, sequence entry or tagged node holds more
145    /// than one node, as in `k: [q] x`, which YAML does not allow but the
146    /// parser accepts. A [`YamlValue`] can only keep one of them.
147    ExtraNode {
148        /// The source text of the node after the first.
149        node: String,
150    },
151}
152
153impl std::fmt::Display for ResolveError {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            ResolveError::UndefinedAlias { alias } => {
157                write!(f, "alias to undefined anchor {alias:?}")
158            }
159            ResolveError::RecursiveAlias { alias } => {
160                write!(f, "alias {alias:?} refers to an anchor it is nested in")
161            }
162            ResolveError::InvalidMergeOperand { operand } => {
163                write!(f, "merge key operand {operand:?} is not a mapping")
164            }
165            ResolveError::KeyCollision { first, second } => {
166                write!(
167                    f,
168                    "distinct keys {first:?} and {second:?} have the same string form"
169                )
170            }
171            ResolveError::DuplicateKey { first, second } => {
172                write!(f, "duplicate mapping key: {second:?} equals {first:?}")
173            }
174            ResolveError::InvalidSetContent { content } => {
175                write!(f, "!!set content {content:?} is not a mapping")
176            }
177            ResolveError::InvalidSetMember { member } => {
178                write!(f, "!!set member {member:?} is not a scalar")
179            }
180            ResolveError::NonNullSetValue { member, value } => {
181                write!(f, "!!set member {member:?} has non-null value {value:?}")
182            }
183            ResolveError::InvalidPairsItem { tag, item } => {
184                write!(
185                    f,
186                    "{tag} item {item:?} is not a mapping with a single entry"
187                )
188            }
189            ResolveError::InvalidPairsContent { tag, content } => {
190                write!(f, "{tag} content {content:?} is not a sequence")
191            }
192            ResolveError::UnresolvedAlias { alias } => {
193                write!(
194                    f,
195                    "alias {alias:?} cannot be converted without resolving it; \
196                     use get_resolved to resolve aliases"
197                )
198            }
199            ResolveError::ExtraNode { node } => {
200                write!(f, "node {node:?} follows another in the same place")
201            }
202        }
203    }
204}
205
206impl std::error::Error for ResolveError {}
207
208/// An alias that does not refer to a node.
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub(crate) enum AliasError {
211    /// No anchor of the alias's name is defined before it.
212    Undefined { alias: String },
213    /// The alias refers to an anchor it is nested in.
214    Recursive { alias: String },
215}
216
217impl From<AliasError> for ResolveError {
218    fn from(err: AliasError) -> Self {
219        match err {
220            AliasError::Undefined { alias } => ResolveError::UndefinedAlias { alias },
221            AliasError::Recursive { alias } => ResolveError::RecursiveAlias { alias },
222        }
223    }
224}
225
226/// An error collecting the entries of a [`MergedMapping`].
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub(crate) enum MergeError {
229    Alias(AliasError),
230    /// A merge key operand is not a mapping; holds its source text.
231    InvalidOperand {
232        operand: String,
233    },
234}
235
236impl From<AliasError> for MergeError {
237    fn from(err: AliasError) -> Self {
238        MergeError::Alias(err)
239    }
240}
241
242impl From<MergeError> for ResolveError {
243    fn from(err: MergeError) -> Self {
244        match err {
245            MergeError::Alias(err) => err.into(),
246            MergeError::InvalidOperand { operand } => ResolveError::InvalidMergeOperand { operand },
247        }
248    }
249}
250
251/// A registry of anchors defined in a YAML document
252#[derive(Debug, Clone)]
253pub struct AnchorRegistry {
254    /// Map from anchor name to every definition of it, in document order,
255    /// as (offset of the `&name` token, anchored node)
256    anchors: HashMap<String, Vec<(rowan::TextSize, SyntaxNode)>>,
257}
258
259impl AnchorRegistry {
260    /// Create a new empty anchor registry
261    pub fn new() -> Self {
262        Self {
263            anchors: HashMap::new(),
264        }
265    }
266
267    /// Build a registry from a Document
268    pub fn from_document(doc: &crate::yaml::Document) -> Self {
269        if let Some(node) = doc.as_node() {
270            Self::from_tree(node)
271        } else {
272            Self::new()
273        }
274    }
275
276    /// Build a registry by scanning a syntax tree
277    pub fn from_tree(root: &SyntaxNode) -> Self {
278        let mut registry = Self::new();
279        registry.collect_anchors_from_tree(root);
280        registry
281    }
282
283    /// Recursively collect all anchors from a syntax tree
284    fn collect_anchors_from_tree(&mut self, node: &SyntaxNode) {
285        // Look for ANCHOR tokens in this node
286        for child in node.children_with_tokens() {
287            if let Some(token) = child.as_token() {
288                if token.kind() == SyntaxKind::ANCHOR {
289                    // Extract anchor name (remove '&' prefix)
290                    let text = token.text();
291                    if let Some(name) = text.strip_prefix('&') {
292                        // Find the associated value node
293                        if let Some(value_node) = self.find_anchored_value(node) {
294                            self.anchors
295                                .entry(name.to_string())
296                                .or_default()
297                                .push((token.text_range().start(), value_node));
298                        }
299                    }
300                }
301            } else if let Some(child_node) = child.as_node() {
302                // Recurse into child nodes
303                self.collect_anchors_from_tree(child_node);
304            }
305        }
306    }
307
308    /// Find the value node that an anchor refers to (low-level helper)
309    fn find_anchored_value(&self, node: &SyntaxNode) -> Option<SyntaxNode> {
310        // Node properties come in either order. In `!!map &x` the anchor
311        // sits inside the TAGGED_NODE and anchors it, tag included.
312        if node.kind() == SyntaxKind::TAGGED_NODE {
313            return Some(node.clone());
314        }
315        // The anchored value is typically a sibling MAPPING/SEQUENCE/SCALAR node
316        for child in node.children() {
317            if matches!(
318                child.kind(),
319                SyntaxKind::VALUE
320                    | SyntaxKind::SCALAR
321                    | SyntaxKind::MAPPING
322                    | SyntaxKind::SEQUENCE
323                    | SyntaxKind::TAGGED_NODE
324                    | SyntaxKind::ALIAS
325            ) {
326                return Some(child);
327            }
328        }
329        None
330    }
331
332    /// Look up an anchor by name
333    ///
334    /// If the anchor is defined more than once, this returns the last
335    /// definition in the document.
336    pub fn resolve(&self, name: &str) -> Option<&SyntaxNode> {
337        self.anchors.get(name)?.last().map(|(_, node)| node)
338    }
339
340    /// Look up the node that an alias at `alias` refers to.
341    ///
342    /// An alias refers to the most recent definition of its anchor that
343    /// precedes it in the document, so a redefined anchor only affects the
344    /// aliases that follow the redefinition. `alias` must belong to the same
345    /// tree this registry was built from.
346    pub(crate) fn resolve_alias_at(&self, name: &str, alias: &SyntaxNode) -> Option<&SyntaxNode> {
347        let position = alias.text_range().start();
348        self.anchors
349            .get(name)?
350            .iter()
351            .rev()
352            .find(|(anchor, _)| *anchor < position)
353            .map(|(_, node)| node)
354    }
355
356    /// Look up the node that `alias` refers to like
357    /// [`resolve_alias_at`](Self::resolve_alias_at), following anchors that are
358    /// on aliases themselves, as in `&b *a`, to the node they end at.
359    pub(crate) fn resolve_alias_through(
360        &self,
361        alias: &crate::yaml::Alias,
362    ) -> Result<&SyntaxNode, AliasError> {
363        let mut alias = alias.clone();
364        let mut seen = Vec::new();
365        loop {
366            let name = alias.name();
367            let target = self
368                .resolve_alias_at(&name, alias.syntax())
369                .ok_or_else(|| AliasError::Undefined {
370                    alias: name.clone(),
371                })?;
372            let content = if target.kind() == SyntaxKind::VALUE {
373                target.children().next()
374            } else {
375                Some(target.clone())
376            };
377            let Some(next) = content.and_then(crate::yaml::Alias::cast) else {
378                return Ok(target);
379            };
380            if seen.contains(target) {
381                return Err(AliasError::Recursive { alias: name });
382            }
383            seen.push(target.clone());
384            alias = next;
385        }
386    }
387
388    /// The node `alias` refers to: the last definition of its anchor before
389    /// the alias, following an anchored alias such as `&b *a` to the node it
390    /// ends at.
391    ///
392    /// Unlike [`resolve`](Self::resolve), which looks an anchor up by name
393    /// and returns its last definition anywhere, this sees an anchor
394    /// redefined later in the document only from the aliases after the
395    /// redefinition. `alias` must belong to the tree this registry was built
396    /// from.
397    ///
398    /// Fails if the anchor is not defined before the alias, or if a chain of
399    /// anchored aliases loops back on itself.
400    ///
401    /// ```
402    /// use std::str::FromStr;
403    /// use yaml_edit::{AnchorRegistry, Document};
404    ///
405    /// let doc = Document::from_str("- &x 1\n- *x\n- &x 2\n- *x\n").unwrap();
406    /// let registry = AnchorRegistry::from_document(&doc);
407    /// let seq = doc.as_sequence().unwrap();
408    /// let resolve = |i| {
409    ///     let alias = seq.get(i).unwrap().as_alias().unwrap().clone();
410    ///     registry.resolve_alias(&alias).unwrap().to_i64()
411    /// };
412    /// assert_eq!(resolve(1), Some(1));
413    /// assert_eq!(resolve(3), Some(2));
414    /// ```
415    pub fn resolve_alias(
416        &self,
417        alias: &crate::yaml::Alias,
418    ) -> Result<crate::as_yaml::YamlNode, ResolveError> {
419        Ok(resolve_alias_node(
420            crate::as_yaml::YamlNode::Alias(alias.clone()),
421            self,
422        )?)
423    }
424
425    /// Check if an anchor is defined
426    pub fn contains(&self, name: &str) -> bool {
427        self.anchors.contains_key(name)
428    }
429
430    /// Get all anchor names
431    pub fn anchor_names(&self) -> impl Iterator<Item = &str> {
432        self.anchors.keys().map(|s| s.as_str())
433    }
434}
435
436impl Default for AnchorRegistry {
437    fn default() -> Self {
438        Self::new()
439    }
440}
441
442/// Extension trait for Document to support resolved lookups
443pub trait DocumentResolvedExt {
444    /// Get a value with anchor/alias resolution
445    ///
446    /// This method resolves aliases (*alias) to their anchored values (&anchor),
447    /// and supports merge keys (<<) for combining mappings.
448    ///
449    /// Returns `Ok(None)` if the document root is not a mapping or has no
450    /// such key, and an error if the value contains an undefined or
451    /// recursive alias, an invalid merge key, a mapping with the same key
452    /// twice, or distinct keys that have the same string form, such as `1`
453    /// and `'1'`.
454    ///
455    /// Resolution is read-only. To write a merge or alias back, pass a mapping
456    /// that already contains `<<: *name` (or an alias node) to
457    /// [`Mapping::set`](crate::Mapping::set). `set` does not infer a merge
458    /// from a resolved object.
459    ///
460    /// # Examples
461    ///
462    /// ```
463    /// use yaml_edit::Document;
464    /// use yaml_edit::anchor_resolution::DocumentResolvedExt;
465    /// use std::str::FromStr;
466    ///
467    /// let yaml = r#"
468    /// config: &cfg
469    ///   port: 8080
470    /// server: *cfg
471    /// "#;
472    /// let doc = Document::from_str(yaml).unwrap();
473    ///
474    /// // Get with resolution - expands the *cfg alias
475    /// if let Some(resolved_value) = doc.get_resolved("server").unwrap() {
476    ///     if let Some(server) = resolved_value.as_mapping() {
477    ///         if let Some(port) = server.get("port") {
478    ///             assert_eq!(port.to_i64(), Some(8080));
479    ///         }
480    ///     }
481    /// }
482    /// ```
483    fn get_resolved(&self, key: impl crate::AsYaml) -> Result<Option<YamlValue>, ResolveError>;
484
485    /// Build the anchor registry for this document
486    fn build_anchor_registry(&self) -> AnchorRegistry;
487}
488
489impl DocumentResolvedExt for crate::yaml::Document {
490    fn get_resolved(&self, key: impl crate::AsYaml) -> Result<Option<YamlValue>, ResolveError> {
491        let Some(node) = self.as_mapping().and_then(|m| m.get(&key)) else {
492            return Ok(None);
493        };
494        let registry = self.build_anchor_registry();
495        crate::value::convert::resolved_value(node, Some(&registry), &mut Vec::new())
496    }
497
498    fn build_anchor_registry(&self) -> AnchorRegistry {
499        AnchorRegistry::from_document(self)
500    }
501}
502
503/// Whether `key` is the merge key: a plain `<<`, or `<<` with the merge tag
504/// written out, as in `!!merge <<`. A quoted `'<<'` is an ordinary string.
505pub(crate) fn is_merge_key(key: &crate::as_yaml::YamlNode) -> bool {
506    use crate::as_yaml::YamlNode;
507    match key {
508        YamlNode::Scalar(s) => s.value() == "<<",
509        YamlNode::TaggedNode(t) => {
510            t.resolved_tag().as_deref() == Some("!!merge")
511                && t.value().is_some_and(|s| s.value() == "<<")
512        }
513        _ => false,
514    }
515}
516
517/// A view over a [`Mapping`](crate::yaml::Mapping) that transparently expands
518/// alias references (`*name`) and merge keys (`<<:`).
519///
520/// `MergedMapping` borrows an [`AnchorRegistry`] and collects its entries
521/// when it is created: the merge sources are followed and alias values are
522/// resolved then, so an undefined alias or invalid merge key is reported
523/// by [`new`](Self::new). The underlying syntax tree is not modified and
524/// the returned nodes are live CST nodes, but entries added, removed or
525/// replaced after the view was created are not seen.
526///
527/// Direct keys in the base mapping always shadow keys contributed by merge
528/// sources, matching the YAML 1.1 merge-key semantics. When a merge value is
529/// a sequence of aliases (`<<: [*a, *b]`), earlier aliases take precedence
530/// over later ones, again per the YAML 1.1 spec.
531///
532/// Returned values are [`YamlNode`](crate::as_yaml::YamlNode)s backed by real
533/// CST nodes, so they preserve the original formatting and quoting style.
534///
535/// This view is read-only. [`Mapping::set`](crate::Mapping::set) replaces the
536/// value node you pass; it does not turn a resolved map back into `<<: *name`.
537///
538/// # Examples
539///
540/// ```
541/// use yaml_edit::{Document, anchor_resolution::{DocumentResolvedExt, MappingMergedExt}};
542/// use std::str::FromStr;
543///
544/// let yaml = r#"
545/// defaults: &defaults
546///   timeout: 30
547///   retries: 3
548///
549/// production:
550///   <<: *defaults
551///   host: prod.example.com
552///   timeout: 60
553/// "#;
554///
555/// let doc = Document::from_str(yaml).unwrap();
556/// let root = doc.as_mapping().unwrap();
557/// let registry = doc.build_anchor_registry();
558///
559/// let prod = root.get_mapping("production").unwrap();
560/// let merged = prod.merged(&registry).unwrap();
561///
562/// // Direct key wins over merged value.
563/// assert_eq!(merged.get("timeout").unwrap().to_i64(), Some(60));
564/// // Merged key from defaults is visible.
565/// assert_eq!(merged.get("retries").unwrap().to_i64(), Some(3));
566/// // Direct-only key is visible.
567/// assert!(merged.get("host").is_some());
568/// // `<<` itself is hidden.
569/// assert!(merged.get("<<").is_none());
570/// ```
571#[derive(Clone)]
572pub struct MergedMapping<'a> {
573    base: crate::yaml::Mapping,
574    registry: &'a AnchorRegistry,
575    entries: Vec<MergedEntry>,
576}
577
578impl<'a> MergedMapping<'a> {
579    /// Create a new merged view over `base`, resolving aliases against `registry`.
580    ///
581    /// Fails if a `<<` operand in `base`, or in a mapping merged into it,
582    /// is an undefined alias or not a mapping, if a value visible
583    /// through the view is an undefined alias, or if a key is or holds an
584    /// undefined or recursive alias.
585    pub fn new(
586        base: crate::yaml::Mapping,
587        registry: &'a AnchorRegistry,
588    ) -> Result<Self, ResolveError> {
589        Ok(Self::build(base, registry)?)
590    }
591
592    /// Like [`new`](Self::new), failing with the errors only a merged view
593    /// can meet.
594    pub(crate) fn build(
595        base: crate::yaml::Mapping,
596        registry: &'a AnchorRegistry,
597    ) -> Result<Self, MergeError> {
598        let entries = merged_entries(&base, registry)?;
599        Ok(Self {
600            base,
601            registry,
602            entries,
603        })
604    }
605
606    /// Return the underlying (un-merged) [`Mapping`](crate::yaml::Mapping).
607    ///
608    /// Useful when you want to mutate the original mapping or read its raw
609    /// (non-resolved) contents.
610    pub fn base(&self) -> &crate::yaml::Mapping {
611        &self.base
612    }
613
614    /// Return the [`AnchorRegistry`] this view resolves against.
615    pub fn registry(&self) -> &AnchorRegistry {
616        self.registry
617    }
618
619    /// Get the value associated with `key`, resolving aliases and merge keys.
620    ///
621    /// Direct entries in the base mapping take precedence over keys
622    /// contributed by `<<:` merge sources. The merge key itself is hidden:
623    /// looking up `"<<"` only finds a quoted `'<<'` key, which is an ordinary
624    /// string.
625    ///
626    /// If the matched value is an alias (`*name`), the resolved target node
627    /// is returned.
628    pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
629        // A key that cannot be resolved, such as an undefined alias, is in
630        // no mapping.
631        let identity = crate::as_yaml::identity_of(&key, Some(self.registry))
632            .ok()
633            .flatten()?;
634        self.entries
635            .iter()
636            .find(|(k, _, _)| *k == identity)
637            .map(|(_, _, v)| v.clone())
638    }
639
640    /// Returns `true` if a value would be returned by [`get`](Self::get) for
641    /// `key`.
642    pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
643        self.get(key).is_some()
644    }
645
646    /// Iterate over `(key, value)` pairs in the merged view.
647    ///
648    /// Direct entries appear first, in their original document order.
649    /// Merged-in entries follow, with duplicates (and any direct-key matches
650    /// already yielded) filtered out. Keys are compared as YAML values, so
651    /// the integer `1` and the string `'1'` are distinct. The `<<` merge key
652    /// itself is never yielded.
653    pub fn iter(
654        &self,
655    ) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
656        self.entries.iter().map(|(_, k, v)| (k.clone(), v.clone()))
657    }
658
659    /// Iterate over the keys of the merged view.
660    pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
661        self.iter().map(|(k, _)| k)
662    }
663
664    /// Iterate over the values of the merged view.
665    pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
666        self.iter().map(|(_, v)| v)
667    }
668
669    /// Number of distinct keys visible through the merged view.
670    pub fn len(&self) -> usize {
671        self.iter().count()
672    }
673
674    /// Returns `true` if the merged view has no entries.
675    pub fn is_empty(&self) -> bool {
676        self.iter().next().is_none()
677    }
678
679    /// Get a nested mapping by `key` and return it as another `MergedMapping`
680    /// that shares the same registry.
681    ///
682    /// Returns `Ok(None)` if the key does not exist or the value is not a
683    /// mapping, and fails as [`new`](Self::new) does on the nested mapping.
684    pub fn get_merged(
685        &self,
686        key: impl crate::AsYaml,
687    ) -> Result<Option<MergedMapping<'a>>, ResolveError> {
688        let Some(mapping) = self.get(key).and_then(|node| node.as_mapping().cloned()) else {
689            return Ok(None);
690        };
691        MergedMapping::new(mapping, self.registry).map(Some)
692    }
693}
694
695impl std::fmt::Debug for MergedMapping<'_> {
696    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697        f.debug_struct("MergedMapping")
698            .field("len", &self.len())
699            .finish()
700    }
701}
702
703/// Walk the `<<:` entries of `base` and return the resolved source mappings,
704/// in precedence order (highest first).
705///
706/// Each source is followed by the sources it merges in itself, since its own
707/// keys shadow those it inherits. A mapping is only collected once, which
708/// also stops a merge that refers back to an enclosing anchor.
709pub(crate) fn merge_sources(
710    base: &crate::yaml::Mapping,
711    registry: &AnchorRegistry,
712) -> Result<Vec<crate::yaml::Mapping>, MergeError> {
713    let mut sources = Vec::new();
714    collect_merge_sources(base, registry, &mut sources)?;
715    Ok(sources)
716}
717
718/// The operands of a `<<` entry with value `value`: the items of the
719/// sequence it is, or else the value itself.
720pub(crate) fn merge_operands(
721    value: crate::as_yaml::YamlNode,
722    registry: &AnchorRegistry,
723) -> Result<Vec<crate::as_yaml::YamlNode>, AliasError> {
724    use crate::as_yaml::YamlNode;
725
726    // An alias stands for its node, so it can name a sequence of mappings
727    // too.
728    let sequence = match resolve_alias_node(value.clone(), registry)? {
729        YamlNode::Sequence(seq) => Some(seq),
730        YamlNode::TaggedNode(tagged) => tagged.as_sequence(),
731        _ => None,
732    };
733    Ok(match sequence {
734        Some(seq) => seq.values().collect(),
735        None => vec![value],
736    })
737}
738
739/// The mapping `operand`, an operand of a `<<` entry, merges in, or `None`
740/// if it is not a mapping.
741pub(crate) fn merge_source(
742    operand: &crate::as_yaml::YamlNode,
743    registry: &AnchorRegistry,
744) -> Result<Option<crate::yaml::Mapping>, AliasError> {
745    use crate::as_yaml::YamlNode;
746
747    Ok(match resolve_alias_node(operand.clone(), registry)? {
748        YamlNode::Mapping(m) => Some(m),
749        YamlNode::TaggedNode(tagged) => tagged.as_mapping(),
750        _ => None,
751    })
752}
753
754fn collect_merge_sources(
755    base: &crate::yaml::Mapping,
756    registry: &AnchorRegistry,
757    sources: &mut Vec<crate::yaml::Mapping>,
758) -> Result<(), MergeError> {
759    for (key, value) in base.iter() {
760        if !is_merge_key(&key) {
761            continue;
762        }
763        for operand in merge_operands(value, registry)? {
764            let source =
765                merge_source(&operand, registry)?.ok_or_else(|| MergeError::InvalidOperand {
766                    operand: operand.to_string(),
767                })?;
768            if sources.iter().any(|s| s.syntax() == source.syntax()) {
769                continue;
770            }
771            sources.push(source.clone());
772            collect_merge_sources(&source, registry, sources)?;
773        }
774    }
775    Ok(())
776}
777
778/// The entries visible through a merged view of `base`: its own entries,
779/// then those of its merge sources that are not shadowed, with alias
780/// values resolved.
781fn merged_entries(
782    base: &crate::yaml::Mapping,
783    registry: &AnchorRegistry,
784) -> Result<Vec<MergedEntry>, MergeError> {
785    let mut entries: Vec<MergedEntry> = Vec::new();
786    for (key, value) in base.iter().filter(|(k, _)| !is_merge_key(k)) {
787        let identity = key_identity(&key, Some(registry))?;
788        entries.push((identity, key, resolve_alias_node(value, registry)?));
789    }
790    for source in merge_sources(base, registry)? {
791        for (key, value) in source.iter() {
792            if is_merge_key(&key) {
793                continue;
794            }
795            let identity = key_identity(&key, Some(registry))?;
796            if entries.iter().any(|(k, _, _)| *k == identity) {
797                continue;
798            }
799            entries.push((identity, key, resolve_alias_node(value, registry)?));
800        }
801    }
802    Ok(entries)
803}
804
805/// An entry of a merged view: the key's identity, the key as written and
806/// the value, with an alias resolved.
807type MergedEntry = (
808    NodeIdentity,
809    crate::as_yaml::YamlNode,
810    crate::as_yaml::YamlNode,
811);
812
813/// The document `node` is in, or the root of its tree outside one.
814/// Anchors are scoped to a single document.
815pub(crate) fn document_scope(node: &SyntaxNode) -> SyntaxNode {
816    node.ancestors()
817        .find(|n| n.kind() == SyntaxKind::DOCUMENT)
818        .unwrap_or_else(|| node.ancestors().last().expect("ancestors has self"))
819}
820
821/// If `node` is an alias, resolve it via `registry`; otherwise return it as-is.
822pub(crate) fn resolve_alias_node(
823    node: crate::as_yaml::YamlNode,
824    registry: &AnchorRegistry,
825) -> Result<crate::as_yaml::YamlNode, AliasError> {
826    use crate::as_yaml::YamlNode;
827
828    let YamlNode::Alias(alias) = &node else {
829        return Ok(node);
830    };
831    resolve_alias_with(alias, registry, SyntaxNode::clone)
832}
833
834/// Resolve `alias` via `registry` to the node built from `adjust` applied
835/// to the anchored node.
836pub(crate) fn resolve_alias_with(
837    alias: &crate::yaml::Alias,
838    registry: &AnchorRegistry,
839    adjust: impl FnOnce(&SyntaxNode) -> SyntaxNode,
840) -> Result<crate::as_yaml::YamlNode, AliasError> {
841    let target = adjust(registry.resolve_alias_through(alias)?);
842    crate::as_yaml::YamlNode::from_syntax_peeled(target).ok_or(AliasError::Undefined {
843        alias: alias.name(),
844    })
845}
846
847/// Extension trait that adds [`merged`](Self::merged) to [`Mapping`](crate::yaml::Mapping).
848pub trait MappingMergedExt {
849    /// Return a [`MergedMapping`] view over this mapping that resolves
850    /// aliases and merge keys against `registry`.
851    ///
852    /// The view is read-only and does not modify the underlying CST.
853    ///
854    /// Fails as [`MergedMapping::new`] does.
855    fn merged<'a>(&self, registry: &'a AnchorRegistry) -> Result<MergedMapping<'a>, ResolveError>;
856}
857
858impl MappingMergedExt for crate::yaml::Mapping {
859    fn merged<'a>(&self, registry: &'a AnchorRegistry) -> Result<MergedMapping<'a>, ResolveError> {
860        MergedMapping::new(self.clone(), registry)
861    }
862}
863
864/// Extension trait that adds [`merged`](Self::merged) to [`Document`](crate::yaml::Document).
865pub trait DocumentMergedExt {
866    /// Return a [`MergedMapping`] view over the document's root mapping.
867    ///
868    /// The anchor registry is built from this document automatically.
869    /// Returns `Ok(None)` if the document's root is not a mapping, and fails
870    /// as [`MergedMapping::new`] does.
871    ///
872    /// Because the registry is owned by the returned view, this method
873    /// returns an owned [`MergedView`] container rather than a borrowing
874    /// `MergedMapping<'_>`. Use [`MergedView::as_mapping`] to obtain the
875    /// borrowing view for lookups.
876    fn merged(&self) -> Result<Option<MergedView>, ResolveError>;
877}
878
879impl DocumentMergedExt for crate::yaml::Document {
880    fn merged(&self) -> Result<Option<MergedView>, ResolveError> {
881        let Some(mapping) = self.as_mapping() else {
882            return Ok(None);
883        };
884        let registry = self.build_anchor_registry();
885        let entries = merged_entries(&mapping, &registry).map_err(ResolveError::from)?;
886        Ok(Some(MergedView {
887            mapping,
888            registry,
889            entries,
890        }))
891    }
892}
893
894/// An owning container for a [`MergedMapping`] view of a document's root.
895///
896/// Holds both the root [`Mapping`](crate::yaml::Mapping) and the
897/// [`AnchorRegistry`] built from the document, so the view can be returned
898/// from a function without lifetime gymnastics. Call
899/// [`as_mapping`](Self::as_mapping) (or use the convenience methods
900/// directly) to query the view.
901pub struct MergedView {
902    mapping: crate::yaml::Mapping,
903    registry: AnchorRegistry,
904    entries: Vec<MergedEntry>,
905}
906
907impl MergedView {
908    /// Borrow this view as a [`MergedMapping`] for lookups.
909    pub fn as_mapping(&self) -> MergedMapping<'_> {
910        MergedMapping {
911            base: self.mapping.clone(),
912            registry: &self.registry,
913            entries: self.entries.clone(),
914        }
915    }
916
917    /// Get a value through the merged view.
918    pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
919        self.as_mapping().get(key)
920    }
921
922    /// Check whether `key` is reachable through the merged view.
923    pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
924        self.as_mapping().contains_key(key)
925    }
926
927    /// Return the registry built from the document.
928    pub fn registry(&self) -> &AnchorRegistry {
929        &self.registry
930    }
931}
932
933impl std::fmt::Debug for MergedView {
934    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
935        f.debug_struct("MergedView").finish()
936    }
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942    use crate::Document;
943    use std::str::FromStr;
944
945    fn doc(text: &str) -> Document {
946        Document::from_str(text).expect("parse")
947    }
948
949    #[test]
950    fn get_resolved_tagged_alias() {
951        // The alias's own tag applies to what it refers to, as when an
952        // edit replaces it with a copy.
953        for (yaml, expected) in [
954            ("x: &x 1\na: !t *x\n", "a: !t 1\n"),
955            ("x: &x '1'\na: !!int *x\n", "a: !!int '1'\n"),
956            ("x: &x [1]\na: !t *x\n", "a: !t [1]\n"),
957            ("x: &x {k: 1}\na: !t *x\n", "a: !t {k: 1}\n"),
958            ("x: &x {k: }\na: !!set *x\n", "a: !!set {k: }\n"),
959            ("x: &x [{k: 1}]\na: !!omap *x\n", "a: !!omap [{k: 1}]\n"),
960            ("y: &y 2\nx: &x [*y]\na: !t *x\n", "a: !t [2]\n"),
961            ("x: &x !u 1\na: !t *x\n", "a: !t 1\n"),
962            ("x: !u &x [1]\na: !t *x\n", "a: !t [1]\n"),
963            ("x: &x !u {k: }\na: !!set *x\n", "a: !!set {k: }\n"),
964        ] {
965            assert_eq!(
966                doc(yaml).get_resolved("a"),
967                doc(expected).get_resolved("a"),
968                "{yaml:?}"
969            );
970        }
971        assert_eq!(
972            doc("x: &x 1\na: !t *x\n").get_resolved("a"),
973            Ok(Some(YamlValue::Scalar(
974                crate::ScalarValue::parse("1").with_resolved_tag("!t".to_string())
975            )))
976        );
977    }
978}