153 lines
4.5 KiB
Go
153 lines
4.5 KiB
Go
// 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
|
|
}
|