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
212 changes: 212 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2729,3 +2729,215 @@ func TestIssue459(t *testing.T) {
assertErr(t, err)
assertEq(t, "unexpected result", "{}", string(b))
}

type issue503IgnoredBackReferenceRoot struct {
Child *issue503IgnoredBackReferenceChild `json:"child,omitempty"`
}

type issue503IgnoredBackReferenceChild struct {
Root *issue503IgnoredBackReferenceRoot `json:"-"`
First *int `json:"first,omitempty"`
Value *string `json:"value,omitempty"`
}

type issue503UnexportedBackReferenceRoot struct {
Child *issue503UnexportedBackReferenceChild `json:"child,omitempty"`
}

type issue503UnexportedBackReferenceChild struct {
root *issue503UnexportedBackReferenceRoot
First *int `json:"first,omitempty"`
Value *string `json:"value,omitempty"`
}

type issue503RecursiveRoot struct {
Child *issue503RecursiveChild `json:"child,omitempty"`
}

type issue503RecursiveChild struct {
Root *issue503RecursiveRoot `json:"root,omitempty"`
Value string `json:"value,omitempty"`
}

func TestIssue503(t *testing.T) {
type Child struct {
First int `json:"first"`
Later string `json:"later"`
}
type DirectRoot struct {
Child *Child `json:"child,omitempty"`
}
type BoolChild struct {
First bool `json:"first"`
}
type BoolRoot struct {
Child *BoolChild `json:"child,omitempty"`
}
type StringChild struct {
First string `json:"first"`
}
type StringRoot struct {
Child *StringChild `json:"child,omitempty"`
}
type IndirectRoot struct {
Child *Child `json:"child,omitempty"`
Tail string `json:"tail"`
}
type PrefixedRoot struct {
Prefix string `json:"prefix"`
Child *Child `json:"child,omitempty"`
}
type DoubleRoot struct {
Child **Child `json:"child,omitempty"`
}
type Nested struct {
Child *Child `json:"child,omitempty"`
}
type NestedRoot struct {
Nested *Nested `json:"nested,omitempty"`
}
type NestedWithTail struct {
Child *Child `json:"child,omitempty"`
Tail string `json:"tail"`
}
type NestedWithTailRoot struct {
Nested *NestedWithTail `json:"nested,omitempty"`
}
type PointerFieldChild struct {
First *int `json:"first,omitempty"`
Value *string `json:"value,omitempty"`
}
type PointerFieldRoot struct {
Child *PointerFieldChild `json:"child,omitempty"`
}
type DoublePointerFieldRoot struct {
Child **PointerFieldChild `json:"child,omitempty"`
}
type IgnoredMarkerRoot struct {
Marker struct{} `json:"-"`
Child *PointerFieldChild `json:"child,omitempty"`
}
type SelfRecursiveRoot struct {
Next *SelfRecursiveRoot `json:"next,omitempty"`
}

var nilChild *Child
child := &Child{}
pointerValue := "present"
pointerFieldChild := &PointerFieldChild{Value: &pointerValue}
tests := []struct {
name string
value interface{}
}{
{
name: "direct pointer with zero first and non-zero later field",
value: DirectRoot{Child: &Child{Later: "kept"}},
},
{
name: "direct pointer with all-zero fields",
value: DirectRoot{Child: &Child{}},
},
{
name: "direct pointer with false first field",
value: BoolRoot{Child: &BoolChild{}},
},
{
name: "direct pointer with empty string first field",
value: StringRoot{Child: &StringChild{}},
},
{
name: "direct nil pointer",
value: DirectRoot{},
},
{
name: "indirect root with pointer first",
value: IndirectRoot{Child: &Child{}, Tail: "tail"},
},
{
name: "indirect root with pointer after prefix",
value: PrefixedRoot{Prefix: "prefix", Child: &Child{}},
},
{
name: "double pointer to zero-value child",
value: DoubleRoot{Child: &child},
},
{
name: "double pointer with nil inner pointer",
value: DoubleRoot{Child: &nilChild},
},
{
name: "double nil pointer",
value: DoubleRoot{},
},
{
name: "nested non-nil pointers",
value: NestedRoot{Nested: &Nested{Child: &Child{}}},
},
{
name: "nested nil child pointer",
value: NestedRoot{Nested: &Nested{}},
},
{
name: "nested nil child pointer with non-zero later field",
value: NestedWithTailRoot{Nested: &NestedWithTail{Tail: "kept"}},
},
{
name: "nested child pointer with non-zero later field",
value: NestedWithTailRoot{Nested: &NestedWithTail{Child: &Child{}, Tail: "kept"}},
},
{
name: "nested nil pointer",
value: NestedRoot{},
},
{
name: "direct pointer with pointer fields",
value: PointerFieldRoot{Child: pointerFieldChild},
},
{
name: "direct double pointer with pointer fields",
value: DoublePointerFieldRoot{Child: &pointerFieldChild},
},
{
name: "direct pointer after ignored zero-size marker",
value: IgnoredMarkerRoot{Child: pointerFieldChild},
},
{
name: "direct pointer with ignored tagged back reference",
value: issue503IgnoredBackReferenceRoot{Child: &issue503IgnoredBackReferenceChild{Value: &pointerValue}},
},
{
name: "direct pointer with unexported back reference",
value: issue503UnexportedBackReferenceRoot{Child: &issue503UnexportedBackReferenceChild{Value: &pointerValue}},
},
{
name: "visible recursive back reference",
value: issue503RecursiveRoot{Child: &issue503RecursiveChild{Value: "present"}},
},
{
name: "self-recursive root",
value: SelfRecursiveRoot{Next: &SelfRecursiveRoot{}},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
expected, err := stdjson.Marshal(test.value)
assertErr(t, err)
got, err := json.Marshal(test.value)
assertErr(t, err)
assertEq(t, "unexpected result", string(expected), string(got))
got, err = json.MarshalWithOption(test.value, json.Colorize(&json.ColorScheme{}))
assertErr(t, err)
assertEq(t, "unexpected color result", string(expected), string(got))

expected, err = stdjson.MarshalIndent(test.value, "", " ")
assertErr(t, err)
got, err = json.MarshalIndent(test.value, "", " ")
assertErr(t, err)
assertEq(t, "unexpected indented result", string(expected), string(got))
got, err = json.MarshalIndentWithOption(test.value, "", " ", json.Colorize(&json.ColorScheme{}))
assertErr(t, err)
assertEq(t, "unexpected indented color result", string(expected), string(got))
})
}
}
29 changes: 26 additions & 3 deletions internal/cmd/generator/vm.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,28 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
code = code.End.Next
break
}
store(ctxptr, code.Idx, ptrToNPtr(p, code.PtrNum))
fallthrough
if (code.Flags & encoder.IndirectFlags) != 0 {
p = ptrToNPtr(p, code.PtrNum)
if p == 0 {
if code.Flags&encoder.AnonymousHeadFlags == 0 {
b = appendNullComma(ctx, b)
}
code = code.End.Next
break
}
}
store(ctxptr, code.Idx, p)
if code.Flags&encoder.AnonymousHeadFlags == 0 {
b = appendStructHead(ctx, b)
}
p += uintptr(code.Offset)
if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && ptrToPtr(p) == 0 {
code = code.NextField
} else {
b = appendStructKey(ctx, code, b)
code = code.Next
store(ctxptr, code.Idx, p)
}
case encoder.OpStructHeadOmitEmpty:
p := load(ctxptr, code.Idx)
if p == 0 && ((code.Flags&encoder.IndirectFlags) != 0 || code.Next.Op == encoder.OpStructEnd) {
Expand All @@ -582,9 +602,12 @@ func Run(ctx *encoder.RuntimeContext, b []byte, codeSet *encoder.OpcodeSet) ([]b
b = appendStructHead(ctx, b)
}
p += uintptr(code.Offset)
if p == 0 || (ptrToPtr(p) == 0 && (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0) {
if p == 0 || ((code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) != 0 && ptrToPtr(p) == 0) {
code = code.NextField
} else {
if (code.Flags&encoder.IsNextOpPtrTypeFlags) != 0 && (code.Flags&encoder.IndirectFlags) == 0 && code.PtrNum > 1 {
p = ptrToNPtr(p, code.PtrNum-1)
}
b = appendStructKey(ctx, code, b)
code = code.Next
store(ctxptr, code.Idx, p)
Expand Down
61 changes: 60 additions & 1 deletion internal/encoder/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func getFilteredCodeSetIfNeeded(ctx *RuntimeContext, codeSet *OpcodeSet) (*Opcod

type Compiler struct {
structTypeToCode map[uintptr]*StructCode
rootType *runtime.Type
}

func newCompiler() *Compiler {
Expand All @@ -130,6 +131,7 @@ func newCompiler() *Compiler {
func (c *Compiler) compile(typeptr uintptr) (*OpcodeSet, error) {
// noescape trick for header.typ ( reflect.*rtype )
typ := *(**runtime.Type)(unsafe.Pointer(&typeptr))
c.rootType = typ
code, err := c.typeToCode(typ)
if err != nil {
return nil, err
Expand Down Expand Up @@ -630,7 +632,7 @@ 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 {
} else if !c.normalizeDirectInterfaceStructField(code, field, structCode, isPtr) {
// 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.
// Do this only if indirectConversion is enabled at the end of compileStruct.
Expand All @@ -650,6 +652,63 @@ func (c *Compiler) structCode(typ *runtime.Type, isPtr bool) (*StructCode, error
return code, nil
}

// normalizeDirectInterfaceStructField removes the pointer level already consumed by
// the interface representation of a direct-interface root struct. Nested instances
// still use the regular addressed representation, so recursive roots are unchanged.
func (c *Compiler) normalizeDirectInterfaceStructField(parent *StructCode, field *StructFieldCode, child *StructCode, isPtr bool) bool {
if isPtr || parent.typ != c.rootType || runtime.IfaceIndir(parent.typ) {
return false
}
ptr, ok := field.value.(*PtrCode)
if !ok || ptr.ptrNum == 0 || ptr.value != child {
return false
}
if codeReferencesType(child, parent.typ, map[Code]struct{}{}) {
return false
}

if ptr.ptrNum == 1 {
field.value = child
field.isNextOpPtrType = false
} else {
field.value = &PtrCode{
typ: ptr.typ.Elem(),
value: ptr.value,
ptrNum: ptr.ptrNum - 1,
}
}
child.isIndirect = true
return true
}

func codeReferencesType(code Code, target *runtime.Type, seen map[Code]struct{}) bool {
if structCode, ok := code.(*StructCode); ok && structCode.typ == target {
return true
}
if _, exists := seen[code]; exists {
return false
}
seen[code] = struct{}{}

switch code := code.(type) {
case *PtrCode:
return codeReferencesType(code.value, target, seen)
case *StructCode:
for _, field := range code.fields {
if codeReferencesType(field.value, target, seen) {
return true
}
}
case *SliceCode:
return codeReferencesType(code.value, target, seen)
case *ArrayCode:
return codeReferencesType(code.value, target, seen)
case *MapCode:
return codeReferencesType(code.key, target, seen) || codeReferencesType(code.value, target, seen)
}
return false
}

func toElemType(t *runtime.Type) *runtime.Type {
for t.Kind() == reflect.Ptr {
t = t.Elem()
Expand Down
29 changes: 26 additions & 3 deletions internal/encoder/vm/vm.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading