Repository: goccy/go-yaml
Pull request #597 (2024-12-17 15:45:58)

Support merge tag
fix https://github.com/goccy/go-yaml/issues/596

codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/goccy/go-yaml/pull/597?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) Report
Attention: Patch coverage is `57.77778%` with `19 lines` in your changes missing coverage. Please review.
> Project coverage is 77.37%. Comparing base [(`61bc6c1`)](https://app.codecov.io/gh/goccy/go-yaml/commit/61bc6c1078fcaebe3e6794f1354117794d535cbe?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) to head [(`0df0665`)](https://app.codecov.io/gh/goccy/go-yaml/commit/0df0665cbe9b4eb5bbaac2b520ddb08c18a0872f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima).

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #597      +/-   ##
==========================================
- Coverage   77.47%   77.37%   -0.10%     
==========================================
  Files          21       21              
  Lines        7787     7820      +33     
==========================================
+ Hits         6033     6051      +18     
- Misses       1342     1352      +10     
- Partials      412      417       +5     
```

</details>

Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 53ad0da..b6c9663 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -196,6 +196,7 @@ type Node interface {
 // MapKeyNode type for map key node
 type MapKeyNode interface {
 	Node
+	IsMergeKey() bool
 	// String node to text without comment
 	stringWithoutComment() string
 }
@@ -633,6 +634,11 @@ func (n *NullNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *NullNode) IsMergeKey() bool {
+	return false
+}
+
 // IntegerNode type of integer node
 type IntegerNode struct {
 	*BaseNode
@@ -680,6 +686,11 @@ func (n *IntegerNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *IntegerNode) IsMergeKey() bool {
+	return false
+}
+
 // FloatNode type of float node
 type FloatNode struct {
 	*BaseNode
@@ -728,6 +739,11 @@ func (n *FloatNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *FloatNode) IsMergeKey() bool {
+	return false
+}
+
 // StringNode type of string node
 type StringNode struct {
 	*BaseNode
@@ -758,6 +774,11 @@ func (n *StringNode) GetValue() interface{} {
 	return n.Value
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *StringNode) IsMergeKey() bool {
+	return false
+}
+
 // escapeSingleQuote escapes s to a single quoted scalar.
 // https://yaml.org/spec/1.2.2/#732-single-quoted-style
 func escapeSingleQuote(s string) string {
@@ -895,6 +916,11 @@ func (n *LiteralNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *LiteralNode) IsMergeKey() bool {
+	return false
+}
+
 // MergeKeyNode type of merge key node
 type MergeKeyNode struct {
 	*BaseNode
@@ -938,6 +964,11 @@ func (n *MergeKeyNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *MergeKeyNode) IsMergeKey() bool {
+	return true
+}
+
 // BoolNode type of boolean node
 type BoolNode struct {
 	*BaseNode
@@ -985,6 +1016,11 @@ func (n *BoolNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *BoolNode) IsMergeKey() bool {
+	return false
+}
+
 // InfinityNode type of infinity node
 type InfinityNode struct {
 	*BaseNode
@@ -1032,6 +1068,11 @@ func (n *InfinityNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *InfinityNode) IsMergeKey() bool {
+	return false
+}
+
 // NanNode type of nan node
 type NanNode struct {
 	*BaseNode
@@ -1078,6 +1119,11 @@ func (n *NanNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *NanNode) IsMergeKey() bool {
+	return false
+}
+
 // MapNode interface of MappingValueNode / MappingNode
 type MapNode interface {
 	MapRange() *MapNodeIter
@@ -1281,6 +1327,18 @@ func (n *MappingKeyNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *MappingKeyNode) IsMergeKey() bool {
+	if n.Value == nil {
+		return false
+	}
+	key, ok := n.Value.(MapKeyNode)
+	if !ok {
+		return false
+	}
+	return key.IsMergeKey()
+}
+
 // MappingValueNode type of mapping value
 type MappingValueNode struct {
 	*BaseNode
@@ -1665,6 +1723,18 @@ func (n *AnchorNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *AnchorNode) IsMergeKey() bool {
+	if n.Value == nil {
+		return false
+	}
+	key, ok := n.Value.(MapKeyNode)
+	if !ok {
+		return false
+	}
+	return key.IsMergeKey()
+}
+
 // AliasNode type of alias node
 type AliasNode struct {
 	*BaseNode
@@ -1723,6 +1793,11 @@ func (n *AliasNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *AliasNode) IsMergeKey() bool {
+	return false
+}
+
 // DirectiveNode type of directive node
 type DirectiveNode struct {
 	*BaseNode
@@ -1822,6 +1897,18 @@ func (n *TagNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// IsMergeKey returns whether it is a MergeKey node.
+func (n *TagNode) IsMergeKey() bool {
+	if n.Value == nil {
+		return false
+	}
+	key, ok := n.Value.(MapKeyNode)
+	if !ok {
+		return false
+	}
+	return key.IsMergeKey()
+}
+
 // CommentNode type of comment node
 type CommentNode struct {
 	*BaseNode
diff --git a/decode.go b/decode.go
index 024e9ba..82915cb 100644
--- a/decode.go
+++ b/decode.go
@@ -147,7 +147,7 @@ func (d *Decoder) setToMapValue(node ast.Node, m map[string]interface{}) error {
 	d.setPathToCommentMap(node)
 	switch n := node.(type) {
 	case *ast.MappingValueNode:
-		if n.Key.Type() == ast.MergeKeyType {
+		if n.Key.IsMergeKey() {
 			if err := d.setToMapValue(d.mergeValueNode(n.Value), m); err != nil {
 				return err
 			}
@@ -185,7 +185,7 @@ func (d *Decoder) setToOrderedMapValue(node ast.Node, m *MapSlice) error {
 	d.setPathToCommentMap(node)
 	switch n := node.(type) {
 	case *ast.MappingValueNode:
-		if n.Key.Type() == ast.MergeKeyType {
+		if n.Key.IsMergeKey() {
 			if err := d.setToOrderedMapValue(d.mergeValueNode(n.Value), m); err != nil {
 				return err
 			}
@@ -464,7 +464,7 @@ func (d *Decoder) nodeToValue(node ast.Node) (any, error) {
 	case *ast.MappingKeyNode:
 		return d.nodeToValue(n.Value)
 	case *ast.MappingValueNode:
-		if n.Key.Type() == ast.MergeKeyType {
+		if n.Key.IsMergeKey() {
 			value := d.mergeValueNode(n.Value)
 			if d.useOrderedMap {
 				m := MapSlice{}
@@ -555,7 +555,7 @@ func (d *Decoder) resolveAlias(node ast.Node) (ast.Node, error) {
 		}
 		n.Value = value
 	case *ast.MappingValueNode:
-		if n.Key.Type() == ast.MergeKeyType && n.Value.Type() == ast.AliasType {
+		if n.Key.IsMergeKey() && n.Value.Type() == ast.AliasType {
 			value, err := d.resolveAlias(n.Value)
 			if err != nil {
 				return nil, err
@@ -1186,7 +1186,7 @@ func (d *Decoder) keyToNodeMap(node ast.Node, ignoreMergeKey bool, getKeyOrValue
 	mapIter := mapNode.MapRange()
 	for mapIter.Next() {
 		keyNode := mapIter.Key()
-		if keyNode.Type() == ast.MergeKeyType {
+		if keyNode.IsMergeKey() {
 			if ignoreMergeKey {
 				continue
 			}
@@ -1349,7 +1349,7 @@ func (d *Decoder) getMergeAliasName(src ast.Node) string {
 	for mapIter.Next() {
 		key := mapIter.Key()
 		value := mapIter.Value()
-		if key.Type() == ast.MergeKeyType && value.Type() == ast.AliasType {
+		if key.IsMergeKey() && value.Type() == ast.AliasType {
 			return value.(*ast.AliasNode).Value.GetToken().Value
 		}
 	}
@@ -1639,7 +1639,7 @@ func (d *Decoder) decodeMapItem(ctx context.Context, dst *MapItem, src ast.Node)
 	}
 	key := mapIter.Key()
 	value := mapIter.Value()
-	if key.Type() == ast.MergeKeyType {
+	if key.IsMergeKey() {
 		if err := d.decodeMapItem(ctx, dst, value); err != nil {
 			return err
 		}
@@ -1691,7 +1691,7 @@ func (d *Decoder) decodeMapSlice(ctx context.Context, dst *MapSlice, src ast.Nod
 	for mapIter.Next() {
 		key := mapIter.Key()
 		value := mapIter.Value()
-		if key.Type() == ast.MergeKeyType {
+		if key.IsMergeKey() {
 			var m MapSlice
 			if err := d.decodeMapSlice(ctx, &m, value); err != nil {
 				return err
@@ -1745,7 +1745,7 @@ func (d *Decoder) decodeMap(ctx context.Context, dst reflect.Value, src ast.Node
 	for mapIter.Next() {
 		key := mapIter.Key()
 		value := mapIter.Value()
-		if key.Type() == ast.MergeKeyType {
+		if key.IsMergeKey() {
 			if err := d.decodeMap(ctx, dst, value); err != nil {
 				return err
 			}
diff --git a/parser/token.go b/parser/token.go
index 3d1c06f..42f0a9b 100644
--- a/parser/token.go
+++ b/parser/token.go
@@ -213,7 +213,10 @@ func createGroupedTokens(tokens token.Tokens) ([]*Token, error) {
 	if err != nil {
 		return nil, err
 	}
-	tks = createScalarTagTokenGroups(tks)
+	tks, err = createScalarTagTokenGroups(tks)
+	if err != nil {
+		return nil, err
+	}
 	tks, err = createAnchorWithScalarTagTokenGroups(tks)
 	if err != nil {
 		return nil, err
@@ -348,7 +351,7 @@ func createAnchorAndAliasTokenGroups(tokens []*Token) ([]*Token, error) {
 	return ret, nil
 }
 
-func createScalarTagTokenGroups(tokens []*Token) []*Token {
+func createScalarTagTokenGroups(tokens []*Token) ([]*Token, error) {
 	ret := make([]*Token, 0, len(tokens))
 	for i := 0; i < len(tokens); i++ {
 		tk := tokens[i]
@@ -384,6 +387,29 @@ func createScalarTagTokenGroups(tokens []*Token) []*Token {
 				} else {
 					ret = append(ret, tk)
 				}
+			case token.MergeTag:
+				if len(tokens) <= i+1 {
+					ret = append(ret, tk)
+					continue
+				}
+				if tk.Line() != tokens[i+1].Line() {
+					ret = append(ret, tk)
+					continue
+				}
+				if tokens[i+1].GroupType() == TokenGroupAnchorName {
+					ret = append(ret, tk)
+					continue
+				}
+				if tokens[i+1].Type() != token.MergeKeyType {
+					return nil, errors.ErrSyntax("could not find merge key", tokens[i+1].RawToken())
+				}
+				ret = append(ret, &Token{
+					Group: &TokenGroup{
+						Type:   TokenGroupScalarTag,
+						Tokens: []*Token{tk, tokens[i+1]},
+					},
+				})
+				i++
 			default:
 				ret = append(ret, tk)
 			}
@@ -409,7 +435,7 @@ func createScalarTagTokenGroups(tokens []*Token) []*Token {
 			i++
 		}
 	}
-	return ret
+	return ret, nil
 }
 
 func createAnchorWithScalarTagTokenGroups(tokens []*Token) ([]*Token, error) {
diff --git a/token/token.go b/token/token.go
index 21464cf..54ab179 100644
--- a/token/token.go
+++ b/token/token.go
@@ -405,6 +405,8 @@ const (
 	TimestampTag ReservedTagKeyword = "!!timestamp"
 	// BooleanTag `!!bool` tag
 	BooleanTag ReservedTagKeyword = "!!bool"
+	// MergeTag `!!merge` tag
+	MergeTag ReservedTagKeyword = "!!merge"
 )
 
 var (
@@ -520,6 +522,16 @@ var (
 				Position:      pos,
 			}
 		},
+		MergeTag: func(value, org string, pos *Position) *Token {
+			return &Token{
+				Type:          TagType,
+				CharacterType: CharacterTypeIndicator,
+				Indicator:     NodePropertyIndicator,
+				Value:         value,
+				Origin:        org,
+				Position:      pos,
+			}
+		},
 	}
 )
 


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 643f9aa..4f0fc11 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -590,6 +590,13 @@ func TestDecoder(t *testing.T) {
 			"v: !!bool False",
 			map[string]bool{"v": false},
 		},
+		{
+			`
+!!merge <<: { a: 1, b: 2 }
+c: 3
+`,
+			map[string]any{"a": 1, "b": 2, "c": 3},
+		},
 
 		// Flow sequence
 		{