mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Recursive bound model discovery implemented
This commit is contained in:
parent
572fbc9705
commit
8bbf5ff3f1
27 changed files with 350 additions and 594 deletions
|
|
@ -4,23 +4,19 @@ import (
|
|||
_ "embed"
|
||||
"log"
|
||||
|
||||
"github.com/wailsapp/wails/exp/examples/binding/services"
|
||||
|
||||
"github.com/wailsapp/wails/exp/pkg/application"
|
||||
"github.com/wailsapp/wails/exp/pkg/options"
|
||||
)
|
||||
|
||||
type GreetService struct {
|
||||
SomeVariable int
|
||||
lowerCase string
|
||||
}
|
||||
|
||||
func (*GreetService) Greet(name string) string {
|
||||
return "Hello " + name
|
||||
}
|
||||
type localStruct struct{}
|
||||
|
||||
func main() {
|
||||
app := application.New(options.Application{
|
||||
Bind: []interface{}{
|
||||
&GreetService{},
|
||||
&localStruct{},
|
||||
&services.GreetService{},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/exp/pkg/application"
|
||||
"github.com/wailsapp/wails/exp/pkg/options"
|
||||
)
|
||||
|
||||
//func appOptions() options.Application {
|
||||
// return options.Application{
|
||||
// Bind: []interface{}{
|
||||
// &GreetService{},
|
||||
// },
|
||||
// }
|
||||
//}
|
||||
|
||||
//func appOptions() options.Application {
|
||||
// result := options.Application{
|
||||
// Bind: []interface{}{
|
||||
// &GreetService{},
|
||||
// },
|
||||
// }
|
||||
// return result
|
||||
//}
|
||||
|
||||
func appOptions() options.Application {
|
||||
result := options.Application{
|
||||
Bind: []interface{}{
|
||||
&GreetService{},
|
||||
},
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func run() error {
|
||||
//o := options.Application{
|
||||
// Bind: []interface{}{
|
||||
// &GreetService{},
|
||||
// },
|
||||
//}
|
||||
//app := application.New(o)
|
||||
|
||||
//app := application.New(options.Application{
|
||||
// Bind: []interface{}{
|
||||
// &GreetService{},
|
||||
// },
|
||||
//})
|
||||
|
||||
app := application.New(appOptions())
|
||||
|
||||
return app.Run()
|
||||
}
|
||||
13
exp/examples/binding/services/GreetService.go
Normal file
13
exp/examples/binding/services/GreetService.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package services
|
||||
|
||||
import "github.com/wailsapp/wails/exp/examples/binding/models"
|
||||
|
||||
type GreetService struct {
|
||||
SomeVariable int
|
||||
lowercase string
|
||||
Parent *models.Person
|
||||
}
|
||||
|
||||
func (*GreetService) Greet(name string) string {
|
||||
return "Hello " + name
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ require (
|
|||
github.com/samber/lo v1.37.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/tc-hib/winres v0.1.6
|
||||
golang.org/x/tools v0.1.12
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -37,6 +38,7 @@ require (
|
|||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p
|
|||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -145,6 +147,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
package parser
|
||||
|
||||
import "go/ast"
|
||||
|
||||
type structInfo struct {
|
||||
packageName string
|
||||
structName string
|
||||
}
|
||||
|
||||
func extractBindExprs(imp *ImportSpecInfo) {
|
||||
// Check if the given expression is a composite literal
|
||||
if comp, ok := (*imp.Options).(*ast.CompositeLit); ok {
|
||||
// Iterate through the composite literal fields
|
||||
for _, field := range comp.Elts {
|
||||
// Check if the field key is "Bind"
|
||||
if kv, ok := field.(*ast.KeyValueExpr); ok {
|
||||
if ident, ok := kv.Key.(*ast.Ident); ok {
|
||||
if ident.Name == "Bind" {
|
||||
// Extract the expressions in the Bind field
|
||||
if arr, ok := kv.Value.(*ast.CompositeLit); ok {
|
||||
for _, elt := range arr.Elts {
|
||||
if unaryExpr, ok := elt.(*ast.UnaryExpr); ok {
|
||||
if addr, ok := unaryExpr.X.(*ast.CompositeLit); ok {
|
||||
// Extract the type of the struct
|
||||
if selExpr, ok := addr.Type.(*ast.SelectorExpr); ok {
|
||||
if ident, ok := selExpr.X.(*ast.Ident); ok {
|
||||
imp.BoundStructNames = append(imp.BoundStructNames, &structInfo{
|
||||
packageName: ident.Name,
|
||||
structName: selExpr.Sel.Name,
|
||||
})
|
||||
}
|
||||
} else if ident, ok := addr.Type.(*ast.Ident); ok {
|
||||
imp.BoundStructNames = append(imp.BoundStructNames, &structInfo{
|
||||
packageName: "",
|
||||
structName: ident.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_extractBindExprs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
expectedBoundStructs []*structInfo
|
||||
}{
|
||||
{
|
||||
name: "should find single bound struct literal",
|
||||
dir: "testdata/boundstructs/struct_literal_single",
|
||||
expectedBoundStructs: []*structInfo{{structName: "GreetService"}},
|
||||
},
|
||||
{
|
||||
name: "should find multiple bound struct literal",
|
||||
dir: "testdata/boundstructs/struct_literal_multiple",
|
||||
expectedBoundStructs: []*structInfo{{structName: "GreetService"}, {structName: "OtherService"}},
|
||||
},
|
||||
{
|
||||
name: "should find multiple bound struct literals over multiple files",
|
||||
dir: "testdata/boundstructs/struct_literal_multiple_files",
|
||||
expectedBoundStructs: []*structInfo{{structName: "GreetService"}, {structName: "OtherService"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
imports, err := findFilesImportingPackage(tt.dir, "github.com/wailsapp/wails/exp/pkg/application")
|
||||
require.NoError(t, err)
|
||||
imp := findNewCalls(imports)
|
||||
require.NotNil(t, imp)
|
||||
isOptions := isOptionsApplication(imp.Options)
|
||||
require.True(t, isOptions)
|
||||
extractBindExprs(imp)
|
||||
require.ElementsMatchf(t, tt.expectedBoundStructs, imp.BoundStructNames, "expected %v, got %v", tt.expectedBoundStructs, imp.BoundStructNames)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"os"
|
||||
)
|
||||
|
||||
func findNewCalls(imports []ImportSpecInfo) *ImportSpecInfo {
|
||||
|
||||
var result *ast.Expr
|
||||
var inImport *ImportSpecInfo
|
||||
|
||||
for _, imp := range imports {
|
||||
for _, decl := range imp.File.Decls {
|
||||
ast.Inspect(decl, func(n ast.Node) bool {
|
||||
// check if the current node is a function call
|
||||
callExpr, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// check if the function being called is .New()
|
||||
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
|
||||
if !ok || selExpr.Sel.Name != "New" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get the name of the thing that New is being called on.
|
||||
var receiverName string
|
||||
switch rcv := selExpr.X.(type) {
|
||||
case *ast.Ident:
|
||||
receiverName = rcv.Name
|
||||
case *ast.SelectorExpr:
|
||||
receiverName = rcv.Sel.Name
|
||||
default:
|
||||
receiverName = "unknown"
|
||||
}
|
||||
|
||||
// Check if the receiver is a package name
|
||||
for _, i := range imports {
|
||||
if i.Identifier() == receiverName {
|
||||
if i.ImportSpec.Path.Value == `"github.com/wailsapp/wails/exp/pkg/application"` {
|
||||
// We have a call to application.New()
|
||||
// Parse out the first argument and check it is an option.Application struct
|
||||
// check callExpr.Args[0] is a struct literal
|
||||
if len(callExpr.Args) != 1 {
|
||||
return true
|
||||
}
|
||||
if result != nil {
|
||||
println("Found more than one call to application.New()")
|
||||
os.Exit(1)
|
||||
}
|
||||
result = &(callExpr.Args[0])
|
||||
inImport = &imp
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
inImport.Options = result
|
||||
return inImport
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_findNewCalls(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
expectedExpressions int
|
||||
}{
|
||||
{
|
||||
name: "should find single call to application.New",
|
||||
dir: "testdata/boundstructs/struct_literal_single",
|
||||
expectedExpressions: 1,
|
||||
},
|
||||
{
|
||||
name: "should find single call to application.New in multiple files",
|
||||
dir: "testdata/boundstructs/struct_literal_multiple_files",
|
||||
expectedExpressions: 1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
imports, err := findFilesImportingPackage(tt.dir, "github.com/wailsapp/wails/exp/pkg/application")
|
||||
require.NoError(t, err)
|
||||
got := findNewCalls(imports)
|
||||
require.NotNil(t, got.Options)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ImportSpecInfo struct {
|
||||
FileName string
|
||||
ImportSpec *ast.ImportSpec
|
||||
File *ast.File
|
||||
Alias string
|
||||
Options *ast.Expr
|
||||
BoundStructNames []*structInfo
|
||||
}
|
||||
|
||||
func (i ImportSpecInfo) Identifier() string {
|
||||
if i.Alias != "" {
|
||||
return i.Alias
|
||||
}
|
||||
return strings.Trim(filepath.Base(i.ImportSpec.Path.Value), `"`)
|
||||
}
|
||||
|
||||
func findFilesImportingPackage(dir, pkg string) ([]ImportSpecInfo, error) {
|
||||
var importSpecInfo []ImportSpecInfo
|
||||
|
||||
// Recursively search for .go files in the given directory
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if filepath.Ext(path) != ".go" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Search for import statement in the AST
|
||||
for _, imp := range f.Imports {
|
||||
if imp.Path.Value == "\""+pkg+"\"" {
|
||||
var alias string
|
||||
if imp.Name != nil {
|
||||
alias = imp.Name.Name
|
||||
}
|
||||
importSpecInfo = append(importSpecInfo, ImportSpecInfo{
|
||||
FileName: path,
|
||||
ImportSpec: imp,
|
||||
Alias: alias,
|
||||
File: f,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return importSpecInfo, nil
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func Test_findFilesImportingPackage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
pkg string
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should identify single file importing package",
|
||||
dir: "testdata/imports/single_file",
|
||||
pkg: "github.com/wailsapp/wails/exp/pkg/application",
|
||||
want: []string{"testdata/imports/single_file/main.go"},
|
||||
},
|
||||
{
|
||||
name: "should identify multiple files importing package",
|
||||
dir: "testdata/imports/multiple_files",
|
||||
pkg: "github.com/wailsapp/wails/exp/pkg/application",
|
||||
want: []string{"testdata/imports/multiple_files/app.go"},
|
||||
},
|
||||
{
|
||||
name: "should identify aliases",
|
||||
dir: "testdata/imports/alias",
|
||||
pkg: "github.com/wailsapp/wails/exp/pkg/application",
|
||||
want: []string{"testdata/imports/alias/main.go"},
|
||||
},
|
||||
{
|
||||
name: "should identify packages",
|
||||
dir: "testdata/imports/other_package",
|
||||
pkg: "github.com/wailsapp/wails/exp/pkg/application",
|
||||
want: []string{"testdata/imports/other_package/subpackage/app.go"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := findFilesImportingPackage(tt.dir, tt.pkg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("findFilesImportingPackage() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for _, imp := range got {
|
||||
if !lo.Contains(tt.want, imp.FileName) {
|
||||
t.Errorf("findFilesImportingPackage() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
)
|
||||
|
||||
func isOptionsApplication(expr *ast.Expr) bool {
|
||||
cl, ok := (*expr).(*ast.CompositeLit)
|
||||
if ok {
|
||||
se, ok := cl.Type.(*ast.SelectorExpr)
|
||||
if ok {
|
||||
_, ok := se.X.(*ast.Ident)
|
||||
if ok {
|
||||
if se.Sel.Name == "Application" && se.X.(*ast.Ident).Name == "options" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_isOptionsApplication(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "should return true when expr is a struct literal",
|
||||
dir: "testdata/boundstructs/struct_literal_single",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
imports, err := findFilesImportingPackage(tt.dir, "github.com/wailsapp/wails/exp/pkg/application")
|
||||
require.NoError(t, err)
|
||||
imp := findNewCalls(imports)
|
||||
require.NotNil(t, imp)
|
||||
isOptions := isOptionsApplication(imp.Options)
|
||||
require.Equal(t, tt.want, isOptions)
|
||||
})
|
||||
}
|
||||
}
|
||||
268
exp/internal/parser/packages.go
Normal file
268
exp/internal/parser/packages.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
type parsedPackage struct {
|
||||
name string
|
||||
pkg *ast.Package
|
||||
boundStructs []*ast.TypeSpec
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
packages map[string]*parsedPackage
|
||||
}
|
||||
|
||||
func ParseDirectory(dir string) (*Context, error) {
|
||||
// Parse the directory
|
||||
fset := token.NewFileSet()
|
||||
pkgs, err := parser.ParseDir(fset, dir, nil, parser.AllErrors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
context := &Context{
|
||||
packages: make(map[string]*parsedPackage),
|
||||
}
|
||||
|
||||
// Iterate through the packages
|
||||
for _, pkg := range pkgs {
|
||||
context.packages[pkg.Name] = &parsedPackage{
|
||||
name: pkg.Name,
|
||||
pkg: pkg,
|
||||
}
|
||||
}
|
||||
|
||||
findApplicationNewCalls(context)
|
||||
|
||||
return context, nil
|
||||
}
|
||||
|
||||
func findApplicationNewCalls(context *Context) {
|
||||
// Iterate through the packages
|
||||
currentPackages := lo.Keys(context.packages)
|
||||
|
||||
for _, packageName := range currentPackages {
|
||||
thisPackage := context.packages[packageName]
|
||||
println("Parsing package", packageName)
|
||||
// Iterate through the package's files
|
||||
for _, file := range thisPackage.pkg.Files {
|
||||
// Use an ast.Inspector to find the calls to application.New
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
// Check if the node is a call expression
|
||||
callExpr, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the function being called is "application.New"
|
||||
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if selExpr.Sel.Name != "New" {
|
||||
return true
|
||||
}
|
||||
if id, ok := selExpr.X.(*ast.Ident); !ok || id.Name != "application" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check there is only 1 argument
|
||||
if len(callExpr.Args) != 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check argument 1 is a struct literal
|
||||
structLit, ok := callExpr.Args[0].(*ast.CompositeLit)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check struct literal is of type "options.Application"
|
||||
selectorExpr, ok := structLit.Type.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if selectorExpr.Sel.Name != "Application" {
|
||||
return true
|
||||
}
|
||||
if id, ok := selectorExpr.X.(*ast.Ident); !ok || id.Name != "options" {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, elt := range structLit.Elts {
|
||||
// Find the "Bind" field
|
||||
kvExpr, ok := elt.(*ast.KeyValueExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if id, ok := kvExpr.Key.(*ast.Ident); !ok || id.Name != "Bind" {
|
||||
continue
|
||||
}
|
||||
// Check the value is a slice of interfaces
|
||||
sliceExpr, ok := kvExpr.Value.(*ast.CompositeLit)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var arrayType *ast.ArrayType
|
||||
if arrayType, ok = sliceExpr.Type.(*ast.ArrayType); !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check array type is of type "interface{}"
|
||||
if _, ok := arrayType.Elt.(*ast.InterfaceType); !ok {
|
||||
continue
|
||||
}
|
||||
// Iterate through the slice elements
|
||||
for _, elt := range sliceExpr.Elts {
|
||||
// Check the element is a unary expression
|
||||
unaryExpr, ok := elt.(*ast.UnaryExpr)
|
||||
if ok {
|
||||
// Check the unary expression is a composite lit
|
||||
boundStructLit, ok := unaryExpr.X.(*ast.CompositeLit)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Check if the composite lit is a struct
|
||||
if _, ok := boundStructLit.Type.(*ast.StructType); ok {
|
||||
// Parse struct
|
||||
continue
|
||||
}
|
||||
// Check if the lit is an ident
|
||||
ident, ok := boundStructLit.Type.(*ast.Ident)
|
||||
if ok {
|
||||
if ident.Obj == nil {
|
||||
println("Ident.Obj is nil - check")
|
||||
continue
|
||||
}
|
||||
// Check if the ident is a struct type
|
||||
if _, ok := ident.Obj.Decl.(*ast.TypeSpec); ok {
|
||||
thisPackage.boundStructs = append(thisPackage.boundStructs, ident.Obj.Decl.(*ast.TypeSpec))
|
||||
continue
|
||||
}
|
||||
// Check the typespec decl is a struct
|
||||
if _, ok := ident.Obj.Decl.(*ast.StructType); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
// Check if the lit is a selector
|
||||
selector, ok := boundStructLit.Type.(*ast.SelectorExpr)
|
||||
if ok {
|
||||
getStructsFromSelector(selector, file, context)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStructsFromSelector(selector *ast.SelectorExpr, file *ast.File, context *Context) {
|
||||
// extract package name from selector
|
||||
packageName := selector.X.(*ast.Ident).Name
|
||||
|
||||
if context.packages[packageName] == nil {
|
||||
context.packages[packageName] = &parsedPackage{
|
||||
name: packageName,
|
||||
}
|
||||
}
|
||||
|
||||
// extract struct name from selector
|
||||
structName := selector.Sel.Name
|
||||
|
||||
// Find the package name from the imports
|
||||
for _, imp := range file.Imports {
|
||||
var match bool
|
||||
if imp.Name == nil || imp.Name.Name == packageName {
|
||||
match = true
|
||||
}
|
||||
if match == false {
|
||||
pathSplit := strings.Split(imp.Path.Value, "/")
|
||||
endPath := strings.Trim(pathSplit[len(pathSplit)-1], `"`)
|
||||
match = endPath == packageName
|
||||
}
|
||||
|
||||
if match {
|
||||
// We have the import
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedModule,
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, strings.Trim(imp.Path.Value, `"`))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
foundPackage := pkgs[0]
|
||||
|
||||
// Iterate the files in the package and find struct types
|
||||
for _, parsedFile := range foundPackage.Syntax {
|
||||
ast.Inspect(parsedFile, func(n ast.Node) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
switch n.(type) {
|
||||
case *ast.TypeSpec:
|
||||
typeSpec := n.(*ast.TypeSpec)
|
||||
if typeSpec.Name.Name == structName {
|
||||
context.packages[packageName].boundStructs = append(context.packages[packageName].boundStructs, typeSpec)
|
||||
findNestedStructs(typeSpec, parsedFile, packageName, context)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findNestedStructs(t *ast.TypeSpec, parsedFile *ast.File, pkgName string, context *Context) (localStructs []*ast.TypeSpec, externalStructs []*ast.SelectorExpr) {
|
||||
structType, ok := t.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
for _, field := range structType.Fields.List {
|
||||
for _, ident := range field.Names {
|
||||
switch t := ident.Obj.Decl.(*ast.Field).Type.(type) {
|
||||
case *ast.Ident:
|
||||
if t.Obj == nil {
|
||||
continue
|
||||
}
|
||||
if t.Obj.Kind == ast.Typ {
|
||||
if _, ok := t.Obj.Decl.(*ast.TypeSpec); ok {
|
||||
context.packages[pkgName].boundStructs = append(context.packages[pkgName].boundStructs, t.Obj.Decl.(*ast.TypeSpec))
|
||||
}
|
||||
}
|
||||
case *ast.SelectorExpr:
|
||||
if ident, ok := t.X.(*ast.Ident); ok {
|
||||
if ident.IsExported() {
|
||||
getStructsFromSelector(t, parsedFile, context)
|
||||
}
|
||||
}
|
||||
case *ast.StarExpr:
|
||||
if sel, ok := t.X.(*ast.SelectorExpr); ok {
|
||||
if _, ok := sel.X.(*ast.Ident); ok {
|
||||
if ident.IsExported() {
|
||||
getStructsFromSelector(sel, parsedFile, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return localStructs, externalStructs
|
||||
}
|
||||
58
exp/internal/parser/packages_test.go
Normal file
58
exp/internal/parser/packages_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseDirectory(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: []string{"GreetService"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should find multiple bound services",
|
||||
dir: "testdata/struct_literal_multiple",
|
||||
want: []string{"main.GreetService", "main.OtherService"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should find multiple bound services over multiple files",
|
||||
dir: "testdata/struct_literal_multiple_files",
|
||||
want: []string{"main.GreetService", "main.OtherService"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should find bound services from other packages",
|
||||
dir: "../../examples/binding",
|
||||
want: []string{"main.localStruct", "services.GreetService", "models.Person"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseDirectory(tt.dir)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseDirectory() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for name, pkg := range got.packages {
|
||||
println("Got package", name)
|
||||
for _, boundStruct := range pkg.boundStructs {
|
||||
println("Got bound struct", boundStruct.Name.Name)
|
||||
require.True(t, lo.Contains(tt.want, name+"."+boundStruct.Name.Name))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
|
||||
bob "github.com/wailsapp/wails/exp/pkg/application"
|
||||
"github.com/wailsapp/wails/exp/pkg/options"
|
||||
)
|
||||
|
||||
type GreetService struct {
|
||||
SomeVariable int
|
||||
lowerCase string
|
||||
}
|
||||
|
||||
func (*GreetService) Greet(name string) string {
|
||||
return "Hello " + name
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := bob.New(options.Application{
|
||||
Bind: []interface{}{
|
||||
&GreetService{},
|
||||
},
|
||||
})
|
||||
|
||||
app.NewWebviewWindow()
|
||||
|
||||
err := app.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
|
||||
"github.com/wailsapp/wails/exp/pkg/application"
|
||||
"github.com/wailsapp/wails/exp/pkg/options"
|
||||
)
|
||||
|
||||
type GreetService struct {
|
||||
SomeVariable int
|
||||
lowerCase string
|
||||
}
|
||||
|
||||
func (*GreetService) Greet(name string) string {
|
||||
return "Hello " + name
|
||||
}
|
||||
|
||||
func run() {
|
||||
app := application.New(options.Application{
|
||||
Bind: []interface{}{
|
||||
&GreetService{},
|
||||
},
|
||||
})
|
||||
|
||||
app.NewWebviewWindow()
|
||||
|
||||
err := app.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
func main() {
|
||||
run()
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
module other_package
|
||||
|
||||
require github.com/wailsapp/wails/exp v0
|
||||
|
||||
go 1.19
|
||||
|
||||
replace github.com/wailsapp/wails/exp v0 => ../../../../../../
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"other_package/subpackage"
|
||||
)
|
||||
|
||||
func main() {
|
||||
subpackage.Run()
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package subpackage
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
|
||||
"github.com/wailsapp/wails/exp/pkg/application"
|
||||
"github.com/wailsapp/wails/exp/pkg/options"
|
||||
)
|
||||
|
||||
type GreetService struct {
|
||||
SomeVariable int
|
||||
lowerCase string
|
||||
}
|
||||
|
||||
func (*GreetService) Greet(name string) string {
|
||||
return "Hello " + name
|
||||
}
|
||||
|
||||
func Run() {
|
||||
app := application.New(options.Application{
|
||||
Bind: []interface{}{
|
||||
&GreetService{},
|
||||
},
|
||||
})
|
||||
|
||||
app.NewWebviewWindow()
|
||||
|
||||
err := app.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
|
||||
"github.com/wailsapp/wails/exp/pkg/application"
|
||||
"github.com/wailsapp/wails/exp/pkg/options"
|
||||
)
|
||||
|
||||
type GreetService struct {
|
||||
SomeVariable int
|
||||
lowerCase string
|
||||
}
|
||||
|
||||
func (*GreetService) Greet(name string) string {
|
||||
return "Hello " + name
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := application.New(options.Application{
|
||||
Bind: []interface{}{
|
||||
&GreetService{},
|
||||
},
|
||||
})
|
||||
|
||||
app.NewWebviewWindow()
|
||||
|
||||
err := app.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue