Refactor of parser

This commit is contained in:
Lea Anthony
2020-11-09 21:55:35 +11:00
parent 4f5d333d74
commit db38d13693
32 changed files with 789 additions and 1670 deletions

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ v2/test/hidden/icon.png
v2/internal/ffenestri/runtime.c
v2/internal/runtime/assets/desktop.js
v2/test/kitchensink/frontend/public/bundle.*
v2/pkg/parser/testproject/frontend/wails

View File

@@ -0,0 +1,22 @@
package generate
import (
"io"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/pkg/parser"
)
// AddSubcommand adds the `dev` command for the Wails application
func AddSubcommand(app *clir.Cli, w io.Writer) error {
command := app.NewSubCommand("generate", "Code Generation Tools")
// Backend API
backendAPI := command.NewSubCommand("api", "Generates a JS module for the frontend to interface with the backend")
backendAPI.Action(func() error {
return parser.GenerateWailsFrontendPackage()
})
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/dev"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/generate"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/initialise"
)
@@ -37,6 +38,11 @@ func main() {
fatal(err.Error())
}
err = generate.AddSubcommand(app, os.Stdout)
if err != nil {
fatal(err.Error())
}
err = app.Run()
if err != nil {
println("\n\nERROR: " + err.Error())

View File

@@ -8,7 +8,7 @@ import (
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/project"
"github.com/wailsapp/wails/v2/pkg/clilogger"
"github.com/wailsapp/wails/v2/pkg/commands/build/internal/backendjs"
"github.com/wailsapp/wails/v2/pkg/parser"
)
// Mode is the type used to indicate the build modes
@@ -92,7 +92,7 @@ func Build(options *Options) (string, error) {
// Generate Frontend JS Package
outputLogger.Println(" - Generating Backend JS Package")
err = backendjs.GenerateBackendJSPackage()
err = parser.GenerateWailsFrontendPackage()
if err != nil {
return "", err
}

View File

@@ -1,107 +0,0 @@
package backendjs
import (
"go/ast"
"github.com/davecgh/go-spew/spew"
)
func (p *Parser) parseAssignment(assignStmt *ast.AssignStmt, pkg *Package) {
for _, rhs := range assignStmt.Rhs {
ce, ok := rhs.(*ast.CallExpr)
if ok {
se, ok := ce.Fun.(*ast.SelectorExpr)
if ok {
i, ok := se.X.(*ast.Ident)
if ok {
// Have we found the wails package name?
if i.Name == p.wailsPackageVariable {
// Check we are calling a function to create the app
if se.Sel.Name == "CreateApp" || se.Sel.Name == "CreateAppWithOptions" {
if len(assignStmt.Lhs) == 1 {
i, ok := assignStmt.Lhs[0].(*ast.Ident)
if ok {
// Found the app variable name
p.applicationVariable = i.Name
}
}
}
}
}
} else {
// Check for function assignment
// a := newMyStruct()
fe, ok := ce.Fun.(*ast.Ident)
if ok {
if len(assignStmt.Lhs) == 1 {
i, ok := assignStmt.Lhs[0].(*ast.Ident)
if ok {
// Store the variable -> Function mapping
// so we can later resolve the type
pkg.variablesThatWereAssignedByFunctions[i.Name] = fe.Name
}
}
}
}
} else {
// Check for literal assignment of struct
// EG: myvar := MyStruct{}
ue, ok := rhs.(*ast.UnaryExpr)
if ok {
cl, ok := ue.X.(*ast.CompositeLit)
if ok {
t, ok := cl.Type.(*ast.Ident)
if ok {
if len(assignStmt.Lhs) == 1 {
i, ok := assignStmt.Lhs[0].(*ast.Ident)
if ok {
pkg.variablesThatWereAssignedByStructLiterals[i.Name] = t.Name
}
}
} else {
e, ok := cl.Type.(*ast.SelectorExpr)
if ok {
var thisType = ""
var thisPackage = ""
switch x := e.X.(type) {
case *ast.Ident:
thisPackage = x.Name
}
thisType = e.Sel.Name
if len(assignStmt.Lhs) == 1 {
i, ok := assignStmt.Lhs[0].(*ast.Ident)
if ok {
sn := &StructName{
Name: thisType,
Package: thisPackage,
}
pkg.variablesThatWereAssignedByExternalStructLiterals[i.Name] = sn
}
}
}
}
}
} else {
cl, ok := rhs.(*ast.CompositeLit)
if ok {
t, ok := cl.Type.(*ast.Ident)
if ok {
if len(assignStmt.Lhs) == 1 {
i, ok := assignStmt.Lhs[0].(*ast.Ident)
if ok {
pkg.variablesThatWereAssignedByStructLiterals[i.Name] = t.Name
} else {
println("herer")
}
}
} else {
println("herer")
}
} else {
println("herer")
spew.Dump(rhs)
}
}
}
}
}

View File

@@ -1,117 +0,0 @@
package backendjs
import (
"go/token"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/fs"
"golang.org/x/tools/go/packages"
)
// GenerateBackendJSPackage will generate a Javascript/Typescript
// package in `<project>/frontend/backend` that defines which methods
// and structs are bound to your frontend
func GenerateBackendJSPackage() error {
dir, err := os.Getwd()
if err != nil {
return err
}
p := NewParser()
err = p.parseProject(dir)
if err != nil {
return err
}
err = p.generateModule()
return err
}
func (p *Parser) parseProject(projectPath string) error {
mode := packages.NeedName |
packages.NeedFiles |
packages.NeedSyntax |
packages.NeedTypes |
packages.NeedImports |
packages.NeedTypesInfo
var fset = token.NewFileSet()
cfg := &packages.Config{Fset: fset, Mode: mode, Dir: projectPath}
pkgs, err := packages.Load(cfg, "./...")
if err != nil {
return errors.Wrap(err, "Problem loading packages")
}
if packages.PrintErrors(pkgs) > 0 {
return errors.Wrap(err, "Errors during parsing")
}
for _, pkg := range pkgs {
parsedPackage, err := p.parsePackage(pkg, fset)
if err != nil {
return err
}
p.Packages[parsedPackage.Name] = parsedPackage
}
// Resolve all the loose ends from parsing
err = p.resolve()
if err != nil {
return err
}
return nil
}
func (p *Parser) generateModule() error {
moduleDir, err := createBackendJSDirectory()
if err != nil {
return err
}
for _, pkg := range p.Packages {
// We should only output packages that need generating
if !pkg.ShouldBeGenerated() {
continue
}
// Calculate directory
dir := filepath.Join(moduleDir, pkg.Name)
// Create the directory if it doesn't exist
fs.Mkdir(dir)
err := generatePackage(pkg, dir)
if err != nil {
return err
}
}
return nil
}
func createBackendJSDirectory() (string, error) {
// Calculate the package directory
// Note this is *always* called from the project directory
// so using paths relative to CWD is fine
dir, err := fs.RelativeToCwd("./frontend/backend")
if err != nil {
return "", errors.Wrap(err, "Error creating backend js directory")
}
// Remove directory if it exists - REGENERATION!
err = os.RemoveAll(dir)
if err != nil {
return "", errors.Wrap(err, "Error removing module directory")
}
// Make the directory
err = fs.Mkdir(dir)
return dir, err
}

View File

@@ -1,85 +0,0 @@
package backendjs
import (
"go/ast"
"github.com/davecgh/go-spew/spew"
)
func (p *Parser) parseCallExpressions(x *ast.CallExpr, pkg *Package) {
f, ok := x.Fun.(*ast.SelectorExpr)
if ok {
n, ok := f.X.(*ast.Ident)
if ok {
//Check this is the Bind() call associated with the app variable
if n.Name == p.applicationVariable && f.Sel.Name == "Bind" {
if len(x.Args) == 1 {
ce, ok := x.Args[0].(*ast.CallExpr)
if ok {
fn, ok := ce.Fun.(*ast.Ident)
if ok {
pkg.structMethodsThatWereBound.Add(fn.Name)
}
} else {
// We also want to check for Bind( &MyStruct{} )
ue, ok := x.Args[0].(*ast.UnaryExpr)
if ok {
if ue.Op.String() == "&" {
cl, ok := ue.X.(*ast.CompositeLit)
if ok {
t, ok := cl.Type.(*ast.Ident)
if ok {
pkg.structPointerLiteralsThatWereBound.Add(t.Name)
} else {
e, ok := cl.Type.(*ast.SelectorExpr)
if ok {
var thisType = ""
var thisPackage = ""
switch x := e.X.(type) {
case *ast.Ident:
thisPackage = x.Name
default:
println("Identifier in binding not supported:")
spew.Dump(ue.X)
return
}
thisType = e.Sel.Name
// Save this reference in the package
pkg := p.getOrCreatePackage(thisPackage)
pkg.structPointerLiteralsThatWereBound.Add(thisType)
} else {
println("Binding not supported:")
spew.Dump(ue.X)
}
}
}
}
} else {
// Let's check when the user binds a struct,
// rather than a struct pointer: Bind( MyStruct{} )
// We do this to provide better hints to the user
cl, ok := x.Args[0].(*ast.CompositeLit)
if ok {
t, ok := cl.Type.(*ast.Ident)
if ok {
pkg.structLiteralsThatWereBound.Add(t.Name)
}
} else {
// Also check for when we bind a variable
// myVariable := &MyStruct{}
// app.Bind( myVariable )
i, ok := x.Args[0].(*ast.Ident)
if ok {
pkg.variablesThatWereBound.Add(i.Name)
} else {
}
}
}
}
}
}
}
}
}

View File

@@ -1,21 +0,0 @@
package backendjs
import (
"go/ast"
"strings"
)
func (p *Parser) parseComments(comments *ast.CommentGroup) []string {
var result []string
if comments == nil {
return result
}
for _, comment := range comments.List {
commentText := strings.TrimPrefix(comment.Text, "//")
result = append(result, commentText)
}
return result
}

View File

@@ -1,149 +0,0 @@
package backendjs
import (
"go/ast"
"os"
"strings"
"github.com/davecgh/go-spew/spew"
)
func (p *Parser) parseField(field *ast.Field, strct *Struct, pkg *Package) (string, *StructName) {
var structName *StructName
var fieldType string
switch t := field.Type.(type) {
case *ast.Ident:
fieldType = t.Name
case *ast.StarExpr:
fieldType = "struct"
structName = p.parseStructNameFromStarExpr(t)
// Save external reference if we have it
if structName.Package == "" {
// pkg.packageReferences.AddUnique(structName.Package)
pkg.structsUsedAsData.AddUnique(structName.Name)
} else {
// Save this reference to the external struct
referencedPackage := p.Packages[structName.Package]
if referencedPackage == nil {
// TODO: Move this to a global reference list instead of bombing out
println("WARNING: Unknown package referenced by field:", structName.Package)
}
referencedPackage.structsUsedAsData.AddUnique(structName.Name)
strct.packageReferences.AddUnique(structName.Package)
}
default:
spew.Dump(field)
println("Unhandled Field type")
os.Exit(1)
}
return fieldType, structName
}
func (p *Parser) parseFunctionDeclaration(funcDecl *ast.FuncDecl, pkg *Package) {
if funcDecl.Recv != nil {
// This is a struct method
for _, field := range funcDecl.Recv.List {
se, ok := field.Type.(*ast.StarExpr)
if ok {
// This is a struct pointer method
i, ok := se.X.(*ast.Ident)
if ok {
// We want to ignore Internal functions
if p.internalMethods.Contains(funcDecl.Name.Name) {
continue
}
// If we haven't already found this struct,
// Create a placeholder in the cache
parsedStruct := pkg.Structs[i.Name]
if parsedStruct == nil {
pkg.Structs[i.Name] = &Struct{
Name: i.Name,
}
parsedStruct = pkg.Structs[i.Name]
}
// If this method is Public
if string(funcDecl.Name.Name[0]) == strings.ToUpper((string(funcDecl.Name.Name[0]))) {
structMethod := &Method{
Name: funcDecl.Name.Name,
}
// Check if the method has comments.
// If so, save it with the parsed method
if funcDecl.Doc != nil {
structMethod.Comments = p.parseComments(funcDecl.Doc)
}
// Save the input parameters
if funcDecl.Type.Params != nil {
for _, inputField := range funcDecl.Type.Params.List {
fieldType, structName := p.parseField(inputField, parsedStruct, pkg)
for _, name := range inputField.Names {
structMethod.Inputs = append(structMethod.Inputs, &Field{
Name: name.Name,
Type: fieldType,
Struct: structName,
})
}
}
}
// Save the output parameters
if funcDecl.Type.Results != nil {
for _, outputField := range funcDecl.Type.Results.List {
fieldType, structName := p.parseField(outputField, parsedStruct, pkg)
if len(outputField.Names) == 0 {
structMethod.Returns = append(structMethod.Returns, &Field{
Type: fieldType,
Struct: structName,
})
} else {
for _, name := range outputField.Names {
structMethod.Returns = append(structMethod.Returns, &Field{
Name: name.Name,
Type: fieldType,
Struct: structName,
})
}
}
}
}
// Append this method to the parsed struct
parsedStruct.Methods = append(parsedStruct.Methods, structMethod)
}
}
}
}
} else {
// This is a function declaration
// We care about its name and return type
// This will allow us to resolve types later
functionName := funcDecl.Name.Name
// Look for one that returns a single value
if funcDecl.Type != nil && funcDecl.Type.Results != nil && funcDecl.Type.Results.List != nil {
if len(funcDecl.Type.Results.List) == 1 {
// Check for *struct
t, ok := funcDecl.Type.Results.List[0].Type.(*ast.StarExpr)
if ok {
s, ok := t.X.(*ast.Ident)
if ok {
// println("*** Function", functionName, "found which returns: *"+s.Name)
pkg.functionsThatReturnStructPointers[functionName] = s.Name
}
} else {
// Check for functions that return a struct
// This is to help us provide hints if the user binds a struct
t, ok := funcDecl.Type.Results.List[0].Type.(*ast.Ident)
if ok {
// println("*** Function", functionName, "found which returns: "+t.Name)
pkg.functionsThatReturnStructs[functionName] = t.Name
}
}
}
}
}
}

View File

@@ -1,270 +0,0 @@
package backendjs
import (
"bytes"
"go/ast"
"go/token"
"io/ioutil"
"path/filepath"
"text/template"
"github.com/davecgh/go-spew/spew"
"github.com/leaanthony/slicer"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/fs"
"golang.org/x/tools/go/packages"
)
// Package defines a parsed package
type Package struct {
Name string
Structs map[string]*Struct
// These are the structs declared in this package
// that are used as data by either this or other packages
structsUsedAsData slicer.StringSlicer
// A list of functions that return struct pointers
functionsThatReturnStructPointers map[string]string
// A list of functions that return structs
functionsThatReturnStructs map[string]string
// A list of struct literals that were bound to the application
// EG: app.Bind( &mystruct{} )
structLiteralsThatWereBound slicer.StringSlicer
// A list of struct pointer literals that were bound to the application
// EG: app.Bind( &mystruct{} )
structPointerLiteralsThatWereBound slicer.StringSlicer
// A list of methods that returns structs to the Bind method
// EG: app.Bind( newMyStruct() )
structMethodsThatWereBound slicer.StringSlicer
// A list of variables that were used for binding
// Eg: myVar := &mystruct{}; app.Bind( myVar )
variablesThatWereBound slicer.StringSlicer
// A list of variables that were assigned using a function call
// EG: myVar := newStruct()
variablesThatWereAssignedByFunctions map[string]string
// A map of variables that were assigned using a struct literal
// EG: myVar := MyStruct{}
variablesThatWereAssignedByStructLiterals map[string]string
// A map of variables that were assigned using a struct literal
// in a different package
// EG: myVar := mypackage.MyStruct{}
variablesThatWereAssignedByExternalStructLiterals map[string]*StructName
}
func newPackage(name string) *Package {
return &Package{
Name: name,
Structs: make(map[string]*Struct),
functionsThatReturnStructPointers: make(map[string]string),
functionsThatReturnStructs: make(map[string]string),
variablesThatWereAssignedByFunctions: make(map[string]string),
variablesThatWereAssignedByStructLiterals: make(map[string]string),
variablesThatWereAssignedByExternalStructLiterals: make(map[string]*StructName),
}
}
func (p *Parser) parsePackage(pkg *packages.Package, fset *token.FileSet) (*Package, error) {
result := p.Packages[pkg.Name]
if result == nil {
result = newPackage(pkg.Name)
}
// Get the absolute path to the project's main.go file
pathToMain, err := fs.RelativeToCwd("main.go")
if err != nil {
return nil, err
}
// Work out if this is the main package
goFiles := slicer.String(pkg.GoFiles)
if goFiles.Contains(pathToMain) {
// This is the program entrypoint file
// Scan the imports for the wails v2 import
for key, details := range pkg.Imports {
if key == "github.com/wailsapp/wails/v2" {
p.wailsPackageVariable = details.Name
}
}
}
for _, fileAst := range pkg.Syntax {
var parseError error
ast.Inspect(fileAst, func(n ast.Node) bool {
// if typeDecl, ok := n.(*ast.TypeSpec); ok {
// // Parse struct definitions
// if structType, ok := typeDecl.Type.(*ast.StructType); ok {
// structName := typeDecl.Name.Name
// // findInFields(structTy.Fields, n, pkg.TypesInfo, fset)
// structDef, err := p.ParseStruct(structType, structName, result)
// if err != nil {
// parseError = err
// return false
// }
// // Parse comments
// structDef.Comments = p.parseComments(typeDecl.Doc)
// result.Structs[structName] = structDef
// }
// }
if genDecl, ok := n.(*ast.GenDecl); ok {
println("GenDecl:")
for _, spec := range genDecl.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
if structType, ok := typeSpec.Type.(*ast.StructType); ok {
structName := typeSpec.Name.Name
structDef, err := p.ParseStruct(structType, structName, result)
if err != nil {
parseError = err
return false
}
// Parse comments
structDef.Comments = p.parseComments(genDecl.Doc)
result.Structs[structName] = structDef
}
}
}
spew.Dump(genDecl)
}
// Capture call expressions
if callExpr, ok := n.(*ast.CallExpr); ok {
p.parseCallExpressions(callExpr, result)
}
// Parse Assignments
if assignStmt, ok := n.(*ast.AssignStmt); ok {
p.parseAssignment(assignStmt, result)
}
// Parse Function declarations
if funcDecl, ok := n.(*ast.FuncDecl); ok {
p.parseFunctionDeclaration(funcDecl, result)
}
return true
})
if parseError != nil {
return nil, parseError
}
}
return result, nil
}
func generatePackage(pkg *Package, moduledir string) error {
// Get path to local file
typescriptTemplateFile := fs.RelativePath("./package.d.template")
// Load typescript template
typescriptTemplateData := fs.MustLoadString(typescriptTemplateFile)
typescriptTemplate, err := template.New("typescript").Parse(typescriptTemplateData)
if err != nil {
return errors.Wrap(err, "Error creating template")
}
// Execute javascript template
var buffer bytes.Buffer
err = typescriptTemplate.Execute(&buffer, pkg)
if err != nil {
return errors.Wrap(err, "Error generating code")
}
// Save typescript file
err = ioutil.WriteFile(filepath.Join(moduledir, "index.d.ts"), buffer.Bytes(), 0755)
if err != nil {
return errors.Wrap(err, "Error writing backend package file")
}
// Get path to local file
javascriptTemplateFile := fs.RelativePath("./package.template")
// Load javascript template
javascriptTemplateData := fs.MustLoadString(javascriptTemplateFile)
javascriptTemplate, err := template.New("javascript").Parse(javascriptTemplateData)
if err != nil {
return errors.Wrap(err, "Error creating template")
}
// Reset the buffer
buffer.Reset()
err = javascriptTemplate.Execute(&buffer, pkg)
if err != nil {
return errors.Wrap(err, "Error generating code")
}
// Save javascript file
err = ioutil.WriteFile(filepath.Join(moduledir, "index.js"), buffer.Bytes(), 0755)
if err != nil {
return errors.Wrap(err, "Error writing backend package file")
}
return nil
}
// DeclarationReferences returns the typescript declaration references for the package
func (p *Package) DeclarationReferences() []string {
var result []string
for _, strct := range p.Structs {
if strct.IsBound {
refs := strct.packageReferences.AsSlice()
result = append(result, refs...)
}
}
return result
}
// StructIsUsedAsData returns true if the given struct name has
// been used in structs, inputs or outputs by other packages
func (p *Package) StructIsUsedAsData(structName string) bool {
return p.structsUsedAsData.Contains(structName)
}
func (p *Package) resolveBoundStructLiterals() {
p.structLiteralsThatWereBound.Each(func(structName string) {
strct := p.Structs[structName]
if strct == nil {
println("Warning: Cannot find bound struct", structName, "in package", p.Name)
return
}
println("Bound struct", strct.Name, "in package", p.Name)
strct.IsBound = true
})
}
func (p *Package) resolveBoundStructPointerLiterals() {
p.structPointerLiteralsThatWereBound.Each(func(structName string) {
strct := p.Structs[structName]
if strct == nil {
println("Warning: Cannot find bound struct", structName, "in package", p.Name)
return
}
println("Bound struct pointer", strct.Name, "in package", p.Name)
strct.IsBound = true
})
}
// ShouldBeGenerated indicates if the package should be generated
// The package should be generated only if we have structs that are
// bound or structs that are used as data
func (p *Package) ShouldBeGenerated() bool {
for _, strct := range p.Structs {
if strct.IsBound || strct.IsUsedAsData {
return true
}
}
return false
}

