diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ded0f370..8b24df53 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,3 +18,5 @@ jobs: go-version: "1.24" - name: Run golangci-lint uses: golangci/golangci-lint-action@v9 + with: + version: v2.8.0 diff --git a/AGENTS.md b/AGENTS.md index 14a9d34e..dafe44d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,13 @@ go-toml is a TOML library for Go. The goal is to provide an easy-to-use and effi - Follow existing code format and structure - Code must pass `go fmt` +- Code must pass linting with the same golangci-lint version as CI (see version in `.github/workflows/lint.yml`): + ```bash + # Install specific version (check lint.yml for current version) + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin + # Run linter + golangci-lint run ./... + ``` ### Commit Messages diff --git a/errors.go b/errors.go index 4c5b1373..d68835df 100644 --- a/errors.go +++ b/errors.go @@ -260,7 +260,7 @@ func positionAtEnd(b []byte) (row int, column int) { } } - return + return row, column } // subsliceOffset returns the byte offset of subslice within data. diff --git a/internal/imported_tests/unmarshal_imported_test.go b/internal/imported_tests/unmarshal_imported_test.go index 91c69fc6..ebead59c 100644 --- a/internal/imported_tests/unmarshal_imported_test.go +++ b/internal/imported_tests/unmarshal_imported_test.go @@ -1996,7 +1996,7 @@ func TestDecoderStrict(t *testing.T) { var se *toml.StrictMissingError assert.True(t, errors.As(err, &se)) - keys := []toml.Key{} + keys := make([]toml.Key, 0, len(se.Errors)) for _, e := range se.Errors { keys = append(keys, e.Key()) diff --git a/marshaler.go b/marshaler.go index ba4f0423..9ff3a752 100644 --- a/marshaler.go +++ b/marshaler.go @@ -705,7 +705,14 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte v := iter.Value() if isNil(v) { - continue + // For nil pointers, convert to zero value of the element type. + // This allows round-trip marshaling of maps with nil pointer values. + // For nil interfaces and nil maps, skip since we can't derive a type. + if v.Kind() == reflect.Ptr { + v = reflect.Zero(v.Type().Elem()) + } else { + continue + } } k, err := enc.keyToString(iter.Key()) diff --git a/marshaler_test.go b/marshaler_test.go index 057b0a57..5617e74f 100644 --- a/marshaler_test.go +++ b/marshaler_test.go @@ -619,12 +619,36 @@ hello = 'world' expected: ``, }, { - desc: "nil value in map is ignored", + desc: "nil interface value in map is ignored", v: map[string]interface{}{ "A": nil, }, expected: ``, }, + { + desc: "nil pointer to struct in map produces empty table", + v: map[string]*struct{}{ + "A": nil, + }, + expected: `[A] +`, + }, + { + desc: "nil pointer to int in map produces zero value", + v: map[string]*int{ + "A": nil, + }, + expected: `A = 0 +`, + }, + { + desc: "nil pointer to string in map produces empty string", + v: map[string]*string{ + "A": nil, + }, + expected: `A = '' +`, + }, { desc: "new line in table key", v: map[string]interface{}{ @@ -2193,3 +2217,25 @@ port = 4242 ` assert.Equal(t, expected, string(out)) } + +// TestMarshalIssue975 tests that nil pointer values in maps are marshaled as +// empty tables, allowing round-trip marshaling to work correctly. +// See https://github.com/pelletier/go-toml/issues/975 +func TestMarshalIssue975(t *testing.T) { + // Test case from the issue: map[string]*struct{} + oldMap := map[string]*struct{}{ + "foo": nil, + } + + doc, err := toml.Marshal(&oldMap) + assert.NoError(t, err) + assert.Equal(t, "[foo]\n", string(doc)) + + var newMap map[string]*struct{} + err = toml.Unmarshal(doc, &newMap) + assert.NoError(t, err) + + // Verify the key is preserved after round-trip + _, exists := newMap["foo"] + assert.True(t, exists, "key 'foo' should exist after round-trip") +}