From 9069204acf8cadec39e844fe649428d860d9050d Mon Sep 17 00:00:00 2001 From: "dengyi.0818" Date: Tue, 23 Sep 2025 21:18:52 +0800 Subject: [PATCH 1/2] fix: cycle ref --- core/conf/config.go | 85 +++++++++++++++++++++------------------- core/conf/config_test.go | 47 +++++++++++++++++++++- 2 files changed, 89 insertions(+), 43 deletions(-) diff --git a/core/conf/config.go b/core/conf/config.go index 0ca8eccf39ff..767fdf8dd4bf 100644 --- a/core/conf/config.go +++ b/core/conf/config.go @@ -77,7 +77,7 @@ func LoadConfig(file string, v any, opts ...Option) error { // LoadFromJsonBytes loads config into v from content json bytes. func LoadFromJsonBytes(content []byte, v any) error { - info, err := buildFieldsInfo(reflect.TypeOf(v), "") + info, err := buildFieldsInfo(reflect.TypeOf(v), "", make(map[reflect.Type]*fieldInfo)) if err != nil { return err } @@ -152,10 +152,11 @@ func addOrMergeFields(info *fieldInfo, key string, child *fieldInfo, fullName st return nil } -func buildAnonymousFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type, fullName string) error { +func buildAnonymousFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type, + fullName string, visited map[reflect.Type]*fieldInfo) error { switch ft.Kind() { case reflect.Struct: - fields, err := buildFieldsInfo(ft, fullName) + fields, err := buildFieldsInfo(ft, fullName, visited) if err != nil { return err } @@ -166,7 +167,7 @@ func buildAnonymousFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.T } } case reflect.Map: - elemField, err := buildFieldsInfo(mapping.Deref(ft.Elem()), fullName) + elemField, err := buildFieldsInfo(mapping.Deref(ft.Elem()), fullName, visited) if err != nil { return err } @@ -192,14 +193,44 @@ func buildAnonymousFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.T return nil } -func buildFieldsInfo(tp reflect.Type, fullName string) (*fieldInfo, error) { +func buildFieldsInfo(tp reflect.Type, fullName string, + visited map[reflect.Type]*fieldInfo) (*fieldInfo, error) { tp = mapping.Deref(tp) + if finfo, ok := visited[tp]; ok { + return finfo, nil + } switch tp.Kind() { case reflect.Struct: - return buildStructFieldsInfo(tp, fullName) + info := &fieldInfo{ + children: make(map[string]*fieldInfo), + } + visited[tp] = info + + for i := 0; i < tp.NumField(); i++ { + field := tp.Field(i) + if !field.IsExported() { + continue + } + + name := getTagName(field) + lowerCaseName := toLowerCase(name) + ft := mapping.Deref(field.Type) + // flatten anonymous fields + if field.Anonymous { + if err := buildAnonymousFieldInfo(info, lowerCaseName, ft, + getFullName(fullName, lowerCaseName), visited); err != nil { + return nil, err + } + } else if err := buildNamedFieldInfo(info, lowerCaseName, ft, + getFullName(fullName, lowerCaseName), visited); err != nil { + return nil, err + } + } + + return info, nil case reflect.Array, reflect.Slice, reflect.Map: - return buildFieldsInfo(mapping.Deref(tp.Elem()), fullName) + return buildFieldsInfo(mapping.Deref(tp.Elem()), fullName, visited) case reflect.Chan, reflect.Func: return nil, fmt.Errorf("unsupported type: %s, fullName: %s", tp.Kind(), fullName) default: @@ -209,23 +240,24 @@ func buildFieldsInfo(tp reflect.Type, fullName string) (*fieldInfo, error) { } } -func buildNamedFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type, fullName string) error { +func buildNamedFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type, + fullName string, visited map[reflect.Type]*fieldInfo) error { var finfo *fieldInfo var err error switch ft.Kind() { case reflect.Struct: - finfo, err = buildFieldsInfo(ft, fullName) + finfo, err = buildFieldsInfo(ft, fullName, visited) if err != nil { return err } case reflect.Array, reflect.Slice: - finfo, err = buildFieldsInfo(ft.Elem(), fullName) + finfo, err = buildFieldsInfo(ft.Elem(), fullName, visited) if err != nil { return err } case reflect.Map: - elemInfo, err := buildFieldsInfo(mapping.Deref(ft.Elem()), fullName) + elemInfo, err := buildFieldsInfo(mapping.Deref(ft.Elem()), fullName, visited) if err != nil { return err } @@ -235,7 +267,7 @@ func buildNamedFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type, mapField: elemInfo, } default: - finfo, err = buildFieldsInfo(ft, fullName) + finfo, err = buildFieldsInfo(ft, fullName, visited) if err != nil { return err } @@ -244,35 +276,6 @@ func buildNamedFieldInfo(info *fieldInfo, lowerCaseName string, ft reflect.Type, return addOrMergeFields(info, lowerCaseName, finfo, fullName) } -func buildStructFieldsInfo(tp reflect.Type, fullName string) (*fieldInfo, error) { - info := &fieldInfo{ - children: make(map[string]*fieldInfo), - } - - for i := 0; i < tp.NumField(); i++ { - field := tp.Field(i) - if !field.IsExported() { - continue - } - - name := getTagName(field) - lowerCaseName := toLowerCase(name) - ft := mapping.Deref(field.Type) - // flatten anonymous fields - if field.Anonymous { - if err := buildAnonymousFieldInfo(info, lowerCaseName, ft, - getFullName(fullName, lowerCaseName)); err != nil { - return nil, err - } - } else if err := buildNamedFieldInfo(info, lowerCaseName, ft, - getFullName(fullName, lowerCaseName)); err != nil { - return nil, err - } - } - - return info, nil -} - // getTagName get the tag name of the given field, if no tag name, use file.Name. // field.Name is returned on tags like `json:""` and `json:",optional"`. func getTagName(field reflect.StructField) string { diff --git a/core/conf/config_test.go b/core/conf/config_test.go index 11b05294c492..0f04214b5674 100644 --- a/core/conf/config_test.go +++ b/core/conf/config_test.go @@ -984,7 +984,7 @@ func TestLoadNamedFieldOverwritten(t *testing.T) { }) t.Run("overwritten named *struct", func(t *testing.T) { - type ( + type ( Elem string Named struct { Elem string @@ -1328,7 +1328,7 @@ func Test_buildFieldsInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := buildFieldsInfo(tt.t, "") + _, err := buildFieldsInfo(tt.t, "", make(map[reflect.Type]*fieldInfo)) if tt.ok { assert.NoError(t, err) } else { @@ -1339,6 +1339,49 @@ func Test_buildFieldsInfo(t *testing.T) { } } +func TestLoadWithCycleReference(t *testing.T) { + type Node struct { + Name string `json:"name"` + Children []*Node `json:"children,optional"` + Parent *Node `json:"parent,optional"` // 指向父节点的指针 + } + + var c Node + input := []byte(` +name: root +children: + - name: child1 + children: + - name: grandchild1 + - name: child2 +`) + err := LoadFromYamlBytes(input, &c) + assert.NoError(t, err) + + // 手动回填父指针以构建完整的双向树 + var fillParent func(n *Node) + fillParent = func(n *Node) { + for _, child := range n.Children { + child.Parent = n + fillParent(child) + } + } + fillParent(&c) + + assert.Equal(t, "root", c.Name) + assert.Len(t, c.Children, 2) + assert.Equal(t, "child1", c.Children[0].Name) + assert.Equal(t, "child2", c.Children[1].Name) + assert.Len(t, c.Children[0].Children, 1) + assert.Equal(t, "grandchild1", c.Children[0].Children[0].Name) + + // 验证父指针 + assert.NotNil(t, c.Children[0].Parent) + assert.Equal(t, "root", c.Children[0].Parent.Name) + assert.NotNil(t, c.Children[0].Children[0].Parent) + assert.Equal(t, "child1", c.Children[0].Children[0].Parent.Name) +} + func createTempFile(t *testing.T, ext, text string) (string, error) { tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext) if err != nil { From 0f63f2d3a3805dd596cbb325580b0c1cb2ba54dc Mon Sep 17 00:00:00 2001 From: "dengyi.0818" Date: Tue, 23 Sep 2025 21:26:11 +0800 Subject: [PATCH 2/2] fix --- core/conf/config_test.go | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/core/conf/config_test.go b/core/conf/config_test.go index 0f04214b5674..36994c558bac 100644 --- a/core/conf/config_test.go +++ b/core/conf/config_test.go @@ -984,7 +984,7 @@ func TestLoadNamedFieldOverwritten(t *testing.T) { }) t.Run("overwritten named *struct", func(t *testing.T) { - type ( + type ( Elem string Named struct { Elem string @@ -1343,7 +1343,6 @@ func TestLoadWithCycleReference(t *testing.T) { type Node struct { Name string `json:"name"` Children []*Node `json:"children,optional"` - Parent *Node `json:"parent,optional"` // 指向父节点的指针 } var c Node @@ -1358,28 +1357,12 @@ children: err := LoadFromYamlBytes(input, &c) assert.NoError(t, err) - // 手动回填父指针以构建完整的双向树 - var fillParent func(n *Node) - fillParent = func(n *Node) { - for _, child := range n.Children { - child.Parent = n - fillParent(child) - } - } - fillParent(&c) - assert.Equal(t, "root", c.Name) assert.Len(t, c.Children, 2) assert.Equal(t, "child1", c.Children[0].Name) assert.Equal(t, "child2", c.Children[1].Name) assert.Len(t, c.Children[0].Children, 1) assert.Equal(t, "grandchild1", c.Children[0].Children[0].Name) - - // 验证父指针 - assert.NotNil(t, c.Children[0].Parent) - assert.Equal(t, "root", c.Children[0].Parent.Name) - assert.NotNil(t, c.Children[0].Children[0].Parent) - assert.Equal(t, "child1", c.Children[0].Children[0].Parent.Name) } func createTempFile(t *testing.T, ext, text string) (string, error) {