Repository: goccy/go-yaml
Pull request #603 (2024-12-22 07:33:12)

Fix sequence value for merge key
resolve https://github.com/goccy/go-yaml/issues/340

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/603?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 `50.00000%` with `34 lines` in your changes missing coverage. Please review.
> Project coverage is 77.03%. Comparing base [(`aeed806`)](https://app.codecov.io/gh/goccy/go-yaml/commit/aeed806d55e542e47b07218d47f638d660586cf3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) to head [(`fc88084`)](https://app.codecov.io/gh/goccy/go-yaml/commit/fc8808431a99190a5ffcdeba415308390c8c744d?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     #603      +/-   ##
==========================================
- Coverage   77.19%   77.03%   -0.17%     
==========================================
  Files          21       22       +1     
  Lines        7850     7873      +23     
==========================================
+ Hits         6060     6065       +5     
- Misses       1369     1385      +16     
- Partials      421      423       +2     
```

</details>

Patch:
diff --git a/ast/ast.go b/ast/ast.go
index b6c9663..c9bbf9d 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -1157,6 +1157,11 @@ func (m *MapNodeIter) Value() Node {
 	return m.values[m.idx].Value
 }
 
+// KeyValue returns the MappingValueNode of the iterator's current map node entry.
+func (m *MapNodeIter) KeyValue() *MappingValueNode {
+	return m.values[m.idx]
+}
+
 // MappingNode type of mapping node
 type MappingNode struct {
 	*BaseNode
@@ -1653,6 +1658,28 @@ func (n *SequenceNode) MarshalYAML() ([]byte, error) {
 	return []byte(n.String()), nil
 }
 
+// SequenceMergeValue creates SequenceMergeValueNode instance.
+func SequenceMergeValue(values ...MapNode) *SequenceMergeValueNode {
+	return &SequenceMergeValueNode{
+		values: values,
+	}
+}
+
+// SequenceMergeValueNode is used to convert the Sequence node specified for the merge key into a MapNode format.
+type SequenceMergeValueNode struct {
+	values []MapNode
+}
+
+// MapRange returns MapNodeIter instance.
+func (n *SequenceMergeValueNode) MapRange() *MapNodeIter {
+	ret := &MapNodeIter{idx: startRangeIndex}
+	for _, value := range n.values {
+		iter := value.MapRange()
+		ret.values = append(ret.values, iter.values...)
+	}
+	return ret
+}
+
 // AnchorNode type of anchor node
 type AnchorNode struct {
 	*BaseNode
diff --git a/context.go b/context.go
new file mode 100644
index 0000000..aa17d3c
--- /dev/null
+++ b/context.go
@@ -0,0 +1,17 @@
+package yaml
+
+import "context"
+
+type ctxMergeKey struct{}
+
+func withMerge(ctx context.Context) context.Context {
+	return context.WithValue(ctx, ctxMergeKey{}, true)
+}
+
+func isMerge(ctx context.Context) bool {
+	v, ok := ctx.Value(ctxMergeKey{}).(bool)
+	if !ok {
+		return false
+	}
+	return v
+}
diff --git a/decode.go b/decode.go
index 7df6cb1..fda5a6d 100644
--- a/decode.go
+++ b/decode.go
@@ -114,15 +114,6 @@ func (d *Decoder) castToFloat(v interface{}) interface{} {
 	return 0
 }
 
-func (d *Decoder) mergeValueNode(value ast.Node) ast.Node {
-	if value.Type() == ast.AliasType {
-		aliasNode, _ := value.(*ast.AliasNode)
-		aliasName := aliasNode.Value.GetToken().Value
-		return d.anchorNodeMap[aliasName]
-	}
-	return value
-}
-
 func (d *Decoder) mapKeyNodeToString(node ast.MapKeyNode) (string, error) {
 	key, err := d.nodeToValue(node)
 	if err != nil {
@@ -148,9 +139,16 @@ func (d *Decoder) setToMapValue(node ast.Node, m map[string]interface{}) error {
 	switch n := node.(type) {
 	case *ast.MappingValueNode:
 		if n.Key.IsMergeKey() {
-			if err := d.setToMapValue(d.mergeValueNode(n.Value), m); err != nil {
+			value, err := d.getMapNode(n.Value, true)
+			if err != nil {
 				return err
 			}
+			iter := value.MapRange()
+			for iter.Next() {
+				if err := d.setToMapValue(iter.KeyValue(), m); err != nil {
+					return err
+				}
+			}
 		} else {
 			key, err := d.mapKeyNodeToString(n.Key)
 			if err != nil {
@@ -186,9 +184,16 @@ func (d *Decoder) setToOrderedMapValue(node ast.Node, m *MapSlice) error {
 	switch n := node.(type) {
 	case *ast.MappingValueNode:
 		if n.Key.IsMergeKey() {
-			if err := d.setToOrderedMapValue(d.mergeValueNode(n.Value), m); err != nil {
+			value, err := d.getMapNode(n.Value, true)
+			if err != nil {
 				return err
 			}
+			iter := value.MapRange()
+			for iter.Next() {
+				if err := d.setToOrderedMapValue(iter.KeyValue(), m); err != nil {
+					return err
+				}
+			}
 		} else {
 			key, err := d.mapKeyNodeToString(n.Key)
 			if err != nil {
@@ -468,17 +473,25 @@ func (d *Decoder) nodeToValue(node ast.Node) (any, error) {
 		return d.nodeToValue(n.Value)
 	case *ast.MappingValueNode:
 		if n.Key.IsMergeKey() {
-			value := d.mergeValueNode(n.Value)
+			value, err := d.getMapNode(n.Value, true)
+			if err != nil {
+				return nil, err
+			}
+			iter := value.MapRange()
 			if d.useOrderedMap {
 				m := MapSlice{}
-				if err := d.setToOrderedMapValue(value, &m); err != nil {
-					return nil, err
+				for iter.Next() {
+					if err := d.setToOrderedMapValue(iter.KeyValue(), &m); err != nil {
+						return nil, err
+					}
 				}
 				return m, nil
 			}
-			m := map[string]interface{}{}
-			if err := d.setToMapValue(value, m); err != nil {
-				return nil, err
+			m := make(map[string]any)
+			for iter.Next() {
+				if err := d.setToMapValue(iter.KeyValue(), m); err != nil {
+					return nil, err
+				}
 			}
 			return m, nil
 		}
@@ -598,40 +611,42 @@ func (d *Decoder) resolveAlias(node ast.Node) (ast.Node, error) {
 	return node, nil
 }
 
-func (d *Decoder) getMapNode(node ast.Node) (ast.MapNode, error) {
+func (d *Decoder) getMapNode(node ast.Node, isMerge bool) (ast.MapNode, error) {
 	d.stepIn()
 	defer d.stepOut()
 	if d.isExceededMaxDepth() {
 		return nil, ErrExceededMaxDepth
 	}
 
-	if _, ok := node.(*ast.NullNode); ok {
-		return nil, nil
-	}
-	if anchor, ok := node.(*ast.AnchorNode); ok {
-		mapNode, ok := anchor.Value.(ast.MapNode)
-		if ok {
-			return mapNode, nil
-		}
-		return nil, errors.ErrUnexpectedNodeType(anchor.Value.Type(), ast.MappingType, node.GetToken())
-	}
-	if alias, ok := node.(*ast.AliasNode); ok {
-		aliasName := alias.Value.GetToken().Value
+	switch n := node.(type) {
+	case ast.MapNode:
+		return n, nil
+	case *ast.AnchorNode:
+		anchorName := n.Name.GetToken().Value
+		d.anchorNodeMap[anchorName] = n.Value
+		return d.getMapNode(n.Value, isMerge)
+	case *ast.AliasNode:
+		aliasName := n.Value.GetToken().Value
 		node := d.anchorNodeMap[aliasName]
 		if node == nil {
 			return nil, fmt.Errorf("cannot find anchor by alias name %s", aliasName)
 		}
-		mapNode, ok := node.(ast.MapNode)
-		if ok {
-			return mapNode, nil
+		return d.getMapNode(node, isMerge)
+	case *ast.SequenceNode:
+		if !isMerge {
+			return nil, errors.ErrUnexpectedNodeType(node.Type(), ast.MappingType, node.GetToken())
 		}
-		return nil, errors.ErrUnexpectedNodeType(node.Type(), ast.MappingType, node.GetToken())
-	}
-	mapNode, ok := node.(ast.MapNode)
-	if !ok {
-		return nil, errors.ErrUnexpectedNodeType(node.Type(), ast.MappingType, node.GetToken())
+		var mapNodes []ast.MapNode
+		for _, value := range n.Values {
+			mapNode, err := d.getMapNode(value, false)
+			if err != nil {
+				return nil, err
+			}
+			mapNodes = append(mapNodes, mapNode)
+		}
+		return ast.SequenceMergeValue(mapNodes...), nil
 	}
-	return mapNode, nil
+	return nil, errors.ErrUnexpectedNodeType(node.Type(), ast.MappingType, node.GetToken())
 }
 
 func (d *Decoder) getArrayNode(node ast.Node) (ast.ArrayNode, error) {
@@ -1191,15 +1206,12 @@ func (d *Decoder) keyToNodeMap(node ast.Node, ignoreMergeKey bool, getKeyOrValue
 		return nil, ErrExceededMaxDepth
 	}
 
-	mapNode, err := d.getMapNode(node)
+	mapNode, err := d.getMapNode(node, false)
 	if err != nil {
 		return nil, err
 	}
 	keyMap := map[string]struct{}{}
 	keyToNodeMap := map[string]ast.Node{}
-	if mapNode == nil {
-		return keyToNodeMap, nil
-	}
 	mapIter := mapNode.MapRange()
 	for mapIter.Next() {
 		keyNode := mapIter.Key()
@@ -1358,13 +1370,10 @@ func (d *Decoder) decodeDuration(ctx context.Context, dst reflect.Value, src ast
 
 // getMergeAliasName support single alias only
 func (d *Decoder) getMergeAliasName(src ast.Node) string {
-	mapNode, err := d.getMapNode(src)
+	mapNode, err := d.getMapNode(src, true)
 	if err != nil {
 		return ""
 	}
-	if mapNode == nil {
-		return ""
-	}
 	mapIter := mapNode.MapRange()
 	for mapIter.Next() {
 		key := mapIter.Key()
@@ -1649,13 +1658,10 @@ func (d *Decoder) decodeMapItem(ctx context.Context, dst *MapItem, src ast.Node)
 		return ErrExceededMaxDepth
 	}
 
-	mapNode, err := d.getMapNode(src)
+	mapNode, err := d.getMapNode(src, isMerge(ctx))
 	if err != nil {
 		return err
 	}
-	if mapNode == nil {
-		return nil
-	}
 	mapIter := mapNode.MapRange()
 	if !mapIter.Next() {
 		return nil
@@ -1663,7 +1669,7 @@ func (d *Decoder) decodeMapItem(ctx context.Context, dst *MapItem, src ast.Node)
 	key := mapIter.Key()
 	value := mapIter.Value()
 	if key.IsMergeKey() {
-		if err := d.decodeMapItem(ctx, dst, value); err != nil {
+		if err := d.decodeMapItem(withMerge(ctx), dst, value); err != nil {
 			return err
 		}
 		return nil
@@ -1701,13 +1707,10 @@ func (d *Decoder) decodeMapSlice(ctx context.Context, dst *MapSlice, src ast.Nod
 		return ErrExceededMaxDepth
 	}
 
-	mapNode, err := d.getMapNode(src)
+	mapNode, err := d.getMapNode(src, isMerge(ctx))
 	if err != nil {
 		return err
 	}
-	if mapNode == nil {
-		return nil
-	}
 	mapSlice := MapSlice{}
 	mapIter := mapNode.MapRange()
 	keyMap := map[string]struct{}{}
@@ -1716,7 +1719,7 @@ func (d *Decoder) decodeMapSlice(ctx context.Context, dst *MapSlice, src ast.Nod
 		value := mapIter.Value()
 		if key.IsMergeKey() {
 			var m MapSlice
-			if err := d.decodeMapSlice(ctx, &m, value); err != nil {
+			if err := d.decodeMapSlice(withMerge(ctx), &m, value); err != nil {
 				return err
 			}
 			for _, v := range m {
@@ -1751,13 +1754,10 @@ func (d *Decoder) decodeMap(ctx context.Context, dst reflect.Value, src ast.Node
 		return ErrExceededMaxDepth
 	}
 
-	mapNode, err := d.getMapNode(src)
+	mapNode, err := d.getMapNode(src, isMerge(ctx))
 	if err != nil {
 		return err
 	}
-	if mapNode == nil {
-		return nil
-	}
 	mapType := dst.Type()
 	mapValue := reflect.MakeMap(mapType)
 	keyType := mapValue.Type().Key()
@@ -1769,7 +1769,7 @@ func (d *Decoder) decodeMap(ctx context.Context, dst reflect.Value, src ast.Node
 		key := mapIter.Key()
 		value := mapIter.Value()
 		if key.IsMergeKey() {
-			if err := d.decodeMap(ctx, dst, value); err != nil {
+			if err := d.decodeMap(withMerge(ctx), dst, value); err != nil {
 				return err
 			}
 			iter := dst.MapRange()
diff --git a/parser/context.go b/parser/context.go
index 36ceb58..dc11b51 100644
--- a/parser/context.go
+++ b/parser/context.go
@@ -127,6 +127,20 @@ func (c *context) insertNullToken(tk *Token) *Token {
 	return nullToken
 }
 
+func (c *context) addNullValueToken(tk *Token) *Token {
+	nullToken := c.createNullToken(tk)
+	rawTk := nullToken.RawToken()
+
+	// add space for map or sequence value.
+	rawTk.Origin = " null"
+	rawTk.Position.Column++
+
+	c.addToken(nullToken)
+	c.goNext()
+
+	return nullToken
+}
+
 func (c *context) createNullToken(base *Token) *Token {
 	pos := *(base.RawToken().Position)
 	pos.Column++
@@ -157,3 +171,16 @@ func (c *context) insertToken(tk *Token) {
 	ref.tokens[idx] = tk
 	ref.size = len(ref.tokens)
 }
+
+func (c *context) addToken(tk *Token) {
+	ref := c.tokenRef
+	lastTk := ref.tokens[ref.size-1]
+	if lastTk.Group != nil {
+		lastTk = lastTk.Group.Last()
+	}
+	lastTk.RawToken().Next = tk.RawToken()
+	tk.RawToken().Prev = lastTk.RawToken()
+
+	ref.tokens = append(ref.tokens, tk)
+	ref.size = len(ref.tokens)
+}
diff --git a/parser/parser.go b/parser/parser.go
index e32b976..070d894 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -705,7 +705,7 @@ func (p *parser) mapKeyText(n ast.Node) string {
 func (p *parser) parseMapValue(ctx *context, key ast.MapKeyNode, colonTk *Token) (ast.Node, error) {
 	tk := ctx.currentToken()
 	if tk == nil {
-		return newNullNode(ctx, ctx.insertNullToken(colonTk))
+		return newNullNode(ctx, ctx.addNullValueToken(colonTk))
 	}
 
 	if ctx.isComment() {
@@ -1091,7 +1091,7 @@ func (p *parser) parseSequence(ctx *context) (*ast.SequenceNode, error) {
 func (p *parser) parseSequenceValue(ctx *context, seqTk *Token) (ast.Node, error) {
 	tk := ctx.currentToken()
 	if tk == nil {
-		return newNullNode(ctx, ctx.insertNullToken(seqTk))
+		return newNullNode(ctx, ctx.addNullValueToken(seqTk))
 	}
 
 	if ctx.isComment() {


Test patch:
diff --git a/decode_test.go b/decode_test.go
index d90587f..f5308cf 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -598,6 +598,38 @@ c: 3
 			map[string]any{"a": 1, "b": 2, "c": 3},
 		},
 
+		// merge
+		{
+			`
+a: &a
+ foo: 1
+b: &b
+ bar: 2
+merge:
+ <<: [*a, *b]
+`,
+			map[string]map[string]any{
+				"a":     {"foo": 1},
+				"b":     {"bar": 2},
+				"merge": {"foo": 1, "bar": 2},
+			},
+		},
+		{
+			`
+a: &a
+ foo: 1
+b: &b
+ bar: 2
+merge:
+ <<: [*a, *b]
+`,
+			map[string]yaml.MapSlice{
+				"a":     {{Key: "foo", Value: 1}},
+				"b":     {{Key: "bar", Value: 2}},
+				"merge": {{Key: "foo", Value: 1}, {Key: "bar", Value: 2}},
+			},
+		},
+
 		// Flow sequence
 		{
 			"v: [A,B]",