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(&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`](Self::resolve_alias), 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 =
368                self.resolve_alias(&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    /// Check if an anchor is defined
389    pub fn contains(&self, name: &str) -> bool {
390        self.anchors.contains_key(name)
391    }
392
393    /// Get all anchor names
394    pub fn anchor_names(&self) -> impl Iterator<Item = &str> {
395        self.anchors.keys().map(|s| s.as_str())
396    }
397}
398
399impl Default for AnchorRegistry {
400    fn default() -> Self {
401        Self::new()
402    }
403}
404
405/// Extension trait for Document to support resolved lookups
406pub trait DocumentResolvedExt {
407    /// Get a value with anchor/alias resolution
408    ///
409    /// This method resolves aliases (*alias) to their anchored values (&anchor),
410    /// and supports merge keys (<<) for combining mappings.
411    ///
412    /// Returns `Ok(None)` if the document root is not a mapping or has no
413    /// such key, and an error if the value contains an undefined or
414    /// recursive alias, an invalid merge key, a mapping with the same key
415    /// twice, or distinct keys that have the same string form, such as `1`
416    /// and `'1'`.
417    ///
418    /// Resolution is read-only. To write a merge or alias back, pass a mapping
419    /// that already contains `<<: *name` (or an alias node) to
420    /// [`Mapping::set`](crate::Mapping::set). `set` does not infer a merge
421    /// from a resolved object.
422    ///
423    /// # Examples
424    ///
425    /// ```
426    /// use yaml_edit::Document;
427    /// use yaml_edit::anchor_resolution::DocumentResolvedExt;
428    /// use std::str::FromStr;
429    ///
430    /// let yaml = r#"
431    /// config: &cfg
432    ///   port: 8080
433    /// server: *cfg
434    /// "#;
435    /// let doc = Document::from_str(yaml).unwrap();
436    ///
437    /// // Get with resolution - expands the *cfg alias
438    /// if let Some(resolved_value) = doc.get_resolved("server").unwrap() {
439    ///     if let Some(server) = resolved_value.as_mapping() {
440    ///         if let Some(port) = server.get("port") {
441    ///             assert_eq!(port.to_i64(), Some(8080));
442    ///         }
443    ///     }
444    /// }
445    /// ```
446    fn get_resolved(&self, key: impl crate::AsYaml) -> Result<Option<YamlValue>, ResolveError>;
447
448    /// Build the anchor registry for this document
449    fn build_anchor_registry(&self) -> AnchorRegistry;
450}
451
452impl DocumentResolvedExt for crate::yaml::Document {
453    fn get_resolved(&self, key: impl crate::AsYaml) -> Result<Option<YamlValue>, ResolveError> {
454        let Some(node) = self.as_mapping().and_then(|m| m.get(&key)) else {
455            return Ok(None);
456        };
457        let registry = self.build_anchor_registry();
458        crate::value::convert::resolved_value(node, Some(&registry), &mut Vec::new())
459    }
460
461    fn build_anchor_registry(&self) -> AnchorRegistry {
462        AnchorRegistry::from_document(self)
463    }
464}
465
466/// Whether `key` is the merge key. Only a plain `<<` is one; a quoted
467/// `'<<'` is an ordinary string.
468pub(crate) fn is_merge_key(key: &crate::as_yaml::YamlNode) -> bool {
469    key.as_scalar().is_some_and(|s| s.value() == "<<")
470}
471
472/// A view over a [`Mapping`](crate::yaml::Mapping) that transparently expands
473/// alias references (`*name`) and merge keys (`<<:`).
474///
475/// `MergedMapping` borrows an [`AnchorRegistry`] and collects its entries
476/// when it is created: the merge sources are followed and alias values are
477/// resolved then, so an undefined alias or invalid merge key is reported
478/// by [`new`](Self::new). The underlying syntax tree is not modified and
479/// the returned nodes are live CST nodes, but entries added, removed or
480/// replaced after the view was created are not seen.
481///
482/// Direct keys in the base mapping always shadow keys contributed by merge
483/// sources, matching the YAML 1.1 merge-key semantics. When a merge value is
484/// a sequence of aliases (`<<: [*a, *b]`), earlier aliases take precedence
485/// over later ones, again per the YAML 1.1 spec.
486///
487/// Returned values are [`YamlNode`](crate::as_yaml::YamlNode)s backed by real
488/// CST nodes, so they preserve the original formatting and quoting style.
489///
490/// This view is read-only. [`Mapping::set`](crate::Mapping::set) replaces the
491/// value node you pass; it does not turn a resolved map back into `<<: *name`.
492///
493/// # Examples
494///
495/// ```
496/// use yaml_edit::{Document, anchor_resolution::{DocumentResolvedExt, MappingMergedExt}};
497/// use std::str::FromStr;
498///
499/// let yaml = r#"
500/// defaults: &defaults
501///   timeout: 30
502///   retries: 3
503///
504/// production:
505///   <<: *defaults
506///   host: prod.example.com
507///   timeout: 60
508/// "#;
509///
510/// let doc = Document::from_str(yaml).unwrap();
511/// let root = doc.as_mapping().unwrap();
512/// let registry = doc.build_anchor_registry();
513///
514/// let prod = root.get_mapping("production").unwrap();
515/// let merged = prod.merged(&registry).unwrap();
516///
517/// // Direct key wins over merged value.
518/// assert_eq!(merged.get("timeout").unwrap().to_i64(), Some(60));
519/// // Merged key from defaults is visible.
520/// assert_eq!(merged.get("retries").unwrap().to_i64(), Some(3));
521/// // Direct-only key is visible.
522/// assert!(merged.get("host").is_some());
523/// // `<<` itself is hidden.
524/// assert!(merged.get("<<").is_none());
525/// ```
526#[derive(Clone)]
527pub struct MergedMapping<'a> {
528    base: crate::yaml::Mapping,
529    registry: &'a AnchorRegistry,
530    entries: Vec<MergedEntry>,
531}
532
533impl<'a> MergedMapping<'a> {
534    /// Create a new merged view over `base`, resolving aliases against `registry`.
535    ///
536    /// Fails if a `<<` operand in `base`, or in a mapping merged into it,
537    /// is an undefined alias or not a mapping, if a value visible
538    /// through the view is an undefined alias, or if a key is or holds an
539    /// undefined or recursive alias.
540    pub fn new(
541        base: crate::yaml::Mapping,
542        registry: &'a AnchorRegistry,
543    ) -> Result<Self, ResolveError> {
544        Ok(Self::build(base, registry)?)
545    }
546
547    /// Like [`new`](Self::new), failing with the errors only a merged view
548    /// can meet.
549    pub(crate) fn build(
550        base: crate::yaml::Mapping,
551        registry: &'a AnchorRegistry,
552    ) -> Result<Self, MergeError> {
553        let entries = merged_entries(&base, registry)?;
554        Ok(Self {
555            base,
556            registry,
557            entries,
558        })
559    }
560
561    /// Return the underlying (un-merged) [`Mapping`](crate::yaml::Mapping).
562    ///
563    /// Useful when you want to mutate the original mapping or read its raw
564    /// (non-resolved) contents.
565    pub fn base(&self) -> &crate::yaml::Mapping {
566        &self.base
567    }
568
569    /// Return the [`AnchorRegistry`] this view resolves against.
570    pub fn registry(&self) -> &AnchorRegistry {
571        self.registry
572    }
573
574    /// Get the value associated with `key`, resolving aliases and merge keys.
575    ///
576    /// Direct entries in the base mapping take precedence over keys
577    /// contributed by `<<:` merge sources. The merge key itself is hidden:
578    /// looking up `"<<"` only finds a quoted `'<<'` key, which is an ordinary
579    /// string.
580    ///
581    /// If the matched value is an alias (`*name`), the resolved target node
582    /// is returned.
583    pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
584        // A key that cannot be resolved, such as an undefined alias, is in
585        // no mapping.
586        let identity = crate::as_yaml::identity_of(&key, Some(self.registry))
587            .ok()
588            .flatten()?;
589        self.entries
590            .iter()
591            .find(|(k, _, _)| *k == identity)
592            .map(|(_, _, v)| v.clone())
593    }
594
595    /// Returns `true` if a value would be returned by [`get`](Self::get) for
596    /// `key`.
597    pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
598        self.get(key).is_some()
599    }
600
601    /// Iterate over `(key, value)` pairs in the merged view.
602    ///
603    /// Direct entries appear first, in their original document order.
604    /// Merged-in entries follow, with duplicates (and any direct-key matches
605    /// already yielded) filtered out. Keys are compared as YAML values, so
606    /// the integer `1` and the string `'1'` are distinct. The `<<` merge key
607    /// itself is never yielded.
608    pub fn iter(
609        &self,
610    ) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
611        self.entries.iter().map(|(_, k, v)| (k.clone(), v.clone()))
612    }
613
614    /// Iterate over the keys of the merged view.
615    pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
616        self.iter().map(|(k, _)| k)
617    }
618
619    /// Iterate over the values of the merged view.
620    pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
621        self.iter().map(|(_, v)| v)
622    }
623
624    /// Number of distinct keys visible through the merged view.
625    pub fn len(&self) -> usize {
626        self.iter().count()
627    }
628
629    /// Returns `true` if the merged view has no entries.
630    pub fn is_empty(&self) -> bool {
631        self.iter().next().is_none()
632    }
633
634    /// Get a nested mapping by `key` and return it as another `MergedMapping`
635    /// that shares the same registry.
636    ///
637    /// Returns `Ok(None)` if the key does not exist or the value is not a
638    /// mapping, and fails as [`new`](Self::new) does on the nested mapping.
639    pub fn get_merged(
640        &self,
641        key: impl crate::AsYaml,
642    ) -> Result<Option<MergedMapping<'a>>, ResolveError> {
643        let Some(mapping) = self.get(key).and_then(|node| node.as_mapping().cloned()) else {
644            return Ok(None);
645        };
646        MergedMapping::new(mapping, self.registry).map(Some)
647    }
648}
649
650impl std::fmt::Debug for MergedMapping<'_> {
651    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
652        f.debug_struct("MergedMapping")
653            .field("len", &self.len())
654            .finish()
655    }
656}
657
658/// Walk the `<<:` entries of `base` and return the resolved source mappings,
659/// in precedence order (highest first).
660///
661/// Each source is followed by the sources it merges in itself, since its own
662/// keys shadow those it inherits. A mapping is only collected once, which
663/// also stops a merge that refers back to an enclosing anchor.
664pub(crate) fn merge_sources(
665    base: &crate::yaml::Mapping,
666    registry: &AnchorRegistry,
667) -> Result<Vec<crate::yaml::Mapping>, MergeError> {
668    let mut sources = Vec::new();
669    collect_merge_sources(base, registry, &mut sources)?;
670    Ok(sources)
671}
672
673/// The operands of a `<<` entry with value `value`: the items of the
674/// sequence it is, or else the value itself.
675pub(crate) fn merge_operands(
676    value: crate::as_yaml::YamlNode,
677    registry: &AnchorRegistry,
678) -> Result<Vec<crate::as_yaml::YamlNode>, AliasError> {
679    use crate::as_yaml::YamlNode;
680
681    // An alias stands for its node, so it can name a sequence of mappings
682    // too.
683    let sequence = match resolve_alias_node(value.clone(), registry)? {
684        YamlNode::Sequence(seq) => Some(seq),
685        YamlNode::TaggedNode(tagged) => tagged.as_sequence(),
686        _ => None,
687    };
688    Ok(match sequence {
689        Some(seq) => seq.values().collect(),
690        None => vec![value],
691    })
692}
693
694/// The mapping `operand`, an operand of a `<<` entry, merges in, or `None`
695/// if it is not a mapping.
696pub(crate) fn merge_source(
697    operand: &crate::as_yaml::YamlNode,
698    registry: &AnchorRegistry,
699) -> Result<Option<crate::yaml::Mapping>, AliasError> {
700    use crate::as_yaml::YamlNode;
701
702    Ok(match resolve_alias_node(operand.clone(), registry)? {
703        YamlNode::Mapping(m) => Some(m),
704        YamlNode::TaggedNode(tagged) => tagged.as_mapping(),
705        _ => None,
706    })
707}
708
709fn collect_merge_sources(
710    base: &crate::yaml::Mapping,
711    registry: &AnchorRegistry,
712    sources: &mut Vec<crate::yaml::Mapping>,
713) -> Result<(), MergeError> {
714    for (key, value) in base.iter() {
715        if !is_merge_key(&key) {
716            continue;
717        }
718        for operand in merge_operands(value, registry)? {
719            let source =
720                merge_source(&operand, registry)?.ok_or_else(|| MergeError::InvalidOperand {
721                    operand: operand.to_string(),
722                })?;
723            if sources.iter().any(|s| s.syntax() == source.syntax()) {
724                continue;
725            }
726            sources.push(source.clone());
727            collect_merge_sources(&source, registry, sources)?;
728        }
729    }
730    Ok(())
731}
732
733/// The entries visible through a merged view of `base`: its own entries,
734/// then those of its merge sources that are not shadowed, with alias
735/// values resolved.
736fn merged_entries(
737    base: &crate::yaml::Mapping,
738    registry: &AnchorRegistry,
739) -> Result<Vec<MergedEntry>, MergeError> {
740    let mut entries: Vec<MergedEntry> = Vec::new();
741    for (key, value) in base.iter().filter(|(k, _)| !is_merge_key(k)) {
742        let identity = key_identity(&key, Some(registry))?;
743        entries.push((identity, key, resolve_alias_node(value, registry)?));
744    }
745    for source in merge_sources(base, registry)? {
746        for (key, value) in source.iter() {
747            if is_merge_key(&key) {
748                continue;
749            }
750            let identity = key_identity(&key, Some(registry))?;
751            if entries.iter().any(|(k, _, _)| *k == identity) {
752                continue;
753            }
754            entries.push((identity, key, resolve_alias_node(value, registry)?));
755        }
756    }
757    Ok(entries)
758}
759
760/// An entry of a merged view: the key's identity, the key as written and
761/// the value, with an alias resolved.
762type MergedEntry = (
763    NodeIdentity,
764    crate::as_yaml::YamlNode,
765    crate::as_yaml::YamlNode,
766);
767
768/// The document `node` is in, or the root of its tree outside one.
769/// Anchors are scoped to a single document.
770pub(crate) fn document_scope(node: &SyntaxNode) -> SyntaxNode {
771    node.ancestors()
772        .find(|n| n.kind() == SyntaxKind::DOCUMENT)
773        .unwrap_or_else(|| node.ancestors().last().expect("ancestors has self"))
774}
775
776/// If `node` is an alias, resolve it via `registry`; otherwise return it as-is.
777pub(crate) fn resolve_alias_node(
778    node: crate::as_yaml::YamlNode,
779    registry: &AnchorRegistry,
780) -> Result<crate::as_yaml::YamlNode, AliasError> {
781    use crate::as_yaml::YamlNode;
782
783    let YamlNode::Alias(alias) = &node else {
784        return Ok(node);
785    };
786    resolve_alias_with(alias, registry, SyntaxNode::clone)
787}
788
789/// Resolve `alias` via `registry` to the node built from `adjust` applied
790/// to the anchored node.
791pub(crate) fn resolve_alias_with(
792    alias: &crate::yaml::Alias,
793    registry: &AnchorRegistry,
794    adjust: impl FnOnce(&SyntaxNode) -> SyntaxNode,
795) -> Result<crate::as_yaml::YamlNode, AliasError> {
796    let target = adjust(registry.resolve_alias_through(alias)?);
797    crate::as_yaml::YamlNode::from_syntax_peeled(target).ok_or(AliasError::Undefined {
798        alias: alias.name(),
799    })
800}
801
802/// Extension trait that adds [`merged`](Self::merged) to [`Mapping`](crate::yaml::Mapping).
803pub trait MappingMergedExt {
804    /// Return a [`MergedMapping`] view over this mapping that resolves
805    /// aliases and merge keys against `registry`.
806    ///
807    /// The view is read-only and does not modify the underlying CST.
808    ///
809    /// Fails as [`MergedMapping::new`] does.
810    fn merged<'a>(&self, registry: &'a AnchorRegistry) -> Result<MergedMapping<'a>, ResolveError>;
811}
812
813impl MappingMergedExt for crate::yaml::Mapping {
814    fn merged<'a>(&self, registry: &'a AnchorRegistry) -> Result<MergedMapping<'a>, ResolveError> {
815        MergedMapping::new(self.clone(), registry)
816    }
817}
818
819/// Extension trait that adds [`merged`](Self::merged) to [`Document`](crate::yaml::Document).
820pub trait DocumentMergedExt {
821    /// Return a [`MergedMapping`] view over the document's root mapping.
822    ///
823    /// The anchor registry is built from this document automatically.
824    /// Returns `Ok(None)` if the document's root is not a mapping, and fails
825    /// as [`MergedMapping::new`] does.
826    ///
827    /// Because the registry is owned by the returned view, this method
828    /// returns an owned [`MergedView`] container rather than a borrowing
829    /// `MergedMapping<'_>`. Use [`MergedView::as_mapping`] to obtain the
830    /// borrowing view for lookups.
831    fn merged(&self) -> Result<Option<MergedView>, ResolveError>;
832}
833
834impl DocumentMergedExt for crate::yaml::Document {
835    fn merged(&self) -> Result<Option<MergedView>, ResolveError> {
836        let Some(mapping) = self.as_mapping() else {
837            return Ok(None);
838        };
839        let registry = self.build_anchor_registry();
840        let entries = merged_entries(&mapping, &registry).map_err(ResolveError::from)?;
841        Ok(Some(MergedView {
842            mapping,
843            registry,
844            entries,
845        }))
846    }
847}
848
849/// An owning container for a [`MergedMapping`] view of a document's root.
850///
851/// Holds both the root [`Mapping`](crate::yaml::Mapping) and the
852/// [`AnchorRegistry`] built from the document, so the view can be returned
853/// from a function without lifetime gymnastics. Call
854/// [`as_mapping`](Self::as_mapping) (or use the convenience methods
855/// directly) to query the view.
856pub struct MergedView {
857    mapping: crate::yaml::Mapping,
858    registry: AnchorRegistry,
859    entries: Vec<MergedEntry>,
860}
861
862impl MergedView {
863    /// Borrow this view as a [`MergedMapping`] for lookups.
864    pub fn as_mapping(&self) -> MergedMapping<'_> {
865        MergedMapping {
866            base: self.mapping.clone(),
867            registry: &self.registry,
868            entries: self.entries.clone(),
869        }
870    }
871
872    /// Get a value through the merged view.
873    pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
874        self.as_mapping().get(key)
875    }
876
877    /// Check whether `key` is reachable through the merged view.
878    pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
879        self.as_mapping().contains_key(key)
880    }
881
882    /// Return the registry built from the document.
883    pub fn registry(&self) -> &AnchorRegistry {
884        &self.registry
885    }
886}
887
888impl std::fmt::Debug for MergedView {
889    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
890        f.debug_struct("MergedView").finish()
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use crate::Document;
898    use std::str::FromStr;
899
900    fn doc(text: &str) -> Document {
901        Document::from_str(text).expect("parse")
902    }
903
904    #[test]
905    fn get_resolved_tagged_alias() {
906        // The alias's own tag applies to what it refers to, as when an
907        // edit replaces it with a copy.
908        for (yaml, expected) in [
909            ("x: &x 1\na: !t *x\n", "a: !t 1\n"),
910            ("x: &x '1'\na: !!int *x\n", "a: !!int '1'\n"),
911            ("x: &x [1]\na: !t *x\n", "a: !t [1]\n"),
912            ("x: &x {k: 1}\na: !t *x\n", "a: !t {k: 1}\n"),
913            ("x: &x {k: }\na: !!set *x\n", "a: !!set {k: }\n"),
914            ("x: &x [{k: 1}]\na: !!omap *x\n", "a: !!omap [{k: 1}]\n"),
915            ("y: &y 2\nx: &x [*y]\na: !t *x\n", "a: !t [2]\n"),
916            ("x: &x !u 1\na: !t *x\n", "a: !t 1\n"),
917            ("x: !u &x [1]\na: !t *x\n", "a: !t [1]\n"),
918            ("x: &x !u {k: }\na: !!set *x\n", "a: !!set {k: }\n"),
919        ] {
920            assert_eq!(
921                doc(yaml).get_resolved("a"),
922                doc(expected).get_resolved("a"),
923                "{yaml:?}"
924            );
925        }
926        assert_eq!(
927            doc("x: &x 1\na: !t *x\n").get_resolved("a"),
928            Ok(Some(YamlValue::Scalar(
929                crate::ScalarValue::parse("1").with_resolved_tag("!t".to_string())
930            )))
931        );
932    }
933}