[v3] Improved parser for bound structs

This commit is contained in:
Lea Anthony
2023-02-17 20:57:31 +11:00
parent bd184cab85
commit e1279a054f
7 changed files with 1246 additions and 509 deletions
-1
View File
@@ -5,7 +5,6 @@ import (
"log"
"github.com/wailsapp/wails/v3/examples/binding/services"
"github.com/wailsapp/wails/v3/pkg/application"
)
+94 -93
View File
@@ -1,95 +1,96 @@
package parser
import (
"bytes"
"fmt"
"go/ast"
"go/types"
"sort"
"strings"
"unicode"
)
func GenerateModels(context *Context) ([]byte, error) {
var buf bytes.Buffer
var pkgs []Package
specs := context.GetBoundStructs()
for pkg, pkgSpecs := range specs {
pkgs = append(pkgs, Package{Name: pkg, Specs: pkgSpecs})
}
knownStructs := newAllModels(specs)
sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Name < pkgs[j].Name })
for _, pkg := range pkgs {
if _, err := fmt.Fprintf(&buf, "namespace %s {\n", pkg.Name); err != nil {
return nil, err
}
sort.Slice(pkg.Specs, func(i, j int) bool { return pkg.Specs[i].Name.Name < pkg.Specs[j].Name.Name })
for _, spec := range pkg.Specs {
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 {
// Ignore field names that have a lower case first letter
if !unicode.IsUpper(rune(field.Names[0].Name[0])) {
continue
}
// Get the Go type of the field
goType := types.ExprString(field.Type)
// Check if the type is an array
if arrayType, ok := field.Type.(*ast.ArrayType); ok {
// Get the element type of the array
elementType := types.ExprString(arrayType.Elt)
// Look up the corresponding TypeScript type
tsType, ok := goToTS[elementType]
if !ok {
// strip off the * prefix if it is there
if strings.HasPrefix(elementType, "*") {
elementType = elementType[1:]
}
if knownStructs.exists(elementType) {
tsType = elementType
} else {
tsType = "any"
}
}
// Output the field as an array of the corresponding TypeScript type
if _, err := fmt.Fprintf(&buf, " %s: %s[];\n", field.Names[0].Name, tsType); err != nil {
return nil, err
}
} else {
// strip off the * prefix if it is there
if strings.HasPrefix(goType, "*") {
goType = goType[1:]
}
// Look up the corresponding TypeScript type
tsType, ok := goToTS[goType]
if !ok {
if knownStructs.exists(goType) {
tsType = goType
} else {
tsType = "any"
}
}
// Output the field as the corresponding TypeScript type
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\n"); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
//
//import (
// "bytes"
// "fmt"
// "go/ast"
// "go/types"
// "sort"
// "strings"
// "unicode"
//)
//
//func GenerateModels(context *Context) ([]byte, error) {
// var buf bytes.Buffer
// var pkgs []Package
// specs := context.GetBoundStructs()
// for pkg, pkgSpecs := range specs {
// pkgs = append(pkgs, Package{Name: pkg, Specs: pkgSpecs})
// }
// knownStructs := newAllModels(specs)
// sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Name < pkgs[j].Name })
// for _, pkg := range pkgs {
// if _, err := fmt.Fprintf(&buf, "namespace %s {\n", pkg.Name); err != nil {
// return nil, err
// }
// sort.Slice(pkg.Specs, func(i, j int) bool { return pkg.Specs[i].Name.Name < pkg.Specs[j].Name.Name })
// for _, spec := range pkg.Specs {
// 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 {
//
// // Ignore field names that have a lower case first letter
// if !unicode.IsUpper(rune(field.Names[0].Name[0])) {
// continue
// }
//
// // Get the Go type of the field
// goType := types.ExprString(field.Type)
// // Check if the type is an array
// if arrayType, ok := field.Type.(*ast.ArrayType); ok {
// // Get the element type of the array
// elementType := types.ExprString(arrayType.Elt)
// // Look up the corresponding TypeScript type
// tsType, ok := goToTS[elementType]
// if !ok {
// // strip off the * prefix if it is there
// if strings.HasPrefix(elementType, "*") {
// elementType = elementType[1:]
// }
// if knownStructs.exists(elementType) {
// tsType = elementType
// } else {
// tsType = "any"
// }
// }
// // Output the field as an array of the corresponding TypeScript type
// if _, err := fmt.Fprintf(&buf, " %s: %s[];\n", field.Names[0].Name, tsType); err != nil {
// return nil, err
// }
// } else {
// // strip off the * prefix if it is there
// if strings.HasPrefix(goType, "*") {
// goType = goType[1:]
// }
// // Look up the corresponding TypeScript type
// tsType, ok := goToTS[goType]
// if !ok {
// if knownStructs.exists(goType) {
// tsType = goType
// } else {
// tsType = "any"
// }
// }
// // Output the field as the corresponding TypeScript type
// 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\n"); err != nil {
// return nil, err
// }
// }
// return buf.Bytes(), nil
//}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+88 -86
View File
@@ -34,9 +34,9 @@ func TestParseDirectory(t *testing.T) {
wantErr: false,
},
{
name: "should find bound services from other packages",
dir: "../../examples/binding",
want: []string{"main.localStruct", "services.GreetService", "models.Person"},
name: "should find multiple bound services over multiple packages",
dir: "testdata/struct_literal_multiple_other",
want: []string{"main.GreetService", "services.OtherService"},
wantErr: false,
},
}
@@ -50,7 +50,8 @@ func TestParseDirectory(t *testing.T) {
}
for name, pkg := range got.packages {
for structName := range pkg.boundStructs {
for structName, structType := range pkg.boundStructs {
require.NotNil(t, structType)
require.True(t, lo.Contains(tt.want, name+"."+structName))
tt.want = lo.Without(tt.want, name+"."+structName)
}
@@ -58,87 +59,88 @@ 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: `namespace main {
class GreetService {
SomeVariable: number;
}
}
`,
wantErr: false,
},
{
name: "should find multiple bound services",
dir: "testdata/struct_literal_multiple",
want: `namespace main {
class GreetService {
SomeVariable: number;
}
class OtherService {
}
}
`,
wantErr: false,
},
{
name: "should find multiple bound services over multiple files",
dir: "testdata/struct_literal_multiple_files",
want: `namespace main {
class GreetService {
SomeVariable: number;
}
class OtherService {
}
}
`,
wantErr: false,
},
{
name: "should find bound services from other packages",
dir: "../../examples/binding",
want: `namespace main {
class localStruct {
}
}
namespace models {
class Person {
Name: string;
}
}
namespace services {
class GreetService {
SomeVariable: number;
Parent: models.Person;
}
}
`,
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 := GenerateModels(context)
require.NoError(t, err)
require.Equal(t, tt.want, string(ts))
})
}
}
//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: `namespace main {
// class GreetService {
// SomeVariable: number;
// }
//}
//`,
// wantErr: false,
// },
// {
// name: "should find multiple bound services",
// dir: "testdata/struct_literal_multiple",
// want: `namespace main {
// class GreetService {
// SomeVariable: number;
// }
// class OtherService {
// }
//}
//`,
// wantErr: false,
// },
// {
// name: "should find multiple bound services over multiple files",
// dir: "testdata/struct_literal_multiple_files",
// want: `namespace main {
// class GreetService {
// SomeVariable: number;
// }
// class OtherService {
// }
//}
//`,
// wantErr: false,
// },
// {
// name: "should find bound services from other packages",
// dir: "../../examples/binding",
// want: `namespace main {
// class localStruct {
// }
//}
//namespace models {
// class Person {
// Name: string;
// }
//}
//namespace services {
// class GreetService {
// SomeVariable: number;
// Parent: models.Person;
// }
//}
//`,
// 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 := GenerateModels(context)
// require.NoError(t, err)
// require.Equal(t, tt.want, string(ts))
//
// })
// }
//}
@@ -0,0 +1,38 @@
package main
import (
_ "embed"
"log"
"github.com/wailsapp/wails/v3/internal/parser/testdata/struct_literal_multiple_other/services"
"github.com/wailsapp/wails/v3/pkg/application"
)
// GreetService is great
type GreetService struct {
SomeVariable int
lowerCase string
}
// Greet does XYZ
func (*GreetService) Greet(name string) string {
return "Hello " + name
}
func main() {
app := application.New(application.Options{
Bind: []interface{}{
&GreetService{},
&services.OtherService{},
},
})
app.NewWebviewWindow()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
@@ -0,0 +1,12 @@
package services
// OtherService is a struct
// that does things
type OtherService struct {
t int
}
// Yay does this and that
func (o *OtherService) Yay() {
}