From 7575588d4a41ec10b2e71993c03da98847a7c5c3 Mon Sep 17 00:00:00 2001 From: Skyenought Date: Mon, 4 Aug 2025 07:38:14 +0800 Subject: [PATCH 1/2] feat(go/parser): add visited map for getNamedTypes --- lang/golang/parser/ctx.go | 3 +- lang/golang/parser/utils.go | 39 +++--- lang/golang/parser/utils_test.go | 197 +++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+), 17 deletions(-) create mode 100644 lang/golang/parser/utils_test.go diff --git a/lang/golang/parser/ctx.go b/lang/golang/parser/ctx.go index debfbd8e..b3372750 100644 --- a/lang/golang/parser/ctx.go +++ b/lang/golang/parser/ctx.go @@ -446,7 +446,8 @@ func (p *GoParser) collectTypes(ctx *fileContext, typ ast.Expr, st *Type, inline // get type id and tells if it is std or builtin func (ctx *fileContext) getTypeinfo(typ types.Type) (ti typeInfo) { - tobjs, isPointer, isNamed := getNamedTypes(typ) + visited := make(map[types.Type]bool) + tobjs, isPointer, isNamed := getNamedTypes(typ, visited) ti.IsPointer = isPointer ti.Ty = typ ti.IsNamed = isNamed diff --git a/lang/golang/parser/utils.go b/lang/golang/parser/utils.go index 257d06be..a04cabd3 100644 --- a/lang/golang/parser/utils.go +++ b/lang/golang/parser/utils.go @@ -183,30 +183,37 @@ func getTypeKind(n ast.Expr) TypeKind { } } -func getNamedTypes(typ types.Type) (tys []types.Object, isPointer bool, isNamed bool) { +func getNamedTypes(typ types.Type, visited map[types.Type]bool) (tys []types.Object, isPointer bool, isNamed bool) { + if visited[typ] { + return nil, false, false + } + + visited[typ] = true + defer delete(visited, typ) + switch t := typ.(type) { case *types.Pointer: isPointer = true - typs, _, isNamed2 := getNamedTypes(t.Elem()) + var typs []types.Object + typs, _, isNamed = getNamedTypes(t.Elem(), visited) tys = append(tys, typs...) - isNamed = isNamed2 case *types.Slice: - typs, _, _ := getNamedTypes(t.Elem()) + typs, _, _ := getNamedTypes(t.Elem(), visited) tys = append(tys, typs...) case *types.Array: - typs, _, _ := getNamedTypes(t.Elem()) + typs, _, _ := getNamedTypes(t.Elem(), visited) tys = append(tys, typs...) case *types.Chan: - typs, _, _ := getNamedTypes(t.Elem()) + typs, _, _ := getNamedTypes(t.Elem(), visited) tys = append(tys, typs...) case *types.Tuple: for i := 0; i < t.Len(); i++ { - typs, _, _ := getNamedTypes(t.At(i).Type()) + typs, _, _ := getNamedTypes(t.At(i).Type(), visited) tys = append(tys, typs...) } case *types.Map: - typs2, _, _ := getNamedTypes(t.Elem()) - typs1, _, _ := getNamedTypes(t.Key()) + typs2, _, _ := getNamedTypes(t.Elem(), visited) + typs1, _, _ := getNamedTypes(t.Key(), visited) tys = append(tys, typs1...) tys = append(tys, typs2...) case *types.Named: @@ -214,32 +221,32 @@ func getNamedTypes(typ types.Type) (tys []types.Object, isPointer bool, isNamed isNamed = true case *types.Struct: for i := 0; i < t.NumFields(); i++ { - typs, _, _ := getNamedTypes(t.Field(i).Type()) + typs, _, _ := getNamedTypes(t.Field(i).Type(), visited) tys = append(tys, typs...) } case *types.Interface: for i := 0; i < t.NumEmbeddeds(); i++ { - typs, _, _ := getNamedTypes(t.EmbeddedType(i)) + typs, _, _ := getNamedTypes(t.EmbeddedType(i), visited) tys = append(tys, typs...) } for i := 0; i < t.NumExplicitMethods(); i++ { - typs, _, _ := getNamedTypes(t.ExplicitMethod(i).Type()) + typs, _, _ := getNamedTypes(t.ExplicitMethod(i).Type(), visited) tys = append(tys, typs...) } case *types.TypeParam: - typs, _, _ := getNamedTypes(t.Constraint()) + typs, _, _ := getNamedTypes(t.Constraint(), visited) tys = append(tys, typs...) case *types.Alias: var typs []types.Object - typs, isPointer, isNamed = getNamedTypes(t.Rhs()) + typs, isPointer, isNamed = getNamedTypes(t.Rhs(), visited) tys = append(tys, typs...) case *types.Signature: for i := 0; i < t.Params().Len(); i++ { - typs, _, _ := getNamedTypes(t.Params().At(i).Type()) + typs, _, _ := getNamedTypes(t.Params().At(i).Type(), visited) tys = append(tys, typs...) } for i := 0; i < t.Results().Len(); i++ { - typs, _, _ := getNamedTypes(t.Results().At(i).Type()) + typs, _, _ := getNamedTypes(t.Results().At(i).Type(), visited) tys = append(tys, typs...) } } diff --git a/lang/golang/parser/utils_test.go b/lang/golang/parser/utils_test.go new file mode 100644 index 00000000..fdd7a0e2 --- /dev/null +++ b/lang/golang/parser/utils_test.go @@ -0,0 +1,197 @@ +// Copyright 2025 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "slices" + "testing" + + "github.com/stretchr/testify/require" +) + +func getTypeForTest(t *testing.T, src, name string) types.Type { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, 0) + require.NoError(t, err, "Failed to parse source code for %s", name) + + conf := types.Config{Importer: importer.Default()} + pkg, err := conf.Check("test", fset, []*ast.File{f}, nil) + require.NoError(t, err, "Failed to type-check source code for %s", name) + + obj := pkg.Scope().Lookup(name) + require.NotNil(t, obj, "Object '%s' not found in source", name) + + return obj.Type() +} + +func objectsToNames(objs []types.Object) []string { + names := make([]string, len(objs)) + for i, obj := range objs { + if obj.Pkg() != nil { + names[i] = obj.Pkg().Path() + "." + obj.Name() + } else { + names[i] = obj.Name() + } + } + slices.Sort(names) + return names +} + +func Test_getNamedTypes(t *testing.T) { + testCases := []struct { + name string + source string + targetVar string + expectedNames []string + expectedIsPointer bool + expectedIsNamed bool + }{ + { + name: "Simple Named Type", + source: `package main +type MyInt int`, + targetVar: "MyInt", + expectedNames: []string{"test.MyInt"}, + expectedIsPointer: false, + expectedIsNamed: true, + }, + { + name: "Pointer to Named Type", + source: `package main +type MyInt int +var p *MyInt`, + targetVar: "p", + expectedNames: []string{"test.MyInt"}, + expectedIsPointer: true, + expectedIsNamed: true, + }, + { + name: "Slice of Named Type", + source: `package main +type MyStruct struct{}; var s []*MyStruct`, + targetVar: "s", + expectedNames: []string{"test.MyStruct"}, + expectedIsPointer: false, + expectedIsNamed: false, + }, + { + name: "Array of Named Type", + source: `package main +type MyInt int; var a [5]MyInt`, + targetVar: "a", + expectedNames: []string{"test.MyInt"}, + expectedIsPointer: false, + expectedIsNamed: false, + }, + { + name: "Map with Named Types", + source: `package main +type KeyType int; type ValueType string; var m map[*KeyType]ValueType`, + targetVar: "m", + expectedNames: []string{"test.KeyType", "test.ValueType"}, + expectedIsPointer: false, + expectedIsNamed: false, + }, + { + name: "Struct with Named Fields", + source: `package main + type MyInt int + type MyString string + var s struct { + Field1 MyInt + Field2 *MyString + }`, + targetVar: "s", + expectedNames: []string{"test.MyInt", "test.MyString"}, + expectedIsPointer: false, + expectedIsNamed: false, + }, + { + name: "Interface with Embedded and Explicit Methods", + source: `package main + import "io" + type MyInterface interface{ + io.Reader + MyMethod(arg io.Writer) + }`, + targetVar: "MyInterface", + expectedNames: []string{"test.MyInterface"}, + expectedIsPointer: false, + expectedIsNamed: true, + }, + { + name: "Function Signature", + source: `package main; import "bytes"; type MyInt int; var fn func(a MyInt) *bytes.Buffer`, + targetVar: "fn", + expectedNames: []string{"bytes.Buffer", "test.MyInt"}, + expectedIsPointer: false, + expectedIsNamed: false, + }, + { + name: "Type Alias", + source: `package main; type MyInt int; type IntAlias = MyInt`, + targetVar: "IntAlias", + expectedNames: []string{"test.MyInt"}, + expectedIsPointer: false, + expectedIsNamed: true, + }, + { + name: "Recursive Struct (Cycle)", + source: `package main; type Node struct{ Next *Node }`, + targetVar: "Node", + expectedNames: []string{"test.Node"}, + expectedIsPointer: false, + expectedIsNamed: true, + }, + { + name: "No Named Types", + source: `package main; var i int`, + targetVar: "i", + expectedNames: []string{}, + expectedIsPointer: false, + expectedIsNamed: false, + }, + { + name: "Tuple from function return", + source: `package main +import "net/http" +var f func() (*http.Request, error)`, + targetVar: "f", + expectedNames: []string{"error", "net/http.Request"}, // error is a builtin interface, not considered a named type object here + expectedIsPointer: false, + expectedIsNamed: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + typ := getTypeForTest(t, tc.source, tc.targetVar) + visited := make(map[types.Type]bool) + + tys, isPointer, isNamed := getNamedTypes(typ, visited) + + actualNames := objectsToNames(tys) + + require.Equal(t, tc.expectedNames, actualNames, "Named types mismatch") + require.Equal(t, tc.expectedIsPointer, isPointer, "isPointer mismatch") + require.Equal(t, tc.expectedIsNamed, isNamed, "isNamed mismatch") + }) + } +} From 2c122529748f6be6577947df278dcbeb88a90055 Mon Sep 17 00:00:00 2001 From: Skyenought Date: Mon, 4 Aug 2025 12:12:44 +0800 Subject: [PATCH 2/2] update --- lang/golang/parser/utils.go | 1 - 1 file changed, 1 deletion(-) diff --git a/lang/golang/parser/utils.go b/lang/golang/parser/utils.go index a04cabd3..338c31f7 100644 --- a/lang/golang/parser/utils.go +++ b/lang/golang/parser/utils.go @@ -189,7 +189,6 @@ func getNamedTypes(typ types.Type, visited map[types.Type]bool) (tys []types.Obj } visited[typ] = true - defer delete(visited, typ) switch t := typ.(type) { case *types.Pointer: