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
|
schema walk recursed through the embedded type forever, so such a
|
||||||
`Unmarshal` call hung the process; the walk now tracks the struct types on
|
`Unmarshal` call hung the process; the walk now tracks the struct types on
|
||||||
the current path and stops when one repeats.
|
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
|
- 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
|
(`a = {b = {}}` followed by `[[a.b.c]]`) is rejected. The frozen-inline-table
|
||||||
check covered `[table]` headers and dotted keys but not the intermediate
|
check covered `[table]` headers and dotted keys but not the intermediate
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -323,6 +324,15 @@ func newStructSchema(t reflect.Type) structSchema {
|
|||||||
return s
|
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
|
// fieldByIndex walks an index path from a struct value, allocating nil
|
||||||
// pointers along the way so a key can reach through an embedded pointer
|
// 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.
|
// 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 ---------------------------------------------
|
// --- reflection walk: struct ---------------------------------------------
|
||||||
|
|
||||||
func buildStructDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
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()
|
t := v.Type()
|
||||||
for i := range t.NumField() {
|
for i := range t.NumField() {
|
||||||
if i%ctxCheckInterval == 0 {
|
if i%ctxCheckInterval == 0 {
|
||||||
@@ -155,6 +165,7 @@ func buildStructDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
|||||||
if f.PkgPath != "" {
|
if f.PkgPath != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
path := append(append([]int{}, prefix...), i)
|
||||||
if f.Anonymous {
|
if f.Anonymous {
|
||||||
tag, _ := f.Tag.Lookup("toml")
|
tag, _ := f.Tag.Lookup("toml")
|
||||||
if tag == "-" {
|
if tag == "-" {
|
||||||
@@ -169,12 +180,15 @@ func buildStructDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
|||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
if isScalarStruct(fv.Type()) {
|
if isScalarStruct(fv.Type()) {
|
||||||
name := strings.ToLower(f.Name)
|
name := strings.ToLower(f.Name)
|
||||||
|
if !schema.ownsKey(name, path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := doc.appendScalar(name, fv.Interface(), ctx); err != nil {
|
if err := doc.appendScalar(name, fv.Interface(), ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := buildStructDoc(fv, doc, ctx); err != nil {
|
if err := walkStructDoc(fv, doc, ctx, path, schema); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -190,6 +204,9 @@ func buildStructDoc(v reflect.Value, doc *tomlDoc, ctx string) error {
|
|||||||
if name == "-" {
|
if name == "-" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !schema.ownsKey(strings.ToLower(name), path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if fieldOmitted(f, v.Field(i)) {
|
if fieldOmitted(f, v.Field(i)) {
|
||||||
continue
|
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) {
|
func TestMarshalEmbeddedScalarStruct(t *testing.T) {
|
||||||
// A field declared directly as a scalar-struct type (here LocalDateTime)
|
// 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
|
// must be encoded as a TOML scalar at the parent level, not rendered as
|
||||||
|
|||||||
Reference in New Issue
Block a user