diff --git a/encode_test.go b/encode_test.go index e71a9992..9e5b713f 100644 --- a/encode_test.go +++ b/encode_test.go @@ -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) + } +} diff --git a/internal/encoder/compiler.go b/internal/encoder/compiler.go index dca68061..d9c3de24 100644 --- a/internal/encoder/compiler.go +++ b/internal/encoder/compiler.go @@ -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: @@ -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: @@ -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 @@ -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. @@ -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()