From 8de00caefb5c40a6016646938a7a201021fca951 Mon Sep 17 00:00:00 2001 From: sincos Date: Tue, 11 Jun 2024 00:59:50 +0800 Subject: [PATCH] feat(typex): typex for univeral reflect and go type abstract --- go.mod | 8 +- go.sum | 20 +- typex/convert.go | 332 ++++++++++++++++++++++ typex/convert_test.go | 321 +++++++++++++++++++++ typex/from_reflect.go | 150 ++++++++++ typex/from_types.go | 525 +++++++++++++++++++++++++++++++++++ typex/testdata/types.go | 137 +++++++++ typex/testdata/xoxo/types.go | 5 + typex/typex.go | 189 +++++++++++++ typex/typex_test.go | 368 ++++++++++++++++++++++++ 10 files changed, 2044 insertions(+), 11 deletions(-) create mode 100644 typex/convert.go create mode 100644 typex/convert_test.go create mode 100644 typex/from_reflect.go create mode 100644 typex/from_types.go create mode 100644 typex/testdata/types.go create mode 100644 typex/testdata/xoxo/types.go create mode 100644 typex/typex.go create mode 100644 typex/typex_test.go diff --git a/go.mod b/go.mod index c8e8d5f..43b3831 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,14 @@ go 1.22.0 require ( github.com/onsi/gomega v1.33.1 github.com/pkg/errors v0.9.1 + golang.org/x/tools v0.22.0 ) require ( github.com/google/go-cmp v0.6.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index daf1413..7f055e8 100644 --- a/go.sum +++ b/go.sum @@ -12,14 +12,18 @@ github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/typex/convert.go b/typex/convert.go new file mode 100644 index 0000000..dfa6a15 --- /dev/null +++ b/typex/convert.go @@ -0,0 +1,332 @@ +package typex + +import ( + "go/token" + "go/types" + "reflect" + "strconv" + "strings" + "sync" + + "golang.org/x/tools/go/packages" + + "github.com/xoctopus/x/misc/must" +) + +var ( + gGoTypeCache = sync.Map{} + gBasicsCache = sync.Map{} + gPackagesCache = sync.Map{} +) + +var ( + LoadFiles = packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles + LoadImports = LoadFiles | packages.NeedImports + LoadTypes = LoadImports | packages.NeedTypes | packages.NeedTypesSizes +) + +func init() { + for i := range types.Typ { + basic := types.Typ[i] + gBasicsCache.Store( + types.TypeString(basic, nil), + func() types.Type { return basic }, + ) + } + gBasicsCache.Store("interface {}", func() types.Type { + return types.NewInterfaceType(nil, nil) + }) + gBasicsCache.Store("any", func() types.Type { + return types.NewInterfaceType(nil, nil) + }) + gBasicsCache.Store("comparable", func() types.Type { + return types.NewInterfaceType(nil, nil) + }) + gBasicsCache.Store("error", func() types.Type { + return NewPackage("errors"). + Scope(). + Lookup("New"). + Type(). + Underlying().(*types.Signature). + Results(). + At(0). + Type() + }) +} + +func TypeByImportAndName(path, name string) types.Type { + if path == "" { + return TypeByID(name) + } + return TypeByID(path + "." + name) +} + +func TypeByID(id string) (t types.Type) { + if v, ok := gGoTypeCache.Load(id); ok { + return v.(types.Type) + } + + defer func() { + if t == nil { + t = types.Typ[types.Invalid] + } + gGoTypeCache.Store(id, t) + }() + + if id == "" { + return nil + } + + if v, ok := gBasicsCache.Load(id); ok { + t = v.(func() types.Type)() + return + } + + // map[x] + if idxMapLsb := strings.Index(id, "map["); idxMapLsb == 0 { + idxMapRsb := strings.Index(id, "]") + if idxMapRsb == -1 { + return nil + } + kt := TypeByID(id[4:idxMapRsb]) + if kt == nil || kt == types.Typ[types.Invalid] { + return nil + } + vt := TypeByID(id[idxMapRsb+1:]) + if vt == nil || vt == types.Typ[types.Invalid] { + return nil + } + t = types.NewMap(kt, vt) + return t + } + + // []x [n]x + idxLsb := strings.Index(id, "[") + if idxLsb == 0 { + idxRsb := strings.Index(id, "]") + if idxRsb == -1 { + return nil + } + elem := TypeByID(id[idxRsb+1:]) + if elem == nil || elem == types.Typ[types.Invalid] { + return nil + } + if idxRsb == idxLsb+1 { + t = types.NewSlice(elem) + return t + } + n, err := strconv.ParseInt(id[1:idxRsb], 10, 64) + if err != nil { + return t + } + t = types.NewArray(elem, n) + return t + } + + // path.Type + if idxLsb == -1 { + if idxDot := strings.LastIndex(id, "."); idxDot > 0 { + pkg := NewPackage(id[0:idxDot]) + if pkg == nil { + return nil + } + tpe, ok := pkg.Scope().Lookup(id[idxDot+1:]).(*types.TypeName) + if !ok || tpe == nil { + return nil + } + t = types.NewTypeName(token.NoPos, pkg, tpe.Name(), tpe.Type()).Type() + // t = tpe.Type() + return t + } + return t + } + + // path.Type[generics parameters...] + idxRsb := strings.Index(id, "]") + if idxRsb == -1 { + return nil + } + typeid := id[0:idxLsb] + idxDot := strings.LastIndex(typeid, ".") + if idxDot == -1 { + return nil + } + pkg := NewPackage(typeid[:idxDot]) + if pkg == nil { + return nil + } + tpe, ok := pkg.Scope().Lookup(typeid[idxDot+1:]).(*types.TypeName) + if !ok || tpe == nil { + return nil + } + named, ok := tpe.Type().(*types.Named) + if !ok { + return nil + } + cloned := types.NewNamed(tpe, tpe.Type().Underlying(), nil) + for i := 0; i < named.NumMethods(); i++ { + cloned.AddMethod(named.Method(i)) + } + paramNames := strings.Split(id[idxLsb+1:idxRsb], ",") + paramList := named.TypeParams() + if len(paramNames) == 0 || paramList == nil || paramList.Len() != len(paramNames) { + return nil + } + paramTypes := make([]*types.TypeParam, paramList.Len()) + for i := 0; i < paramList.Len(); i++ { + paramType := TypeByID(paramNames[i]) + if paramType == nil || paramType == types.Typ[types.Invalid] { + return nil + } + paramTypes[i] = types.NewTypeParam(paramList.At(i).Obj(), paramType) + } + cloned.SetTypeParams(paramTypes) + t = cloned + return t +} + +func NewPackage(path string) *types.Package { + if path == "" { + return nil + } + + if v, ok := gPackagesCache.Load(path); ok { + return v.(*types.Package) + } + + pkgs, err := packages.Load(&packages.Config{ + Overlay: make(map[string][]byte), + Tests: true, + Mode: LoadTypes, + }, path) + must.NoErrorWrap(err, "failed to load packages from %s", path) + + // if len(pkgs[0].Errors) != 0 { + // return nil + // } + + pkg := pkgs[0].Types + gPackagesCache.Store(path, pkg) + + return pkg +} + +func NewGoTypeFromReflectType(t reflect.Type) types.Type { + if t == nil { + return nil + } + + refs := 0 + for t.Kind() == reflect.Pointer { + refs++ + t = t.Elem() + } + + tpe, pkg := t.Name(), t.PkgPath() + if tpe == "error" && pkg == "" { + return must.BeTrueV(gBasicsCache.Load("error")).(func() types.Type)() + } + + var gt types.Type + if pkg != "" { + gt = TypeByID(pkg + "." + tpe) + } else { + gt = underlying(t) + } + + for refs > 0 { + gt = types.NewPointer(gt) + refs-- + } + return gt +} + +func underlying(t reflect.Type) types.Type { + if tt, ok := ReflectKindToTypesKind[t.Kind()]; ok { + return types.Typ[tt] + } + + switch t.Kind() { + case reflect.Array: + return types.NewArray( + NewGoTypeFromReflectType(t.Elem()), + int64(t.Len()), + ) + case reflect.Chan: + var dir = types.SendRecv + switch t.ChanDir() { + case reflect.RecvDir: // 1 + dir = types.RecvOnly // 2 + case reflect.SendDir: // 2 + dir = types.SendOnly // 1 + case reflect.BothDir: // 3 + dir = types.SendRecv // 0 + } + return types.NewChan(dir, NewGoTypeFromReflectType(t.Elem())) + case reflect.Slice: + return types.NewSlice( + NewGoTypeFromReflectType(t.Elem()), + ) + case reflect.Map: + return types.NewMap( + NewGoTypeFromReflectType(t.Key()), + NewGoTypeFromReflectType(t.Elem()), + ) + case reflect.Func: + ins := make([]*types.Var, t.NumIn()) + outs := make([]*types.Var, t.NumOut()) + for i := range ins { + in := t.In(i) + ins[i] = types.NewParam( + 0, + NewPackage(in.PkgPath()), + "", + NewGoTypeFromReflectType(in), + ) + } + for i := range outs { + out := t.Out(i) + outs[i] = types.NewParam( + 0, + NewPackage(out.PkgPath()), + "", + NewGoTypeFromReflectType(out), + ) + } + return types.NewSignatureType( + nil, nil, nil, + types.NewTuple(ins...), + types.NewTuple(outs...), + t.IsVariadic(), + ) + case reflect.Interface: + fns := make([]*types.Func, t.NumMethod()) + for i := range fns { + f := t.Method(i) + fns[i] = types.NewFunc( + 0, + NewPackage(f.PkgPath), + f.Name, + NewGoTypeFromReflectType(f.Type).(*types.Signature), + ) + } + return types.NewInterfaceType(fns, nil).Complete() + case reflect.Struct: + fields := make([]*types.Var, t.NumField()) + tags := make([]string, len(fields)) + for i := range fields { + f := t.Field(i) + fields[i] = types.NewField( + 0, + NewPackage(f.PkgPath), + f.Name, + NewGoTypeFromReflectType(f.Type), + f.Anonymous, + ) + tags[i] = string(f.Tag) + } + return types.NewStruct(fields, tags) + default: // [never entered this case] reflect.Invalid, reflect.Pointer + return nil + } +} diff --git a/typex/convert_test.go b/typex/convert_test.go new file mode 100644 index 0000000..2b10824 --- /dev/null +++ b/typex/convert_test.go @@ -0,0 +1,321 @@ +package typex_test + +import ( + "errors" + "reflect" + "testing" + + . "github.com/onsi/gomega" + pkgerrors "github.com/pkg/errors" + + "github.com/xoctopus/x/ptrx" + "github.com/xoctopus/x/typex" + "github.com/xoctopus/x/typex/testdata" + _ "github.com/xoctopus/x/typex/testdata/xoxo" +) + +func TestTypeByImportAndName(t *testing.T) { + cases := []struct { + name string + path string + id string + want string + }{ + { + "NamedTypeXoxoPart", + "github.com/xoctopus/x/typex/testdata/xoxo", "Part", + "github.com/xoctopus/x/typex/testdata/xoxo.Part", + }, { + "NamedTypeString", + "github.com/xoctopus/x/typex/testdata", "String", + "github.com/xoctopus/x/typex/testdata.String", + }, { + "NamedTypeInt", + "github.com/xoctopus/x/typex/testdata", "Int", + "github.com/xoctopus/x/typex/testdata.Int", + }, { + "NamedTypeInt8", + "github.com/xoctopus/x/typex/testdata", "Int8", + "github.com/xoctopus/x/typex/testdata.Int8", + }, { + "NamedTypeInt16", + "github.com/xoctopus/x/typex/testdata", "Int16", + "github.com/xoctopus/x/typex/testdata.Int16", + }, { + "NamedTypeInt32", + "github.com/xoctopus/x/typex/testdata", "Int32", + "github.com/xoctopus/x/typex/testdata.Int32", + }, { + "NamedTypeInt64", + "github.com/xoctopus/x/typex/testdata", "Int64", + "github.com/xoctopus/x/typex/testdata.Int64", + }, { + "NamedTypeUint", + "github.com/xoctopus/x/typex/testdata", "Uint", + "github.com/xoctopus/x/typex/testdata.Uint", + }, { + "NamedTypeUint8", + "github.com/xoctopus/x/typex/testdata", "Uint8", + "github.com/xoctopus/x/typex/testdata.Uint8", + }, { + "NamedTypeUint16", + "github.com/xoctopus/x/typex/testdata", "Uint16", + "github.com/xoctopus/x/typex/testdata.Uint16", + }, { + "NamedTypeUint32", + "github.com/xoctopus/x/typex/testdata", "Uint32", + "github.com/xoctopus/x/typex/testdata.Uint32", + }, { + "NamedTypeUint64", + "github.com/xoctopus/x/typex/testdata", "Uint64", + "github.com/xoctopus/x/typex/testdata.Uint64", + }, { + "NamedTypeUintptr", + "github.com/xoctopus/x/typex/testdata", "Uintptr", + "github.com/xoctopus/x/typex/testdata.Uintptr", + }, { + "NamedTypeFloat32", + "github.com/xoctopus/x/typex/testdata", "Float32", + "github.com/xoctopus/x/typex/testdata.Float32", + }, { + "NamedTypeFloat64", + "github.com/xoctopus/x/typex/testdata", "Float64", + "github.com/xoctopus/x/typex/testdata.Float64", + }, { + "NamedTypeComplex64", + "github.com/xoctopus/x/typex/testdata", "Complex64", + "github.com/xoctopus/x/typex/testdata.Complex64", + }, { + "NamedTypeComplex128", + "github.com/xoctopus/x/typex/testdata", "Complex128", + "github.com/xoctopus/x/typex/testdata.Complex128", + }, { + "BasicInt", + "", "int", "int", + }, { + "BasicString", + "", "string", "string", + }, { + "BasicMap", + "", "map[int]int", "map[int]int", + }, { + "BasicIntSlice", + "", "[]int", "[]int", + }, { + "BasicIntArray", + "", "[2]int", "[2]int", + }, { + "BasicError", + "", "error", "error", + }, { + "BasicInterface", + "", "interface {}", "interface{}", + }, { + "BasicAny", + "", "any", "interface{}", + }, { + "BasicComparable", + "", "comparable", "interface{}", + }, { + "GenericTypeAlias", + "github.com/xoctopus/x/typex/testdata", "EnumMap", + "github.com/xoctopus/x/typex/testdata.AnyMap[github.com/xoctopus/x/typex/testdata.Enum, any]", + }, { + "EmptyInvalid", + "", "", "invalid type", + }, { + "InvalidMapMissingRsb", + "", "map[int", "invalid type", + }, { + "InvalidMapMissingElemType", + "", "map[int]", "invalid type", + }, { + "InvalidMapMissingKeyType", + "", "map[]any", "invalid type", + }, { + "InvalidSliceMissingRsb", + "", "[any", "invalid type", + }, { + "InvalidSliceMissingElem", + "", "[]", "invalid type", + }, { + "InvalidArray", + "", "[aaa]int", "invalid type", + }, { + "InvalidImport", + "xxx", "any", + "invalid type", + }, { + "InvalidType", + "github.com/xoctopus/x/typex/testdata", "Invalid", + "invalid type", + }, { + "InvalidTypeFromCache", // from cache + "github.com/xoctopus/x/typex/testdata", "Invalid", + "invalid type", + }, { + "InvalidImportedPath1", + "", "abc", "invalid type", + }, { + "InvalidGenericTypeMissingRsb", + "github.com/xoctopus/x/typex/testata", "AnyMap[", + "invalid type", + }, { + "InvalidGenericTypeInvalidImportPath", + "", "Any[comparable,any]", + "invalid type", + }, { + "InvalidGenericTypeInvalidImportPath", + "xxx", "Any[comparable,any]", + "invalid type", + }, { + "InvalidGenericTypeInvalidTypename", + "github.com/xoctopus/x/typex/testdata", "SomeType[comparable,any]", + "invalid type", + }, { + "InvalidGenericTypeInvalidTypeParam", + "github.com/xoctopus/x/typex/testdata", "AnyMap[Invalid,any]", + "invalid type", + }, { + "InvalidGenericTypeInvalidTypeParamUnmatchedParamCount", + "github.com/xoctopus/x/typex/testdata", "AnyMap[any]", + "invalid type", + }, { + "GenericTypeWithoutTypeParams", + "github.com/xoctopus/x/typex/testdata", "AnyMap", + "github.com/xoctopus/x/typex/testdata.AnyMap[K comparable, V any]", + }, { + "GenericType", + "github.com/xoctopus/x/typex/testdata", "AnyMap[int,string]", + "github.com/xoctopus/x/typex/testdata.AnyMap[K int, V string]", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + gt := typex.TypeByImportAndName(c.path, c.id) + NewWithT(t).Expect(gt.String()).To(Equal(c.want)) + }) + } +} + +func TestNewPackage(t *testing.T) { + NewWithT(t).Expect(typex.NewPackage("")).To(BeNil()) + // ok + pkg := typex.NewPackage("github.com/pkg/errors") + NewWithT(t).Expect(pkg).NotTo(BeNil()) + // gopkg.in/yaml.v2 need import + NewWithT(t).Expect(typex.NewPackage("gopkg.in/yaml.v2").Name()).To(BeEmpty()) +} + +func TestNewGoTypeFromReflectType(t *testing.T) { + cases := []struct { + name string + v reflect.Type + want string + }{ + { + "Int", + reflect.TypeOf(int(0)), + "int", + }, { + "IntPtr", + reflect.TypeOf(ptrx.Ptr(int(0))), + "*int", + }, { + "IntPtrPtr", + reflect.TypeOf(ptrx.Ptr(int(0))), + "*int", + }, { + "PkgError", + reflect.TypeOf(pkgerrors.New("any")), + "*github.com/pkg/errors.fundamental", + }, { + "Error", + reflect.TypeOf(errors.New("any")), + "*errors.errorString", + }, { + "NilError", + reflect.TypeOf(error(nil)), + "nil", + }, { + "IntSlice", + reflect.TypeOf([]int{}), + "[]int", + }, { + "IntArray", + reflect.TypeOf([3]int{}), + "[3]int", + }, { + "ErrorSlice", + reflect.TypeOf([]error{}), + "[]error", + }, { + "ErrorArray", + reflect.TypeOf([5]error{}), + "[5]error", + }, { + "IntChanSendOnly", + reflect.TypeOf(make(<-chan int)), + "<-chan int", + }, { + "IntChanRecvOnly", + reflect.TypeOf(make(chan<- int)), + "chan<- int", + }, { + "IntChanBothDir", + reflect.TypeOf(make(chan int)), + "chan int", + }, { + "Map", + reflect.TypeOf(map[int]string{}), + "map[int]string", + }, { + "Func", + reflect.TypeOf(func() {}), + "func()", + }, { + "FuncWithInAndOut", + reflect.TypeOf(func(a string, b reflect.Type, c testdata.Interface) (err error, res testdata.Interface) { + return nil, nil + }), + "func(string, reflect.Type, github.com/xoctopus/x/typex/testdata.Interface) (error, github.com/xoctopus/x/typex/testdata.Interface)", + }, { + "MapInterface", + reflect.TypeOf(map[int]testdata.Interface{}), + "map[int]github.com/xoctopus/x/typex/testdata.Interface", + }, { + "MapStruct", + reflect.TypeOf(map[int]*testdata.Struct{}), + "map[int]*github.com/xoctopus/x/typex/testdata.Struct", + }, { + "StructPtr", + reflect.TypeOf(&testdata.Struct{}), + "*github.com/xoctopus/x/typex/testdata.Struct", + }, { + "StructSlice", + reflect.TypeOf([]struct { + A int + B string + }{}), + "[]struct{A int; B string}", + }, { + "InterfaceSlicePtr", + reflect.TypeOf(&[]interface { + String() string + Bytes() []byte + }{}), + "*[]interface{Bytes() []uint8; String() string}", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + tt := typex.NewGoTypeFromReflectType(c.v) + if tt == nil { + NewWithT(t).Expect("nil").To(Equal(c.want)) + } else { + NewWithT(t).Expect(tt.String()).To(Equal(c.want)) + } + }) + } +} diff --git a/typex/from_reflect.go b/typex/from_reflect.go new file mode 100644 index 0000000..f0c62ed --- /dev/null +++ b/typex/from_reflect.go @@ -0,0 +1,150 @@ +package typex + +import ( + "go/types" + "reflect" +) + +func NewReflectType(t reflect.Type) *ReflectType { + return &ReflectType{Type: t} +} + +type ReflectType struct { + reflect.Type +} + +var _ Type = (*ReflectType)(nil) + +func (t *ReflectType) Unwrap() any { + return t.Type +} + +func (t *ReflectType) String() string { + return typename(t) +} + +func (t *ReflectType) Implements(u Type) bool { + switch x := u.(type) { + case *ReflectType: + return t.Type.Implements(x.Type) + case *GoType: + if t.PkgPath() == "" { + return false + } + if v, ok := x.Type.(*types.Interface); ok { + return types.Implements(NewGoTypeFromReflectType(t.Type), v) + } + } + return false +} + +func (t *ReflectType) AssignableTo(u Type) bool { + if x, ok := u.(*ReflectType); ok { + return t.Type.AssignableTo(x.Type) + } + return false +} + +func (t *ReflectType) ConvertibleTo(u Type) bool { + if x, ok := u.(*ReflectType); ok { + return t.Type.ConvertibleTo(x.Type) + } + return false +} + +func (t *ReflectType) Key() Type { + return NewReflectType(t.Type.Key()) +} + +func (t *ReflectType) Elem() Type { + return NewReflectType(t.Type.Elem()) +} + +func (t *ReflectType) Field(i int) StructField { + return &ReflectStructField{ + StructField: t.Type.Field(i), + } +} + +func (t *ReflectType) FieldByName(name string) (StructField, bool) { + if f, ok := t.Type.FieldByName(name); ok { + return &ReflectStructField{ + StructField: f, + }, true + } + return nil, false +} + +func (t *ReflectType) FieldByNameFunc(match func(string) bool) (StructField, bool) { + if f, ok := t.Type.FieldByNameFunc(match); ok { + return &ReflectStructField{ + StructField: f, + }, true + } + return nil, false +} + +func (t *ReflectType) Method(i int) Method { + return &ReflectMethod{ + Method: t.Type.Method(i), + } +} + +func (t *ReflectType) MethodByName(name string) (Method, bool) { + if m, ok := t.Type.MethodByName(name); ok { + return &ReflectMethod{m}, true + } + return nil, false +} + +func (t *ReflectType) In(i int) Type { + return NewReflectType(t.Type.In(i)) +} + +func (t *ReflectType) Out(i int) Type { + return NewReflectType(t.Type.Out(i)) +} + +type ReflectMethod struct { + Method reflect.Method +} + +var _ Method = (*ReflectMethod)(nil) + +func (m *ReflectMethod) PkgPath() string { + return m.Method.PkgPath +} + +func (m *ReflectMethod) Name() string { + return m.Method.Name +} + +func (m *ReflectMethod) Type() Type { + return NewReflectType(m.Method.Type) +} + +type ReflectStructField struct { + StructField reflect.StructField +} + +var _ StructField = (*ReflectStructField)(nil) + +func (f *ReflectStructField) PkgPath() string { + return f.StructField.PkgPath +} + +func (f *ReflectStructField) Name() string { + return f.StructField.Name +} + +func (f *ReflectStructField) Tag() reflect.StructTag { + return f.StructField.Tag +} + +func (f *ReflectStructField) Type() Type { + return NewReflectType(f.StructField.Type) +} + +func (f *ReflectStructField) Anonymous() bool { + return f.StructField.Anonymous +} diff --git a/typex/from_types.go b/typex/from_types.go new file mode 100644 index 0000000..1fc8c28 --- /dev/null +++ b/typex/from_types.go @@ -0,0 +1,525 @@ +package typex + +import ( + "bytes" + "go/ast" + "go/types" + "reflect" + "strings" +) + +func NewGoType(t types.Type) *GoType { + gt := &GoType{Type: t} + if p, ok := t.(*types.TypeParam); ok { + gt.Type = p.Constraint() + } + + var scan func(t types.Type) []*GoMethod + + scan = func(t types.Type) []*GoMethod { + methods := make([]*GoMethod, 0) + switch x := t.(type) { + case *types.Named: + for i := range x.NumMethods() { + m := &GoMethod{fn: x.Method(i)} + if _, ok := m.fn.Type().(*types.Signature).Recv().Type().(*types.Pointer); ok { + m.ptr = true + } + methods = append(methods, m) + } + return append(methods, scan(x.Underlying())...) + case *types.Pointer: + return append(methods, scan(x.Elem())...) + case *types.Struct: + for i := range x.NumFields() { + if f := x.Field(i); f.Anonymous() { + methods = append(methods, scan(f.Type())...) + } + } + return methods + } + return nil + } + + methods := scan(gt.Type) + for _, m := range methods { + if !m.ptr { + gt.methods = append(gt.methods, m.fn) + } + gt.ptrMethods = append(gt.ptrMethods, m.fn) + } + return gt +} + +type GoType struct { + Type types.Type + methods []*types.Func + ptrMethods []*types.Func +} + +var _ Type = (*GoType)(nil) + +func (t *GoType) Unwrap() any { + return t.Type +} + +func (t *GoType) PkgPath() string { + switch x := t.Type.(type) { + case *types.Named: + if pkg := x.Obj().Pkg(); pkg != nil { + return pkg.Path() + } + if x.String() == "error" { + return "" + } + case *types.Basic: + if strings.HasPrefix(x.String(), "unsafe.") { + return "unsafe" + } + return "" + } + return "" +} + +func (t *GoType) Name() string { + switch x := t.Type.(type) { + case *types.Named: + buf := bytes.NewBuffer(nil) + buf.WriteString(x.Obj().Name()) + if params := x.TypeParams(); params.Len() > 0 { + buf.WriteString("[") + for i := range params.Len() { + if i > 0 { + buf.WriteByte(',') + } + p := params.At(i).Constraint().(*types.Interface) + if p.NumEmbeddeds() > 0 { + buf.WriteString(typename(NewGoType(p.EmbeddedType(0)))) + } else { + buf.WriteString(typename(NewGoType(p))) + } + } + buf.WriteString("]") + } + return buf.String() + case *types.Basic: + return x.Name() + } + return "" +} + +func (t *GoType) String() string { + return typename(t) +} + +func (t *GoType) Kind() reflect.Kind { + switch x := t.Type.(type) { + case *types.Named: + if pkg := x.Obj().Pkg(); pkg != nil && + pkg.Name() == "unsafe" && + x.Obj().Name() == "Pointer" { + return reflect.UnsafePointer + } + return NewGoType(x.Underlying()).Kind() + case *types.Interface: + return reflect.Interface + case *types.Pointer: + return reflect.Pointer + case *types.Struct: + return reflect.Struct + case *types.Slice: + return reflect.Slice + case *types.Array: + return reflect.Array + case *types.Map: + return reflect.Map + case *types.Chan: + return reflect.Chan + case *types.Signature: + return reflect.Func + case *types.Basic: + return TypesKindToReflectKind[x.Kind()] + } + return reflect.Invalid +} + +func (t *GoType) Implements(u Type) bool { + switch x := u.(type) { + case *GoType: + if xi, ok := x.Type.(*types.Interface); ok { + return types.Implements(t.Type, xi) + } + case *ReflectType: + var ( + tt Type = t + ptr = false + ) + for tt.Kind() == reflect.Pointer { + tt = tt.Elem() + ptr = true + } + if tt.PkgPath() == "" || x.PkgPath() == "" { + return false + } + xi, ok := NewGoTypeFromReflectType(x.Type).Underlying().(*types.Interface) + if !ok { + return false + } + t2 := TypeByImportAndName(tt.PkgPath(), tt.Name()) + if ptr { + t2 = types.NewPointer(t2) + } + return types.Implements(t2, xi) + } + return false +} + +func (t *GoType) AssignableTo(u Type) bool { + if x, ok := u.(*GoType); ok { + return types.AssignableTo(t.Type, x.Type) + } + return false +} + +func (t *GoType) ConvertibleTo(u Type) bool { + if x, ok := u.(*GoType); ok { + return types.ConvertibleTo(t.Type, x.Type) + } + return false +} + +func (t *GoType) Comparable() bool { + if x, ok := t.Type.(*types.Named); ok { + return types.Comparable(ConstrainUnderlying(x.TypeParams(), x.Underlying())) + } + return t.Kind() == reflect.Struct || + types.Comparable(t.Type) +} + +func (t *GoType) Key() Type { + switch x := t.Type.(type) { + case *types.Named: + return NewGoType(ConstrainUnderlying(x.TypeParams(), x.Underlying())).Key() + case interface{ Key() types.Type }: + return NewGoType(x.Key()) + } + return nil +} + +func (t *GoType) Elem() Type { + switch x := t.Type.(type) { + case *types.Named: + return NewGoType(ConstrainUnderlying(x.TypeParams(), x.Underlying())).Elem() + case interface{ Elem() types.Type }: + return NewGoType(x.Elem()) + } + return nil +} + +func (t *GoType) Len() int { + switch x := t.Type.(type) { + case *types.Named: + return NewGoType(x.Underlying()).Len() + case *types.Array: + return int(x.Len()) + } + return 0 +} + +func (t *GoType) NumField() int { + switch x := t.Type.(type) { + case *types.Pointer: + return NewGoType(x.Elem()).NumField() + case *types.Named: + return NewGoType(x.Underlying()).NumField() + case *types.Struct: + return x.NumFields() + } + return 0 +} + +func (t *GoType) Field(i int) StructField { + switch x := t.Type.(type) { + case *types.Named: + return NewGoType(ConstrainUnderlying(x.TypeParams(), x.Underlying())).Field(i) + case *types.Struct: + return &GoStructField{Var: x.Field(i), tag: x.Tag(i)} + } + return nil +} + +func (t *GoType) FieldByName(name string) (StructField, bool) { + return t.FieldByNameFunc(func(s string) bool { return name == s }) +} + +func (t *GoType) FieldByNameFunc(match func(string) bool) (StructField, bool) { + for i := range t.NumField() { + f := t.Field(i) + if match(f.Name()) { + return f, true + } + if f.Anonymous() { + if sf, ok := f.Type().FieldByNameFunc(match); ok { + return sf, true + } + } + } + return nil, false +} + +func (t *GoType) NumMethod() int { + if t.Kind() == reflect.Interface { + switch x := t.Type.(type) { + case *types.Named: + return x.Underlying().(*types.Interface).NumMethods() + case *types.Interface: + return x.NumMethods() + } + } + + switch t.Type.(type) { + case *types.Pointer: + return len(t.ptrMethods) + default: + return len(t.methods) + } +} + +func (t *GoType) Method(i int) Method { + if t.Kind() == reflect.Interface { + switch x := t.Type.(type) { + case *types.Named: + return &GoMethod{fn: x.Underlying().(*types.Interface).Method(i)} + case *types.Interface: + return &GoMethod{fn: x.Method(i)} + } + } + + switch t.Type.(type) { + case *types.Pointer: + if i >= 0 && i < len(t.ptrMethods) { + return &GoMethod{ptr: true, recv: t, fn: t.ptrMethods[i]} + } + default: + if i >= 0 && i < len(t.methods) { + return &GoMethod{recv: t, fn: t.methods[i]} + } + } + return nil +} + +func (t *GoType) MethodByName(name string) (Method, bool) { + for i := range t.NumMethod() { + if m := t.Method(i); m.Name() == name { + return m, true + } + } + return nil, false +} + +func (t *GoType) IsVariadic() bool { + if s, ok := t.Type.(*types.Signature); ok { + return s.Variadic() + } + return false +} + +func (t *GoType) NumIn() int { + switch x := t.Type.(type) { + case *types.Named: + return NewGoType(x.Underlying()).NumIn() + case *types.Signature: + return x.Params().Len() + } + return 0 +} + +func (t *GoType) In(i int) Type { + switch x := t.Type.(type) { + case *types.Named: + return NewGoType(x.Underlying()).In(i) + case *types.Signature: + return NewGoType(x.Params().At(i).Type()) + } + return nil +} + +func (t *GoType) NumOut() int { + switch x := t.Type.(type) { + case *types.Named: + return NewGoType(x.Underlying()).NumOut() + case *types.Signature: + return x.Results().Len() + } + return 0 +} + +func (t *GoType) Out(i int) Type { + switch x := t.Type.(type) { + case *types.Named: + return NewGoType(x.Underlying()).Out(i) + case *types.Signature: + return NewGoType(x.Results().At(i).Type()) + } + return nil +} + +type GoMethod struct { + ptr bool + recv *GoType + fn *types.Func +} + +var _ Method = (*GoMethod)(nil) + +func (m *GoMethod) PkgPath() string { + if ast.IsExported(m.Name()) { + return "" + } + if pkg := m.fn.Pkg(); pkg != nil { + return pkg.Path() + } + return "" +} + +func (m *GoMethod) Name() string { + return m.fn.Name() +} + +func (m *GoMethod) Type() Type { + sig := m.fn.Type().(*types.Signature) + + if m.recv == nil { + return NewGoType(sig) + } + + ins := make([]*types.Var, sig.Params().Len()+1) + ins[0] = types.NewVar(0, nil, "", m.recv.Type) + for i := range sig.Params().Len() { + ins[i+1] = sig.Params().At(i) + } + return NewGoType(types.NewSignatureType( + nil, nil, nil, + types.NewTuple(ins...), + sig.Results(), + sig.Variadic(), + )) +} + +type GoStructField struct { + *types.Var + tag string +} + +var _ StructField = (*GoStructField)(nil) + +func (f *GoStructField) PkgPath() string { + if ast.IsExported(f.Name()) { + return "" + } + if pkg := f.Var.Pkg(); pkg != nil { + return pkg.Path() + } + return "" +} + +func (f *GoStructField) Tag() reflect.StructTag { + return reflect.StructTag(f.tag) +} + +func (f *GoStructField) Type() Type { + return NewGoType(f.Var.Type()) +} + +func ConstrainUnderlying(params *types.TypeParamList, underlying types.Type) types.Type { + if params.Len() == 0 { + return underlying + } + + switch t := underlying.(type) { + case *types.TypeParam: + p := params.At(t.Index()).Constraint().(*types.Interface) + if p.NumEmbeddeds() > 0 { + return p.EmbeddedType(0) + } + return p + case *types.Map: + return types.NewMap( + ConstrainUnderlying(params, t.Key()), + ConstrainUnderlying(params, t.Elem()), + ) + case *types.Slice: + return types.NewSlice( + ConstrainUnderlying(params, t.Elem()), + ) + case *types.Array: + return types.NewArray( + ConstrainUnderlying(params, t.Elem()), + t.Len(), + ) + case *types.Struct: + num := t.NumFields() + tags, fields := make([]string, num), make([]*types.Var, num) + for i := range num { + f := t.Field(i) + fields[i] = types.NewField( + f.Pos(), + f.Pkg(), + f.Name(), + ConstrainUnderlying(params, f.Type()), + f.Embedded(), + ) + tags[i] = t.Tag(i) + } + return types.NewStruct(fields, tags) + } + return underlying +} + +var ReflectKindToTypesKind = map[reflect.Kind]types.BasicKind{ + reflect.Bool: types.Bool, + reflect.Int: types.Int, + reflect.Int8: types.Int8, + reflect.Int16: types.Int16, + reflect.Int32: types.Int32, + reflect.Int64: types.Int64, + reflect.Uint: types.Uint, + reflect.Uint8: types.Uint8, + reflect.Uint16: types.Uint16, + reflect.Uint32: types.Uint32, + reflect.Uint64: types.Uint64, + reflect.Uintptr: types.Uintptr, + reflect.Float32: types.Float32, + reflect.Float64: types.Float64, + reflect.Complex64: types.Complex64, + reflect.Complex128: types.Complex128, + reflect.String: types.String, + reflect.UnsafePointer: types.UnsafePointer, +} + +var TypesKindToReflectKind = map[types.BasicKind]reflect.Kind{ + types.Bool: reflect.Bool, + types.Int: reflect.Int, + types.Int8: reflect.Int8, + types.Int16: reflect.Int16, + types.Int32: reflect.Int32, + types.Int64: reflect.Int64, + types.Uint: reflect.Uint, + types.Uint8: reflect.Uint8, + types.Uint16: reflect.Uint16, + types.Uint32: reflect.Uint32, + types.Uint64: reflect.Uint64, + types.Uintptr: reflect.Uintptr, + types.Float32: reflect.Float32, + types.Float64: reflect.Float64, + types.Complex64: reflect.Complex64, + types.Complex128: reflect.Complex128, + types.String: reflect.String, + types.UnsafePointer: reflect.UnsafePointer, + types.UntypedBool: reflect.Bool, + types.UntypedInt: reflect.Int, + types.UntypedRune: reflect.Int32, + types.UntypedFloat: reflect.Float32, + types.UntypedComplex: reflect.Complex64, + types.UntypedString: reflect.String, +} diff --git a/typex/testdata/types.go b/typex/testdata/types.go new file mode 100644 index 0000000..d0db6cd --- /dev/null +++ b/typex/testdata/types.go @@ -0,0 +1,137 @@ +package testdata + +import ( + "context" + "encoding" + "fmt" + + "github.com/xoctopus/x/typex/testdata/xoxo" +) + +type ( + String string + Boolean bool + Int int + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + Uint uint + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Uint64 uint64 + Uintptr uintptr + Float32 float32 + Float64 float64 + Complex64 complex64 + Complex128 complex128 +) + +type ( + Array [1]string + Map map[string]string + Slice []string + Chan chan string + Func func(x, y string) bool +) + +func F() {} + +type Interface interface { + String() string +} + +type Struct struct { + Interface + a string + A string `json:"a"` + B string `json:"b"` + Boolean `json:"bool,omitempty"` + xoxo.Part + Part2 Part `json:",omitempty"` +} + +func (Struct) String() string { + return "" +} + +func (Struct) WithArg(arg any) {} + +type Part struct { + C string `json:"c"` +} + +func (Part) Value() string { + return "" +} + +func (*Part) PtrValue() string { + return "" +} + +type Compose struct { + Struct +} + +type Enum int + +const ( + ENUM__ONE Enum = iota + 1 // 1 + ENUM__TWO // 2 +) + +func (e *Enum) UnmarshalText(text []byte) error { + switch string(text) { + case "ONE": + *e = ENUM__ONE + case "TWO": + *e = ENUM__TWO + } + return fmt.Errorf("unknown") +} + +func (e Enum) MarshalText() ([]byte, error) { + switch e { + case ENUM__ONE: + return []byte("ONE"), nil + case ENUM__TWO: + return []byte("TWO"), nil + } + return nil, fmt.Errorf("unknown") +} + +func (e Enum) String() string { + switch e { + case ENUM__ONE: + return "ONE" + case ENUM__TWO: + return "TWO" + } + return "" +} + +type MixedInterface interface { + encoding.TextMarshaler + Stringify(ctx context.Context, vs ...any) string + Add(a, b string) string + Bytes() []byte + str() string +} + +type AnySlice[V any] []V + +func (s AnySlice[V]) Each() {} + +type AnyArray[V any] [2]V + +type AnyMap[K comparable, V any] map[K]V + +type EnumMap = AnyMap[Enum, any] + +type AnyStruct[V any] struct { + Struct + Name V +} + +var Var string diff --git a/typex/testdata/xoxo/types.go b/typex/testdata/xoxo/types.go new file mode 100644 index 0000000..c29ac7b --- /dev/null +++ b/typex/testdata/xoxo/types.go @@ -0,0 +1,5 @@ +package xoxo + +type Part struct { + C string `json:"c"` +} diff --git a/typex/typex.go b/typex/typex.go new file mode 100644 index 0000000..b9775a5 --- /dev/null +++ b/typex/typex.go @@ -0,0 +1,189 @@ +package typex + +import ( + "bytes" + "fmt" + "reflect" +) + +type Type interface { + // Unwrap to types.Type or reflect.Type + Unwrap() any + + PkgPath() string + Name() string + String() string + Kind() reflect.Kind + + Implements(Type) bool + AssignableTo(Type) bool + ConvertibleTo(Type) bool + Comparable() bool + + Key() Type + Elem() Type + Len() int + + NumField() int + Field(int) StructField + FieldByName(string) (StructField, bool) + FieldByNameFunc(func(string) bool) (StructField, bool) + + NumMethod() int + Method(int) Method + MethodByName(string) (Method, bool) + + IsVariadic() bool + NumIn() int + In(int) Type + NumOut() int + Out(int) Type +} + +type Method interface { + PkgPath() string + Name() string + Type() Type +} + +type StructField interface { + PkgPath() string + Name() string + Type() Type + Tag() reflect.StructTag + Anonymous() bool +} + +func Typename(t Type) string { + if t == nil || t.Unwrap() == nil { + return "nil" + } + + buf := bytes.NewBuffer(nil) + for t.Kind() == reflect.Pointer { + buf.WriteByte('*') + t = t.Elem() + } + + if name := t.Name(); name != "" { + if pkg := t.PkgPath(); pkg != "" { + buf.WriteString(pkg) + buf.WriteRune('.') + } + buf.WriteString(name) + return buf.String() + } + buf.WriteString(t.String()) + return buf.String() +} + +func Deref(t Type) Type { + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + return t +} + +func typename(t Type) string { + if pkg := t.PkgPath(); pkg != "" { + return pkg + "." + t.Name() + } + + kind := t.Kind() + if _, ok := ReflectKindToTypesKind[kind]; ok { + return kind.String() + } + + switch kind { + case reflect.Slice: + return "[]" + typename(t.Elem()) + case reflect.Array: + return fmt.Sprintf("[%d]%s", t.Len(), typename(t.Elem())) + case reflect.Chan: + return "chan " + typename(t.Elem()) + case reflect.Map: + return fmt.Sprintf("map[%s]%s", typename(t.Key()), typename(t.Elem())) + case reflect.Pointer: + return "*" + typename(t.Elem()) + case reflect.Struct: + buf := bytes.NewBuffer(nil) + buf.WriteString("struct {") + for i := 0; i < t.NumField(); i++ { + buf.WriteRune(' ') + f := t.Field(i) + if !f.Anonymous() { + buf.WriteString(f.Name()) + buf.WriteRune(' ') + } + buf.WriteString(typename(f.Type())) + if tag := f.Tag(); tag != "" { + buf.WriteRune(' ') + buf.WriteString("`" + string(tag) + "`") + } + if i == t.NumField()-1 { + buf.WriteRune(' ') + } else { + buf.WriteRune(';') + } + } + buf.WriteString("}") + return buf.String() + case reflect.Interface: + if t.Name() == "error" { + return "error" + } + buf := bytes.NewBuffer(nil) + buf.WriteString("interface {") + for i := 0; i < t.NumMethod(); i++ { + m := t.Method(i) + buf.WriteRune(' ') + if pkg := m.PkgPath(); pkg != "" { + buf.WriteString(NewPackage(pkg).Name()) + buf.WriteRune('.') + } + buf.WriteString(m.Name()) + buf.WriteString(m.Type().String()[4:]) + if i == t.NumMethod()-1 { + buf.WriteRune(' ') + } else { + buf.WriteRune(';') + } + } + buf.WriteString("}") + return buf.String() + case reflect.Func: + buf := bytes.NewBuffer(nil) + buf.WriteString("func(") + for i := 0; i < t.NumIn(); i++ { + p := t.In(i) + if i == t.NumIn()-1 && t.IsVariadic() { + buf.WriteString("...") + buf.WriteString(typename(p.Elem())) + } else { + buf.WriteString(typename(p)) + } + if i < t.NumIn()-1 { + buf.WriteString(", ") + } + } + buf.WriteRune(')') + if t.NumOut() > 0 { + buf.WriteRune(' ') + } + if t.NumOut() > 1 { + buf.WriteRune('(') + } + for i := 0; i < t.NumOut(); i++ { + if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(typename(t.Out(i))) + } + if t.NumOut() > 1 { + buf.WriteRune(')') + } + return buf.String() + default: + return t.Name() + } +} diff --git a/typex/typex_test.go b/typex/typex_test.go new file mode 100644 index 0000000..b2c2f30 --- /dev/null +++ b/typex/typex_test.go @@ -0,0 +1,368 @@ +package typex_test + +import ( + "bytes" + "encoding" + "go/types" + "io" + "reflect" + "testing" + "unsafe" + + . "github.com/onsi/gomega" + + "github.com/xoctopus/x/ptrx" + . "github.com/xoctopus/x/typex" + "github.com/xoctopus/x/typex/testdata" +) + +func TestType(t *testing.T) { + cases := []struct { + name string + v any + typename string + }{ + {"GenericStruct", testdata.AnyStruct[string]{Name: "any"}, "github.com/xoctopus/x/typex/testdata.AnyStruct[string]"}, + {"GenericSlice", testdata.AnySlice[string]{}, "github.com/xoctopus/x/typex/testdata.AnySlice[string]"}, + {"GenericMap", testdata.AnyMap[int, string]{}, "github.com/xoctopus/x/typex/testdata.AnyMap[int,string]"}, + {"GenericArray", testdata.AnyArray[string]{}, "github.com/xoctopus/x/typex/testdata.AnyArray[string]"}, + {"InstancedGeneric", testdata.EnumMap{}, "github.com/xoctopus/x/typex/testdata.AnyMap[github.com/xoctopus/x/typex/testdata.Enum,interface {}]"}, + {"ComposeStruct", testdata.Compose{}, "github.com/xoctopus/x/typex/testdata.Compose"}, + { + "Func", + func() *testdata.Enum { + v := testdata.ENUM__ONE + return &v + }, + "func() *github.com/xoctopus/x/typex/testdata.Enum", + }, + {"Enum", testdata.ENUM__ONE, "github.com/xoctopus/x/typex/testdata.Enum"}, + {"RtEncodingTextMarshaler", reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem(), "encoding.TextMarshaler"}, + {"RtMixedInterface", reflect.TypeOf((*testdata.MixedInterface)(nil)).Elem(), "github.com/xoctopus/x/typex/testdata.MixedInterface"}, + {"UnsafePointer", unsafe.Pointer(t), "unsafe.Pointer"}, + {"NamedChan", make(testdata.Chan), "github.com/xoctopus/x/typex/testdata.Chan"}, + {"Chan", make(chan int, 100), "chan int"}, + {"GlobalFunc", testdata.F, "func()"}, + {"NamedFunc", testdata.Func(func(string, string) bool { return true }), "github.com/xoctopus/x/typex/testdata.Func"}, + {"NamedStringArray", testdata.Array{}, "github.com/xoctopus/x/typex/testdata.Array"}, + {"StringArray", [1]string{}, "[1]string"}, + {"NamedStringSlice", testdata.Slice{}, "github.com/xoctopus/x/typex/testdata.Slice"}, + {"StringSlice", []string{}, "[]string"}, + {"NamedMap", testdata.Map{}, "github.com/xoctopus/x/typex/testdata.Map"}, + {"Map", map[string]string{}, "map[string]string"}, + {"Struct", testdata.Struct{}, "github.com/xoctopus/x/typex/testdata.Struct"}, + {"StructPtr", &testdata.Struct{}, "*github.com/xoctopus/x/typex/testdata.Struct"}, + { + "UnameStruct", + struct { + A string `json:"a"` + B *testdata.Part `json:"part"` + }{}, + "struct { A string `json:\"a\"`; B *github.com/xoctopus/x/typex/testdata.Part `json:\"part\"` }", + }, + {"EmptyUnnameStruct", struct{}{}, "struct {}"}, + { + "UnameComposedInterfaceSlice", + []interface { + io.Reader + io.Closer + shouldConstrained() + }{}, + "[]interface { Close() error; Read([]uint8) (int, error); .shouldConstrained() }", + }, + {"FuncWithInAndOut", func(string, string) bool { return true }, "func(string, string) bool"}, + {"NamedString", testdata.String(""), "github.com/xoctopus/x/typex/testdata.String"}, + {"String", "", "string"}, + {"NamedBoolean", testdata.Boolean(true), "github.com/xoctopus/x/typex/testdata.Boolean"}, + {"Boolean", false, "bool"}, + {"NamedInt", testdata.Int(0), "github.com/xoctopus/x/typex/testdata.Int"}, + {"NamedIntPtr", ptrx.Ptr(testdata.Int(0)), "*github.com/xoctopus/x/typex/testdata.Int"}, + {"Int", int(0), "int"}, + {"IntPtr", ptrx.Ptr(int(0)), "*int"}, + {"NamedInt8", testdata.Int8(0), "github.com/xoctopus/x/typex/testdata.Int8"}, + {"NamedInt16", testdata.Int16(0), "github.com/xoctopus/x/typex/testdata.Int16"}, + {"NamedInt32", testdata.Int32(0), "github.com/xoctopus/x/typex/testdata.Int32"}, + {"NamedInt64", testdata.Int64(0), "github.com/xoctopus/x/typex/testdata.Int64"}, + {"NamedUint", testdata.Uint(0), "github.com/xoctopus/x/typex/testdata.Uint"}, + {"NamedUint8", testdata.Uint8(0), "github.com/xoctopus/x/typex/testdata.Uint8"}, + {"NamedUint16", testdata.Uint16(0), "github.com/xoctopus/x/typex/testdata.Uint16"}, + {"NamedUint32", testdata.Uint32(0), "github.com/xoctopus/x/typex/testdata.Uint32"}, + {"NamedUint64", testdata.Uint64(0), "github.com/xoctopus/x/typex/testdata.Uint64"}, + {"NamedUintptr", testdata.Uintptr(0), "github.com/xoctopus/x/typex/testdata.Uintptr"}, + {"NamedFloat32", testdata.Float32(0), "github.com/xoctopus/x/typex/testdata.Float32"}, + {"NamedFloat64", testdata.Float64(0), "github.com/xoctopus/x/typex/testdata.Float64"}, + {"NamedComplex64", testdata.Complex64(0), "github.com/xoctopus/x/typex/testdata.Complex64"}, + {"NamedComplex128", testdata.Complex128(0), "github.com/xoctopus/x/typex/testdata.Complex128"}, + {"Nil", nil, "nil"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + rt, ok := c.v.(reflect.Type) + if !ok { + rt = reflect.TypeOf(c.v) + } + gt := NewGoTypeFromReflectType(rt) + + rtt := NewReflectType(rt) + gtt := NewGoType(gt) + + tt := NewWithT(t) + tt.Expect(Typename(rtt)).To(Equal(c.typename)) + if rtt.Unwrap() == nil { + tt.Expect(rtt.Unwrap()).To(BeNil()) + tt.Expect(gtt.Unwrap()).To(BeNil()) + return + } + tt.Expect(rtt.Unwrap()).To(Equal(rt)) + tt.Expect(gtt.Unwrap()).To(Equal(gt)) + tt.Expect(rtt.String()).To(Equal(gtt.String())) + tt.Expect(rtt.Kind().String()).To(Equal(gtt.Kind().String())) + tt.Expect(rtt.Name()).To(Equal(gtt.Name())) + tt.Expect(rtt.PkgPath()).To(Equal(gtt.PkgPath())) + tt.Expect(rtt.Comparable()).To(Equal(gtt.Comparable())) + tt.Expect(rtt.AssignableTo(NewReflectType(reflect.TypeOf("")))). + To(Equal(gtt.AssignableTo(NewGoType(types.Typ[types.String])))) + tt.Expect(rtt.AssignableTo(NewReflectType(reflect.TypeOf(0)))). + To(Equal(gtt.AssignableTo(NewGoType(types.Typ[types.Int])))) + tt.Expect(rtt.ConvertibleTo(NewReflectType(reflect.TypeOf("")))). + To(Equal(gtt.ConvertibleTo(NewGoType(types.Typ[types.String])))) + tt.Expect(rtt.ConvertibleTo(NewReflectType(reflect.TypeOf(0)))). + To(Equal(gtt.ConvertibleTo(NewGoType(types.Typ[types.Int])))) + tt.Expect(rtt.AssignableTo(gtt)).To(BeFalse()) + tt.Expect(gtt.AssignableTo(rtt)).To(BeFalse()) + tt.Expect(rtt.ConvertibleTo(gtt)).To(BeFalse()) + tt.Expect(gtt.ConvertibleTo(rtt)).To(BeFalse()) + if rtt.Kind() == reflect.Struct { + tt.Expect(rtt.NumField()).To(Equal(gtt.NumField())) + } + if rtt.Kind() == reflect.Pointer && Deref(rtt).Kind() == reflect.Struct { + tt.Expect(Deref(rtt).NumField()).To(Equal(gtt.NumField())) + } + tt.Expect(rtt.NumMethod()).To(Equal(gtt.NumMethod())) + for i := 0; i < rtt.NumMethod(); i++ { + rm := rtt.Method(i) + gm, exists := gtt.MethodByName(rm.Name()) + tt.Expect(exists).To(BeTrue()) + tt.Expect(rm.Name()).To(Equal(gm.Name())) + tt.Expect(rm.PkgPath()).To(Equal(gm.PkgPath())) + tt.Expect(rm.Type().String()).To(Equal(gm.Type().String())) + } + + _, rStringMethodExists := rtt.MethodByName("String") + _, gStringMethodExists := gtt.MethodByName("String") + tt.Expect(rStringMethodExists).To(Equal(gStringMethodExists)) + + if rtt.Kind() == reflect.Array { + tt.Expect(rtt.Len()).To(Equal(gtt.Len())) + tt.Expect(Typename(rtt.Elem())).To(Equal(Typename(gtt.Elem()))) + } + + if rtt.Kind() == reflect.Map { + tt.Expect(Typename(rtt.Key())).To(Equal(Typename(gtt.Key()))) + tt.Expect(Typename(rtt.Elem())).To(Equal(Typename(gtt.Elem()))) + } + + if rtt.Kind() == reflect.Slice { + tt.Expect(Typename(rtt.Elem())).To(Equal(Typename(gtt.Elem()))) + } + + if rtt.Kind() == reflect.Struct { + tt.Expect(rtt.NumField()).To(Equal(gtt.NumField())) + for i := 0; i < rtt.NumField(); i++ { + rsf := rtt.Field(i) + gsf := gtt.Field(i) + + tt.Expect(rsf.Anonymous()).To(Equal(gsf.Anonymous())) + tt.Expect(rsf.Tag()).To(Equal(gsf.Tag())) + tt.Expect(rsf.Name()).To(Equal(gsf.Name())) + tt.Expect(rsf.PkgPath()).To(Equal(gsf.PkgPath())) + tt.Expect(Typename(rsf.Type())).To(Equal(Typename(gsf.Type()))) + } + + rsf, exists1 := rtt.FieldByName("A") + gsf, exists2 := gtt.FieldByName("A") + tt.Expect(exists1).To(Equal(exists2)) + if exists2 { + tt.Expect(rsf.Anonymous()).To(Equal(gsf.Anonymous())) + tt.Expect(rsf.Tag()).To(Equal(gsf.Tag())) + tt.Expect(rsf.Name()).To(Equal(gsf.Name())) + tt.Expect(rsf.PkgPath()).To(Equal(gsf.PkgPath())) + tt.Expect(Typename(rsf.Type())).To(Equal(Typename(gsf.Type()))) + } + + _, exists1 = rtt.FieldByName("_") + _, exists2 = gtt.FieldByName("_") + tt.Expect(exists1).To(Equal(exists2)) + tt.Expect(exists1).To(BeFalse()) + + match := func(s string) bool { + return s == "A" + } + rsf, exists1 = rtt.FieldByNameFunc(match) + gsf, exists2 = gtt.FieldByNameFunc(match) + tt.Expect(exists1).To(Equal(exists2)) + if exists1 { + tt.Expect(rsf.Anonymous()).To(Equal(gsf.Anonymous())) + tt.Expect(rsf.Tag()).To(Equal(gsf.Tag())) + tt.Expect(rsf.Name()).To(Equal(gsf.Name())) + tt.Expect(rsf.PkgPath()).To(Equal(gsf.PkgPath())) + tt.Expect(Typename(rsf.Type())).To(Equal(Typename(gsf.Type()))) + } + + match = func(s string) bool { return false } + _, exists1 = rtt.FieldByNameFunc(match) + _, exists2 = gtt.FieldByNameFunc(match) + tt.Expect(exists1).To(Equal(exists2)) + tt.Expect(exists1).To(BeFalse()) + } + + if rtt.Kind() == reflect.Func { + tt.Expect(rtt.NumIn()).To(Equal(gtt.NumIn())) + tt.Expect(rtt.NumOut()).To(Equal(gtt.NumOut())) + for i := 0; i < rtt.NumIn(); i++ { + tt.Expect(rtt.In(i).String()).To(Equal(gtt.In(i).String())) + } + for i := 0; i < rtt.NumOut(); i++ { + tt.Expect(rtt.Out(i).String()).To(Equal(gtt.Out(i).String())) + } + } + + tt.Expect(Deref(rtt).String()).To(Equal(Deref(gtt).String())) + }) + } +} + +func TestGoTypeExt(t *testing.T) { + t.Run("BuildUnsafePointerGoType", func(t *testing.T) { + tpe := types.NewTypeName(0, NewPackage("unsafe"), "Pointer", types.Typ[types.UnsafePointer]) + gt := types.NewNamed(tpe, tpe.Type().Underlying(), nil) + NewWithT(t).Expect(NewGoType(gt).Kind()).To(Equal(reflect.UnsafePointer)) + }) + + t.Run("InvalidGoType", func(t *testing.T) { + gt := NewGoType(types.Typ[types.Invalid]) + NewWithT(t).Expect(gt.Key()).To(BeNil()) + NewWithT(t).Expect(gt.Elem()).To(BeNil()) + NewWithT(t).Expect(gt.Len()).To(BeZero()) + NewWithT(t).Expect(gt.Kind()).To(Equal(reflect.Invalid)) + NewWithT(t).Expect(gt.Field(1)).To(BeNil()) + NewWithT(t).Expect(gt.Method(1)).To(BeNil()) + NewWithT(t).Expect(gt.IsVariadic()).To(BeFalse()) + NewWithT(t).Expect(gt.NumIn()).To(BeZero()) + NewWithT(t).Expect(gt.In(0)).To(BeNil()) + NewWithT(t).Expect(gt.NumOut()).To(BeZero()) + NewWithT(t).Expect(gt.Out(0)).To(BeNil()) + + gt = NewGoType(nil) + NewWithT(t).Expect(gt.Kind()).To(Equal(reflect.Invalid)) + }) + + t.Run("ConstrainedInterface", func(t *testing.T) { + gt := NewGoType(types.NewInterfaceType([]*types.Func{ + types.NewFunc( + 0, nil, "shouldBeConstrained", + types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewVar(0, nil, "v", types.NewInterfaceType(nil, nil))), + nil, false, + )), + }, nil)) + NewWithT(t).Expect(gt.String()).To(Equal("interface { shouldBeConstrained(interface {}) }")) + NewWithT(t).Expect(gt.Method(0).Name()).To(Equal("shouldBeConstrained")) + NewWithT(t).Expect(gt.Method(0).Type().String()).To(Equal("func(interface {})")) + NewWithT(t).Expect(gt.Method(0).PkgPath()).To(BeEmpty()) + }) + + t.Run("UnexportedStructField", func(t *testing.T) { + gt := NewGoType(NewGoTypeFromReflectType(reflect.TypeOf(testdata.Struct{}))) + f, exists := gt.FieldByName("a") + NewWithT(t).Expect(exists).To(BeTrue()) + NewWithT(t).Expect(f.PkgPath()).To(Equal(gt.PkgPath())) + + gt = NewGoType(NewGoTypeFromReflectType(reflect.TypeOf(struct{ a string }{}))) + f, exists = gt.FieldByName("a") + NewWithT(t).Expect(exists).To(BeTrue()) + NewWithT(t).Expect(f.PkgPath()).To(Equal("github.com/xoctopus/x/typex_test")) + }) + + t.Run("NewGoTypeFromTypeParam", func(t *testing.T) { + gt := NewGoType(NewGoTypeFromReflectType(reflect.TypeOf(testdata.AnyArray[string]{}))) + gt = NewGoType(gt.Type.(*types.Named).TypeParams().At(0)) + // TODO if use the underlying type + NewWithT(t).Expect(gt.Type.String()).To(Equal("string")) + NewWithT(t).Expect(gt.String()).To(Equal("interface {}")) + }) + + t.Run("Implements", func(t *testing.T) { + gt := NewGoType(NewGoTypeFromReflectType(reflect.TypeOf(ptrx.Ptr(testdata.Enum(1))))) + t.Run("GoType", func(t *testing.T) { + xgt := NewGoType(types.NewInterfaceType( + []*types.Func{ + types.NewFunc(0, nil, "String", types.NewSignatureType( + nil, nil, nil, nil, + types.NewTuple( + types.NewVar(0, nil, "", types.Typ[types.String]), + ), false, + )), + }, + nil, + )) + NewWithT(t).Expect(gt.Implements(xgt)).To(BeTrue()) + }) + t.Run("ReflectType", func(t *testing.T) { + rt := reflect.TypeOf([]encoding.TextMarshaler{}).Elem() + xrt := NewReflectType(rt) + NewWithT(t).Expect(gt.Implements(xrt)).To(BeTrue()) + }) + t.Run("Invalid", func(t *testing.T) { + t.Run("Nil", func(t *testing.T) { + NewWithT(t).Expect(gt.Implements(nil)).To(BeFalse()) + }) + t.Run("NotInterface", func(t *testing.T) { + NewWithT(t).Expect(gt.Implements(NewGoType(types.Typ[types.Int]))).To(BeFalse()) + NewWithT(t).Expect(gt.Implements(NewReflectType(reflect.TypeOf(1)))).To(BeFalse()) + NewWithT(t).Expect(gt.Implements(NewReflectType(reflect.TypeOf(bytes.Buffer{})))).To(BeFalse()) + }) + }) + }) +} + +func TestReflectTypeExt(t *testing.T) { + rtt := NewReflectType(reflect.TypeOf(testdata.Enum(1))) + xgt := NewGoType(types.NewInterfaceType( + []*types.Func{ + types.NewFunc(0, nil, "String", types.NewSignatureType( + nil, nil, nil, nil, + types.NewTuple( + types.NewVar(0, nil, "", types.Typ[types.String]), + ), false, + )), + }, + nil, + )) + xrt := NewReflectType(reflect.TypeOf([]encoding.TextMarshaler{}).Elem()) + NewWithT(t).Expect(rtt.Implements(xgt)).To(BeTrue()) + NewWithT(t).Expect(rtt.Implements(xrt)).To(BeTrue()) + NewWithT(t).Expect(rtt.Implements(nil)).To(BeFalse()) + + rtt = NewReflectType(reflect.TypeOf(1)) + NewWithT(t).Expect(rtt.Implements(xgt)).To(BeFalse()) +} + +func TestComparable(t *testing.T) { + gt := NewGoType(NewGoTypeFromReflectType(reflect.TypeOf([2]string{}))) + // true + t.Log(gt.Comparable()) + + rt := reflect.TypeOf(testdata.AnyArray[string]{}) + gt = NewGoType(NewGoTypeFromReflectType(rt)) + // true + t.Log(rt.Comparable()) + // false + t.Log(types.Comparable(gt.Type)) + // false + t.Log(types.Comparable(gt.Type.Underlying())) + // true + t.Log(types.Comparable(ConstrainUnderlying(gt.Type.(*types.Named).TypeParams(), gt.Type.Underlying()))) +}