// Copyright 2025 The go-yaml Project Contributors // SPDX-License-Identifier: Apache-2.0 // Desolver stage: Removes inferable tags from YAML nodes. // This is the inverse of the Resolver - it walks a tagged node tree and // removes tags that can be inferred during parsing, producing cleaner YAML // output without unnecessary type annotations. package libyaml // Desolver handles tag removal for YAML nodes during serialization. // It removes tags that would be automatically resolved to the same type // during parsing, making the output cleaner and more readable. type Desolver struct { opts *Options } // NewDesolver creates a new Desolver with the given options. func NewDesolver(opts *Options) *Desolver { return &Desolver{opts: opts} } // Desolve walks the node tree and removes tags that can be inferred. // This is the inverse of Resolver - it takes a fully-tagged node tree // (from Representer) and removes unnecessary tags to produce clean output. // // For scalar nodes: if the value would resolve to the same tag when parsed, // the tag is removed. For strings that would resolve differently, the tag is // removed and quoting style is set to preserve the string type. // // For collection nodes (maps/sequences): default tags (!!map, !!seq) are // removed since they're implied by the structure. func (d *Desolver) Desolve(n *Node) { if n == nil { return } switch n.Kind { case ScalarNode: d.desolveScalar(n) case DocumentNode, SequenceNode, MappingNode: d.desolveCollection(n) // Recursively desolve children for _, child := range n.Content { d.Desolve(child) } case AliasNode: // Alias nodes don't have tags to remove } } // desolveScalar removes tags from scalar nodes when they can be inferred. func (d *Desolver) desolveScalar(n *Node) { // If explicitly tagged by user (TaggedStyle), keep it if n.Style&TaggedStyle != 0 { return } // Empty tag means it's already untagged - nothing to do if n.Tag == "" { return } stag := shortTag(n.Tag) // Check if this is a standard scalar tag that we can potentially remove isStandardTag := false switch stag { case nullTag, boolTag, strTag, intTag, floatTag, timestampTag: isStandardTag = true case binaryTag: // Binary scalars are not implicitly resolvable - never remove. return case mergeTag: // Elide the implicit !!merge tag when the value is the canonical // merge key marker. The TaggedStyle early-return above already // preserves !!merge when it was explicit in the source. if n.Value == "<<" { n.Tag = "" } return default: // Custom tag - preserve it return } // Only process standard tags from here if !isStandardTag { return } // What tag would this value resolve to? rtag, _ := resolve("", n.Value) // If resolved tag matches current tag, we can elide the tag if rtag == stag { // Tag can be inferred - remove it n.Tag = "" } else if stag == strTag { // This is a string type, but would resolve to something else. // Remove the tag and force quoting to preserve string type. n.Tag = "" // If not already quoted, set quote style based on content if n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) == 0 { // Determine quote style based on options or default to single quotes if d.opts != nil { // Convert ScalarStyle to Style switch d.opts.QuotePreference.ScalarStyle() { case DOUBLE_QUOTED_SCALAR_STYLE: n.Style |= DoubleQuotedStyle default: n.Style |= SingleQuotedStyle } } else { n.Style |= SingleQuotedStyle } } } else if stag == floatTag || stag == intTag { // For numeric type mismatches (like float64(1) → "1" with !!float tag): // Elide the tag and let YAML resolve naturally. // Without the tag, "1" resolves as !!int, which may change the type, // but that's acceptable for cleaner output (and matches old behavior). n.Tag = "" } // For other standard tags with mismatches, keep the tag to preserve type } // desolveCollection removes default tags from collection nodes. func (d *Desolver) desolveCollection(n *Node) { // If explicitly tagged by user, keep it if n.Style&TaggedStyle != 0 { return } stag := shortTag(n.Tag) switch n.Kind { case MappingNode: // !!map is the default for mappings - remove it if stag == mapTag { n.Tag = "" } case SequenceNode: // !!seq is the default for sequences - remove it if stag == seqTag { n.Tag = "" } case DocumentNode: // Documents don't have tags in YAML output n.Tag = "" } // For other tags, keep them - they're explicit type information }