From 6093c513f4e8abf9f50b8164ed4d34b54505166f Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Mon, 2 Nov 2020 21:38:36 +1100 Subject: [PATCH] Reimagined parser --- .../build/internal/backendjs/backendjs.go | 90 +++- .../build/internal/backendjs/comments.go | 21 + .../build/internal/backendjs/conversion.go | 71 --- .../build/internal/backendjs/index.template | 5 - .../internal/backendjs/package.d.template | 9 +- .../build/internal/backendjs/package.go | 82 +++ .../build/internal/backendjs/package.template | 17 - .../build/internal/backendjs/packages.go | 254 --------- .../build/internal/backendjs/parse.go | 501 ------------------ .../build/internal/backendjs/struct.go | 135 +++++ .../build/internal/backendjs/structs.go | 13 - .../build/internal/backendjs/values.go | 55 -- 12 files changed, 318 insertions(+), 935 deletions(-) create mode 100644 v2/pkg/commands/build/internal/backendjs/comments.go delete mode 100644 v2/pkg/commands/build/internal/backendjs/conversion.go delete mode 100644 v2/pkg/commands/build/internal/backendjs/index.template create mode 100644 v2/pkg/commands/build/internal/backendjs/package.go delete mode 100644 v2/pkg/commands/build/internal/backendjs/package.template delete mode 100644 v2/pkg/commands/build/internal/backendjs/packages.go delete mode 100644 v2/pkg/commands/build/internal/backendjs/parse.go create mode 100644 v2/pkg/commands/build/internal/backendjs/struct.go delete mode 100644 v2/pkg/commands/build/internal/backendjs/structs.go delete mode 100644 v2/pkg/commands/build/internal/backendjs/values.go diff --git a/v2/pkg/commands/build/internal/backendjs/backendjs.go b/v2/pkg/commands/build/internal/backendjs/backendjs.go index cfb0448e..a6b8adb6 100644 --- a/v2/pkg/commands/build/internal/backendjs/backendjs.go +++ b/v2/pkg/commands/build/internal/backendjs/backendjs.go @@ -1,45 +1,103 @@ -// Package backendjs deals with generating the `backend` Javascript module -// used by the frontend to interact with Go 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 attempt to create the backend javascript package -// used by the frontend to access methods and structs func GenerateBackendJSPackage() error { - // Create directory - err := createBackendJSDirectory() + dir, err := os.Getwd() if err != nil { - return errors.Wrap(err, "Error creating backend directory:") + return err + } + packages, err := parseProject(dir) + if err != nil { + return err } - // Generate Packages - err = generatePackages() + err = generateModule(packages) + + return err +} + +func parseProject(projectPath string) ([]*Package, 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, "Error generating method wrappers:") + return nil, errors.Wrap(err, "Problem loading packages") + } + if packages.PrintErrors(pkgs) > 0 { + return nil, errors.Wrap(err, "Errors during parsing") } + var result []*Package + + for _, pkg := range pkgs { + parsedPackage, err := parsePackage(pkg, fset) + if err != nil { + return nil, err + } + result = append(result, parsedPackage) + } + + return result, nil +} + +func generateModule(pkgs []*Package) error { + + moduleDir, err := createBackendJSDirectory() + if err != nil { + return err + } + + for _, pkg := range pkgs { + + // 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() error { +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") + return "", errors.Wrap(err, "Error creating backend js directory") } - // Only create the directory if it doesn't exit - if !fs.DirExists(dir) { - return fs.Mkdir(dir) + // Remove directory if it exists - REGENERATION! + err = os.RemoveAll(dir) + if err != nil { + return "", errors.Wrap(err, "Error removing module directory") } - return nil + // Make the directory + err = fs.Mkdir(dir) + + return dir, err } diff --git a/v2/pkg/commands/build/internal/backendjs/comments.go b/v2/pkg/commands/build/internal/backendjs/comments.go new file mode 100644 index 00000000..21dda1e5 --- /dev/null +++ b/v2/pkg/commands/build/internal/backendjs/comments.go @@ -0,0 +1,21 @@ +package backendjs + +import ( + "go/ast" + "strings" +) + +func 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 +} diff --git a/v2/pkg/commands/build/internal/backendjs/conversion.go b/v2/pkg/commands/build/internal/backendjs/conversion.go deleted file mode 100644 index c24c4c1e..00000000 --- a/v2/pkg/commands/build/internal/backendjs/conversion.go +++ /dev/null @@ -1,71 +0,0 @@ -package backendjs - -// JSType represents a javascript type -type JSType string - -const ( - // JsString is a JS string - JsString JSType = "string" - // JsBoolean is a JS bool - JsBoolean = "boolean" - // JsInt is a JS number - JsInt = "number" - // JsFloat is a JS number - JsFloat = "number" - // JsArray is a JS array - JsArray = "Array" - // JsObject is a JS object - JsObject = "Object" - // JsUnsupported represents a type that cannot be converted - JsUnsupported = "*" -) - -func goTypeToJS(input string) JSType { - switch input { - case "string": - return JsString - case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": - return JsInt - case "float32", "float64": - return JsFloat - case "bool": - return JsBoolean - // 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 *ParsedParameter) string { - var result string - switch input.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 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 - } - - if input.IsArray { - result = "Array<" + result + ">" - } - - return result -} diff --git a/v2/pkg/commands/build/internal/backendjs/index.template b/v2/pkg/commands/build/internal/backendjs/index.template deleted file mode 100644 index 197a5b5a..00000000 --- a/v2/pkg/commands/build/internal/backendjs/index.template +++ /dev/null @@ -1,5 +0,0 @@ -// index.js -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT -{{range .}} -export { default as {{.Name}} } from './{{.Name}}';{{end}} diff --git a/v2/pkg/commands/build/internal/backendjs/package.d.template b/v2/pkg/commands/build/internal/backendjs/package.d.template index 0a4db6da..b5b35df8 100644 --- a/v2/pkg/commands/build/internal/backendjs/package.d.template +++ b/v2/pkg/commands/build/internal/backendjs/package.d.template @@ -1,9 +1,12 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -declare module {{.Struct.Name}} { - {{range .Struct.Methods}} +declare module {{.Name}} { + {{range .Structs}} {{if .Comments }}{{range .Comments}}// {{ . }}{{end}}{{end}} - function {{.Name}}({{.InputsAsTSText}}): Promise<{{.OutputsAsTSText}}>; + interface {{.Name}} { {{range .Fields}}{{if .Comments }} + {{range .Comments}}//{{ . }}{{end}}{{end}} + {{.Name}}: {{.TypeAsTSType}}; {{end}} + } {{end}} } \ No newline at end of file diff --git a/v2/pkg/commands/build/internal/backendjs/package.go b/v2/pkg/commands/build/internal/backendjs/package.go new file mode 100644 index 00000000..d53e2570 --- /dev/null +++ b/v2/pkg/commands/build/internal/backendjs/package.go @@ -0,0 +1,82 @@ +package backendjs + +import ( + "bytes" + "go/ast" + "go/token" + "html/template" + "io/ioutil" + "path/filepath" + + "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 +} + +func parsePackage(pkg *packages.Package, fset *token.FileSet) (*Package, error) { + result := &Package{ + Name: pkg.Name, + Structs: make(map[string]*Struct), + } + for _, fileAst := range pkg.Syntax { + var parseError error + ast.Inspect(fileAst, func(n ast.Node) bool { + if typeDecl, ok := n.(*ast.TypeSpec); ok { + if structType, ok := typeDecl.Type.(*ast.StructType); ok { + + // spew.Dump(structType) + structName := typeDecl.Name.Name + // findInFields(structTy.Fields, n, pkg.TypesInfo, fset) + structDef, err := parseStruct(structType, structName) + if err != nil { + parseError = err + return false + } + + // Parse comments + structDef.Comments = parseComments(typeDecl.Doc) + + result.Structs[structName] = structDef + } + } + 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") + } + return nil +} diff --git a/v2/pkg/commands/build/internal/backendjs/package.template b/v2/pkg/commands/build/internal/backendjs/package.template deleted file mode 100644 index efa55e18..00000000 --- a/v2/pkg/commands/build/internal/backendjs/package.template +++ /dev/null @@ -1,17 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -{{range .Struct.Methods }} -/**{{if .Comments }} -{{range .Comments}} * {{ . }}{{end}} - *{{end}} - * @function {{.Name}} -{{range .Inputs}} * @param {{"{"}}{{.JSType}}{{"}"}} {{.Name}} -{{end}} * - * @returns {Promise<{{.OutputsAsTSText}}>} - */ -export function {{.Name}}({{.InputsAsJSText}}) { - return window.backend.{{$.PackageName}}.{{$.Struct.Name}}.{{.Name}}({{.InputsAsJSText}}); -} -{{end}} - diff --git a/v2/pkg/commands/build/internal/backendjs/packages.go b/v2/pkg/commands/build/internal/backendjs/packages.go deleted file mode 100644 index 77febd94..00000000 --- a/v2/pkg/commands/build/internal/backendjs/packages.go +++ /dev/null @@ -1,254 +0,0 @@ -package backendjs - -import ( - "bytes" - "io/ioutil" - "os" - "path/filepath" - "text/template" - - "github.com/pkg/errors" - "github.com/wailsapp/wails/v2/internal/fs" -) - -// Package defines a single package that contains bound structs -type Package struct { - Name string - Comments []string - Structs []*ParsedStruct -} - -func generatePackages() error { - - packages, err := parsePackages() - if err != nil { - return errors.Wrap(err, "Error parsing struct packages:") - } - - err = generateJSFiles(packages) - if err != nil { - return errors.Wrap(err, "Error generating struct js file:") - } - - return nil -} - -func parsePackages() ([]*Package, error) { - - 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", - // 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 -} - -func generateJSFiles(packages []*Package) error { - - err := generateIndexJS(packages) - if err != nil { - return errors.Wrap(err, "Error generating index.js file") - } - - err = generatePackageFiles(packages) - if err != nil { - return errors.Wrap(err, "Error generating packages") - } - - return nil -} - -func generateIndexJS(packages []*Package) error { - - // Get path to local file - templateFile := fs.RelativePath("./index.template") - - // Load template - javascriptTemplateData := fs.MustLoadString(templateFile) - packagesTemplate, err := template.New("index").Parse(javascriptTemplateData) - if err != nil { - return errors.Wrap(err, "Error creating template") - } - - // Execute template - var buffer bytes.Buffer - err = packagesTemplate.Execute(&buffer, packages) - if err != nil { - return errors.Wrap(err, "Error generating code") - } - - // Calculate target filename - indexJS, err := fs.RelativeToCwd("./frontend/backend/index.js") - if err != nil { - return errors.Wrap(err, "Error calculating index js path") - } - - err = ioutil.WriteFile(indexJS, buffer.Bytes(), 0755) - if err != nil { - return errors.Wrap(err, "Error writing backend package index.js file") - } - - return nil -} - -func generatePackageFiles(packages []*Package) error { - - // 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") - } - - // 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") - } - - // Iterate over each package - for _, thisPackage := range packages { - err := generatePackage(thisPackage, typescriptTemplate, javascriptTemplate) - if err != nil { - return err - } - } - - 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, data) - if err != nil { - return errors.Wrap(err, "Error generating code") - } - - // Save javascript file - err = ioutil.WriteFile(filepath.Join(packageDir, strct.Name+".js"), buffer.Bytes(), 0755) - if err != nil { - return errors.Wrap(err, "Error writing backend package file") - } - - // Clear buffer - buffer.Reset() - - // Execute typescript template - err = typescriptTemplate.Execute(&buffer, data) - if err != nil { - return errors.Wrap(err, "Error generating code") - } - - // Save typescript file - 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") - } - } - - return nil -} diff --git a/v2/pkg/commands/build/internal/backendjs/parse.go b/v2/pkg/commands/build/internal/backendjs/parse.go deleted file mode 100644 index f9d4d1a4..00000000 --- a/v2/pkg/commands/build/internal/backendjs/parse.go +++ /dev/null @@ -1,501 +0,0 @@ -package backendjs - -import ( - "fmt" - "go/ast" - "os" - "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 - boundStructs map[string]*ParsedStruct - 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 - IsArray bool -} - -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)) - 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 { - result = append(result, goTypeToTS(output)) - } - 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), - boundStructs: make(map[string]*ParsedStruct), - } -} - -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 - - p := NewParser() - - // Iterate the packages - for _, pkg := range pkgs { - - 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) - - } - - // Resolve links between data - err = p.Resolve() - if err != nil { - return nil, err - } - - 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 { - // Check for basic types - t, ok := outputField.Type.(*ast.Ident) - if !ok { - // Check for arrays - a, ok := outputField.Type.(*ast.ArrayType) - if ok { - // spew.Dump(a) - ident := a.Elt.(*ast.Ident) - if len(outputField.Names) == 0 { - structMethod.Returns = append(structMethod.Returns, &ParsedParameter{Type: ident.Name, IsArray: true}) - } else { - for _, name := range outputField.Names { - structMethod.Returns = append(structMethod.Returns, &ParsedParameter{Name: name.Name, Type: ident.Name, IsArray: true}) - } - } - - } - - } else { - 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 -} - -func (p *Parser) Resolve() error { - // Resolve bound Methods - for _, method := range p.boundMethods { - s, ok := p.structPointerFunctionDecls[method] - if !ok { - s, ok = p.structFunctionDecls[method] - if !ok { - return fmt.Errorf("bind statement using " + method + " but cannot find " + method + " declaration") - } else { - return fmt.Errorf("cannot bind struct using method `" + method + "` because it returns a struct (" + s + "). Return a pointer to " + s + " instead.") - } - } - structDefinition := p.structCache[s] - if structDefinition == nil { - return fmt.Errorf("Fatal: Bind statement using `" + method + "` but cannot find struct " + s + " definition") - } - p.boundStructs[s] = structDefinition - } - - // Resolve bound vars - for _, structLiteral := range p.boundStructPointerLiterals { - s, ok := p.structCache[structLiteral] - if !ok { - return fmt.Errorf("bind statement using " + structLiteral + " but cannot find " + structLiteral + " declaration") - } - p.boundStructs[structLiteral] = s - } - - var err error - - // Resolve bound variables - p.boundVariables.Each(func(variable string) { - v, ok := p.variableStructDecls[variable] - if !ok { - method, ok := p.variableFunctionDecls[variable] - if !ok { - if err == nil { - err = fmt.Errorf("bind statement using variable `" + variable + "` which does not resolve to a struct pointer") - } - } - - // Resolve function name - v, ok = p.structPointerFunctionDecls[method] - if !ok { - v, ok = p.structFunctionDecls[method] - if !ok { - if err == nil { - err = fmt.Errorf("bind statement using " + method + " but cannot find " + method + " declaration") - } - } else { - if err == nil { - err = fmt.Errorf("cannot bind variable `" + variable + "` because it resolves to a struct (" + v + "). Return a pointer to " + v + " instead.") - } - } - } - } - - s, ok := p.structCache[v] - if !ok { - println("Fatal: Bind statement using variable `" + variable + "` which resolves to a `" + v + "` but cannot find its declaration") - os.Exit(1) - } - p.boundStructs[v] = s - }) - - // Return first error when resolving bound variables - if err != nil { - return err - } - - // Check for struct literals - if p.boundStructLiterals.Length() > 0 { - return fmt.Errorf("cannot bind structs using struct literals. Create a pointer to the struct instead: %s", p.boundStructLiterals.Join(", ")) - } - return nil -} diff --git a/v2/pkg/commands/build/internal/backendjs/struct.go b/v2/pkg/commands/build/internal/backendjs/struct.go new file mode 100644 index 00000000..6878b215 --- /dev/null +++ b/v2/pkg/commands/build/internal/backendjs/struct.go @@ -0,0 +1,135 @@ +package backendjs + +import ( + "go/ast" + "os" + + "github.com/davecgh/go-spew/spew" +) + +// Struct defines a parsed struct +type Struct struct { + Name string + Comments []string + Fields []*Field +} + +type StructName struct { + Name string + Package string +} + +// Field defines a parsed struct field +type Field struct { + Name string + Type string + Struct *StructName + Comments []string +} + +// 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 parseStruct(structType *ast.StructType, name string) (*Struct, error) { + result := &Struct{Name: name} + + for _, field := range structType.Fields.List { + result.Fields = append(result.Fields, parseField(field)...) + } + return result, nil +} + +func parseField(field *ast.Field) []*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: + pkg := "" + name := "" + // Determine the FQN + switch x := t.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) + } + fieldType = "struct" + structName = &StructName{ + Name: name, + Package: pkg, + } + + default: + FieldNotSupported(t) + } + + // Loop over names + for _, name := range field.Names { + + // Create a field per name + thisField := &Field{ + Comments: 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) +} diff --git a/v2/pkg/commands/build/internal/backendjs/structs.go b/v2/pkg/commands/build/internal/backendjs/structs.go deleted file mode 100644 index 82168b20..00000000 --- a/v2/pkg/commands/build/internal/backendjs/structs.go +++ /dev/null @@ -1,13 +0,0 @@ -package backendjs - -import "reflect" - -type Struct struct { - Name string - Fields []*Field -} - -type Field struct { - Name string - Type reflect.Value -} diff --git a/v2/pkg/commands/build/internal/backendjs/values.go b/v2/pkg/commands/build/internal/backendjs/values.go deleted file mode 100644 index c5096bf9..00000000 --- a/v2/pkg/commands/build/internal/backendjs/values.go +++ /dev/null @@ -1,55 +0,0 @@ -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))