Now parsing actual code

This commit is contained in:
Lea Anthony
2020-10-31 15:31:44 +11:00
parent 608663fd87
commit f046c0c2ee
10 changed files with 624 additions and 170 deletions

View File

@@ -3,6 +3,7 @@ module github.com/wailsapp/wails/v2
go 1.13
require (
github.com/davecgh/go-spew v1.1.1
github.com/fsnotify/fsnotify v1.4.9
github.com/imdario/mergo v0.3.11
github.com/leaanthony/clir v1.0.4

View File

@@ -65,7 +65,6 @@ func ParseProject(projectPath string) (BoundStructs, error) {
var wailsPkgVar = ""
ast.Inspect(file, func(n ast.Node) bool {
var s string
switch x := n.(type) {
// Parse import declarations
case *ast.ImportSpec:

View File

@@ -1,7 +1,5 @@
package backendjs
import "reflect"
// JSType represents a javascript type
type JSType string
@@ -22,22 +20,45 @@ const (
JsUnsupported = "*"
)
func goTypeToJS(input reflect.Kind) JSType {
func goTypeToJS(input string) JSType {
switch input {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return JsInt
case reflect.String:
case "string":
return JsString
case reflect.Float32, reflect.Float64, reflect.Complex64:
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
return JsInt
case "float32", "float64":
return JsFloat
case reflect.Bool:
case "bool":
return JsBoolean
case reflect.Array, reflect.Slice:
return JsArray
case reflect.Ptr, reflect.Struct, reflect.Map, reflect.Interface:
return JsObject
// case reflect.Array, reflect.Slice:
// return JsArray
// case reflect.Ptr, reflect.Struct, reflect.Map, reflect.Interface:
// return JsObject
default:
println("UNSUPPORTED: ", input)
return JsUnsupported
}
}
func goTypeToTS(input string) string {
switch input {
case "string":
return "string"
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
return "number"
case "float32", "float64":
return "number"
case "bool":
return "boolean"
// case reflect.Array, reflect.Slice:
// return string(JsArray)
// case reflect.Ptr, reflect.Struct:
// fqt := input.Type().String()
// return strings.Split(fqt, ".")[1]
// case reflect.Map, reflect.Interface:
// return string(JsObject)
default:
println("UNSUPPORTED: ", input)
return JsUnsupported
}
}

View File

@@ -1,77 +0,0 @@
package backendjs
import (
"fmt"
"reflect"
"strings"
)
// Parameter defines a parameter used by a struct method
type Parameter struct {
Name string
Type reflect.Kind
StructName string
}
// JSType returns the Javascript equivalent of the
// parameter type
func (p *Parameter) JSType() string {
return string(goTypeToJS(p.Type))
}
// Method defines a struct method
type Method struct {
Name string
Inputs []*Parameter
Outputs []*Parameter
Comments []string
}
// 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, ", ")
}
// 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, goTypeToJS(input.Type))
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.Outputs) == 0 {
return "void"
}
var result []string
for _, output := range m.Outputs {
jsType := goTypeToJS(output.Type)
switch jsType {
case JsArray:
result = append(result, "Array<any>")
case JsObject:
result = append(result, "any")
default:
result = append(result, string(jsType))
}
}
return strings.Join(result, ", ")
}

View File

@@ -1,9 +1,9 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
{{range .Comments}}// {{.}}{{end}}
declare module {{.Name}} {
{{range .Methods}}
declare module {{.Struct.Name}} {
{{range .Struct.Methods}}
{{if .Comments }}{{range .Comments}}// {{ . }}{{end}}{{end}}
function {{.Name}}({{.InputsAsTSText}}): Promise<{{.OutputsAsTSText}}>;{{end}}
function {{.Name}}({{.InputsAsTSText}}): Promise<{{.OutputsAsTSText}}>;
{{end}}
}

View File

@@ -1,8 +1,7 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
{{range .Comments}}// {{.}}{{end}}
{{range .Methods}}
{{range .Struct.Methods }}
/**{{if .Comments }}
{{range .Comments}} * {{ . }}{{end}}
*{{end}}
@@ -12,6 +11,7 @@
* @returns {Promise<{{.OutputsAsTSText}}>}
*/
export function {{.Name}}({{.InputsAsJSText}}) {
return window.backend.{{$.Name}}.{{.Name}}({{.InputsAsJSText}});
return window.backend.{{$.PackageName}}.{{$.Struct.Name}}.{{.Name}}({{.InputsAsJSText}});
}
{{end}}

View File

@@ -3,8 +3,8 @@ package backendjs
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"text/template"
"github.com/pkg/errors"
@@ -15,7 +15,7 @@ import (
type Package struct {
Name string
Comments []string
Methods []*Method
Structs []*ParsedStruct
}
func generatePackages() error {
@@ -35,67 +35,83 @@ func generatePackages() error {
func parsePackages() ([]*Package, error) {
// STUB!
var result []*Package
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
result, err := parseProject(cwd)
if err != nil {
return nil, err
}
result = append(result, &Package{
Name: "mypackage",
Comments: []string{"mypackage is awesome"},
Methods: []*Method{
{
Name: "Naked",
Comments: []string{"Naked is a method that does nothing"},
},
},
})
result = append(result, &Package{
Name: "otherpackage",
Comments: []string{"otherpackage is awesome"},
Methods: []*Method{
{
Name: "OneInput",
Comments: []string{"OneInput does stuff"},
Inputs: []*Parameter{
{
Name: "name",
Type: reflect.String,
},
},
},
{
Name: "TwoInputs",
Inputs: []*Parameter{
{
Name: "name",
Type: reflect.String,
},
{
Name: "age",
Type: reflect.Uint8,
},
},
},
{
Name: "TwoInputsAndOutput",
Inputs: []*Parameter{
{
Name: "name",
Type: reflect.String,
},
{
Name: "age",
Type: reflect.Uint8,
},
},
Outputs: []*Parameter{
{
Name: "result",
Type: reflect.Bool,
},
},
},
},
})
// result = append(result, &Package{
// Name: "mypackage",
// Comments: []string{"mypackage is awesome"},
// Methods: []*Method{
// {
// Name: "Naked",
// Comments: []string{"Naked is a method that does nothing"},
// },
// },
// })
// result = append(result, &Package{
// Name: "otherpackage",
// Comments: []string{"otherpackage is awesome"},
// Methods: []*Method{
// {
// Name: "OneInput",
// Comments: []string{"OneInput does stuff"},
// Inputs: []*Parameter{
// {
// Name: "name",
// Value: String,
// },
// },
// },
// {
// Name: "TwoInputs",
// Inputs: []*Parameter{
// {
// Name: "name",
// Value: String,
// },
// {
// Name: "age",
// Value: Uint8,
// },
// },
// },
// {
// Name: "TwoInputsAndOutput",
// Inputs: []*Parameter{
// {
// Name: "name",
// Value: String,
// },
// {
// Name: "age",
// Value: Uint8,
// },
// },
// Outputs: []*Parameter{
// {
// Name: "result",
// Value: Bool,
// },
// },
// },
// {
// Name: "StructInput",
// Comments: []string{"StructInput takes a person"},
// Inputs: []*Parameter{
// {
// Name: "person",
// Value: NewPerson("John Thomas", 46),
// },
// },
// },
// },
// })
return result, nil
}
@@ -172,25 +188,48 @@ func generatePackageFiles(packages []*Package) error {
// Iterate over each package
for _, thisPackage := range packages {
// Calculate target directory
packageDir, err := fs.RelativeToCwd("./frontend/backend/" + thisPackage.Name)
err := generatePackage(thisPackage, typescriptTemplate, javascriptTemplate)
if err != nil {
return errors.Wrap(err, "Error calculating package path")
return err
}
}
// Make the dir but ignore if it already exists
fs.Mkdir(packageDir)
return nil
}
func generatePackage(thisPackage *Package, typescriptTemplate *template.Template, javascriptTemplate *template.Template) error {
// Calculate target directory
packageDir, err := fs.RelativeToCwd("./frontend/backend/" + thisPackage.Name)
if err != nil {
return errors.Wrap(err, "Error calculating package path")
}
// Make the dir but ignore if it already exists
fs.Mkdir(packageDir)
type TemplateData struct {
PackageName string
Struct *ParsedStruct
}
// Loop over structs
for _, strct := range thisPackage.Structs {
var data = &TemplateData{
PackageName: thisPackage.Name,
Struct: strct,
}
// Execute javascript template
var buffer bytes.Buffer
err = javascriptTemplate.Execute(&buffer, thisPackage)
err = javascriptTemplate.Execute(&buffer, data)
if err != nil {
return errors.Wrap(err, "Error generating code")
}
// Save javascript file
err = ioutil.WriteFile(filepath.Join(packageDir, "index.js"), buffer.Bytes(), 0755)
err = ioutil.WriteFile(filepath.Join(packageDir, strct.Name+".js"), buffer.Bytes(), 0755)
if err != nil {
return errors.Wrap(err, "Error writing backend package file")
}
@@ -199,13 +238,13 @@ func generatePackageFiles(packages []*Package) error {
buffer.Reset()
// Execute typescript template
err = typescriptTemplate.Execute(&buffer, thisPackage)
err = typescriptTemplate.Execute(&buffer, data)
if err != nil {
return errors.Wrap(err, "Error generating code")
}
// Save typescript file
err = ioutil.WriteFile(filepath.Join(packageDir, "index.d.ts"), buffer.Bytes(), 0755)
err = ioutil.WriteFile(filepath.Join(packageDir, strct.Name+".d.ts"), buffer.Bytes(), 0755)
if err != nil {
return errors.Wrap(err, "Error writing backend package file")
}

View File

@@ -0,0 +1,403 @@
package backendjs
import (
"fmt"
"go/ast"
"strings"
"github.com/leaanthony/slicer"
"github.com/pkg/errors"
"golang.org/x/tools/go/packages"
)
type Parser struct {
wailsPkgVar string
appVarName string
boundStructLiterals slicer.StringSlicer
boundMethods []string
boundStructPointerLiterals []string
boundVariables slicer.StringSlicer
variableFunctionDecls map[string]string
variableStructDecls map[string]string
internalMethods slicer.StringSlicer
structCache map[string]*ParsedStruct
structPointerFunctionDecls map[string]string
structFunctionDecls map[string]string
}
type ParsedParameter struct {
Name string
Type string
}
func (p *ParsedParameter) JSType() string {
return string(goTypeToJS(p.Type))
}
type ParsedMethod struct {
Struct string
Name string
Comments []string
Inputs []*ParsedParameter
Returns []*ParsedParameter
}
// InputsAsTSText generates a string with the method inputs
// formatted in a way acceptable to Typescript
func (m *ParsedMethod) InputsAsTSText() string {
var inputs []string
for _, input := range m.Inputs {
inputText := fmt.Sprintf("%s: %s", input.Name, goTypeToTS(input.Type))
inputs = append(inputs, inputText)
}
return strings.Join(inputs, ", ")
}
// InputsAsJSText generates a string with the method inputs
// formatted in a way acceptable to Javascript
func (m *ParsedMethod) InputsAsJSText() string {
var inputs []string
for _, input := range m.Inputs {
inputs = append(inputs, input.Name)
}
return strings.Join(inputs, ", ")
}
// OutputsAsTSText generates a string with the method inputs
// formatted in a way acceptable to Javascript
func (m *ParsedMethod) OutputsAsTSText() string {
if len(m.Returns) == 0 {
return "void"
}
var result []string
for _, output := range m.Returns {
jsType := goTypeToJS(output.Type)
switch jsType {
case JsArray:
result = append(result, "Array<any>")
case JsObject:
result = append(result, "any")
default:
result = append(result, string(jsType))
}
}
return strings.Join(result, ", ")
}
type ParsedStruct struct {
Name string
Methods []*ParsedMethod
}
func NewParser() *Parser {
return &Parser{
variableFunctionDecls: make(map[string]string),
variableStructDecls: make(map[string]string),
internalMethods: *slicer.String([]string{"WailsInit", "WailsShutdown"}),
structCache: make(map[string]*ParsedStruct),
structPointerFunctionDecls: make(map[string]string),
structFunctionDecls: make(map[string]string),
}
}
func parseProject(projectPath string) ([]*Package, error) {
cfg := &packages.Config{
Mode: packages.NeedName |
packages.NeedFiles |
packages.NeedSyntax |
packages.NeedTypes |
packages.NeedTypesInfo,
}
pkgs, err := packages.Load(cfg, projectPath)
if err != nil {
return nil, errors.Wrap(err, "Problem loading packages")
}
if packages.PrintErrors(pkgs) > 0 {
return nil, errors.Wrap(err, "Errors during parsing")
}
var result []*Package
// Iterate the packages
for _, pkg := range pkgs {
p := NewParser()
thisPackage, err := p.parsePackage(pkg)
if err != nil {
return nil, err
}
for k := range p.structCache {
thisPackage.Structs = append(thisPackage.Structs, p.structCache[k])
}
result = append(result, thisPackage)
}
return result, nil
}
func (p *Parser) parsePackage(pkg *packages.Package) (*Package, error) {
result := &Package{Name: pkg.Name}
for _, file := range pkg.Syntax {
err := p.parseFile(file)
if err != nil {
return nil, err
}
}
return result, nil
}
func (p *Parser) parseFile(file *ast.File) error {
ast.Inspect(file, func(n ast.Node) bool {
switch x := n.(type) {
// Parse import declarations
case *ast.ImportSpec:
// Determine what wails has been imported as
if x.Path.Value == `"github.com/wailsapp/wails/v2"` {
p.wailsPkgVar = x.Name.Name
}
// Parse calls. We are looking for app.Bind() calls
case *ast.CallExpr:
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.appVarName && f.Sel.Name == "Bind" {
if len(x.Args) == 1 {
ce, ok := x.Args[0].(*ast.CallExpr)
if ok {
n, ok := ce.Fun.(*ast.Ident)
if ok {
// We found a bind method using a function call
// EG: app.Bind( newMyStruct() )
p.boundMethods = append(p.boundMethods, n.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 {
// We have found Bind( &MyStruct{} )
p.boundStructPointerLiterals = append(p.boundStructPointerLiterals, t.Name)
}
}
}
} 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 {
p.boundStructLiterals.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 {
p.boundVariables.Add(i.Name)
}
}
}
}
}
}
}
}
// We scan assignments for a number of reasons:
// * Determine the variable containing the main application
// * Determine the type of variables that get used in Bind()
// * Determine the type of variables that get created with var := &MyStruct{}
case *ast.AssignStmt:
for _, rhs := range x.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.wailsPkgVar {
// Check we are calling a function to create the app
if se.Sel.Name == "CreateApp" || se.Sel.Name == "CreateAppWithOptions" {
if len(x.Lhs) == 1 {
i, ok := x.Lhs[0].(*ast.Ident)
if ok {
// Found the app variable name
p.appVarName = i.Name
}
}
}
}
}
} else {
// Check for function assignment
// a := newMyStruct()
fe, ok := ce.Fun.(*ast.Ident)
if ok {
if len(x.Lhs) == 1 {
i, ok := x.Lhs[0].(*ast.Ident)
if ok {
// Store the variable -> Function mapping
// so we can later resolve the type
p.variableFunctionDecls[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(x.Lhs) == 1 {
i, ok := x.Lhs[0].(*ast.Ident)
if ok {
p.variableStructDecls[i.Name] = t.Name
}
}
}
}
}
}
}
// We scan for functions to build up a list of function names
// for a number of reasons:
// * Determine which functions are struct methods that are bound
// * Determine
case *ast.FuncDecl:
if x.Recv != nil {
// This is a struct method
for _, field := range x.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(x.Name.Name) {
continue
}
// If we haven't already found this struct,
// Create a placeholder in the cache
parsedStruct := p.structCache[i.Name]
if parsedStruct == nil {
p.structCache[i.Name] = &ParsedStruct{
Name: i.Name,
}
parsedStruct = p.structCache[i.Name]
}
// If this method is Public
if string(x.Name.Name[0]) == strings.ToUpper((string(x.Name.Name[0]))) {
structMethod := &ParsedMethod{
Struct: i.Name,
Name: x.Name.Name,
}
// Check if the method has comments.
// If so, save it with the parsed method
if x.Doc != nil {
for _, comment := range x.Doc.List {
stringComment := strings.TrimPrefix(comment.Text, "//")
structMethod.Comments = append(structMethod.Comments, strings.TrimSpace(stringComment))
}
}
// Save the input parameters
for _, inputField := range x.Type.Params.List {
t, ok := inputField.Type.(*ast.Ident)
if !ok {
continue
}
for _, name := range inputField.Names {
structMethod.Inputs = append(structMethod.Inputs, &ParsedParameter{Name: name.Name, Type: t.Name})
}
}
// Save the output parameters
if x.Type.Results != nil {
for _, outputField := range x.Type.Results.List {
t, ok := outputField.Type.(*ast.Ident)
if !ok {
continue
}
if len(outputField.Names) == 0 {
structMethod.Returns = append(structMethod.Returns, &ParsedParameter{Type: t.Name})
} else {
for _, name := range outputField.Names {
structMethod.Returns = append(structMethod.Returns, &ParsedParameter{Name: name.Name, Type: t.Name})
}
}
}
}
// 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 := x.Name.Name
// Look for one that returns a single value
if x.Type != nil && x.Type.Results != nil && x.Type.Results.List != nil {
if len(x.Type.Results.List) == 1 {
// Check for *struct
t, ok := x.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)
p.structPointerFunctionDecls[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 := x.Type.Results.List[0].Type.(*ast.Ident)
if ok {
// println("*** Function", functionName, "found which returns: "+t.Name)
p.structFunctionDecls[functionName] = t.Name
}
}
}
}
}
}
return true
})
// spew.Dump(file)
return nil
}

View File

@@ -0,0 +1,13 @@
package backendjs
import "reflect"
type Struct struct {
Name string
Fields []*Field
}
type Field struct {
Name string
Type reflect.Value
}

View File

@@ -0,0 +1,55 @@
package backendjs
import "reflect"
var BoolValue bool = true
var IntValue int = 0
var Int8Value int8 = 0
var Int16Value int16 = 0
var Int32Value int32 = 0
var Int64Value int64 = 0
var UintValue uint = 0
var Uint8Value uint8 = 0
var Uint16Value uint16 = 0
var Uint32Value uint32 = 0
var Uint64Value uint64 = 0
var UintptrValue uintptr = 0
var Float32Value float32 = 0
var Float64Value float64 = 0
var Complex64Value complex64 = 0
var Complex128Value complex128 = 0
var StringValue string = ""
type Person struct {
Name string
Age uint8
}
type GuestList struct {
People []*Person
}
func NewPerson(name string, age uint8) reflect.Value {
return reflect.New(reflect.TypeOf(&Person{
Name: name,
Age: age,
}))
}
var Bool = reflect.New(reflect.TypeOf(BoolValue))
var Int = reflect.New(reflect.TypeOf(IntValue))
var Int8 = reflect.New(reflect.TypeOf(Int8Value))
var Int16 = reflect.New(reflect.TypeOf(Int16Value))
var Int32 = reflect.New(reflect.TypeOf(Int32Value))
var Int64 = reflect.New(reflect.TypeOf(Int64Value))
var Uint = reflect.New(reflect.TypeOf(UintValue))
var Uint8 = reflect.New(reflect.TypeOf(Uint8Value))
var Uint16 = reflect.New(reflect.TypeOf(Uint16Value))
var Uint32 = reflect.New(reflect.TypeOf(Uint32Value))
var Uint64 = reflect.New(reflect.TypeOf(Uint64Value))
var Uintptr = reflect.New(reflect.TypeOf(UintptrValue))
var Float32 = reflect.New(reflect.TypeOf(Float32Value))
var Float64 = reflect.New(reflect.TypeOf(Float64Value))
var Complex64 = reflect.New(reflect.TypeOf(Complex64Value))
var Complex128 = reflect.New(reflect.TypeOf(Complex128Value))
var String = reflect.New(reflect.TypeOf(StringValue))