Repository: goccy/go-yaml
Pull request #674 (2025-03-06 01:26:58)

encodeMap() error handling and TestEncoder_UnmarshallableTypes()
## Problem

I encountered an issue where unmarshallable types in maps do not properly handle errors.

`bytes, err := yaml.Marshal(map[string]any{"a": make(chan int)})` returns a nil error and `string(bytes)` is `<nil>\n`.

Similarly, calling Marshal on nested `map[string]any{"a": map[string]any{"b": make(chan string)}}` returns a nil error and `string(bytes)` is `%!v(PANIC=String method: runtime error: invalid memory address or nil pointer dereference)\n`

## Solution

Add error to return value in `func (e *Encoder) encodeMap(ctx context.Context, value reflect.Value, column int) (ast.Node, error)` and return the error when value can't be encoded.

## Testing

Add new `func TestEncoder_UnmarshallableTypes(t *testing.T)` with these test cases:

* channel
* function
* complex number
* unsafe pointer
* uintptr
* map with channel
* nested map with func
* slice with channel
* nested slice with complex number
* struct with unsafe pointer

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/674?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.84%. Comparing base [(`944206a`)](https://app.codecov.io/gh/goccy/go-yaml/commit/944206aba524110bfefdf0c53d864ce45257651c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) to head [(`84979cc`)](https://app.codecov.io/gh/goccy/go-yaml/commit/84979cc11ee43e77dbd8435cf1040274dff74266?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     #674      +/-   ##
==========================================
+ Coverage   77.63%   77.84%   +0.21%     
==========================================
  Files          22       22              
  Lines        7959     7959              
==========================================
+ Hits         6179     6196      +17     
+ Misses       1362     1350      -12     
+ Partials      418      413       -5     
```

</details>
<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/encode.go b/encode.go
index 0a83017..e0c795e 100644
--- a/encode.go
+++ b/encode.go
@@ -492,7 +492,7 @@ func (e *Encoder) encodeValue(ctx context.Context, v reflect.Value, column int)
 		if value := e.encodePtrAnchor(v, column); value != nil {
 			return value, nil
 		}
-		return e.encodeMap(ctx, v, column), nil
+		return e.encodeMap(ctx, v, column)
 	default:
 		return nil, fmt.Errorf("unknown value type %s", v.Type().String())
 	}
@@ -684,7 +684,7 @@ func (e *Encoder) isTagAndMapNode(node ast.Node) bool {
 	return ok && e.isMapNode(tn.Value)
 }
 
-func (e *Encoder) encodeMap(ctx context.Context, value reflect.Value, column int) ast.Node {
+func (e *Encoder) encodeMap(ctx context.Context, value reflect.Value, column int) (ast.Node, error) {
 	node := ast.Mapping(token.New("", "", e.pos(column)), e.isFlowStyle)
 	keys := make([]interface{}, len(value.MapKeys()))
 	for i, k := range value.MapKeys() {
@@ -698,7 +698,7 @@ func (e *Encoder) encodeMap(ctx context.Context, value reflect.Value, column int
 		v := value.MapIndex(k)
 		value, err := e.encodeValue(ctx, v, column)
 		if err != nil {
-			return nil
+			return nil, err
 		}
 		if e.isMapNode(value) {
 			value.AddColumn(e.indentNum)
@@ -724,7 +724,7 @@ func (e *Encoder) encodeMap(ctx context.Context, value reflect.Value, column int
 		))
 		e.setSmartAnchor(vRef, keyText)
 	}
-	return node
+	return node, nil
 }
 
 // IsZeroer is used to check whether an object is zero to determine


Test patch:
diff --git a/encode_test.go b/encode_test.go
index ef507c9..a82988f 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 	"testing"
 	"time"
+	"unsafe"
 
 	"github.com/goccy/go-yaml"
 	"github.com/goccy/go-yaml/ast"
@@ -1345,6 +1346,81 @@ func TestEncoder_MultipleDocuments(t *testing.T) {
 	}
 }
 
+func TestEncoder_UnmarshallableTypes(t *testing.T) {
+	for _, test := range []struct {
+		desc        string
+		input       any
+		expectedErr string
+	}{
+		{
+			desc:        "channel",
+			input:       make(chan int),
+			expectedErr: "unknown value type chan int",
+		},
+		{
+			desc:        "function",
+			input:       func() {},
+			expectedErr: "unknown value type func()",
+		},
+		{
+			desc:        "complex number",
+			input:       complex(10, 11),
+			expectedErr: "unknown value type complex128",
+		},
+		{
+			desc:        "unsafe pointer",
+			input:       unsafe.Pointer(&struct{}{}),
+			expectedErr: "unknown value type unsafe.Pointer",
+		},
+		{
+			desc:        "uintptr",
+			input:       uintptr(0x1234),
+			expectedErr: "unknown value type uintptr",
+		},
+		{
+			desc:        "map with channel",
+			input:       map[string]any{"key": make(chan string)},
+			expectedErr: "unknown value type chan string",
+		},
+		{
+			desc: "nested map with func",
+			input: map[string]any{
+				"a": map[string]any{
+					"b": func(_ string) {},
+				},
+			},
+			expectedErr: "unknown value type func(string)",
+		},
+		{
+			desc:        "slice with channel",
+			input:       []any{make(chan bool)},
+			expectedErr: "unknown value type chan bool",
+		},
+		{
+			desc:        "nested slice with complex number",
+			input:       []any{[]any{complex(10, 11)}},
+			expectedErr: "unknown value type complex128",
+		},
+		{
+			desc: "struct with unsafe pointer",
+			input: struct {
+				Field unsafe.Pointer `yaml:"field"`
+			}{},
+			expectedErr: "unknown value type unsafe.Pointer",
+		},
+	} {
+		t.Run(test.desc, func(t *testing.T) {
+			var buf bytes.Buffer
+			err := yaml.NewEncoder(&buf).Encode(test.input)
+			if err == nil {
+				t.Errorf("expect error:\n%s\nbut got none\n", test.expectedErr)
+			} else if err.Error() != test.expectedErr {
+				t.Errorf("expect error:\n%s\nactual\n%s\n", test.expectedErr, err)
+			}
+		})
+	}
+}
+
 func ExampleMarshal_node() {
 	type T struct {
 		Text ast.Node `yaml:"text"`