Repository: goccy/go-yaml
Pull request #510 (2024-11-06 07:26:29)

Fix ordered map comment
I took over [this pull request](https://github.com/goccy/go-yaml/pull/346) and added tests for the changes. I also slightly refactored the original test cases to adopt a table-driven test style. Thanks for the review!
Closes https://github.com/goccy/go-yaml/issues/345

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/510?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 76.64%. Comparing base [(`29b57b4`)](https://app.codecov.io/gh/goccy/go-yaml/commit/29b57b4cc6d61b4652c4a389ab4e4726a2a7faf6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) to head [(`e6d3192`)](https://app.codecov.io/gh/goccy/go-yaml/commit/e6d319205790356a7dc457e4fa01d6a573053314?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima).
> Report is 1 commits behind head on master.

: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     #510      +/-   ##
==========================================
- Coverage   76.86%   76.64%   -0.22%     
==========================================
  Files          18       18              
  Lines        6206     6209       +3     
==========================================
- Hits         4770     4759      -11     
- Misses       1123     1132       +9     
- Partials      313      318       +5     
```

</details>

Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 47039ec..e5341df 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -341,118 +341,30 @@ func Bool(tk *token.Token) *BoolNode {
 
 // Integer create node for integer value
 func Integer(tk *token.Token) *IntegerNode {
-	switch tk.Type {
-	case token.BinaryIntegerType:
-		// skip two characters because binary token starts with '0b'
-		parsedNum := parseNumber("0b", tk.Value)
-		if parsedNum.isNegative {
-			i, _ := strconv.ParseInt(parsedNum.String(), 2, 64)
-			return &IntegerNode{
-				BaseNode: &BaseNode{},
-				Token:    tk,
-				Value:    i,
-			}
-		}
-		i, _ := strconv.ParseUint(parsedNum.String(), 2, 64)
-		return &IntegerNode{
-			BaseNode: &BaseNode{},
-			Token:    tk,
-			Value:    i,
-		}
-	case token.OctetIntegerType:
-		// octet token starts with '0o' or '-0o' or '0' or '-0'
-		parsedNum := parseNumber("0o", tk.Value)
-		if parsedNum.isNegative {
-			i, _ := strconv.ParseInt(parsedNum.String(), 8, 64)
-			return &IntegerNode{
-				BaseNode: &BaseNode{},
-				Token:    tk,
-				Value:    i,
-			}
-		}
-		i, _ := strconv.ParseUint(parsedNum.String(), 8, 64)
-		return &IntegerNode{
-			BaseNode: &BaseNode{},
-			Token:    tk,
-			Value:    i,
-		}
-	case token.HexIntegerType:
-		// hex token starts with '0x' or '-0x'
-		parsedNum := parseNumber("0x", tk.Value)
-		if parsedNum.isNegative {
-			i, _ := strconv.ParseInt(parsedNum.String(), 16, 64)
-			return &IntegerNode{
-				BaseNode: &BaseNode{},
-				Token:    tk,
-				Value:    i,
-			}
-		}
-		i, _ := strconv.ParseUint(parsedNum.String(), 16, 64)
-		return &IntegerNode{
-			BaseNode: &BaseNode{},
-			Token:    tk,
-			Value:    i,
-		}
-	}
-	parsedNum := parseNumber("", tk.Value)
-	if parsedNum.isNegative {
-		i, _ := strconv.ParseInt(parsedNum.String(), 10, 64)
-		return &IntegerNode{
-			BaseNode: &BaseNode{},
-			Token:    tk,
-			Value:    i,
-		}
+	var v any
+	if num := token.ToNumber(tk.Value); num != nil {
+		v = num.Value
 	}
-	i, _ := strconv.ParseUint(parsedNum.String(), 10, 64)
 	return &IntegerNode{
 		BaseNode: &BaseNode{},
 		Token:    tk,
-		Value:    i,
-	}
-}
-
-type parsedNumber struct {
-	isNegative bool
-	num        string
-}
-
-func (n *parsedNumber) String() string {
-	if n.isNegative {
-		return "-" + n.num
-	}
-	return n.num
-}
-
-func parseNumber(prefix, value string) *parsedNumber {
-	isNegative := value[0] == '-'
-	trimmed := strings.TrimPrefix(value, "+")
-	trimmed = strings.TrimPrefix(trimmed, "-")
-	trimmed = strings.TrimPrefix(trimmed, prefix)
-
-	num := make([]rune, 0, len(trimmed))
-	for _, v := range trimmed {
-		if v == '_' {
-			continue
-		}
-		num = append(num, v)
-	}
-	if len(num) == 0 {
-		num = append(num, '0')
-	}
-	return &parsedNumber{
-		isNegative: isNegative,
-		num:        string(num),
+		Value:    v,
 	}
 }
 
 // Float create node for float value
 func Float(tk *token.Token) *FloatNode {
-	parsedNum := parseNumber("", tk.Value)
-	f, _ := strconv.ParseFloat(parsedNum.String(), 64)
+	var v float64
+	if num := token.ToNumber(tk.Value); num != nil && num.Type == token.NumberTypeFloat {
+		value, ok := num.Value.(float64)
+		if ok {
+			v = value
+		}
+	}
 	return &FloatNode{
 		BaseNode: &BaseNode{},
 		Token:    tk,
-		Value:    f,
+		Value:    v,
 	}
 }
 
diff --git a/decode.go b/decode.go
index 27171f1..afc005c 100644
--- a/decode.go
+++ b/decode.go
@@ -154,6 +154,7 @@ func (d *Decoder) setToMapValue(node ast.Node, m map[string]interface{}) error {
 }
 
 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 {
diff --git a/token/token.go b/token/token.go
index fce8903..2b3d99d 100644
--- a/token/token.go
+++ b/token/token.go
@@ -2,6 +2,7 @@ package token
 
 import (
 	"fmt"
+	"strconv"
 	"strings"
 )
 
@@ -508,104 +509,95 @@ var (
 	}
 )
 
-type numType int
+type NumberType string
 
 const (
-	numTypeNone numType = iota
-	numTypeBinary
-	numTypeOctet
-	numTypeHex
-	numTypeFloat
+	NumberTypeDecimal NumberType = "decimal"
+	NumberTypeBinary  NumberType = "binary"
+	NumberTypeOctet   NumberType = "octet"
+	NumberTypeHex     NumberType = "hex"
+	NumberTypeFloat   NumberType = "float"
 )
 
-type numStat struct {
-	isNum bool
-	typ   numType
+type NumberValue struct {
+	Type  NumberType
+	Value any
+	Text  string
 }
 
-func getNumberStat(value string) *numStat {
-	stat := &numStat{}
-	if value == "" {
-		return stat
+func ToNumber(value string) *NumberValue {
+	if len(value) == 0 {
+		return nil
+	}
+	if strings.HasPrefix(value, "_") {
+		return nil
 	}
 	dotCount := strings.Count(value, ".")
 	if dotCount > 1 {
-		return stat
+		return nil
 	}
 
-	trimmed := strings.TrimPrefix(strings.TrimPrefix(value, "+"), "-")
+	isNegative := strings.HasPrefix(value, "-")
+	normalized := strings.ReplaceAll(strings.TrimPrefix(strings.TrimPrefix(value, "+"), "-"), "_", "")
 
-	var typ numType
+	var (
+		typ  NumberType
+		base int
+	)
 	switch {
-	case strings.HasPrefix(trimmed, "0x"):
-		trimmed = strings.TrimPrefix(trimmed, "0x")
-		typ = numTypeHex
-	case strings.HasPrefix(trimmed, "0o"):
-		trimmed = strings.TrimPrefix(trimmed, "0o")
-		typ = numTypeOctet
-	case strings.HasPrefix(trimmed, "0b"):
-		trimmed = strings.TrimPrefix(trimmed, "0b")
-		typ = numTypeBinary
+	case strings.HasPrefix(normalized, "0x"):
+		normalized = strings.TrimPrefix(normalized, "0x")
+		base = 16
+		typ = NumberTypeHex
+	case strings.HasPrefix(normalized, "0o"):
+		normalized = strings.TrimPrefix(normalized, "0o")
+		base = 8
+		typ = NumberTypeOctet
+	case strings.HasPrefix(normalized, "0b"):
+		normalized = strings.TrimPrefix(normalized, "0b")
+		base = 2
+		typ = NumberTypeBinary
+	case strings.HasPrefix(normalized, "0") && len(normalized) > 1 && dotCount == 0:
+		base = 8
+		typ = NumberTypeOctet
 	case dotCount == 1:
-		typ = numTypeFloat
+		typ = NumberTypeFloat
+	default:
+		typ = NumberTypeDecimal
+		base = 10
 	}
 
-	if trimmed == "" {
-		return stat
+	text := normalized
+	if isNegative {
+		text = "-" + text
 	}
 
-	var numCount int
-	for idx, c := range trimmed {
-		if isNumber(c) {
-			numCount++
-			continue
+	var v any
+	if typ == NumberTypeFloat {
+		f, err := strconv.ParseFloat(text, 64)
+		if err != nil {
+			return nil
 		}
-		switch c {
-		case '_', '.':
-			continue
-		case 'a', 'b', 'c', 'd', 'f', 'A', 'B', 'C', 'D', 'F':
-			if typ != numTypeHex && typ != numTypeBinary {
-				return stat
-			}
-		case 'e', 'E':
-			if typ == numTypeHex || typ == numTypeBinary {
-				continue
-			}
-			if typ != numTypeFloat {
-				return stat
-			}
-
-			// looks like exponent number.
-			if len(trimmed) <= idx+2 {
-				return stat
-			}
-			sign := trimmed[idx+1]
-			if sign != '+' && sign != '-' {
-				return stat
-			}
-			for _, c := range trimmed[idx+2:] {
-				if !isNumber(c) {
-					return stat
-				}
-			}
-			stat.isNum = true
-			stat.typ = typ
-			return stat
-		default:
-			return stat
+		v = f
+	} else if isNegative {
+		i, err := strconv.ParseInt(text, base, 64)
+		if err != nil {
+			return nil
 		}
+		v = i
+	} else {
+		u, err := strconv.ParseUint(text, base, 64)
+		if err != nil {
+			return nil
+		}
+		v = u
 	}
-	if numCount > 1 && trimmed[0] == '0' && typ == numTypeNone {
-		// YAML 1.1 Spec ?
-		typ = numTypeOctet
-	}
-	stat.isNum = true
-	stat.typ = typ
-	return stat
-}
 
-func isNumber(c rune) bool {
-	return c >= '0' && c <= '9'
+	return &NumberValue{
+		Type:  typ,
+		Value: v,
+		Text:  text,
+	}
 }
 
 func looksLikeTimeValue(value string) bool {
@@ -632,7 +624,7 @@ func IsNeedQuoted(value string) bool {
 	if _, exists := reservedEncKeywordMap[value]; exists {
 		return true
 	}
-	if stat := getNumberStat(value); stat.isNum {
+	if num := ToNumber(value); num != nil {
 		return true
 	}
 	first := value[0]
@@ -683,7 +675,7 @@ func New(value string, org string, pos *Position) *Token {
 	if fn != nil {
 		return fn(value, org, pos)
 	}
-	if stat := getNumberStat(value); stat.isNum {
+	if num := ToNumber(value); num != nil {
 		tk := &Token{
 			Type:          IntegerType,
 			CharacterType: CharacterTypeMiscellaneous,
@@ -692,14 +684,14 @@ func New(value string, org string, pos *Position) *Token {
 			Origin:        org,
 			Position:      pos,
 		}
-		switch stat.typ {
-		case numTypeFloat:
+		switch num.Type {
+		case NumberTypeFloat:
 			tk.Type = FloatType
-		case numTypeBinary:
+		case NumberTypeBinary:
 			tk.Type = BinaryIntegerType
-		case numTypeOctet:
+		case NumberTypeOctet:
 			tk.Type = OctetIntegerType
-		case numTypeHex:
+		case NumberTypeHex:
 			tk.Type = HexIntegerType
 		}
 		return tk


Test patch:
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index 001e886..bd2bd24 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -86,6 +86,18 @@ func TestTokenize(t *testing.T) {
 				},
 			},
 		},
+		{
+			YAML: `0.123e+123`,
+			Tokens: token.Tokens{
+				{
+					Type:          token.FloatType,
+					CharacterType: token.CharacterTypeMiscellaneous,
+					Indicator:     token.NotIndicator,
+					Value:         "0.123e+123",
+					Origin:        "0.123e+123",
+				},
+			},
+		},
 		{
 			YAML: `{}
 		`,
@@ -2257,6 +2269,42 @@ s: >-3
 				},
 			},
 		},
+		{
+			YAML: `0b98765`,
+			Tokens: token.Tokens{
+				{
+					Type:          token.StringType,
+					CharacterType: token.CharacterTypeMiscellaneous,
+					Indicator:     token.NotIndicator,
+					Value:         "0b98765",
+					Origin:        "0b98765",
+				},
+			},
+		},
+		{
+			YAML: `098765`,
+			Tokens: token.Tokens{
+				{
+					Type:          token.StringType,
+					CharacterType: token.CharacterTypeMiscellaneous,
+					Indicator:     token.NotIndicator,
+					Value:         "098765",
+					Origin:        "098765",
+				},
+			},
+		},
+		{
+			YAML: `0o98765`,
+			Tokens: token.Tokens{
+				{
+					Type:          token.StringType,
+					CharacterType: token.CharacterTypeMiscellaneous,
+					Indicator:     token.NotIndicator,
+					Value:         "0o98765",
+					Origin:        "0o98765",
+				},
+			},
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.YAML, func(t *testing.T) {
diff --git a/yaml_test.go b/yaml_test.go
index c2dd670..da4461b 100644
--- a/yaml_test.go
+++ b/yaml_test.go
@@ -744,85 +744,51 @@ hoge:
 }
 
 func Test_CommentToMapOption(t *testing.T) {
-	t.Run("line comment", func(t *testing.T) {
-		yml := `
+	type testCase struct {
+		name     string
+		yml      string
+		options  []yaml.DecodeOption
+		expected []struct {
+			path     string
+			comments []*yaml.Comment
+		}
+	}
+
+	tests := []testCase{
+		{
+			name: "line comment",
+			yml: `
 foo: aaa #foo comment
 bar: #bar comment
   bbb: ccc #bbb comment
 baz:
   x: 10 #x comment
-`
-		var (
-			v  interface{}
-			cm = yaml.CommentMap{}
-		)
-		if err := yaml.UnmarshalWithOptions([]byte(yml), &v, yaml.CommentToMap(cm)); err != nil {
-			t.Fatal(err)
-		}
-		expected := []struct {
-			path     string
-			comments []*yaml.Comment
-		}{
-			{
-				path:     "$.foo",
-				comments: []*yaml.Comment{yaml.LineComment("foo comment")},
-			},
-			{
-				path:     "$.bar",
-				comments: []*yaml.Comment{yaml.LineComment("bar comment")},
-			},
-			{
-				path:     "$.bar.bbb",
-				comments: []*yaml.Comment{yaml.LineComment("bbb comment")},
-			},
-			{
-				path:     "$.baz.x",
-				comments: []*yaml.Comment{yaml.LineComment("x comment")},
+`,
+			expected: []struct {
+				path     string
+				comments []*yaml.Comment
+			}{
+				{"$.foo", []*yaml.Comment{yaml.LineComment("foo comment")}},
+				{"$.bar", []*yaml.Comment{yaml.LineComment("bar comment")}},
+				{"$.bar.bbb", []*yaml.Comment{yaml.LineComment("bbb comment")}},
+				{"$.baz.x", []*yaml.Comment{yaml.LineComment("x comment")}},
 			},
-		}
-		for _, exp := range expected {
-			comments := cm[exp.path]
-			if comments == nil {
-				t.Fatalf("failed to get path %s", exp.path)
-			}
-			if diff := cmp.Diff(exp.comments, comments); diff != "" {
-				t.Errorf("(-got, +want)\n%s", diff)
-			}
-		}
-	})
-	t.Run("line comment2", func(t *testing.T) {
-		yml := `
+		},
+		{
+			name: "line comment2",
+			yml: `
 foo:
-  bar: baz # comment`
-		var (
-			v  interface{}
-			cm = yaml.CommentMap{}
-		)
-		if err := yaml.UnmarshalWithOptions([]byte(yml), &v, yaml.CommentToMap(cm)); err != nil {
-			t.Fatal(err)
-		}
-		expected := []struct {
-			path     string
-			comments []*yaml.Comment
-		}{
-			{
-				path:     "$.foo.bar",
-				comments: []*yaml.Comment{yaml.LineComment(" comment")},
+  bar: baz # comment`,
+			expected: []struct {
+				path     string
+				comments []*yaml.Comment
+			}{
+				{"$.foo.bar", []*yaml.Comment{yaml.LineComment(" comment")}},
 			},
-		}
-		for _, exp := range expected {
-			comments := cm[exp.path]
-			if comments == nil {
-				t.Fatalf("failed to get path %s", exp.path)
-			}
-			if diff := cmp.Diff(exp.comments, comments); diff != "" {
-				t.Errorf("(-got, +want)\n%s", diff)
-			}
-		}
-	})
-
-	t.Run("single head comment", func(t *testing.T) {
-		yml := `
+		},
+		{
+			name: "single head comment",
+			yml: `
 #foo comment
 foo: aaa
 #bar comment
@@ -832,47 +798,52 @@ bar:
 baz:
   #x comment
   x: 10
-`
-		var (
-			v  interface{}
-			cm = yaml.CommentMap{}
-		)
-		if err := yaml.UnmarshalWithOptions([]byte(yml), &v, yaml.CommentToMap(cm)); err != nil {
-			t.Fatal(err)
-		}
-		expected := []struct {
-			path     string
-			comments []*yaml.Comment
-		}{
-			{
-				path:     "$.foo",
-				comments: []*yaml.Comment{yaml.HeadComment("foo comment")},
-			},
-			{
-				path:     "$.bar",
-				comments: []*yaml.Comment{yaml.HeadComment("bar comment")},
-			},
-			{
-				path:     "$.bar.bbb",
-				comments: []*yaml.Comment{yaml.HeadComment("bbb comment")},
+`,
+			expected: []struct {
+				path     string
+				comments []*yaml.Comment
+			}{
+				{"$.foo", []*yaml.Comment{yaml.HeadComment("foo comment")}},
+				{"$.bar", []*yaml.Comment{yaml.HeadComment("bar comment")}},
+				{"$.bar.bbb", []*yaml.Comment{yaml.HeadComment("bbb comment")}},
+				{"$.baz.x", []*yaml.Comment{yaml.HeadComment("x comment")}},
 			},
-			{
-				path:     "$.baz.x",
-				comments: []*yaml.Comment{yaml.HeadComment("x comment")},
+		},
+		{
+			name: "single head comment ordered map",
+			yml: `
+#first comment
+first: value
+#second comment
+second:
+  #third comment
+  third: value
+  #forth comment
+  forth: value
+#fifth comment
+fifth:
+  #sixth comment
+  sixth: value
+  #seventh comment
+  seventh: value
+`,
+			expected: []struct {
+				path     string
+				comments []*yaml.Comment
+			}{
+				{"$.first", []*yaml.Comment{yaml.HeadComment("first comment")}},
+				{"$.second", []*yaml.Comment{yaml.HeadComment("second comment")}},
+				{"$.second.third", []*yaml.Comment{yaml.HeadComment("third comment")}},
+				{"$.second.forth", []*yaml.Comment{yaml.HeadComment("forth comment")}},
+				{"$.fifth", []*yaml.Comment{yaml.HeadComment("fifth comment")}},
+				{"$.fifth.sixth", []*yaml.Comment{yaml.HeadComment("sixth comment")}},
+				{"$.fifth.seventh", []*yaml.Comment{yaml.HeadComment("seventh comment")}},
 			},
-		}
-		for _, exp := range expected {
-			comments := cm[exp.path]
-			if comments == nil {
-				t.Fatalf("failed to get path %s", exp.path)
-			}
-			if diff := cmp.Diff(exp.comments, comments); diff != "" {
-				t.Errorf("(-got, +want)\n%s", diff)
-			}
-		}
-	})
-	t.Run("multiple head comments", func(t *testing.T) {
-		yml := `
+			options: []yaml.DecodeOption{yaml.UseOrderedMap()},
+		},
+		{
+			name: "multiple head comments",
+			yml: `
 #foo comment
 #foo comment2
 foo: aaa
@@ -886,67 +857,20 @@ baz:
   #x comment
   #x comment2
   x: 10
-`
-		var (
-			v  interface{}
-			cm = yaml.CommentMap{}
-		)
-		if err := yaml.UnmarshalWithOptions([]byte(yml), &v, yaml.CommentToMap(cm)); err != nil {
-			t.Fatal(err)
-		}
-		expected := []struct {
-			path     string
-			comments []*yaml.Comment
-		}{
-			{
-				path: "$.foo",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(
-						"foo comment",
-						"foo comment2",
-					),
-				},
-			},
-			{
-				path: "$.bar",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(
-						"bar comment",
-						"bar comment2",
-					),
-				},
-			},
-			{
-				path: "$.bar.bbb",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(
-						"bbb comment",
-						"bbb comment2",
-					),
-				},
-			},
-			{
-				path: "$.baz.x",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(
-						"x comment",
-						"x comment2",
-					),
-				},
+`,
+			expected: []struct {
+				path     string
+				comments []*yaml.Comment
+			}{
+				{"$.foo", []*yaml.Comment{yaml.HeadComment("foo comment", "foo comment2")}},
+				{"$.bar", []*yaml.Comment{yaml.HeadComment("bar comment", "bar comment2")}},
+				{"$.bar.bbb", []*yaml.Comment{yaml.HeadComment("bbb comment", "bbb comment2")}},
+				{"$.baz.x", []*yaml.Comment{yaml.HeadComment("x comment", "x comment2")}},
 			},
-		}
-		for _, exp := range expected {
-			comments := cm[exp.path]
-			if comments == nil {
-				t.Fatalf("failed to get path %s", exp.path)
-			}
-			if diff := cmp.Diff(exp.comments, comments); diff != "" {
-				t.Errorf("(-got, +want)\n%s", diff)
-			}
-		}
-	})
-	t.Run("foot comment", func(t *testing.T) {
-		yml := `
+		},
+		{
+			name: "foot comment",
+			yml: `
 bar:
   bbb: ccc
   #ccc: ddd
@@ -956,43 +880,19 @@ baz:
   #- 3
  # foot comment
 #foot comment2
-`
-		var (
-			v  interface{}
-			cm = yaml.CommentMap{}
-		)
-		if err := yaml.UnmarshalWithOptions([]byte(yml), &v, yaml.CommentToMap(cm)); err != nil {
-			t.Fatal(err)
-		}
-		expected := []struct {
-			path     string
-			comments []*yaml.Comment
-		}{
-			{
-				path:     "$.bar.bbb",
-				comments: []*yaml.Comment{yaml.FootComment("ccc: ddd")},
-			},
-			{
-				path:     "$.baz[1]",
-				comments: []*yaml.Comment{yaml.FootComment("- 3")},
-			},
-			{
-				path:     "$.baz",
-				comments: []*yaml.Comment{yaml.FootComment(" foot comment", "foot comment2")},
+`,
+			expected: []struct {
+				path     string
+				comments []*yaml.Comment
+			}{
+				{"$.bar.bbb", []*yaml.Comment{yaml.FootComment("ccc: ddd")}},
+				{"$.baz[1]", []*yaml.Comment{yaml.FootComment("- 3")}},
+				{"$.baz", []*yaml.Comment{yaml.FootComment(" foot comment", "foot comment2")}},
 			},
-		}
-		for _, exp := range expected {
-			comments := cm[exp.path]
-			if comments == nil {
-				t.Fatalf("failed to get path %s", exp.path)
-			}
-			if diff := cmp.Diff(exp.comments, comments); diff != "" {
-				t.Errorf("(-got, +want)\n%s", diff)
-			}
-		}
-	})
-	t.Run("combination", func(t *testing.T) {
-		yml := `
+		},
+		{
+			name: "combination",
+			yml: `
 # foo head comment
 # foo head comment2
 foo: # foo line comment
@@ -1028,138 +928,56 @@ hoge:
   moga: true # moga line comment
   # moga foot comment
 # hoge foot comment
-`
-		var (
-			v  interface{}
-			cm = yaml.CommentMap{}
-		)
-		if err := yaml.UnmarshalWithOptions([]byte(yml), &v, yaml.CommentToMap(cm)); err != nil {
-			t.Fatal(err)
-		}
-		expected := []struct {
-			path     string
-			comments []*yaml.Comment
-		}{
-			{
-				path: "$.foo",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" foo head comment", " foo head comment2"),
-					yaml.LineComment(" foo line comment"),
-				},
-			},
-			{
-				path: "$.foo.a",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" a head comment"),
-					yaml.LineComment(" a line comment"),
-				},
-			},
-			{
-				path: "$.foo.a.b",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" b head comment"),
-					yaml.LineComment(" b line comment"),
-				},
-			},
-			{
-				path: "$.foo.a.b.c",
-				comments: []*yaml.Comment{
-					yaml.LineComment(" c line comment"),
-				},
-			},
-			{
-				path: "$.o",
-				comments: []*yaml.Comment{
-					yaml.LineComment(" o line comment"),
-				},
-			},
-			{
-				path: "$.o.p",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" p head comment", " p head comment2"),
-					yaml.LineComment(" p line comment"),
-				},
-			},
-			{
-				path: "$.o.p.q",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" q head comment", " q head comment2"),
-					yaml.LineComment(" q line comment"),
-				},
-			},
-			{
-				path: "$.o.p.q.r",
-				comments: []*yaml.Comment{
-					yaml.LineComment(" r line comment"),
-				},
-			},
-			{
-				path: "$.t.u",
-				comments: []*yaml.Comment{
-					yaml.LineComment(" u line comment"),
-				},
-			},
-			{
-				path: "$.bar",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" bar head comment"),
-					yaml.LineComment(" bar line comment"),
-				},
-			},
-			{
-				path: "$.bar.bbb",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" bbb head comment"),
-					yaml.LineComment(" bbb line comment"),
-					yaml.FootComment(" bbb foot comment"),
-				},
-			},
-			{
-				path: "$.baz[0]",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" sequence head comment"),
-					yaml.LineComment(" sequence line comment"),
-				},
-			},
-			{
-				path: "$.baz[1]",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" sequence head comment2"),
-					yaml.LineComment(" sequence line comment2"),
-					yaml.FootComment(" sequence foot comment"),
-				},
-			},
-			{
-				path: "$.baz",
-				comments: []*yaml.Comment{
-					yaml.HeadComment(" baz head comment", " baz head comment2"),
-					yaml.LineComment(" baz line comment"),
-				},
-			},
-			{
-				path: "$.hoge",
-				comments: []*yaml.Comment{
-					yaml.FootComment(" hoge foot comment"),
-				},
-			},
-			{
-				path: "$.hoge.moga",
-				comments: []*yaml.Comment{
-					yaml.LineComment(" moga line comment"),
-					yaml.FootComment(" moga foot comment"),
-				},
+`,
+			expected: []struct {
+				path     string
+				comments []*yaml.Comment
+			}{
+				{"$.foo", []*yaml.Comment{yaml.HeadComment(" foo head comment", " foo head comment2"), yaml.LineComment(" foo line comment")}},
+				{"$.foo.a", []*yaml.Comment{yaml.HeadComment(" a head comment"), yaml.LineComment(" a line comment")}},
+				{"$.foo.a.b", []*yaml.Comment{yaml.HeadComment(" b head comment"), yaml.LineComment(" b line comment")}},
+				{"$.foo.a.b.c", []*yaml.Comment{yaml.LineComment(" c line comment")}},
+				{"$.o", []*yaml.Comment{yaml.LineComment(" o line comment")}},
+				{"$.o.p", []*yaml.Comment{yaml.HeadComment(" p head comment", " p head comment2"), yaml.LineComment(" p line comment")}},
+				{"$.o.p.q", []*yaml.Comment{yaml.HeadComment(" q head comment", " q head comment2"), yaml.LineComment(" q line comment")}},
+				{"$.o.p.q.r", []*yaml.Comment{yaml.LineComment(" r line comment")}},
+				{"$.t.u", []*yaml.Comment{yaml.LineComment(" u line comment")}},
+				{"$.bar", []*yaml.Comment{yaml.HeadComment(" bar head comment"), yaml.LineComment(" bar line comment")}},
+				{"$.bar.bbb", []*yaml.Comment{yaml.HeadComment(" bbb head comment"), yaml.LineComment(" bbb line comment"), yaml.FootComment(" bbb foot comment")}},
+				{"$.baz[0]", []*yaml.Comment{yaml.HeadComment(" sequence head comment"), yaml.LineComment(" sequence line comment")}},
+				{"$.baz[1]", []*yaml.Comment{yaml.HeadComment(" sequence head comment2"), yaml.LineComment(" sequence line comment2"), yaml.FootComment(" sequence foot comment")}},
+				{"$.baz", []*yaml.Comment{yaml.HeadComment(" baz head comment", " baz head comment2"), yaml.LineComment(" baz line comment")}},
+				{"$.hoge", []*yaml.Comment{yaml.FootComment(" hoge foot comment")}},
+				{"$.hoge.moga", []*yaml.Comment{yaml.LineComment(" moga line comment"), yaml.FootComment(" moga foot comment")}},
 			},
-		}
-		for _, exp := range expected {
-			comments := cm[exp.path]
-			if comments == nil {
-				t.Fatalf("failed to get path %s", exp.path)
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			cm := yaml.CommentMap{}
+			opts := []yaml.DecodeOption{yaml.CommentToMap(cm)}
+			opts = append(opts, tc.options...)
+
+			var v interface{}
+			if err := yaml.UnmarshalWithOptions([]byte(tc.yml), &v, opts...); err != nil {
+				t.Fatal(err)
 			}
-			if diff := cmp.Diff(exp.comments, comments); diff != "" {
-				t.Errorf("%s: (-got, +want)\n%s", exp.path, diff)
+
+			if len(cm) != len(tc.expected) {
+				t.Fatalf("comment size does not match: got: %d, expected: %d", len(cm), len(tc.expected))
 			}
-		}
-	})
+			for _, exp := range tc.expected {
+				comments := cm[exp.path]
+				if comments == nil {
+					t.Fatalf("failed to get path %s", exp.path)
+				}
+				if diff := cmp.Diff(exp.comments, comments); diff != "" {
+					t.Errorf("(-got, +want)\n%s", diff)
+				}
+			}
+		})
+	}
 }
 
 func TestCommentMapRoundTrip(t *testing.T) {