Skip to content
Merged
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
3 changes: 2 additions & 1 deletion lang/golang/parser/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 22 additions & 16 deletions lang/golang/parser/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,63 +183,69 @@ 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

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:
tys = append(tys, t.Obj())
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...)
}
}
Expand Down
197 changes: 197 additions & 0 deletions lang/golang/parser/utils_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
}