Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2729,3 +2729,93 @@ func TestIssue459(t *testing.T) {
assertErr(t, err)
assertEq(t, "unexpected result", "{}", string(b))
}

func TestIssue503(t *testing.T) {
type child struct {
Flag *bool `json:"flag,omitempty"`
Label *string `json:"label,omitempty"`
}
// a struct whose only field is a pointer travels inside the interface word
type root struct {
Child *child `json:"child,omitempty"`
}
type rootPlain struct {
Child *child `json:"child"`
}
type rootWide struct {
Child *child `json:"child,omitempty"`
Note string `json:"note"`
}
type rootNested struct {
Mid *root `json:"mid,omitempty"`
}
type zeroInt struct {
V int `json:"v"`
}
type zeroBool struct {
V bool `json:"v"`
}
type zeroString struct {
V string `json:"v"`
}
type zeroIntRoot struct {
C *zeroInt `json:"c,omitempty"`
}
type zeroBoolRoot struct {
C *zeroBool `json:"c,omitempty"`
}
type zeroStringRoot struct {
C *zeroString `json:"c,omitempty"`
}

flag := true
label := "x"

for _, v := range []interface{}{
root{},
root{Child: &child{}},
root{Child: &child{Flag: &flag}},
root{Child: &child{Label: &label}},
root{Child: &child{Flag: &flag, Label: &label}},
rootPlain{},
rootPlain{Child: &child{}},
rootPlain{Child: &child{Label: &label}},
rootWide{Child: &child{Label: &label}, Note: "n"},
rootNested{Mid: &root{Child: &child{Label: &label}}},
zeroIntRoot{C: &zeroInt{V: 0}},
zeroIntRoot{C: &zeroInt{V: 1}},
zeroBoolRoot{C: &zeroBool{V: false}},
zeroBoolRoot{C: &zeroBool{V: true}},
zeroStringRoot{C: &zeroString{V: ""}},
zeroStringRoot{C: &zeroString{V: "a"}},

// the same type reached by an address instead of the interface word
&root{Child: &child{Label: &label}},
[]root{{Child: &child{Label: &label}}, {}},
map[string]root{"k": {Child: &child{Label: &label}}},
struct {
Any interface{} `json:"any"`
}{Any: root{Child: &child{Label: &label}}},
} {
expected, err := stdjson.Marshal(v)
assertErr(t, err)

got, err := json.Marshal(v)
assertErr(t, err)
assertEq(t, "unexpected result", string(expected), string(got))

expectedIndent, err := stdjson.MarshalIndent(v, "", " ")
assertErr(t, err)

gotIndent, err := json.MarshalIndent(v, "", " ")
assertErr(t, err)
assertEq(t, "unexpected indented result", string(expectedIndent), string(gotIndent))

var buf bytes.Buffer
assertErr(t, json.NewEncoder(&buf).Encode(v))
assertEq(t, "unexpected streamed result", string(expected)+"\n", buf.String())

_, err = json.MarshalWithOption(v, json.Colorize(json.DefaultColorScheme))
assertErr(t, err)
}
}
53 changes: 50 additions & 3 deletions internal/encoder/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func (c *Compiler) typeToCode(typ *runtime.Type) (Code, error) {
}
return c.mapCode(typ)
case reflect.Struct:
return c.structCode(typ, isPtr)
return c.structCode(typ, isPtr, true)
case reflect.Int:
return c.intCode(typ, isPtr)
case reflect.Int8:
Expand Down Expand Up @@ -269,7 +269,7 @@ func (c *Compiler) typeToCodeWithPtr(typ *runtime.Type, isPtr bool) (Code, error
case reflect.Map:
return c.mapCode(typ)
case reflect.Struct:
return c.structCode(typ, isPtr)
return c.structCode(typ, isPtr, false)
case reflect.Interface:
return c.interfaceCode(typ, false)
case reflect.Int:
Expand Down Expand Up @@ -592,7 +592,8 @@ func (c *Compiler) mapValueCode(typ *runtime.Type) (Code, error) {
}
}

func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error) {
// isRoot marks the struct the compilation started from: only it can travel inside the interface word
func (c *Compiler) structCode(typ *runtime.Type, isPtr, isRoot bool) (*StructCode, error) {
typeptr := uintptr(unsafe.Pointer(typ))
if code, exists := c.structTypeToCode[typeptr]; exists {
derefCode := *code
Expand Down Expand Up @@ -630,6 +631,11 @@ func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error
if indirect {
// if parent is indirect type, set child indirect property to true
structCode.isIndirect = true
} else if isRoot && !isPtr && isSinglePtrStructField(field, structCode) {
// the interface word already holds the field pointer, nothing may follow it again
field.value = structCode
field.isNextOpPtrType = false
structCode.isIndirect = true
} else {
// if parent is not indirect type, set child indirect property to false.
// but if parent's indirect is false and isPtr is true, then indirect must be true.
Expand All @@ -650,6 +656,47 @@ func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error
return code, nil
}

// isSinglePtrStructField reports whether the field is a single pointer to structCode. a self
// reachable type is excluded: its recursive re-entry reuses the root opcodes, addressed layout and all
func isSinglePtrStructField(field *StructFieldCode, structCode *StructCode) bool {
ptrCode, ok := field.value.(*PtrCode)
if !ok || ptrCode.ptrNum != 1 {
return false
}

value, ok := ptrCode.value.(*StructCode)

return ok && value == structCode && !hasRecursiveCode(structCode, map[*StructCode]struct{}{})
}

func hasRecursiveCode(code Code, seen map[*StructCode]struct{}) bool {
switch c := code.(type) {
case *StructCode:
if c.isRecursive {
return true
}
if _, ok := seen[c]; ok {
return false
}
seen[c] = struct{}{}
for _, field := range c.fields {
if hasRecursiveCode(field.value, seen) {
return true
}
}
case *PtrCode:
return hasRecursiveCode(c.value, seen)
case *SliceCode:
return hasRecursiveCode(c.value, seen)
case *ArrayCode:
return hasRecursiveCode(c.value, seen)
case *MapCode:
return hasRecursiveCode(c.key, seen) || hasRecursiveCode(c.value, seen)
}

return false
}

func toElemType(t *runtime.Type) *runtime.Type {
for t.Kind() == reflect.Ptr {
t = t.Elem()
Expand Down