fix(encode): emit one key when two fields resolve to one name
Assisted-by: GLM 5.3
This commit is contained in:
@@ -49,6 +49,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
schema walk recursed through the embedded type forever, so such a
|
||||
`Unmarshal` call hung the process; the walk now tracks the struct types on
|
||||
the current path and stops when one repeats.
|
||||
- `Marshal` emits exactly one key when two struct fields resolve to the same
|
||||
TOML name, picking the field the decoder would fill (the shallower one, the
|
||||
later declaration at equal depth). Such a struct previously marshalled into
|
||||
a duplicate key, and the output never re-parsed, breaking the round-trip
|
||||
guarantee.
|
||||
- An array-of-tables header whose path runs through an inline table
|
||||
(`a = {b = {}}` followed by `[[a.b.c]]`) is rejected. The frozen-inline-table
|
||||
check covered `[table]` headers and dotted keys but not the intermediate
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -323,6 +324,15 @@ func newStructSchema(t reflect.Type) structSchema {
|
||||
return s
|
||||
}
|
||||
|
||||
// ownsKey reports whether the field at path is the one that resolves key.
|
||||
// The encoder consults it to emit exactly the field the decoder would fill,
|
||||
// so a struct with two fields mapping to one key does not marshal into a
|
||||
// duplicate TOML key.
|
||||
func (s structSchema) ownsKey(key string, path []int) bool {
|
||||
loc, ok := s.byName[key]
|
||||
return ok && slices.Equal(loc.index, path)
|
||||
}
|
||||
|
||||
// fieldByIndex walks an index path from a struct value, allocating nil
|
||||
// pointers along the way so a key can reach through an embedded pointer
|
||||
// struct. Every field on the path is exported, so each step is settable.
|
||||
|
||||
@@ -144,6 +144,16 @@ func (d *tomlDoc) partitionedEntries() (scalars []entry, tables []entry, arrays
|
||||
// --- reflection walk: struct ---------------------------------------------
|
||||
|
||||
func buildStructDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
||||
return walkStructDoc(v, doc, ctx, nil, cachedStructSchema(v.Type()))
|
||||
}
|
||||
|
||||
// walkStructDoc emits the fields of v into doc. prefix is v's index path from
|
||||
// the struct whose schema resolves key conflicts; an embedded struct is walked
|
||||
// with the outer schema and a longer prefix, so every leaf competes under the
|
||||
// decoder's rule: the shallower field wins, the later declaration at equal
|
||||
// depth. A field another field shadows is skipped, because emitting both
|
||||
// would duplicate the key and the output would not re-parse.
|
||||
func walkStructDoc(v reflect.Value, doc *tomlDoc, ctx string, prefix []int, schema structSchema) error {
|
||||
t := v.Type()
|
||||
for i := range t.NumField() {
|
||||
if i%ctxCheckInterval == 0 {
|
||||
@@ -155,6 +165,7 @@ func buildStructDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
||||
if f.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
path := append(append([]int{}, prefix...), i)
|
||||
if f.Anonymous {
|
||||
tag, _ := f.Tag.Lookup("toml")
|
||||
if tag == "-" {
|
||||
@@ -169,12 +180,15 @@ func buildStructDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
||||
case reflect.Struct:
|
||||
if isScalarStruct(fv.Type()) {
|
||||
name := strings.ToLower(f.Name)
|
||||
if !schema.ownsKey(name, path) {
|
||||
continue
|
||||
}
|
||||
if err := doc.appendScalar(name, fv.Interface(), ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := buildStructDoc(fv, doc, ctx); err != nil {
|
||||
if err := walkStructDoc(fv, doc, ctx, path, schema); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
@@ -190,6 +204,9 @@ func buildStructDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
||||
if name == "-" {
|
||||
continue
|
||||
}
|
||||
if !schema.ownsKey(strings.ToLower(name), path) {
|
||||
continue
|
||||
}
|
||||
if fieldOmitted(f, v.Field(i)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -383,6 +383,42 @@ func TestMarshalerErrorPropagates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Two fields that resolve to one TOML key must marshal as one key, resolved
|
||||
// the way the decoder resolves it, or the output would carry a duplicate key
|
||||
// and never re-parse.
|
||||
func TestMarshalDuplicateKeyResolvesToOneField(t *testing.T) {
|
||||
type SameLevel struct {
|
||||
First int `toml:"v"`
|
||||
Second string `toml:"v"`
|
||||
}
|
||||
out, err := Marshal(SameLevel{First: 1, Second: "s"})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if want := "v = \"s\"\n"; string(out) != want {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
|
||||
type Base struct {
|
||||
Name string `toml:"name"`
|
||||
}
|
||||
type Embedded struct {
|
||||
Base
|
||||
Name string `toml:"name"`
|
||||
}
|
||||
out, err = Marshal(Embedded{Base: Base{Name: "inner"}, Name: "outer"})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
// The shallower field wins, matching the decoder.
|
||||
if want := "name = \"outer\"\n"; string(out) != want {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
if _, err := Parse(out); err != nil {
|
||||
t.Errorf("re-parse: %v\ndoc:\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalEmbeddedScalarStruct(t *testing.T) {
|
||||
// A field declared directly as a scalar-struct type (here LocalDateTime)
|
||||
// must be encoded as a TOML scalar at the parent level, not rendered as
|
||||
|
||||
Reference in New Issue
Block a user