View File

@@ -1,191 +0,0 @@
package backendjs
import (
"github.com/leaanthony/slicer"
)
// Parser is our Wails project parser
type Parser struct {
Packages map[string]*Package
// The variable used to store the Wails application
// EG: app := wails.CreateApp()
applicationVariable string
// The name of the wails package that's imported
// import "github.com/wailsapp/wails/v2" -> wails
// import mywails "github.com/wailsapp/wails/v2" -> mywails
wailsPackageVariable string
// Internal methods (WailsInit/WailsShutdown)
internalMethods *slicer.StringSlicer
}
// NewParser creates a new Wails Project parser
func NewParser() *Parser {
return &Parser{
Packages: make(map[string]*Package),
internalMethods: slicer.String([]string{"WailsInit", "WailsShutdown"}),
}
}
func (p *Parser) resolve() error {
// Resolve bound structs
err := p.resolveBoundStructs()
if err != nil {
return err
}
return nil
}
func (p *Parser) resolveBoundStructs() error {
// Resolve Struct Literals
p.resolveBoundStructLiterals()
// Resolve Struct Pointer Literals
p.resolveBoundStructPointerLiterals()
// Resolve functions that were bound
// EG: app.Bind( newBasic() )
p.resolveBoundFunctions()
// Resolve variables that were bound
p.resolveBoundVariables()
return nil
}
func (p *Parser) resolveBoundStructLiterals() {
// Resolve struct literals in each package
for _, pkg := range p.Packages {
pkg.resolveBoundStructLiterals()
}
}
func (p *Parser) resolveBoundStructPointerLiterals() {
// Resolve struct pointer literals
for _, pkg := range p.Packages {
pkg.resolveBoundStructPointerLiterals()
}
}
func (p *Parser) resolveBoundFunctions() {
// Loop over packages
for _, pkg := range p.Packages {
// Iterate over the method names
pkg.structMethodsThatWereBound.Each(func(functionName string) {
// Resolve each method name
structName := p.resolveFunctionReturnType(pkg, functionName)
strct := pkg.Structs[structName]
if strct == nil {
println("WARNING: Unable to find definition for struct", structName)
return
}
strct.IsBound = true
})
}
}
// resolveFunctionReturnType gets the return type for the given package/function name combination
func (p *Parser) resolveFunctionReturnType(pkg *Package, functionName string) string {
structName := pkg.functionsThatReturnStructPointers[functionName]
if structName == "" {
structName = pkg.functionsThatReturnStructs[functionName]
}
if structName == "" {
println("WARNING: Unable to resolve bound function", functionName, "in package", pkg.Name)
}
return structName
}
func (p *Parser) markStructAsBound(pkg *Package, structName string) {
strct := pkg.Structs[structName]
if strct == nil {
println("WARNING: Unable to find definition for struct", structName)
}
println("Found bound struct:", strct.Name)
strct.IsBound = true
}
func (p *Parser) resolveBoundVariables() {
for _, pkg := range p.Packages {
// Iterate over the method names
pkg.variablesThatWereBound.Each(func(variableName string) {
println("Resolving variable: ", variableName)
var structName string
// Resolve each method name
funcName := pkg.variablesThatWereAssignedByFunctions[variableName]
if funcName != "" {
// Found function name - resolve Function return type
structName = p.resolveFunctionReturnType(pkg, funcName)
}
// If we couldn't resolve to a function, then let's try struct literals
if structName == "" {
funcName = pkg.variablesThatWereAssignedByStructLiterals[variableName]
if funcName != "" {
// Found function name - resolve Function return type
structName = p.resolveFunctionReturnType(pkg, funcName)
}
}
// Look for the variable as an external literal reference
if structName == "" {
sn := pkg.variablesThatWereAssignedByExternalStructLiterals[variableName]
if sn != nil {
pkg := p.Packages[sn.Package]
if pkg != nil {
p.markStructAsBound(pkg, sn.Name)
return
}
}
}
if structName == "" {
println("WARNING: Unable to resolve bound variable", variableName, "in package", pkg.Name)
return
}
p.markStructAsBound(pkg, structName)
})
}
}
func (p *Parser) bindStructByStructName(sn *StructName) {
// Get package
pkg := p.Packages[sn.Package]
if pkg == nil {
// Ignore, it will get picked up by the compiler
return
}
strct := pkg.Structs[sn.Name]
if strct == nil {
// Ignore, it will get picked up by the compiler
return
}
println("Found bound Struct:", sn.ToString())
strct.IsBound = true
}
func (p *Parser) getOrCreatePackage(name string) *Package {
result := p.Packages[name]
if result == nil {
result = newPackage(name)
p.Packages[name] = result
}
return result
}

