From 0a09363db70ed8cbe3a40d29015cfba364454737 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Tue, 17 Jan 2023 07:45:41 +1100 Subject: [PATCH] Support struct discovery over multiple files --- exp/internal/parser/packages.go | 93 +++++++++++++++++++++++++++- exp/internal/parser/packages_test.go | 51 ++++++++++++++- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/exp/internal/parser/packages.go b/exp/internal/parser/packages.go index 56283e5d9..07ffda4ff 100644 --- a/exp/internal/parser/packages.go +++ b/exp/internal/parser/packages.go @@ -1,10 +1,12 @@ package parser import ( + "bytes" "fmt" "go/ast" "go/parser" "go/token" + "go/types" "strings" "github.com/samber/lo" @@ -30,6 +32,16 @@ type Context struct { packages map[string]*parsedPackage } +func (c *Context) GetBoundStructs() map[string][]*ast.TypeSpec { + structs := make(map[string][]*ast.TypeSpec) + for _, pkg := range c.packages { + for _, structType := range pkg.boundStructs { + structs[pkg.name] = append(structs[pkg.name], structType) + } + } + return structs +} + func ParseDirectory(dir string) (*Context, error) { // Parse the directory fset := token.NewFileSet() @@ -150,7 +162,9 @@ func findApplicationNewCalls(context *Context) { ident, ok := boundStructLit.Type.(*ast.Ident) if ok { if ident.Obj == nil { - debug("Ident.Obj is nil - check") + structTypeSpec := findStructInPackage(thisPackage.pkg, ident.Name) + thisPackage.boundStructs[ident.Name] = structTypeSpec + findNestedStructs(structTypeSpec, file, packageName, context) continue } // Check if the ident is a struct type @@ -384,3 +398,80 @@ func parseField(field *ast.Field, parsedFile *ast.File, pkgName string, context } } } + +func findStructInPackage(pkg *ast.Package, name string) *ast.TypeSpec { + for _, file := range pkg.Files { + for _, decl := range file.Decls { + if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.TYPE { + for _, spec := range gen.Specs { + if typeSpec, ok := spec.(*ast.TypeSpec); ok { + if typeSpec.Name.Name == name { + if _, ok := typeSpec.Type.(*ast.StructType); ok { + return typeSpec + } + } + } + } + } + } + } + return nil +} + +var goToTS = map[string]string{ + "int": "number", + "int8": "number", + "int16": "number", + "int32": "number", + "int64": "number", + "uint": "number", + "uint8": "number", + "uint16": "number", + "uint32": "number", + "uint64": "number", + "float32": "number", + "float64": "number", + "string": "string", + "bool": "boolean", +} + +func GenerateTypeScript(specs map[string][]*ast.TypeSpec) ([]byte, error) { + var buf bytes.Buffer + + for pkg, pkgSpecs := range specs { + if _, err := fmt.Fprintf(&buf, "namespace %s {\n", pkg); err != nil { + return nil, err + } + + for _, spec := range pkgSpecs { + if structType, ok := spec.Type.(*ast.StructType); ok { + if _, err := fmt.Fprintf(&buf, " class %s {\n", spec.Name.Name); err != nil { + return nil, err + } + + for _, field := range structType.Fields.List { + // Get the Go type of the field + goType := types.ExprString(field.Type) + // Look up the corresponding TypeScript type + tsType, ok := goToTS[goType] + if !ok { + tsType = goType + } + + if _, err := fmt.Fprintf(&buf, " %s: %s;\n", field.Names[0].Name, tsType); err != nil { + return nil, err + } + } + + if _, err := fmt.Fprintf(&buf, " }\n"); err != nil { + return nil, err + } + } + } + + if _, err := fmt.Fprintf(&buf, "}\n"); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} diff --git a/exp/internal/parser/packages_test.go b/exp/internal/parser/packages_test.go index e4f2131d3..339034fd1 100644 --- a/exp/internal/parser/packages_test.go +++ b/exp/internal/parser/packages_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/samber/lo" + "github.com/stretchr/testify/require" ) @@ -17,7 +18,7 @@ func TestParseDirectory(t *testing.T) { { name: "should find single bound service", dir: "testdata/struct_literal_single", - want: []string{"GreetService"}, + want: []string{"main.GreetService"}, wantErr: false, }, { @@ -55,6 +56,54 @@ func TestParseDirectory(t *testing.T) { } } require.Empty(t, tt.want) + }) + } +} + +func TestGenerateTypeScript(t *testing.T) { + tests := []struct { + name string + dir string + want string + wantErr bool + }{ + { + name: "should find single bound service", + dir: "testdata/struct_literal_single", + want: "", + wantErr: false, + }, + { + name: "should find multiple bound services", + dir: "testdata/struct_literal_multiple", + want: "", + wantErr: false, + }, + { + name: "should find multiple bound services over multiple files", + dir: "testdata/struct_literal_multiple_files", + want: "", + wantErr: false, + }, + { + name: "should find bound services from other packages", + dir: "../../examples/binding", + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Debug = true + context, err := ParseDirectory(tt.dir) + if (err != nil) != tt.wantErr { + t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr) + return + } + + ts, err := GenerateTypeScript(context.GetBoundStructs()) + require.NoError(t, err) + require.Equal(t, ts, tt.want) }) }