Repository: goccy/go-yaml
Pull request #594 (2024-12-15 18:01:33)

Fix code format
SSIA.

Before submitting your PR, please confirm the following.

- [x] Describe the purpose for which you created this PR.  
- [x] Create test code that corresponds to the modification

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/594?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 77.43%. Comparing base [(`e1d8782`)](https://app.codecov.io/gh/goccy/go-yaml/commit/e1d8782b74a021e8b6ebce59fc2491bb3c0017e0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) to head [(`5e903a3`)](https://app.codecov.io/gh/goccy/go-yaml/commit/5e903a3a630683f98643de7b0cede1631f3c3ca0?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     #594   +/-   ##
=======================================
  Coverage   77.43%   77.43%           
=======================================
  Files          21       21           
  Lines        7772     7772           
=======================================
  Hits         6018     6018           
  Misses       1342     1342           
  Partials      412      412           
```

</details>

Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 1ad8f15..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
 }
@@ -301,12 +302,12 @@ func checkLineBreak(t *token.Token) bool {
 			}
 			// Due to the way that comment parsing works its assumed that when a null value does not have new line in origin
 			// it was squashed therefore difference is ignored.
-			//foo:
+			// foo:
 			//  bar:
 			//  # comment
 			//  baz: 1
-			//becomes
-			//foo:
+			// becomes
+			// foo:
 			//  bar: null # comment
 			//
 			//  baz: 1
@@ -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/path.go b/path.go
index fcb5681..aaa3477 100644
--- a/path.go
+++ b/path.go
@@ -83,6 +83,9 @@ end:
 }
 
 func parsePathDot(b *PathBuilder, buf []rune, cursor int) (*PathBuilder, []rune, int, error) {
+	if b.root == nil || b.node == nil {
+		return nil, nil, 0, fmt.Errorf("required '$' character at first: %w", ErrInvalidPathString)
+	}
 	length := len(buf)
 	if cursor+1 < length && buf[cursor+1] == '.' {
 		b, buf, c, err := parsePathRecursive(b, buf, cursor)
@@ -119,6 +122,10 @@ end:
 }
 
 func parseQuotedKey(b *PathBuilder, buf []rune, cursor int) (*PathBuilder, []rune, int, error) {
+	if b.root == nil || b.node == nil {
+		return nil, nil, 0, fmt.Errorf("required '$' character at first: %w", ErrInvalidPathString)
+	}
+
 	cursor++ // skip single quote
 	start := cursor
 	length := len(buf)
@@ -156,6 +163,10 @@ end:
 }
 
 func parsePathIndex(b *PathBuilder, buf []rune, cursor int) (*PathBuilder, []rune, int, error) {
+	if b.root == nil || b.node == nil {
+		return nil, nil, 0, fmt.Errorf("required '$' character at first: %w", ErrInvalidPathString)
+	}
+
 	length := len(buf)
 	cursor++ // skip '[' character
 	if length <= cursor {
diff --git a/token/token.go b/token/token.go
index b880bd2..54ab179 100644
--- a/token/token.go
+++ b/token/token.go
@@ -1,6 +1,7 @@
 package token
 
 import (
+	"errors"
 	"fmt"
 	"strconv"
 	"strings"
@@ -404,6 +405,8 @@ const (
 	TimestampTag ReservedTagKeyword = "!!timestamp"
 	// BooleanTag `!!bool` tag
 	BooleanTag ReservedTagKeyword = "!!bool"
+	// MergeTag `!!merge` tag
+	MergeTag ReservedTagKeyword = "!!merge"
 )
 
 var (
@@ -519,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,
+			}
+		},
 	}
 )
 
@@ -539,15 +552,35 @@ type NumberValue struct {
 }
 
 func ToNumber(value string) *NumberValue {
-	if len(value) == 0 {
+	num, err := toNumber(value)
+	if err != nil {
 		return nil
 	}
+	return num
+}
+
+func isNumber(value string) bool {
+	num, err := toNumber(value)
+	if err != nil {
+		var numErr *strconv.NumError
+		if errors.As(err, &numErr) && errors.Is(numErr.Err, strconv.ErrRange) {
+			return true
+		}
+		return false
+	}
+	return num != nil
+}
+
+func toNumber(value string) (*NumberValue, error) {
+	if len(value) == 0 {
+		return nil, nil
+	}
 	if strings.HasPrefix(value, "_") {
-		return nil
+		return nil, nil
 	}
 	dotCount := strings.Count(value, ".")
 	if dotCount > 1 {
-		return nil
+		return nil, nil
 	}
 
 	isNegative := strings.HasPrefix(value, "-")
@@ -589,19 +622,19 @@ func ToNumber(value string) *NumberValue {
 	if typ == NumberTypeFloat {
 		f, err := strconv.ParseFloat(text, 64)
 		if err != nil {
-			return nil
+			return nil, err
 		}
 		v = f
 	} else if isNegative {
 		i, err := strconv.ParseInt(text, base, 64)
 		if err != nil {
-			return nil
+			return nil, err
 		}
 		v = i
 	} else {
 		u, err := strconv.ParseUint(text, base, 64)
 		if err != nil {
-			return nil
+			return nil, err
 		}
 		v = u
 	}
@@ -610,7 +643,7 @@ func ToNumber(value string) *NumberValue {
 		Type:  typ,
 		Value: v,
 		Text:  text,
-	}
+	}, nil
 }
 
 // This is a subset of the formats permitted by the regular expression
@@ -635,7 +668,7 @@ func isTimestamp(value string) bool {
 	return false
 }
 
-// IsNeedQuoted whether need quote for passed string or not
+// IsNeedQuoted checks whether the value needs quote for passed string or not
 func IsNeedQuoted(value string) bool {
 	if value == "" {
 		return true
@@ -643,7 +676,7 @@ func IsNeedQuoted(value string) bool {
 	if _, exists := reservedEncKeywordMap[value]; exists {
 		return true
 	}
-	if num := ToNumber(value); num != nil {
+	if isNumber(value) {
 		return true
 	}
 	first := value[0]


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
 		{
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index 8e94c2f..c36434c 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -178,7 +178,7 @@ func TestTokenize(t *testing.T) {
 					CharacterType: token.CharacterTypeMiscellaneous,
 					Indicator:     token.NotIndicator,
 					Value:         "a",
-					//nolint: gci
+					// nolint: gci
 					Origin: "	a",
 				},
 			},
@@ -2528,7 +2528,7 @@ s: >-3
 					CharacterType: token.CharacterTypeIndicator,
 					Indicator:     token.BlockScalarIndicator,
 					Value:         "|",
-					Origin: "\n|  		  ", //nolint:gci,gofmt
+					Origin:        "\n|  		  ",
 				},
 				{
 					Type:          token.CommentType,
@@ -3222,7 +3222,7 @@ a: |invalid`,
 		},
 		{
 			name: "use tab character as indent",
-			//nolint: gci
+			// nolint: gci
 			src: "	a: b",
 		},
 		{
diff --git a/path_test.go b/path_test.go
index 84e6cc0..5b62d26 100644
--- a/path_test.go
+++ b/path_test.go
@@ -603,6 +603,33 @@ building:
 	}
 }
 
+func TestInvalidPath(t *testing.T) {
+	tests := []struct {
+		name string
+		path string
+	}{
+		{
+			name: "missing root with dot",
+			path: ".foo",
+		},
+		{
+			name: "missing root with index",
+			path: "foo[0]",
+		},
+		{
+			name: "missing root with recursive",
+			path: "..foo",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := yaml.PathString(test.path); err == nil {
+				t.Fatal("expected error")
+			}
+		})
+	}
+}
+
 func ExamplePath_AnnotateSource() {
 	yml := `
 a: 1
diff --git a/token/token_test.go b/token/token_test.go
index e5e5ba8..3b339b4 100644
--- a/token/token_test.go
+++ b/token/token_test.go
@@ -81,6 +81,10 @@ func TestIsNeedQuoted(t *testing.T) {
 		"",
 		"true",
 		"1.234",
+		"0b11111111111111111111111111111111111111111111111111111111111111111",
+		"0o7777777777777777777777777777777777777777",
+		"999999999999999999999999999999999999999999",
+		"0xffffffffffffffffffffffffffffffffffffffff",
 		"1:1",
 		"2001-12-15T02:59:43.1Z",
 		"2001-12-14t21:59:43.10-05:00",