View File

@@ -1,236 +0,0 @@
package backendjs
import (
"fmt"
"go/ast"
"os"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/leaanthony/slicer"
)
// Struct defines a parsed struct
type Struct struct {
Name string
Comments []string
Fields []*Field
Methods []*Method
IsBound bool
// This indicates that the struct is passed as data
// between the frontend and backend
IsUsedAsData bool
// These are references to other packages
packageReferences slicer.StringSlicer
}
// StructName is used to define a fully qualified struct name
// EG: mypackage.Person
type StructName struct {
Name string
Package string
}
// ToString returns a text representation of the struct anme
func (s *StructName) ToString() string {
result := ""
if s.Package != "" {
result = s.Package + "."
}
return result + s.Name
}
// Field defines a parsed struct field
type Field struct {
Name string
Type string
Struct *StructName
Comments []string
}
// JSType returns the Javascript type for this field
func (f *Field) JSType() string {
return string(goTypeToJS(f))
}
// TypeAsTSType converts the Field type to something TS wants
func (f *Field) TypeAsTSType() string {
var result = ""
switch f.Type {
case "string":
result = "string"
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
result = "number"
case "float32", "float64":
result = "number"
case "bool":
result = "boolean"
case "struct":
if f.Struct.Package != "" {
result = f.Struct.Package + "."
}
result = result + f.Struct.Name
default:
result = "any"
}
return result
}
func (p *Parser) ParseStruct(structType *ast.StructType, name string, pkg *Package) (*Struct, error) {
// Check if we've seen this struct before
result := pkg.Structs[name]
// If we haven't create a new one
if result == nil {
result = &Struct{Name: name}
}
for _, field := range structType.Fields.List {
result.Fields = append(result.Fields, p.ParseField(field, result, pkg)...)
}
return result, nil
}
func (p *Parser) parseStructNameFromStarExpr(starExpr *ast.StarExpr) *StructName {
pkg := ""
name := ""
// Determine the FQN
switch x := starExpr.X.(type) {
case *ast.SelectorExpr:
switch i := x.X.(type) {
case *ast.Ident:
pkg = i.Name
default:
println("one")
fieldNotSupported(x)
}
name = x.Sel.Name
case *ast.StarExpr:
switch s := x.X.(type) {
case *ast.Ident:
name = s.Name
default:
println("two")
fieldNotSupported(x)
}
case *ast.Ident:
name = x.Name
default:
println("three")
fieldNotSupported(x)
}
return &StructName{
Name: name,
Package: pkg,
}
}
func (p *Parser) ParseField(field *ast.Field, strct *Struct, pkg *Package) []*Field {
var result []*Field
var fieldType string
var structName *StructName
// Determine type
switch t := field.Type.(type) {
case *ast.Ident:
fieldType = t.Name
case *ast.StarExpr:
fieldType = "struct"
structName = p.parseStructNameFromStarExpr(t)
// Save external reference if we have it
if structName.Package == "" {
pkg.structsUsedAsData.AddUnique(structName.Name)
} else {
// Save this reference to the external struct
referencedPackage := p.Packages[structName.Package]
if referencedPackage == nil {
// Should we be ignoring this?
} else {
strct.packageReferences.AddUnique(structName.Package)
referencedPackage.structsUsedAsData.AddUnique(structName.Name)
}
}
default:
fieldNotSupported(t)
}
// Loop over names
for _, name := range field.Names {
// Create a field per name
thisField := &Field{
Comments: p.parseComments(field.Doc),
}
thisField.Name = name.Name
thisField.Type = fieldType
thisField.Struct = structName
result = append(result, thisField)
}
return result
}
func fieldNotSupported(t interface{}) {
println("Field type not supported:")
spew.Dump(t)
os.Exit(1)
}
// Method defines a struct method
type Method struct {
Name string
Comments []string
Inputs []*Field
Returns []*Field
}
// InputsAsTSText generates a string with the method inputs
// formatted in a way acceptable to Typescript
func (m *Method) InputsAsTSText() string {
var inputs []string
for _, input := range m.Inputs {
inputText := fmt.Sprintf("%s: %s", input.Name, goTypeToTS(input))
inputs = append(inputs, inputText)
}
return strings.Join(inputs, ", ")
}
// OutputsAsTSText generates a string with the method inputs
// formatted in a way acceptable to Javascript
func (m *Method) OutputsAsTSText() string {
if len(m.Returns) == 0 {
return "void"
}
var result []string
for _, output := range m.Returns {
result = append(result, goTypeToTS(output))
}
return strings.Join(result, ", ")
}
// InputsAsJSText generates a string with the method inputs
// formatted in a way acceptable to Javascript
func (m *Method) InputsAsJSText() string {
var inputs []string
for _, input := range m.Inputs {
inputs = append(inputs, input.Name)
}
return strings.Join(inputs, ", ")
}

