diff --git a/cmd/wails/3_generate.go b/cmd/wails/3_generate.go new file mode 100644 index 00000000..cd65faaa --- /dev/null +++ b/cmd/wails/3_generate.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + + "github.com/leaanthony/spinner" + "github.com/wailsapp/wails/lib/stores" +) + +func init() { + + var generateStores = false + + buildSpinner := spinner.NewSpinner() + buildSpinner.SetSpinSpeed(50) + + commandDescription := `This command will generate components` + generateCommand := app.Command("generate", "Generate components"). + LongDescription(commandDescription). + BoolFlag("s", "Generate Stores", &generateStores) + + generateCommand.Action(func() error { + + logger.PrintSmallBanner("Generating") + fmt.Println() + + if generateStores { + err := stores.Generate() + if err != nil { + return err + } + } + + return nil + + }) +} diff --git a/go.mod b/go.mod index 852405bd..5497178c 100644 --- a/go.mod +++ b/go.mod @@ -21,9 +21,9 @@ require ( github.com/stretchr/testify v1.3.0 // indirect github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba golang.org/x/image v0.0.0-20200430140353-33d19683fad8 - golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd golang.org/x/text v0.3.0 + golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82 gopkg.in/AlecAivazis/survey.v1 v1.8.4 gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 ) diff --git a/go.sum b/go.sum index 625063e9..daa7d4b5 100644 --- a/go.sum +++ b/go.sum @@ -64,15 +64,22 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba h1:2DHfQOxcpWdGf5q5IzCUFPNvRX9Icf+09RvQK2VnJq0= github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba/go.mod h1:iLnlXG2Pakcii2CU0cbY07DRCSvpWNa7nFxtevhOChk= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -83,6 +90,13 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20u golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82 h1:shxDsb9Dz27xzk3A0DxP0JuJnZMpKrdg8+E14eiUAX4= +golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/AlecAivazis/survey.v1 v1.8.4 h1:10xXXN3wgIhPheb5NI58zFgZv32Ana7P3Tl4shW+0Qc= gopkg.in/AlecAivazis/survey.v1 v1.8.4/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/lib/stores/generate.go b/lib/stores/generate.go new file mode 100644 index 00000000..8404a757 --- /dev/null +++ b/lib/stores/generate.go @@ -0,0 +1,14 @@ +// Package stores +package stores + +import "os" + +// Generate stores that are found in the project +func Generate() error { + cwd, err := os.Getwd() + if err != nil { + return err + } + ParseProject(cwd) + return nil +} diff --git a/lib/stores/parse.go b/lib/stores/parse.go new file mode 100644 index 00000000..180182fe --- /dev/null +++ b/lib/stores/parse.go @@ -0,0 +1,442 @@ +package stores + +import ( + "fmt" + "go/ast" + "os" + "strings" + + "github.com/leaanthony/slicer" + "golang.org/x/tools/go/packages" +) + +var internalMethods = slicer.String([]string{"WailsInit", "Wails Shutdown"}) + +var structCache = make(map[string]*ParsedStruct) +var boundStructs = make(map[string]*ParsedStruct) +var boundMethods = []string{} +var boundStructPointerLiterals = []string{} +var boundStructLiterals = slicer.StringSlicer{} +var boundVariables = slicer.StringSlicer{} +var app = "" +var structPointerFunctionDecls = make(map[string]string) +var structFunctionDecls = make(map[string]string) +var variableStructDecls = make(map[string]string) +var variableFunctionDecls = make(map[string]string) + +type Parameter struct { + Name string + Type string +} + +type ParsedMethod struct { + Struct string + Name string + Comments []string + Inputs []*Parameter + Returns []*Parameter +} + +type ParsedStruct struct { + Name string + Methods []*ParsedMethod +} + +type BoundStructs []*ParsedStruct + +func ParseProject(projectPath string) { + + cfg := &packages.Config{Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedTypesInfo} + pkgs, err := packages.Load(cfg, projectPath) + if err != nil { + fmt.Fprintf(os.Stderr, "load: %v\n", err) + os.Exit(1) + } + if packages.PrintErrors(pkgs) > 0 { + os.Exit(1) + } + + // Iterate the packages + for _, pkg := range pkgs { + + // Iterate the files + for _, file := range pkg.Syntax { + + var wailsPkgVar = "" + + 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"` { + 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 == app && 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() ) + boundMethods = append(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{} ) + boundStructPointerLiterals = append(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 { + 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 { + 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 == 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 + app = 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 + 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 { + 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 internalMethods.Contains(x.Name.Name) { + continue + } + // If we haven't already found this struct, + // Create a placeholder in the cache + parsedStruct := structCache[i.Name] + if parsedStruct == nil { + structCache[i.Name] = &ParsedStruct{ + Name: i.Name, + } + parsedStruct = 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 + if x.Type.Params != nil { + 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, &Parameter{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, &Parameter{Type: t.Name}) + } else { + for _, name := range outputField.Names { + structMethod.Returns = append(structMethod.Returns, &Parameter{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) + 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) + structFunctionDecls[functionName] = t.Name + } + } + } + } + } + } + return true + }) + // spew.Dump(file) + } + } + + /***** Update bound structs ******/ + + // Resolve bound Methods + for _, method := range boundMethods { + s, ok := structPointerFunctionDecls[method] + if !ok { + s, ok = structFunctionDecls[method] + if !ok { + println("Fatal: Bind statement using", method, "but cannot find", method, "declaration") + } else { + println("Fatal: Cannot bind struct using method `" + method + "` because it returns a struct (" + s + "). Return a pointer to " + s + " instead.") + } + os.Exit(1) + } + structDefinition := structCache[s] + if structDefinition == nil { + println("Fatal: Bind statement using `"+method+"` but cannot find struct", s, "definition") + os.Exit(1) + } + boundStructs[s] = structDefinition + } + + // Resolve bound vars + for _, structLiteral := range boundStructPointerLiterals { + s, ok := structCache[structLiteral] + if !ok { + println("Fatal: Bind statement using", structLiteral, "but cannot find", structLiteral, "declaration") + os.Exit(1) + } + boundStructs[structLiteral] = s + } + + // Resolve bound variables + boundVariables.Each(func(variable string) { + v, ok := variableStructDecls[variable] + if !ok { + method, ok := variableFunctionDecls[variable] + if !ok { + println("Fatal: Bind statement using variable `" + variable + "` which does not resolve to a struct pointer") + os.Exit(1) + } + + // Resolve function name + v, ok = structPointerFunctionDecls[method] + if !ok { + v, ok = structFunctionDecls[method] + if !ok { + println("Fatal: Bind statement using", method, "but cannot find", method, "declaration") + } else { + println("Fatal: Cannot bind variable `" + variable + "` because it resolves to a struct (" + v + "). Return a pointer to " + v + " instead.") + } + os.Exit(1) + } + + } + + s, ok := structCache[v] + if !ok { + println("Fatal: Bind statement using variable `" + variable + "` which resolves to a `" + v + "` but cannot find its declaration") + os.Exit(1) + } + boundStructs[v] = s + + }) + + // Check for struct literals + boundStructLiterals.Each(func(structName string) { + println("Fatal: Cannot bind struct using struct literal `" + structName + "{}`. Create a pointer to " + structName + " instead.") + os.Exit(1) + }) + + // Check for bound variables + // boundVariables.Each(func(varName string) { + // println("Fatal: Cannot bind struct using struct literal `" + structName + "{}`. Create a pointer to " + structName + " instead.") + // }) + + // spew.Dump(boundStructs) + // os.Exit(0) + + // } + // Inspect the AST and print all identifiers and literals. + + println("export {") + + noOfStructs := len(boundStructs) + structCount := 0 + for _, s := range boundStructs { + structCount++ + println() + println(" " + s.Name + ": {") + println() + noOfMethods := len(s.Methods) + for methodCount, m := range s.Methods { + println(" /****************") + for _, comment := range m.Comments { + println(" *", comment) + } + if len(m.Comments) > 0 { + println(" *") + } + inputNames := "" + for _, input := range m.Inputs { + println(" * @param {"+input.Type+"}", input.Name) + inputNames += input.Name + ", " + } + print(" * @return Promise<") + for _, output := range m.Returns { + print(output.Type + "|") + } + println("Error>") + println(" *") + println(" ***/") + if len(inputNames) > 2 { + inputNames = inputNames[:len(inputNames)-2] + } + println(" ", m.Name+": function("+inputNames+") {") + println(" return window.backend." + s.Name + "." + m.Name + "(" + inputNames + ");") + print(" }") + if methodCount < noOfMethods-1 { + print(",") + } + println() + println() + } + print(" }") + if structCount < noOfStructs-1 { + print(",") + } + println() + } + println() + println("}") + println() + +}