Repository: goccy/go-yaml
Pull request #376 (2023-07-03 05:01:31)

Fix handle of space at start or last

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/goccy/go-yaml/pull/376?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) Report
> Merging [#376](https://app.codecov.io/gh/goccy/go-yaml/pull/376?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) (6fd1dab) into [master](https://app.codecov.io/gh/goccy/go-yaml/commit/6a080f2015646314d815754c1284506cd93408c8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) (6a080f2) will **not change** coverage.
> The diff coverage is `100.00%`.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima).

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


```diff
@@           Coverage Diff           @@
##           master     #376   +/-   ##
=======================================
  Coverage   75.61%   75.61%           
=======================================
  Files          13       13           
  Lines        4650     4650           
=======================================
  Hits         3516     3516           
  Misses        874      874           
  Partials      260      260           
```

</details>
ozraru: @goccy Could you review, please?
goccy: @ozraru Basically, the spaces before and after are automatically removed and converted to tokens, so please add specific use cases to the test cases (e.g. `encode_test.go` and `decode_test.go` )
ozraru: @goccy done

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db9f0ce..afbfda9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+# 1.11.1 - 2023-09-14
+
+### Fix bugs
+
+- Handle `\r` in a double-quoted string the same as `\n` ( #372 )
+- Replace loop with n.Values = append(n.Values, target.Values...) ( #380 )
+- Skip encoding an inline field if it is null ( #386 )
+- Fix comment parsing with null value ( #388 )
+
 # 1.11.0 - 2023-04-03
 
 ### Features
diff --git a/encode.go b/encode.go
index 7d8d81e..3b9b298 100644
--- a/encode.go
+++ b/encode.go
@@ -823,6 +823,10 @@ func (e *Encoder) encodeStruct(ctx context.Context, value reflect.Value, column
 			}
 			mapNode, ok := value.(ast.MapNode)
 			if !ok {
+				// if an inline field is null, skip encoding it
+				if _, ok := value.(*ast.NullNode); ok {
+					continue
+				}
 				return nil, xerrors.Errorf("inline value is must be map or struct type")
 			}
 			mapIter := mapNode.MapRange()
diff --git a/parser/parser.go b/parser/parser.go
index 568e6ad..6989a37 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -156,15 +156,38 @@ func (p *parser) createMapValueNode(ctx *context, key ast.MapKeyNode, colonToken
 		ctx.insertToken(ctx.idx, nullToken)
 		return ast.Null(nullToken), nil
 	}
-
+	var comment *ast.CommentGroupNode
+	if tk.Type == token.CommentType {
+		comment = p.parseCommentOnly(ctx)
+		if comment != nil {
+			comment.SetPath(ctx.withChild(key.GetToken().Value).path)
+		}
+		tk = ctx.currentToken()
+	}
 	if tk.Position.Column == key.GetToken().Position.Column && tk.Type == token.StringType {
 		// in this case,
 		// ----
 		// key: <value does not defined>
 		// next
+
 		nullToken := p.createNullToken(colonToken)
 		ctx.insertToken(ctx.idx, nullToken)
-		return ast.Null(nullToken), nil
+		nullNode := ast.Null(nullToken)
+
+		if comment != nil {
+			nullNode.SetComment(comment)
+		} else {
+			// If there is a comment, it is already bound to the key node,
+			// so remove the comment from the key to bind it to the null value.
+			keyComment := key.GetComment()
+			if keyComment != nil {
+				if err := key.SetComment(nil); err != nil {
+					return nil, err
+				}
+				nullNode.SetComment(keyComment)
+			}
+		}
+		return nullNode, nil
 	}
 
 	if tk.Position.Column < key.GetToken().Position.Column {
@@ -174,13 +197,20 @@ func (p *parser) createMapValueNode(ctx *context, key ast.MapKeyNode, colonToken
 		// next
 		nullToken := p.createNullToken(colonToken)
 		ctx.insertToken(ctx.idx, nullToken)
-		return ast.Null(nullToken), nil
+		nullNode := ast.Null(nullToken)
+		if comment != nil {
+			nullNode.SetComment(comment)
+		}
+		return nullNode, nil
 	}
 
 	value, err := p.parseToken(ctx, ctx.currentToken())
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to parse mapping 'value' node")
 	}
+	if comment != nil {
+		value.SetComment(comment)
+	}
 	return value, nil
 }
 
diff --git a/token/token.go b/token/token.go
index 182f4be..c86caab 100644
--- a/token/token.go
+++ b/token/token.go
@@ -623,12 +623,12 @@ func IsNeedQuoted(value string) bool {
 	}
 	first := value[0]
 	switch first {
-	case '*', '&', '[', '{', '}', ']', ',', '!', '|', '>', '%', '\'', '"', '@':
+	case '*', '&', '[', '{', '}', ']', ',', '!', '|', '>', '%', '\'', '"', '@', ' ':
 		return true
 	}
 	last := value[len(value)-1]
 	switch last {
-	case ':':
+	case ':', ' ':
 		return true
 	}
 	if looksLikeTimeValue(value) {


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 0ac4a7f..2373804 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -67,6 +67,18 @@ func TestDecoder(t *testing.T) {
 			"v: 1.234\n",
 			map[string]string{"v": "1.234"},
 		},
+		{
+			"v: \" foo\"\n",
+			map[string]string{"v": " foo"},
+		},
+		{
+			"v: \"foo \"\n",
+			map[string]string{"v": "foo "},
+		},
+		{
+			"v: \" foo \"\n",
+			map[string]string{"v": " foo "},
+		},
 		{
 			"v: false\n",
 			map[string]bool{"v": false},
diff --git a/encode_test.go b/encode_test.go
index 74b1aa5..3ff6f1c 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -303,6 +303,21 @@ func TestEncoder(t *testing.T) {
 			map[string]string{"a": "Hello #comment"},
 			nil,
 		},
+		{
+			"a: \" b\"\n",
+			map[string]string{"a": " b"},
+			nil,
+		},
+		{
+			"a: \"b \"\n",
+			map[string]string{"a": "b "},
+			nil,
+		},
+		{
+			"a: \" b \"\n",
+			map[string]string{"a": " b "},
+			nil,
+		},
 		{
 			"a: 100.5\n",
 			map[string]interface{}{
@@ -980,6 +995,30 @@ c: true
 	}
 }
 
+func TestEncoder_InlineNil(t *testing.T) {
+	type base struct {
+		A int
+		B string
+	}
+	var buf bytes.Buffer
+	enc := yaml.NewEncoder(&buf)
+	if err := enc.Encode(struct {
+		*base `yaml:",inline"`
+		C     bool
+	}{
+		C: true,
+	}); err != nil {
+		t.Fatalf("%+v", err)
+	}
+	expect := `
+c: true
+`
+	actual := "\n" + buf.String()
+	if expect != actual {
+		t.Fatalf("inline marshal error: expect=[%s] actual=[%s]", expect, actual)
+	}
+}
+
 func TestEncoder_Flow(t *testing.T) {
 	var buf bytes.Buffer
 	enc := yaml.NewEncoder(&buf, yaml.Flow(true))
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 6044699..d24b262 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -808,6 +808,74 @@ foo: > # comment
 	}
 }
 
+func TestCommentWithNull(t *testing.T) {
+	t.Run("same line", func(t *testing.T) {
+		content := `
+foo:
+  bar: # comment
+  baz: 1
+`
+		expected := `
+foo:
+  bar: null # comment
+  baz: 1`
+		f, err := parser.ParseBytes([]byte(content), parser.ParseComments)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(f.Docs) != 1 {
+			t.Fatal("failed to parse content with same line comment")
+		}
+		if f.Docs[0].String() != strings.TrimPrefix(expected, "\n") {
+			t.Fatal("failed to parse comment")
+		}
+	})
+	t.Run("next line", func(t *testing.T) {
+		content := `
+foo:
+  bar:
+    # comment
+  baz: 1
+`
+		expected := `
+foo:
+  bar: null # comment
+  baz: 1`
+		f, err := parser.ParseBytes([]byte(content), parser.ParseComments)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(f.Docs) != 1 {
+			t.Fatal("failed to parse content with next line comment")
+		}
+		if f.Docs[0].String() != strings.TrimPrefix(expected, "\n") {
+			t.Fatal("failed to parse comment")
+		}
+	})
+	t.Run("next line and different indent", func(t *testing.T) {
+		content := `
+foo:
+  bar:
+ # comment
+baz: 1`
+		f, err := parser.ParseBytes([]byte(content), parser.ParseComments)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(f.Docs) != 1 {
+			t.Fatal("failed to parse content with next line comment")
+		}
+		expected := `
+foo:
+  bar: null # comment
+baz: 1`
+		if f.Docs[0].String() != strings.TrimPrefix(expected, "\n") {
+			t.Fatal("failed to parse comment")
+		}
+	})
+
+}
+
 func TestNodePath(t *testing.T) {
 	yml := `
 a: # commentA
diff --git a/token/token_test.go b/token/token_test.go
index 4f5764f..dc1b4df 100644
--- a/token/token_test.go
+++ b/token/token_test.go
@@ -118,6 +118,9 @@ func TestIsNeedQuoted(t *testing.T) {
 		"Off",
 		"OFF",
 		"@test",
+		" a",
+		" a ",
+		"a ",
 	}
 	for i, test := range needQuotedTests {
 		if !token.IsNeedQuoted(test) {