View File

@@ -1,55 +0,0 @@
package parser
import (
"go/ast"
"golang.org/x/tools/go/packages"
)
func (p *Parser) getApplicationVariableName(pkg *packages.Package, wailsImportName string) (string, bool) {
var applicationVariableName = ""
// Iterate through the whole package looking for the application name
for _, fileAst := range pkg.Syntax {
ast.Inspect(fileAst, func(n ast.Node) bool {
// Parse Assignments looking for application name
if assignStmt, ok := n.(*ast.AssignStmt); ok {
// Check the RHS is of the form:
// `app := wails.CreateApp()` or
// `app := wails.CreateAppWithOptions`
for _, rhs := range assignStmt.Rhs {
ce, ok := rhs.(*ast.CallExpr)
if !ok {
continue
}
se, ok := ce.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
i, ok := se.X.(*ast.Ident)
if !ok {
continue
}
// Have we found the wails import name?
if i.Name == wailsImportName {
// Check we are calling a function to create the app
if se.Sel.Name == "CreateApp" || se.Sel.Name == "CreateAppWithOptions" {
if len(assignStmt.Lhs) == 1 {
i, ok := assignStmt.Lhs[0].(*ast.Ident)
if ok {
// Found the app variable name
applicationVariableName = i.Name
return false
}
}
}
}
}
}
return true
})
}
return applicationVariableName, applicationVariableName != ""
}

View File

@@ -0,0 +1,50 @@
package parser
import "go/ast"
func (p *Package) getApplicationVariableName(file *ast.File, wailsImportName string) string {
// Iterate through the whole file looking for the application name
applicationVariableName := ""
// Inspect the file
ast.Inspect(file, func(n ast.Node) bool {
// Parse Assignments looking for application name
if assignStmt, ok := n.(*ast.AssignStmt); ok {
// Check the RHS is of the form:
// `app := wails.CreateApp()` or
// `app := wails.CreateAppWithOptions`
for _, rhs := range assignStmt.Rhs {
ce, ok := rhs.(*ast.CallExpr)
if !ok {
continue
}
se, ok := ce.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
i, ok := se.X.(*ast.Ident)
if !ok {
continue
}
// Have we found the wails import name?
if i.Name == wailsImportName {
// Check we are calling a function to create the app
if se.Sel.Name == "CreateApp" || se.Sel.Name == "CreateAppWithOptions" {
if len(assignStmt.Lhs) == 1 {
i, ok := assignStmt.Lhs[0].(*ast.Ident)
if ok {
// Found the app variable name
applicationVariableName = i.Name
return false
}
}
}
}
}
}
return true
})
return applicationVariableName
}

View File

@@ -5,7 +5,7 @@ import (
"strings"
)
func (p *Parser) parseComments(comments *ast.CommentGroup) []string {
func parseComments(comments *ast.CommentGroup) []string {
var result []string
if comments == nil {

View File

@@ -1,4 +1,4 @@
package backendjs
package parser
// JSType represents a javascript type
type JSType string
@@ -35,14 +35,17 @@ func goTypeToJS(input *Field) string {
// case reflect.Ptr, reflect.Struct, reflect.Map, reflect.Interface:
// return JsObject
case "struct":
return input.Struct.ToString()
return input.Struct.Name
default:
println("UNSUPPORTED: ", input)
return "*"
}
}
func goTypeToTS(input *Field) string {
// goTypeToTS converts the given field into a Typescript type
// The pkgName is the package that the field is being output in.
// This is used to ensure we don't qualify local structs.
func goTypeToTS(input *Field, pkgName string) string {
var result string
switch input.Type {
case "string":
@@ -54,8 +57,10 @@ func goTypeToTS(input *Field) string {
case "bool":
result = "boolean"
case "struct":
if input.Struct.Package != "" {
result = input.Struct.Package + "."
if input.Struct.Package.Name != "" {
if input.Struct.Package.Name != pkgName {
result = input.Struct.Package.Name + "."
}
}
result += input.Struct.Name
// case reflect.Array, reflect.Slice:
@@ -70,9 +75,9 @@ func goTypeToTS(input *Field) string {
return JsUnsupported
}
// if input.IsArray {
// result = "Array<" + result + ">"
// }
if input.IsArray {
result = "Array<" + result + ">"
}
return result
}

View File

@@ -3,25 +3,67 @@ package parser
import (
"fmt"
"go/ast"
"github.com/davecgh/go-spew/spew"
)
// Field defines a parsed struct field
type Field struct {
Name string
Type string
Struct *Struct
// Name of the field
Name string
// The type of the field.
// "struct" if it's a struct
Type string
// A pointer to the struct if the Type is "struct"
Struct *Struct
// User comments on the field
Comments []string
// This struct reference is to temporarily hold the name
// of the struct during parsing
structReference *StructReference
// Indicates if the Field is an array of type "Type"
IsArray bool
}
func (p *Parser) parseField(field *ast.Field, thisPackageName string) ([]*Field, error) {
// JSType returns the Javascript type for this field
func (f *Field) JSType() string {
return string(goTypeToJS(f))
}
// TypeAsTSType converts the Field type to something TS wants
func (f *Field) TypeAsTSType(pkgName string) string {
var result = ""
switch f.Type {
case "string":
result = "string"
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
result = "number"
case "float32", "float64":
result = "number"
case "bool":
result = "boolean"
case "struct":
if f.Struct.Package != nil {
if f.Struct.Package.Name != pkgName {
result = f.Struct.Package.Name + "."
}
}
result = result + f.Struct.Name
default:
result = "any"
}
return result
}
func (p *Parser) parseField(file *ast.File, field *ast.Field, pkg *Package) ([]*Field, error) {
var result []*Field
var fieldType string
var structReference *StructReference
var strct *Struct
var isArray bool
// Determine type
switch t := field.Type.(type) {
@@ -29,20 +71,86 @@ func (p *Parser) parseField(field *ast.Field, thisPackageName string) ([]*Field,
fieldType = t.Name
case *ast.StarExpr:
fieldType = "struct"
packageName, structName, err := p.parseStructNameFromStarExpr(t)
packageName, structName, err := parseStructNameFromStarExpr(t)
if err != nil {
return nil, err
}
// If we don't ahve a package name, it means it's in this package
if packageName == "" {
packageName = thisPackageName
// If this is an external package, find it
if packageName != "" {
referencedGoPackage := pkg.getImportByName(packageName, file)
referencedPackage := p.getPackageByID(referencedGoPackage.ID)
// If we found the struct, save it as an external package reference
if referencedPackage != nil {
pkg.addExternalReference(referencedPackage)
}
// We save this to pkg anyway, because we want to know if this package
// was NOT found
pkg = referencedPackage
}
// Temporarily store the struct reference
structReference = newStructReference(packageName, structName)
// If this is a package in our project, parse the struct!
if pkg != nil {
// Parse the struct
strct, err = p.parseStruct(pkg, structName)
if err != nil {
return nil, err
}
// Save the fact this struct is used as a data type
strct.IsUsedAsData = true
}
case *ast.ArrayType:
isArray = true
// Parse the Elt (There must be a better way!)
switch t := t.Elt.(type) {
case *ast.Ident:
fieldType = t.Name
case *ast.StarExpr:
fieldType = "struct"
packageName, structName, err := parseStructNameFromStarExpr(t)
if err != nil {
return nil, err
}
// If this is an external package, find it
if packageName != "" {
referencedGoPackage := pkg.getImportByName(packageName, file)
referencedPackage := p.getPackageByID(referencedGoPackage.ID)
// If we found the struct, save it as an external package reference
if referencedPackage != nil {
pkg.addExternalReference(referencedPackage)
}
// We save this to pkg anyway, because we want to know if this package
// was NOT found
pkg = referencedPackage
}
// If this is a package in our project, parse the struct!
if pkg != nil {
// Parse the struct
strct, err = p.parseStruct(pkg, structName)
if err != nil {
return nil, err
}
// Save the fact this struct is used as a data type
strct.IsUsedAsData = true
}
default:
// We will default to "Array<any>" for eg nested arrays
fieldType = "any"
}
default:
spew.Dump(t)
return nil, fmt.Errorf("Unsupported field found in struct: %+v", t)
}
@@ -52,11 +160,12 @@ func (p *Parser) parseField(field *ast.Field, thisPackageName string) ([]*Field,
// Create a field per name
thisField := &Field{
Comments: p.parseComments(field.Doc),
Comments: parseComments(field.Doc),
}
thisField.Name = name.Name
thisField.Type = fieldType
thisField.structReference = structReference
thisField.Struct = strct
thisField.IsArray = isArray
result = append(result, thisField)
}
@@ -65,48 +174,12 @@ func (p *Parser) parseField(field *ast.Field, thisPackageName string) ([]*Field,
// When we have no name
thisField := &Field{
Comments: p.parseComments(field.Doc),
Comments: parseComments(field.Doc),
}
thisField.Type = fieldType
thisField.structReference = structReference
thisField.Struct = strct
thisField.IsArray = isArray
result = append(result, thisField)
return result, nil
}
func (p *Parser) resolveFieldReferences(fields []*Field) error {
// Loop over fields
for _, field := range fields {
// If we have a struct reference but no actual struct,
// we need to resolve it
if field.structReference != nil && field.Struct == nil {
fqn := field.structReference.FullyQualifiedName()
println("Need to resolve struct reference: ", fqn)
// Check the cache for the struct
structPointer, err := p.ParseStruct(field.structReference.Package, field.structReference.Name)
if err != nil {
return err
}
field.Struct = structPointer
if field.Struct != nil {
// Save the fact that the struct is used as data
field.Struct.UsedAsData = true
println("Resolved struct reference:", fqn)
// Resolve *its* references
err = p.resolveStructReferences(field.Struct)
if err != nil {
return err
}
} else {
println("Unable to resolve struct reference:", fqn)
}
}
}
return nil
}

View File

@@ -1,27 +1,29 @@
package parser
import (
"go/ast"
import "go/ast"
"golang.org/x/tools/go/packages"
)
// findBoundStructs will search through the Wails project looking
// for which structs have been bound using the `Bind()` method
func (p *Parser) findBoundStructs(pkg *Package) error {
func (p *Parser) getImportByName(pkg *packages.Package, importName string) *packages.Package {
// Find package path
for _, imp := range pkg.Imports {
if imp.Name == importName {
return imp
// Iterate through the files in the package looking for the bound structs
for _, fileAst := range pkg.gopackage.Syntax {
// Find the wails import name
wailsImportName := pkg.getWailsImportName(fileAst)
// If this file doesn't import wails, continue
if wailsImportName == "" {
continue
}
}
return nil
}
func (p *Parser) findBoundStructsInPackage(pkg *packages.Package, applicationVariableName string) []*StructReference {
applicationVariableName := pkg.getApplicationVariableName(fileAst, wailsImportName)
if applicationVariableName == "" {
continue
}
var boundStructs []*StructReference
var parseError error
// Iterate through the whole package looking for the bound structs
for _, fileAst := range pkg.Syntax {
ast.Inspect(fileAst, func(n ast.Node) bool {
// Parse Call expressions looking for bind calls
callExpr, ok := n.(*ast.CallExpr)
@@ -92,8 +94,7 @@ func (p *Parser) findBoundStructsInPackage(pkg *packages.Package, applicationVar
// app.Bind( &myStruct{} )
case *ast.Ident:
boundStruct := newStructReference(pkg.Name, boundStructExp.Name)
boundStructs = append(boundStructs, boundStruct)
pkg.boundStructs.Add(boundStructExp.Name)
// app.Bind( &mypackage.myStruct{} )
case *ast.SelectorExpr:
@@ -108,39 +109,39 @@ func (p *Parser) findBoundStructsInPackage(pkg *packages.Package, applicationVar
return true
}
structName = boundStructExp.Sel.Name
referencedPackage := p.getImportByName(pkg, packageName)
boundStruct := newStructReference(referencedPackage.Name, structName)
boundStructs = append(boundStructs, boundStruct)
referencedPackage := pkg.getImportByName(packageName, fileAst)
packageWrapper := p.getPackageByID(referencedPackage.ID)
packageWrapper.boundStructs.Add(structName)
}
// Binding struct literals
case *ast.CompositeLit:
switch literal := boundItem.Type.(type) {
// // Binding struct literals
// case *ast.CompositeLit:
// switch literal := boundItem.Type.(type) {
// app.Bind( myStruct{} )
case *ast.Ident:
structName := literal.Name
boundStruct := newStructReference(pkg.Name, structName)
boundStructs = append(boundStructs, boundStruct)
// // app.Bind( myStruct{} )
// case *ast.Ident:
// structName := literal.Name
// boundStructReference := newStructReference(p.GoPackage, structName)
// p.addBoundStructReference(boundStructReference)
// app.Bind( mypackage.myStruct{} )
case *ast.SelectorExpr:
var structName = ""
var packageName = ""
switch x := literal.X.(type) {
case *ast.Ident:
packageName = x.Name
default:
// TODO: Save these warnings
// println("Identifier in binding not supported:")
return true
}
structName = literal.Sel.Name
// // app.Bind( mypackage.myStruct{} )
// case *ast.SelectorExpr:
// var structName = ""
// var packageName = ""
// switch x := literal.X.(type) {
// case *ast.Ident:
// packageName = x.Name
// default:
// // TODO: Save these warnings
// // println("Identifier in binding not supported:")
// return true
// }
// structName = literal.Sel.Name
referencedPackage := p.getImportByName(pkg, packageName)
boundStruct := newStructReference(referencedPackage.Name, structName)
boundStructs = append(boundStructs, boundStruct)
}
// referencedPackage := p.getImportByName(pkg, packageName)
// boundStructReference := newStructReference(referencedPackage, structName)
// p.addBoundStructReference(boundStructReference)
// }
default:
// TODO: Save these warnings
@@ -150,6 +151,11 @@ func (p *Parser) findBoundStructsInPackage(pkg *packages.Package, applicationVar
return true
})
if parseError != nil {
return parseError
}
}
return boundStructs
return nil
}

View File

@@ -1,5 +0,0 @@
package parser
func (p *Parser) getFunctionReturnType(packageName string, functionName string) *Struct {
return nil
}

131
v2/pkg/parser/generate.go Normal file
View File

@@ -0,0 +1,131 @@
package parser
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"text/template"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/fs"
)
// GenerateWailsFrontendPackage will generate a Javascript/Typescript
// package in `<project>/frontend/wails` that defines which methods
// and structs are bound to your frontend
func GenerateWailsFrontendPackage() error {
dir, err := os.Getwd()
if err != nil {
return err
}
p := NewParser()
err = p.ParseProject(dir)
if err != nil {
return err
}
err = p.generateModule()
return err
}
func (p *Parser) generateModule() error {
moduleDir, err := createBackendJSDirectory()
if err != nil {
return err
}
for _, pkg := range p.packages {
// Calculate directory
dir := filepath.Join(moduleDir, pkg.gopackage.Name)
// Create the directory if it doesn't exist
fs.Mkdir(dir)
err := generatePackage(pkg, dir)
if err != nil {
return err
}
}
return nil
}
func createBackendJSDirectory() (string, error) {
// Calculate the package directory
// Note this is *always* called from the project directory
// so using paths relative to CWD is fine
dir, err := fs.RelativeToCwd("./frontend/wails")
if err != nil {
return "", errors.Wrap(err, "Error creating wails js directory")
}
// Remove directory if it exists - REGENERATION!
err = os.RemoveAll(dir)
if err != nil {
return "", errors.Wrap(err, "Error removing module directory")
}
// Make the directory
err = fs.Mkdir(dir)
return dir, err
}
func generatePackage(pkg *Package, moduledir string) error {
// Get path to local file
typescriptTemplateFile := fs.RelativePath("./package.d.template")
// Load typescript template
typescriptTemplateData := fs.MustLoadString(typescriptTemplateFile)
typescriptTemplate, err := template.New("typescript").Parse(typescriptTemplateData)
if err != nil {
return errors.Wrap(err, "Error creating template")
}
// Execute javascript template
var buffer bytes.Buffer
err = typescriptTemplate.Execute(&buffer, pkg)
if err != nil {
return errors.Wrap(err, "Error generating code")
}
// Save typescript file
err = ioutil.WriteFile(filepath.Join(moduledir, "index.d.ts"), buffer.Bytes(), 0755)
if err != nil {
return errors.Wrap(err, "Error writing backend package file")
}
// Get path to local file
javascriptTemplateFile := fs.RelativePath("./package.template")
// Load javascript template
javascriptTemplateData := fs.MustLoadString(javascriptTemplateFile)
javascriptTemplate, err := template.New("javascript").Parse(javascriptTemplateData)
if err != nil {
return errors.Wrap(err, "Error creating template")
}
// Reset the buffer
buffer.Reset()
err = javascriptTemplate.Execute(&buffer, pkg)
if err != nil {
return errors.Wrap(err, "Error generating code")
}
// Save javascript file
err = ioutil.WriteFile(filepath.Join(moduledir, "index.js"), buffer.Bytes(), 0755)
if err != nil {
return errors.Wrap(err, "Error writing backend package file")
}
return nil
}

View File

@@ -1,6 +1,7 @@
package parser
import (
"fmt"
"go/ast"
"strings"
)
@@ -14,7 +15,8 @@ type Method struct {
}
func (p *Parser) parseStructMethods(boundStruct *Struct) error {
for _, fileAst := range boundStruct.Package.Syntax {
for _, fileAst := range boundStruct.Package.gopackage.Syntax {
// Track errors
var parseError error
@@ -43,7 +45,7 @@ func (p *Parser) parseStructMethods(boundStruct *Struct) error {
}
// We want to ignore Internal functions
if p.internalMethods.Contains(funcDecl.Name.Name) {
if funcDecl.Name.Name == "WailsInit" || funcDecl.Name.Name == "WailsShutdown" {
continue
}
@@ -55,13 +57,13 @@ func (p *Parser) parseStructMethods(boundStruct *Struct) error {
// Create our struct
structMethod := &Method{
Name: funcDecl.Name.Name,
Comments: p.parseComments(funcDecl.Doc),
Comments: parseComments(funcDecl.Doc),
}
// Save the input parameters
if funcDecl.Type.Params != nil {
for _, inputField := range funcDecl.Type.Params.List {
fields, err := p.parseField(inputField, boundStruct.Package.Name)
fields, err := p.parseField(fileAst, inputField, boundStruct.Package)
if err != nil {
parseError = err
return false
@@ -74,7 +76,7 @@ func (p *Parser) parseStructMethods(boundStruct *Struct) error {
// Save the output parameters
if funcDecl.Type.Results != nil {
for _, outputField := range funcDecl.Type.Results.List {
fields, err := p.parseField(outputField, boundStruct.Package.Name)
fields, err := p.parseField(fileAst, outputField, boundStruct.Package)
if err != nil {
parseError = err
return false
@@ -104,3 +106,44 @@ func (p *Parser) parseStructMethods(boundStruct *Struct) error {
return nil
}
// InputsAsTSText generates a string with the method inputs
// formatted in a way acceptable to Typescript
func (m *Method) InputsAsTSText(pkgName string) string {
var inputs []string
for _, input := range m.Inputs {
inputText := fmt.Sprintf("%s: %s", input.Name, goTypeToTS(input, pkgName))
inputs = append(inputs, inputText)
}
return strings.Join(inputs, ", ")
}
// OutputsAsTSText generates a string with the method inputs
// formatted in a way acceptable to Javascript
func (m *Method) OutputsAsTSText(pkgName string) string {
if len(m.Returns) == 0 {
return "void"
}
var result []string
for _, output := range m.Returns {
result = append(result, goTypeToTS(output, pkgName))
}
return strings.Join(result, ", ")
}
// InputsAsJSText generates a string with the method inputs
// formatted in a way acceptable to Javascript
func (m *Method) InputsAsJSText() string {
var inputs []string
for _, input := range m.Inputs {
inputs = append(inputs, input.Name)
}
return strings.Join(inputs, ", ")
}

View File

@@ -6,18 +6,17 @@
/// <reference types="../{{.}}" />{{end}}{{- end}}
declare module {{.Name}} { {{range .Structs}}
{{- $usedAsData := $.StructIsUsedAsData .Name }}
{{- if or .IsBound $usedAsData}}
{{- if or .IsBound .IsUsedAsData}}
{{if .Comments }}{{range .Comments}}// {{ . }}{{end}}{{- end}}
interface {{.Name}} { {{ if $usedAsData }}
interface {{.Name}} { {{ if .IsUsedAsData }}
{{- range .Fields}}{{if .Comments }}
{{range .Comments}}//{{ . }}{{end}}{{- end}}
{{.Name}}: {{.TypeAsTSType}}; {{- end}} {{ end }}
{{.Name}}: {{.TypeAsTSType $.Name}}; {{- end}} {{ end }}
{{- if .IsBound }}
{{- range .Methods}}
{{- range .Comments}}
// {{ . }}{{- end}}
{{.Name}}({{.InputsAsTSText}}): Promise<{{.OutputsAsTSText}}>;
{{.Name}}({{.InputsAsTSText $.Name}}): Promise<{{.OutputsAsTSText $.Name}}>;
{{- end}}{{end}}
}{{- end}}
{{end}}

122
v2/pkg/parser/package.go Normal file
View File

@@ -0,0 +1,122 @@
package parser
import (
"go/ast"
"strings"
"github.com/leaanthony/slicer"
"golang.org/x/tools/go/packages"
)
// Package is a wrapper around the go parsed package
type Package struct {
// A unique Name for this package.
// This is calculated and may not be the same as the one
// defined in Go - but that's ok!
Name string
// the package we are wrapping
gopackage *packages.Package
// a list of struct names that are bound in this package
boundStructs slicer.StringSlicer
// Structs used in this package
parsedStructs map[string]*Struct
// A list of external packages we reference from this package
externalReferences slicer.InterfaceSlicer
}
func newPackage(pkg *packages.Package) *Package {
return &Package{
gopackage: pkg,
parsedStructs: make(map[string]*Struct),
}
}
func (p *Package) getWailsImportName(file *ast.File) string {
// Scan the imports for the wails v2 import
for _, details := range file.Imports {
if details.Path.Value == `"github.com/wailsapp/wails/v2"` {
if details.Name != nil {
return details.Name.Name
}
// Get the import name from the package
imp := p.getImportByPath("github.com/wailsapp/wails/v2")
if imp != nil {
return imp.Name
}
}
}
return ""
}
func (p *Package) getImportByName(importName string, file *ast.File) *packages.Package {
// Check if the file has aliased the import
for _, imp := range file.Imports {
if imp.Name != nil {
if imp.Name.Name == importName {
// Yes it has. Get the import by path
return p.getImportByPath(imp.Path.Value)
}
}
}
// We need to find which package import has this name
for _, imp := range p.gopackage.Imports {
if imp.Name == importName {
return imp
}
}
// Looks like this package is outside the project...
return nil
}
func (p *Package) getImportByPath(packagePath string) *packages.Package {
packagePath = strings.Trim(packagePath, "\"")
return p.gopackage.Imports[packagePath]
}
func (p *Package) getStruct(structName string) *Struct {
return p.parsedStructs[structName]
}
func (p *Package) addStruct(strct *Struct) {
p.parsedStructs[strct.Name] = strct
}
// DeclarationReferences returns a list of external packages
// we reference from this package
func (p *Package) DeclarationReferences() []string {
var referenceNames slicer.StringSlicer
// Generics can't come soon enough!
p.externalReferences.Each(func(p interface{}) {
referenceNames.Add(p.(*Package).Name)
})
return referenceNames.AsSlice()
}
// addExternalReference saves the given package as an external reference
func (p *Package) addExternalReference(pkg *Package) {
p.externalReferences.AddUnique(pkg)
}
// Structs returns the structs that we want to generate
func (p *Package) Structs() []*Struct {
var result []*Struct
for _, elem := range p.parsedStructs {
result = append(result, elem)
}
return result
}

View File

@@ -13,7 +13,7 @@ export const {{.Name}} = {
* @function {{.Name}}
{{range .Inputs}} * @param {{"{"}}{{.JSType}}{{"}"}} {{.Name}}
{{end}} *
* @returns {Promise<{{.OutputsAsTSText}}>}
* @returns {Promise<{{.OutputsAsTSText $.Name}}>}
*/
{{.Name}}: function({{.InputsAsJSText}}) {
return window.backend.{{$.Name}}.{{$struct.Name}}.{{.Name}}({{.InputsAsJSText}});

View File

@@ -0,0 +1,75 @@
package parser
import "go/ast"
func (p *Parser) parseBoundStructs(pkg *Package) error {
// Loop over the bound structs
for _, structName := range pkg.boundStructs.AsSlice() {
strct, err := p.parseStruct(pkg, structName)
if err != nil {
return err
}
strct.IsBound = true
}
return nil
}
// ParseStruct will attempt to parse the given struct using
// the package it references
func (p *Parser) parseStruct(pkg *Package, structName string) (*Struct, error) {
// Check the parser cache for this struct
result := pkg.getStruct(structName)
if result != nil {
return result, nil
}
// Iterate through the whole package looking for the bound structs
for _, fileAst := range pkg.gopackage.Syntax {
// Track errors
var parseError error
ast.Inspect(fileAst, func(n ast.Node) bool {
if genDecl, ok := n.(*ast.GenDecl); ok {
for _, spec := range genDecl.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
if structType, ok := typeSpec.Type.(*ast.StructType); ok {
structDefinitionName := typeSpec.Name.Name
if structDefinitionName == structName {
// Create the new struct
result = &Struct{Name: structName, Package: pkg}
// Save comments
result.Comments = parseComments(genDecl.Doc)
parseError = p.parseStructMethods(result)
if parseError != nil {
return false
}
// Parse the struct fields
parseError = p.parseStructFields(fileAst, structType, result)
// Save this struct
pkg.addStruct(result)
return false
}
}
}
}
}
return true
})
// If we got an error, return it
if parseError != nil {
return nil, parseError
}
}
return result, nil
}

View File

@@ -1,93 +0,0 @@
package parser
import (
"go/ast"
)
// getCachedStruct attempts to get an already parsed struct from the
// struct cache
func (p *Parser) getCachedStruct(packageName string, structName string) *Struct {
fqn := packageName + "." + structName
return p.parsedStructs[fqn]
}
// ParseStruct will attempt to parse the given struct using
// the package it references
func (p *Parser) ParseStruct(packageName string, structName string) (*Struct, error) {
// Check the cache
result := p.getCachedStruct(packageName, structName)
if result != nil {
return result, nil
}
// Find the package
pkg := p.getPackageByName(packageName)
if pkg == nil {
// TODO: Find package via imports?
println("Cannot find package", packageName)
return nil, nil
}
// Iterate through the whole package looking for the bound structs
for _, fileAst := range pkg.Syntax {
// Track errors
var parseError error
ast.Inspect(fileAst, func(n ast.Node) bool {
if genDecl, ok := n.(*ast.GenDecl); ok {
for _, spec := range genDecl.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
if structType, ok := typeSpec.Type.(*ast.StructType); ok {
structDefinitionName := typeSpec.Name.Name
if structDefinitionName == structName {
// Create the new struct
result = p.newStruct(pkg, structDefinitionName)
// Save comments
result.Comments = p.parseComments(genDecl.Doc)
parseError = p.parseStructMethods(result)
if parseError != nil {
return false
}
// Parse the struct fields
parseError = p.parseStructFields(structType, result)
// Cache this struct
key := result.FullyQualifiedName()
p.parsedStructs[key] = result
return false
}
}
}
}
}
return true
})
// If we got an error, return it
if parseError != nil {
return nil, parseError
}
}
return result, nil
}
func (p *Parser) parseStructFields(structType *ast.StructType, boundStruct *Struct) error {
// Parse the fields
for _, field := range structType.Fields.List {
fields, err := p.parseField(field, boundStruct.Package.Name)
if err != nil {
return err
}
boundStruct.Fields = append(boundStruct.Fields, fields...)
}
return nil
}

View File

@@ -0,0 +1,17 @@
package parser
import "go/ast"
func (p *Parser) parseStructFields(fileAst *ast.File, structType *ast.StructType, boundStruct *Struct) error {
// Parse the fields
for _, field := range structType.Fields.List {
fields, err := p.parseField(fileAst, field, boundStruct.Package)
if err != nil {
return err
}
boundStruct.Fields = append(boundStruct.Fields, fields...)
}
return nil
}

View File

@@ -3,34 +3,27 @@ package parser
import (
"go/token"
"github.com/leaanthony/slicer"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
"golang.org/x/tools/go/packages"
)
// Parser is the Wails project parser
type Parser struct {
// Placeholders for Go's parser
goPackages []*packages.Package
fileSet *token.FileSet
internalMethods *slicer.StringSlicer
fileSet *token.FileSet
// This is a map of structs that have been parsed
// The key is <package>.<structname>
parsedStructs map[string]*Struct
// The list of struct names that are bound
BoundStructReferences []*StructReference
// The list of structs that are bound
BoundStructs []*Struct
// The packages we parse
// The map key is the package ID
packages map[string]*Package
}
// NewParser creates a new Wails project parser
func NewParser() *Parser {
return &Parser{
fileSet: token.NewFileSet(),
internalMethods: slicer.String([]string{"WailsInit", "WailsShutdown"}),
parsedStructs: make(map[string]*Struct),
fileSet: token.NewFileSet(),
packages: make(map[string]*Package),
}
}
@@ -44,17 +37,29 @@ func (p *Parser) ParseProject(dir string) error {
return err
}
err = p.findBoundStructs()
if err != nil {
return err
// Find all the bound structs
for _, pkg := range p.packages {
err = p.findBoundStructs(pkg)
if err != nil {
return err
}
}
err = p.parseBoundStructs()
if err != nil {
return err
// Parse the structs
for _, pkg := range p.packages {
err = p.parseBoundStructs(pkg)
if err != nil {
return err
}
}
return err
// Resolve package names
// We do this because some packages may have the same name
p.resolvePackageNames()
spew.Dump(p.packages)
return nil
}
func (p *Parser) loadPackages(projectPath string) error {
@@ -63,7 +68,8 @@ func (p *Parser) loadPackages(projectPath string) error {
packages.NeedSyntax |
packages.NeedTypes |
packages.NeedImports |
packages.NeedTypesInfo
packages.NeedTypesInfo |
packages.NeedModule
cfg := &packages.Config{Fset: p.fileSet, Mode: mode, Dir: projectPath}
pkgs, err := packages.Load(cfg, "./...")
@@ -86,77 +92,14 @@ func (p *Parser) loadPackages(projectPath string) error {
return parseError
}
p.goPackages = pkgs
return nil
}
func (p *Parser) getPackageByName(packageName string) *packages.Package {
for _, pkg := range p.goPackages {
if pkg.Name == packageName {
return pkg
}
}
return nil
}
func (p *Parser) getWailsImportName(pkg *packages.Package) (string, bool) {
// Scan the imports for the wails v2 import
for key, details := range pkg.Imports {
if key == "github.com/wailsapp/wails/v2" {
return details.Name, true
}
}
return "", false
}
// findBoundStructs will search through the Wails project looking
// for which structs have been bound using the `Bind()` method
func (p *Parser) findBoundStructs() error {
// Try each of the packages to find the Bind() calls
for _, pkg := range p.goPackages {
// Does this package import Wails?
wailsImportName, imported := p.getWailsImportName(pkg)
if !imported {
continue
}
// Do we create an app using CreateApp?
appVariableName, created := p.getApplicationVariableName(pkg, wailsImportName)
if !created {
continue
}
boundStructReferences := p.findBoundStructsInPackage(pkg, appVariableName)
p.BoundStructReferences = append(p.BoundStructReferences, boundStructReferences...)
// Create a map of packages
for _, pkg := range pkgs {
p.packages[pkg.ID] = newPackage(pkg)
}
return nil
}
func (p *Parser) parseBoundStructs() error {
// Iterate the structs
for _, boundStructReference := range p.BoundStructReferences {
// Parse the struct
boundStruct, err := p.ParseStruct(boundStructReference.Package, boundStructReference.Name)
if err != nil {
return err
}
p.BoundStructs = append(p.BoundStructs, boundStruct)
}
// Resolve the references between the structs
// This is when a field of one struct is a struct type
for _, boundStruct := range p.BoundStructs {
err := p.resolveStructReferences(boundStruct)
if err != nil {
return err
}
}
return nil
func (p *Parser) getPackageByID(id string) *Package {
return p.packages[id]
}

View File

@@ -1,46 +0,0 @@
package parser
import (
"testing"
"github.com/leaanthony/slicer"
"github.com/matryer/is"
"github.com/wailsapp/wails/v2/internal/fs"
)
func TestParser(t *testing.T) {
is := is.New(t)
// Local project dir
projectDir := fs.RelativePath("./testproject")
p := NewParser()
// Check parsing worked
err := p.ParseProject(projectDir)
is.NoErr(err)
// Expected structs
expectedBoundStructs := slicer.String()
expectedBoundStructs.Add("main.Basic", "mypackage.Manager")
// We expect these to be the same length
is.Equal(expectedBoundStructs.Length(), len(p.BoundStructs))
// Check bound structs
for _, boundStruct := range p.BoundStructs {
// Check the names are correct
fqn := boundStruct.FullyQualifiedName()
is.True(expectedBoundStructs.Contains(fqn))
// Check that the structs have comments
is.True(len(boundStruct.Comments) > 0)
// Check that the structs have methods
is.True(len(boundStruct.Methods) > 0)
}
}

View File

@@ -0,0 +1,35 @@
package parser
import (
"fmt"
"github.com/leaanthony/slicer"
)
// resolvePackageNames will deterine the names for the packages, allowing
// us to create a flat structure for the imports in the frontend module
func (p *Parser) resolvePackageNames() {
// A cache for the names
var packageNameCache slicer.StringSlicer
// Process each package
for _, pkg := range p.packages {
pkgName := pkg.gopackage.Name
// Check for collision
if packageNameCache.Contains(pkgName) {
// https://www.youtube.com/watch?v=otNNGROI0Cs !!!!!
// We start at 2 because having both "pkg" and "pkg1" is 🙄
count := 2
for ok := true; ok; ok = packageNameCache.Contains(pkgName) {
pkgName = fmt.Sprintf("%s%d", pkg.gopackage.Name, count)
}
}
// Save the name!
packageNameCache.Add(pkgName)
pkg.Name = pkgName
}
}

View File

@@ -4,37 +4,36 @@ import (
"fmt"
"go/ast"
"golang.org/x/tools/go/packages"
"github.com/pkg/errors"
)
// Struct represents a struct that is used by the frontend
// in a Wails project
type Struct struct {
Package *packages.Package
Name string
// The name of the struct
Name string
// The package this was declared in
Package *Package
// Comments for the struct
Comments []string
Fields []*Field
Methods []*Method
// This is true when this struct is used as a datatype
UsedAsData bool
// The fields used in this struct
Fields []*Field
// The methods available to the front end
Methods []*Method
// Indicates if this struct is bound to the app
IsBound bool
// Indicates if this struct is used as data
IsUsedAsData bool
}
// newStruct creates a new struct and stores in the cache
func (p *Parser) newStruct(pkg *packages.Package, name string) *Struct {
result := &Struct{
Package: pkg,
Name: name,
}
return result
}
// FullyQualifiedName returns the fully qualified name of this struct
func (s *Struct) FullyQualifiedName() string {
return s.Package.Name + "." + s.Name
}
func (p *Parser) parseStructNameFromStarExpr(starExpr *ast.StarExpr) (string, string, error) {
func parseStructNameFromStarExpr(starExpr *ast.StarExpr) (string, string, error) {
pkg := ""
name := ""
// Determine the FQN
@@ -44,66 +43,26 @@ func (p *Parser) parseStructNameFromStarExpr(starExpr *ast.StarExpr) (string, st
case *ast.Ident:
pkg = i.Name
default:
return "", "", fmt.Errorf("Unsupported Selector expression: %+v", i)
// TODO: Store warnings?
return "", "", errors.WithStack(fmt.Errorf("Unknown type in selector for *ast.SelectorExpr: ", i))
}
name = x.Sel.Name
// TODO: IS this used?
case *ast.StarExpr:
switch s := x.X.(type) {
case *ast.Ident:
name = s.Name
default:
return "", "", fmt.Errorf("Unsupported Star expression: %+v", s)
// TODO: Store warnings?
return "", "", errors.WithStack(fmt.Errorf("Unknown type in selector for *ast.StarExpr: ", s))
}
case *ast.Ident:
name = x.Name
default:
return "", "", fmt.Errorf("Unsupported Star.X expression: %+v", x)
// TODO: Store warnings?
return "", "", errors.WithStack(fmt.Errorf("Unknown type in selector for *ast.StarExpr: ", starExpr))
}
return pkg, name, nil
}
// StructReference defines a reference to a fully qualified struct
type StructReference struct {
Package string
Name string
}
func newStructReference(packageName string, structName string) *StructReference {
return &StructReference{Package: packageName, Name: structName}
}
// FullyQualifiedName returns a string representing the struct reference
func (s *StructReference) FullyQualifiedName() string {
return s.Package + "." + s.Name
}
func (p *Parser) resolveStructReferences(boundStruct *Struct) error {
var err error
// Resolve field references
err = p.resolveFieldReferences(boundStruct.Fields)
if err != nil {
return nil
}
// Check if method fields need resolving
for _, method := range boundStruct.Methods {
// Resolve method inputs
err = p.resolveFieldReferences(method.Inputs)
if err != nil {
return nil
}
// Resolve method outputs
err = p.resolveFieldReferences(method.Returns)
if err != nil {
return nil
}
}
return nil
}

View File

@@ -3,6 +3,8 @@ package main
import (
"fmt"
"testproject/mypackage"
wails "github.com/wailsapp/wails/v2"
)
@@ -43,17 +45,22 @@ func (b *Basic) WailsShutdown() {
// Perform your teardown here
}
// // NewPerson creates a new person
// func (b *Basic) NewPerson(name string, age int) *mypackage.Person {
// return &mypackage.Person{Name: name, Age: age}
// }
// NewPerson creates a new person
func (b *Basic) NewPerson(name string, age int) *mypackage.Person {
return &mypackage.Person{Name: name, Age: age}
}
// Greet returns a greeting for the given name
func (b *Basic) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}
// // RemovePerson Removes the given person
// func (b *Basic) RemovePerson(p *mypackage.Person) {
// // dummy
// }
// MultipleGreets returns greetings for the given name
func (b *Basic) MultipleGreets(name string) []string {
return []string{"hi", "hello", "croeso!"}
}
// RemovePerson Removes the given person
func (b *Basic) RemovePerson(p *mypackage.Person) {
// dummy
}