diff --git a/changelog.md b/changelog.md index 35588349be..c9c7af18fd 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ ### Features - [#3977](https://github.com/ignite/cli/pull/3977) Add `chain lint` command to lint the chain's codebase using `golangci-lint` +- [#3770](https://github.com/ignite/cli/pull/3770) Add `scaffold configs` and `scaffold params` commands ### Changes diff --git a/ignite/cmd/chain_lint.go b/ignite/cmd/chain_lint.go index f85eb52037..5d23e05087 100644 --- a/ignite/cmd/chain_lint.go +++ b/ignite/cmd/chain_lint.go @@ -1,9 +1,10 @@ package ignitecmd import ( + "github.com/spf13/cobra" + "github.com/ignite/cli/v28/ignite/pkg/cliui" "github.com/ignite/cli/v28/ignite/services/chain" - "github.com/spf13/cobra" ) // NewChainLint returns a lint command to build a blockchain app. diff --git a/ignite/cmd/scaffold.go b/ignite/cmd/scaffold.go index 61be7bd836..703a8056a4 100644 --- a/ignite/cmd/scaffold.go +++ b/ignite/cmd/scaffold.go @@ -123,6 +123,8 @@ with an "--ibc" flag. Note that the default module is not IBC-enabled. NewScaffoldMap(), NewScaffoldSingle(), NewScaffoldType(), + NewScaffoldParams(), + NewScaffoldConfigs(), NewScaffoldMessage(), NewScaffoldQuery(), NewScaffoldPacket(), diff --git a/ignite/cmd/scaffold_chain.go b/ignite/cmd/scaffold_chain.go index 77c8351ccf..aa408bf197 100644 --- a/ignite/cmd/scaffold_chain.go +++ b/ignite/cmd/scaffold_chain.go @@ -80,6 +80,7 @@ about Cosmos SDK on https://docs.cosmos.network c.Flags().StringP(flagPath, "p", "", "create a project in a specific path") c.Flags().Bool(flagNoDefaultModule, false, "create a project without a default module") c.Flags().StringSlice(flagParams, []string{}, "add default module parameters") + c.Flags().StringSlice(flagModuleConfigs, []string{}, "add module configs") c.Flags().Bool(flagSkipGit, false, "skip Git repository initialization") c.Flags().Bool(flagMinimal, false, "create a minimal blockchain (with the minimum required Cosmos SDK modules)") @@ -98,10 +99,15 @@ func scaffoldChainHandler(cmd *cobra.Command, args []string) error { skipGit, _ = cmd.Flags().GetBool(flagSkipGit) minimal, _ = cmd.Flags().GetBool(flagMinimal) params, _ = cmd.Flags().GetStringSlice(flagParams) + moduleConfigs, _ = cmd.Flags().GetStringSlice(flagModuleConfigs) ) - if noDefaultModule && len(params) > 0 { - return errors.New("params flag is only supported if the default module is enabled") + if noDefaultModule { + if len(params) > 0 { + return errors.New("params flag is only supported if the default module is enabled") + } else if len(moduleConfigs) > 0 { + return errors.New("module configs flag is only supported if the default module is enabled") + } } cacheStorage, err := newCache(cmd) @@ -120,6 +126,7 @@ func scaffoldChainHandler(cmd *cobra.Command, args []string) error { skipGit, minimal, params, + moduleConfigs, ) if err != nil { return err diff --git a/ignite/cmd/scaffold_configs.go b/ignite/cmd/scaffold_configs.go new file mode 100644 index 0000000000..9a7bf29d1d --- /dev/null +++ b/ignite/cmd/scaffold_configs.go @@ -0,0 +1,79 @@ +package ignitecmd + +import ( + "strings" + + "github.com/spf13/cobra" + + "github.com/ignite/cli/v28/ignite/pkg/cliui" + "github.com/ignite/cli/v28/ignite/pkg/placeholder" + "github.com/ignite/cli/v28/ignite/services/scaffolder" +) + +// NewScaffoldConfigs returns the command to scaffold a Cosmos SDK configs into a module. +func NewScaffoldConfigs() *cobra.Command { + c := &cobra.Command{ + Use: "configs [configs]...", + Short: "Configs for a custom Cosmos SDK module", + Long: `Scaffold a new config for a Cosmos SDK module. + +A Cosmos SDK module can have configurations. An example of a config is "address prefix" of the +"auth" module. A config can be scaffolded into a module using the "--module-configs" into +the scaffold module command or using the "scaffold configs" command. By default +configs are of type "string", but you can specify a type for each config. For example: + + ignite scaffold configs foo baz:uint bar:bool + +Refer to Cosmos SDK documentation to learn more about modules, dependencies and +configs. +`, + Args: cobra.MinimumNArgs(1), + PreRunE: migrationPreRunHandler, + RunE: scaffoldConfigsHandler, + } + + flagSetPath(c) + flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) + + c.Flags().String(flagModule, "", "module to add the query into (default: app's main module)") + + return c +} + +func scaffoldConfigsHandler(cmd *cobra.Command, args []string) error { + var ( + configs = args[0:] + appPath = flagGetPath(cmd) + moduleName = flagGetModule(cmd) + ) + + session := cliui.New(cliui.StartSpinnerWithText(statusScaffolding)) + defer session.End() + + cacheStorage, err := newCache(cmd) + if err != nil { + return err + } + + sc, err := scaffolder.New(appPath) + if err != nil { + return err + } + + sm, err := sc.CreateConfigs(cmd.Context(), cacheStorage, placeholder.New(), moduleName, configs...) + if err != nil { + return err + } + + modificationsStr, err := sourceModificationToString(sm) + if err != nil { + return err + } + + session.Println(modificationsStr) + session.Printf("\nšŸŽ‰ New configs added to the module:\n\n- %s\n\n", strings.Join(configs, "\n- ")) + + return nil +} diff --git a/ignite/cmd/scaffold_module.go b/ignite/cmd/scaffold_module.go index 15bc682741..05b8a2d2c9 100644 --- a/ignite/cmd/scaffold_module.go +++ b/ignite/cmd/scaffold_module.go @@ -25,6 +25,7 @@ const ( flagDep = "dep" flagIBC = "ibc" flagParams = "params" + flagModuleConfigs = "module-configs" flagIBCOrdering = "ordering" flagRequireRegistration = "require-registration" @@ -115,6 +116,7 @@ params. c.Flags().String(flagIBCOrdering, "none", "channel ordering of the IBC module [none|ordered|unordered]") c.Flags().Bool(flagRequireRegistration, false, "fail if module can't be registered") c.Flags().StringSlice(flagParams, []string{}, "add module parameters") + c.Flags().StringSlice(flagModuleConfigs, []string{}, "add module configs") return c } @@ -133,6 +135,11 @@ func scaffoldModuleHandler(cmd *cobra.Command, args []string) error { requireRegistration, _ := cmd.Flags().GetBool(flagRequireRegistration) params, _ := cmd.Flags().GetStringSlice(flagParams) + moduleConfigs, err := cmd.Flags().GetStringSlice(flagModuleConfigs) + if err != nil { + return err + } + cacheStorage, err := newCache(cmd) if err != nil { return err @@ -140,6 +147,7 @@ func scaffoldModuleHandler(cmd *cobra.Command, args []string) error { options := []scaffolder.ModuleCreationOption{ scaffolder.WithParams(params), + scaffolder.WithModuleConfigs(moduleConfigs), } // Check if the module must be an IBC module diff --git a/ignite/cmd/scaffold_params.go b/ignite/cmd/scaffold_params.go new file mode 100644 index 0000000000..57261ca58d --- /dev/null +++ b/ignite/cmd/scaffold_params.go @@ -0,0 +1,81 @@ +package ignitecmd + +import ( + "strings" + + "github.com/spf13/cobra" + + "github.com/ignite/cli/v28/ignite/pkg/cliui" + "github.com/ignite/cli/v28/ignite/pkg/placeholder" + "github.com/ignite/cli/v28/ignite/services/scaffolder" +) + +// NewScaffoldParams returns the command to scaffold a Cosmos SDK parameters into a module. +func NewScaffoldParams() *cobra.Command { + c := &cobra.Command{ + Use: "params [param]...", + Short: "Parameters for a custom Cosmos SDK module", + Long: `Scaffold a new parameter for a Cosmos SDK module. + +A Cosmos SDK module can have parameters (or "params"). Params are values that +can be set at the genesis of the blockchain and can be modified while the +blockchain is running. An example of a param is "Inflation rate change" of the +"mint" module. A params can be scaffolded into a module using the "--params" into +the scaffold module command or using the "scaffold params" command. By default +params are of type "string", but you can specify a type for each param. For example: + + ignite scaffold params foo baz:uint bar:bool + +Refer to Cosmos SDK documentation to learn more about modules, dependencies and +params. +`, + Args: cobra.MinimumNArgs(1), + PreRunE: migrationPreRunHandler, + RunE: scaffoldParamsHandler, + } + + flagSetPath(c) + flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) + + c.Flags().String(flagModule, "", "module to add the query into. Default: app's main module") + + return c +} + +func scaffoldParamsHandler(cmd *cobra.Command, args []string) error { + var ( + params = args[0:] + appPath = flagGetPath(cmd) + moduleName = flagGetModule(cmd) + ) + + session := cliui.New(cliui.StartSpinnerWithText(statusScaffolding)) + defer session.End() + + cacheStorage, err := newCache(cmd) + if err != nil { + return err + } + + sc, err := scaffolder.New(appPath) + if err != nil { + return err + } + + sm, err := sc.CreateParams(cmd.Context(), cacheStorage, placeholder.New(), moduleName, params...) + if err != nil { + return err + } + + modificationsStr, err := sourceModificationToString(sm) + if err != nil { + return err + } + + session.Println(modificationsStr) + session.Printf("\nšŸŽ‰ New parameters added to the module:\n\n- %s\n\n", strings.Join(params, "\n- ")) + + return nil +} diff --git a/ignite/pkg/goanalysis/goanalysis.go b/ignite/pkg/goanalysis/goanalysis.go index 36f16a472f..18568788bb 100644 --- a/ignite/pkg/goanalysis/goanalysis.go +++ b/ignite/pkg/goanalysis/goanalysis.go @@ -338,3 +338,59 @@ func ReplaceCode(pkgPath, oldFunctionName, newFunction string) (err error) { } return nil } + +// HasAnyStructFieldsInPkg finds the struct within a package folder and checks +// if any of the fields are defined in the struct. +func HasAnyStructFieldsInPkg(pkgPath, structName string, fields []string) (bool, error) { + absPath, err := filepath.Abs(pkgPath) + if err != nil { + return false, err + } + fileSet := token.NewFileSet() + all, err := parser.ParseDir(fileSet, absPath, nil, parser.ParseComments) + if err != nil { + return false, err + } + + fieldsNames := make(map[string]struct{}) + for _, field := range fields { + fieldsNames[strings.ToLower(field)] = struct{}{} + } + + exist := false + for _, pkg := range all { + for _, f := range pkg.Files { + ast.Inspect(f, func(x ast.Node) bool { + typeSpec, ok := x.(*ast.TypeSpec) + if !ok { + return true + } + + if _, ok := typeSpec.Type.(*ast.StructType); !ok || + typeSpec.Name.Name != structName || + typeSpec.Type == nil { + return true + } + + // Check if the struct has fields. + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + return true + } + + // Iterate through the fields of the struct. + for _, field := range structType.Fields.List { + for _, fieldName := range field.Names { + if _, ok := fieldsNames[strings.ToLower(fieldName.Name)]; !ok { + continue + } + exist = true + return false + } + } + return true + }) + } + } + return exist, nil +} diff --git a/ignite/pkg/goanalysis/goanalysis_test.go b/ignite/pkg/goanalysis/goanalysis_test.go index 4d1297e3b0..8b7f5b2768 100644 --- a/ignite/pkg/goanalysis/goanalysis_test.go +++ b/ignite/pkg/goanalysis/goanalysis_test.go @@ -145,84 +145,84 @@ func createMainFiles(tmpDir string, mainFiles []string) (pathsWithMain []string, func TestFuncVarExists(t *testing.T) { tests := []struct { name string - testfile string + testFile string goImport string methodSignature string want bool }{ { name: "test a declaration inside a method success", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "Background", goImport: "context", want: true, }, { name: "test global declaration success", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "Join", goImport: "path/filepath", want: true, }, { name: "test a declaration inside an if and inside a method success", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "SplitList", goImport: "path/filepath", want: true, }, { name: "test global variable success assign", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "New", goImport: "errors", want: true, }, { name: "test invalid import", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "Join", goImport: "errors", want: false, }, { name: "test invalid case sensitive assign", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "join", goImport: "context", want: false, }, { name: "test invalid struct assign", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "fooStruct", goImport: "context", want: false, }, { name: "test invalid method signature", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "fooMethod", goImport: "context", want: false, }, { name: "test not found name", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "Invalid", goImport: "context", want: false, }, { name: "test invalid assign with wrong", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "invalid.New", goImport: "context", want: false, }, { name: "test invalid assign with wrong", - testfile: "testdata/varexist", + testFile: "testdata/varexist", methodSignature: "SplitList", goImport: "path/filepath", want: true, @@ -230,7 +230,7 @@ func TestFuncVarExists(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - appPkg, _, err := xast.ParseFile(tt.testfile) + appPkg, _, err := xast.ParseFile(tt.testFile) require.NoError(t, err) got := goanalysis.FuncVarExists(appPkg, tt.goImport, tt.methodSignature) @@ -577,3 +577,95 @@ func NewMethod1() { }) } } + +func TestHasStructFieldsInPkg(t *testing.T) { + tests := []struct { + name string + path string + structName string + fields []string + err error + want bool + }{ + { + name: "test a value with an empty struct", + path: "testdata", + structName: "emptyStruct", + fields: []string{"name"}, + want: false, + }, + { + name: "test no value with an empty struct", + path: "testdata", + structName: "emptyStruct", + fields: []string{""}, + want: false, + }, + { + name: "test a valid field into single field struct", + path: "testdata", + structName: "fooStruct", + fields: []string{"name"}, + want: true, + }, + { + name: "test a not valid field into single field struct", + path: "testdata", + structName: "fooStruct", + fields: []string{"baz"}, + want: false, + }, + { + name: "test a not valid field into struct", + path: "testdata", + structName: "bazStruct", + fields: []string{"baz"}, + want: false, + }, + { + name: "test a valid field into struct", + path: "testdata", + structName: "bazStruct", + fields: []string{"name"}, + want: true, + }, + { + name: "test two valid fields into struct", + path: "testdata", + structName: "bazStruct", + fields: []string{"name", "title"}, + want: true, + }, + { + name: "test a valid and a not valid fields into struct", + path: "testdata", + structName: "bazStruct", + fields: []string{"foo", "title"}, + want: true, + }, + { + name: "test three not valid fields into struct", + path: "testdata", + structName: "bazStruct", + fields: []string{"foo", "baz", "bla"}, + want: false, + }, + { + name: "invalid path", + path: "invalid_path", + err: os.ErrNotExist, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := goanalysis.HasAnyStructFieldsInPkg(tt.path, tt.structName, tt.fields) + if tt.err != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.err) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/ignite/pkg/goanalysis/testdata/fieldexist.go b/ignite/pkg/goanalysis/testdata/fieldexist.go new file mode 100644 index 0000000000..34d9e6c6bc --- /dev/null +++ b/ignite/pkg/goanalysis/testdata/fieldexist.go @@ -0,0 +1,13 @@ +package goanalysis + +type ( + emptyStruct struct{} + fooStruct struct { + name string + } + bazStruct struct { + name string + title string + description string + } +) diff --git a/ignite/pkg/xast/function.go b/ignite/pkg/xast/function.go new file mode 100644 index 0000000000..3469a32f68 --- /dev/null +++ b/ignite/pkg/xast/function.go @@ -0,0 +1,412 @@ +package xast + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "strings" + + "github.com/ignite/cli/v28/ignite/pkg/errors" +) + +type ( + // functionOpts represent the options for functions. + functionOpts struct { + newParams []param + body string + newLines []line + insideCall []call + insideStruct []str + appendCode []string + returnVars []string + } + + // FunctionOptions configures code generation. + FunctionOptions func(*functionOpts) + + str struct { + structName string + paramName string + code string + index int + } + call struct { + name string + code string + index int + } + param struct { + name string + varType string + index int + } + line struct { + code string + number uint64 + } +) + +// AppendFuncParams add a new param value. +func AppendFuncParams(name, varType string, index int) FunctionOptions { + return func(c *functionOpts) { + c.newParams = append(c.newParams, param{ + name: name, + varType: varType, + index: index, + }) + } +} + +// ReplaceFuncBody replace all body of the function, the method will replace first and apply the other options after. +func ReplaceFuncBody(body string) FunctionOptions { + return func(c *functionOpts) { + c.body = body + } +} + +// AppendFuncCode append code before the end or the return, if exists, of a function in Go source code content. +func AppendFuncCode(code string) FunctionOptions { + return func(c *functionOpts) { + c.appendCode = append(c.appendCode, code) + } +} + +// AppendFuncAtLine append a new code at line. +func AppendFuncAtLine(code string, lineNumber uint64) FunctionOptions { + return func(c *functionOpts) { + c.newLines = append(c.newLines, line{ + code: code, + number: lineNumber, + }) + } +} + +// AppendInsideFuncCall add code inside another function call. For instances, the method have a parameter a +// call 'New(param1, param2)' and we want to add the param3 the result will be 'New(param1, param2, param3)'. +func AppendInsideFuncCall(callName, code string, index int) FunctionOptions { + return func(c *functionOpts) { + c.insideCall = append(c.insideCall, call{ + name: callName, + code: code, + index: index, + }) + } +} + +// AppendInsideFuncStruct add code inside another function call. For instances, +// the struct have only one parameter 'Params{Param1: param1}' and we want to add +// the param2 the result will be 'Params{Param1: param1, Param2: param2}'. +func AppendInsideFuncStruct(structName, paramName, code string, index int) FunctionOptions { + return func(c *functionOpts) { + c.insideStruct = append(c.insideStruct, str{ + structName: structName, + paramName: paramName, + code: code, + index: index, + }) + } +} + +// NewFuncReturn replaces return statements in a Go function with a new return statement. +func NewFuncReturn(returnVars ...string) FunctionOptions { + return func(c *functionOpts) { + c.returnVars = append(c.returnVars, returnVars...) + } +} + +func newFunctionOptions() functionOpts { + return functionOpts{ + newParams: make([]param, 0), + body: "", + newLines: make([]line, 0), + insideCall: make([]call, 0), + insideStruct: make([]str, 0), + appendCode: make([]string, 0), + returnVars: make([]string, 0), + } +} + +// ModifyFunction modify a function based in the options. +func ModifyFunction(fileContent, functionName string, functions ...FunctionOptions) (modifiedContent string, err error) { + // Apply function options. + opts := newFunctionOptions() + for _, o := range functions { + o(&opts) + } + + fileSet := token.NewFileSet() + + // Parse the Go source code content. + f, err := parser.ParseFile(fileSet, "", fileContent, parser.ParseComments) + if err != nil { + return "", err + } + + // Parse the content of the new function into an ast. + var newFunctionBody *ast.BlockStmt + if opts.body != "" { + newFuncContent := fmt.Sprintf("package p; func _() { %s }", strings.TrimSpace(opts.body)) + newContent, err := parser.ParseFile(fileSet, "", newFuncContent, parser.ParseComments) + if err != nil { + return "", err + } + newFunctionBody = newContent.Decls[0].(*ast.FuncDecl).Body + } + + // Parse the content of the append code an ast. + appendCode := make([]ast.Stmt, 0) + for _, codeToInsert := range opts.appendCode { + newFuncContent := fmt.Sprintf("package p; func _() { %s }", strings.TrimSpace(codeToInsert)) + newContent, err := parser.ParseFile(fileSet, "", newFuncContent, parser.ParseComments) + if err != nil { + return "", err + } + appendCode = append(appendCode, newContent.Decls[0].(*ast.FuncDecl).Body.List...) + } + + // Parse the content of the return vars into an ast. + returnStmts := make([]ast.Expr, 0) + for _, returnVar := range opts.returnVars { + // Parse the new return var to expression. + newRetExpr, err := parser.ParseExprFrom(fileSet, "", []byte(returnVar), parser.ParseComments) + if err != nil { + return "", err + } + returnStmts = append(returnStmts, newRetExpr) + } + + callMap := make(map[string][]call) + callMapCheck := make(map[string][]call) + for _, c := range opts.insideCall { + calls, ok := callMap[c.name] + if !ok { + calls = []call{} + } + callMap[c.name] = append(calls, c) + callMapCheck[c.name] = append(calls, c) + } + + structMap := make(map[string][]str) + structMapCheck := make(map[string][]str) + for _, s := range opts.insideStruct { + structs, ok := structMap[s.structName] + if !ok { + structs = []str{} + } + structMap[s.structName] = append(structs, s) + structMapCheck[s.structName] = append(structs, s) + } + + // Parse the Go code to insert. + var ( + found bool + errInspect error + ) + ast.Inspect(f, func(n ast.Node) bool { + funcDecl, ok := n.(*ast.FuncDecl) + if !ok || funcDecl.Name.Name != functionName { + return true + } + + for _, p := range opts.newParams { + fieldParam := &ast.Field{ + Names: []*ast.Ident{ast.NewIdent(p.name)}, + Type: ast.NewIdent(p.varType), + } + switch { + case p.index == -1: + // Append the new argument to the end + funcDecl.Type.Params.List = append(funcDecl.Type.Params.List, fieldParam) + case p.index >= 0 && p.index <= len(funcDecl.Type.Params.List): + // Insert the new argument at the specified index + funcDecl.Type.Params.List = append( + funcDecl.Type.Params.List[:p.index], + append([]*ast.Field{fieldParam}, funcDecl.Type.Params.List[p.index:]...)..., + ) + default: + errInspect = errors.Errorf("params index %d out of range", p.index) + return false + } + } + + // Check if the function has the code you want to replace. + if newFunctionBody != nil { + funcDecl.Body = newFunctionBody + } + + // Add the new code at line. + for _, newLine := range opts.newLines { + // Check if the function body has enough lines. + if newLine.number > uint64(len(funcDecl.Body.List))-1 { + errInspect = errors.Errorf("line number %d out of range", newLine.number) + return false + } + // Parse the Go code to insert. + insertionExpr, err := parser.ParseExprFrom(fileSet, "", []byte(newLine.code), parser.ParseComments) + if err != nil { + errInspect = err + return false + } + // Insert code at the specified line number. + funcDecl.Body.List = append( + funcDecl.Body.List[:newLine.number], + append([]ast.Stmt{&ast.ExprStmt{X: insertionExpr}}, funcDecl.Body.List[newLine.number:]...)..., + ) + } + + // Check if there is a return statement in the function. + if len(funcDecl.Body.List) > 0 { + lastStmt := funcDecl.Body.List[len(funcDecl.Body.List)-1] + switch stmt := lastStmt.(type) { + case *ast.ReturnStmt: + // Replace the return statements. + if len(returnStmts) > 0 { + // Remove existing return statements. + stmt.Results = nil + // Add the new return statement. + stmt.Results = append(stmt.Results, returnStmts...) + } + if len(appendCode) > 0 { + // If there is a return, insert before it. + appendCode = append(appendCode, stmt) + funcDecl.Body.List = append(funcDecl.Body.List[:len(funcDecl.Body.List)-1], appendCode...) + } + default: + if len(returnStmts) > 0 { + errInspect = errors.New("return statement not found") + return false + } + // If there is no return, insert at the end of the function body. + if len(appendCode) > 0 { + funcDecl.Body.List = append(funcDecl.Body.List, appendCode...) + } + } + } else { + if len(returnStmts) > 0 { + errInspect = errors.New("return statement not found") + return false + } + // If there are no statements in the function body, insert at the end of the function body. + if len(appendCode) > 0 { + funcDecl.Body.List = append(funcDecl.Body.List, appendCode...) + } + } + + // Add new code to the function callers. + ast.Inspect(funcDecl, func(n ast.Node) bool { + switch expr := n.(type) { + case *ast.CallExpr: // Add a new parameter to a function call. + // Check if the call expression matches the function call name. + name := "" + switch exp := expr.Fun.(type) { + case *ast.Ident: + name = exp.Name + case *ast.SelectorExpr: + name = exp.Sel.Name + default: + return true + } + + calls, ok := callMap[name] + if !ok { + return true + } + + // Construct the new argument to be added + for _, c := range calls { + newArg := ast.NewIdent(c.code) + newArg.NamePos = token.Pos(c.index) + switch { + case c.index == -1: + // Append the new argument to the end + expr.Args = append(expr.Args, newArg) + case c.index >= 0 && c.index <= len(expr.Args): + // Insert the new argument at the specified index + expr.Args = append(expr.Args[:c.index], append([]ast.Expr{newArg}, expr.Args[c.index:]...)...) + default: + errInspect = errors.Errorf("function call index %d out of range", c.index) + return false // Stop the inspection, an error occurred + } + } + delete(callMapCheck, name) + case *ast.CompositeLit: // Add a new parameter to a literal struct. + // Check if the call expression matches the function call name. + name := "" + switch exp := expr.Type.(type) { + case *ast.Ident: + name = exp.Name + case *ast.SelectorExpr: + name = exp.Sel.Name + default: + return true + } + + structs, ok := structMap[name] + if !ok { + return true + } + + // Construct the new argument to be added + for _, s := range structs { + var newArg ast.Expr = ast.NewIdent(s.code) + if s.paramName != "" { + newArg = &ast.KeyValueExpr{ + Key: ast.NewIdent(s.paramName), + Colon: token.Pos(s.index), + Value: ast.NewIdent(s.code), + } + } + + switch { + case s.index == -1: + // Append the new argument to the end + expr.Elts = append(expr.Elts, newArg) + case s.index >= 0 && s.index <= len(expr.Elts): + // Insert the new argument at the specified index + expr.Elts = append(expr.Elts[:s.index], append([]ast.Expr{newArg}, expr.Elts[s.index:]...)...) + default: + errInspect = errors.Errorf("function call index %d out of range", s.index) + return false // Stop the inspection, an error occurred + } + } + delete(structMapCheck, name) + default: + return true + } + return true // Continue the inspection for duplicated calls + }) + if errInspect != nil { + return false + } + if len(callMapCheck) > 0 { + errInspect = errors.Errorf("function calls not found: %v", callMapCheck) + return false + } + if len(structMapCheck) > 0 { + errInspect = errors.Errorf("function structs not found: %v", structMapCheck) + return false + } + + // everything is ok, mark as found and stop the inspect + found = true + return false + }) + if errInspect != nil { + return "", errInspect + } + if !found { + return "", errors.Errorf("function %s not found in file content", functionName) + } + + // Format the modified AST. + var buf bytes.Buffer + if err := format.Node(&buf, fileSet, f); err != nil { + return "", err + } + + // Return the modified content. + return buf.String(), nil +} diff --git a/ignite/pkg/xast/function_test.go b/ignite/pkg/xast/function_test.go new file mode 100644 index 0000000000..1b40988bae --- /dev/null +++ b/ignite/pkg/xast/function_test.go @@ -0,0 +1,321 @@ +package xast + +import ( + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/v28/ignite/pkg/errors" +) + +func TestModifyFunction(t *testing.T) { + existingContent := `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") + New(param1, param2) +} + +func anotherFunction() bool { + p := bla.NewParam() + p.CallSomething("Another call") + return true +}` + + type args struct { + fileContent string + functionName string + functions []FunctionOptions + } + tests := []struct { + name string + args args + want string + err error + }{ + { + name: "add all modifications type", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{ + AppendFuncParams("param1", "string", 0), + ReplaceFuncBody(`return false`), + AppendFuncAtLine(`fmt.Println("Appended at line 0.")`, 0), + AppendFuncAtLine(`SimpleCall(foo, bar)`, 1), + AppendFuncCode(`fmt.Println("Appended code.")`), + AppendFuncCode(`Param{Baz: baz, Foo: foo}`), + NewFuncReturn("1"), + AppendInsideFuncCall("SimpleCall", "baz", 0), + AppendInsideFuncCall("SimpleCall", "bla", -1), + AppendInsideFuncCall("Println", strconv.Quote("test"), -1), + AppendInsideFuncStruct("Param", "Bar", strconv.Quote("bar"), -1), + }, + }, + want: `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") + New(param1, param2) +} + +func anotherFunction(param1 string) bool { + fmt.Println("Appended at line 0.", "test") + SimpleCall(baz, foo, bar, bla) + fmt.Println("Appended code.", "test") + Param{Baz: baz, Foo: foo, Bar: "bar"} + return 1 +} +`, + }, + { + name: "add the replace body", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{ReplaceFuncBody(`return false`)}, + }, + want: `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") + New(param1, param2) +} + +func anotherFunction() bool { return false } +`, + }, + { + name: "add append line and code modification", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{ + AppendFuncAtLine(`fmt.Println("Appended at line 0.")`, 0), + AppendFuncAtLine(`SimpleCall(foo, bar)`, 1), + AppendFuncCode(`fmt.Println("Appended code.")`), + }, + }, + want: `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") + New(param1, param2) +} + +func anotherFunction() bool { + fmt.Println("Appended at line 0.") + SimpleCall(foo, bar) + + p := bla.NewParam() + p.CallSomething("Another call") + fmt.Println("Appended code.") + + return true +} +`, + }, + { + name: "add all modifications type", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{NewFuncReturn("1")}, + }, + want: strings.ReplaceAll(existingContent, "return true", "return 1\n") + "\n", + }, + { + name: "add inside call modifications", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{ + AppendInsideFuncCall("NewParam", "baz", 0), + AppendInsideFuncCall("NewParam", "bla", -1), + AppendInsideFuncCall("CallSomething", strconv.Quote("test1"), -1), + AppendInsideFuncCall("CallSomething", strconv.Quote("test2"), 0), + }, + }, + want: `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") + New(param1, param2) +} + +func anotherFunction() bool { + p := bla.NewParam(baz, bla) + p.CallSomething("test2", "Another call", "test1") + return true +} +`, + }, + { + name: "add inside struct modifications", + args: args{ + fileContent: `package main + +import ( + "fmt" +) + +func anotherFunction() bool { + Param{Baz: baz, Foo: foo} + Client{baz, foo} + return true +}`, + functionName: "anotherFunction", + functions: []FunctionOptions{ + AppendInsideFuncStruct("Param", "Bar", "bar", -1), + AppendInsideFuncStruct("Param", "Bla", "bla", 1), + AppendInsideFuncStruct("Client", "", "bar", 0), + }, + }, + want: `package main + +import ( + "fmt" +) + +func anotherFunction() bool { + Param{Baz: baz, Bla: bla, Foo: foo, Bar: bar} + Client{bar, baz, foo} + return true +} +`, + }, + { + name: "params out of range", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{AppendFuncParams("param1", "string", 1)}, + }, + err: errors.New("params index 1 out of range"), + }, + { + name: "invalid params", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{AppendFuncParams("9#.(c", "string", 0)}, + }, + err: errors.New("format.Node internal error (12:22: expected ')', found 9 (and 1 more errors))"), + }, + { + name: "invalid content for replace body", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{ReplaceFuncBody("9#.(c")}, + }, + err: errors.New("1:24: illegal character U+0023 '#'"), + }, + { + name: "line number out of range", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{AppendFuncAtLine(`fmt.Println("")`, 4)}, + }, + err: errors.New("line number 4 out of range"), + }, + { + name: "invalid code for append at line", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{AppendFuncAtLine("9#.(c", 0)}, + }, + err: errors.New("1:2: illegal character U+0023 '#'"), + }, + { + name: "invalid code append", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{AppendFuncCode("9#.(c")}, + }, + err: errors.New("1:24: illegal character U+0023 '#'"), + }, + { + name: "invalid new return", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{NewFuncReturn("9#.(c")}, + }, + err: errors.New("1:2: illegal character U+0023 '#'"), + }, + { + name: "call name not found", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{AppendInsideFuncCall("FooFunction", "baz", 0)}, + }, + err: errors.New("function calls not found: map[FooFunction:[{FooFunction baz 0}]]"), + }, + { + name: "invalid call param", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{AppendInsideFuncCall("NewParam", "9#.(c", 0)}, + }, + err: errors.New("format.Node internal error (13:21: illegal character U+0023 '#' (and 2 more errors))"), + }, + { + name: "call params out of range", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{AppendInsideFuncCall("NewParam", "baz", 1)}, + }, + err: errors.New("function call index 1 out of range"), + }, + { + name: "empty modifications", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{}, + }, + want: existingContent + "\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ModifyFunction(tt.args.fileContent, tt.args.functionName, tt.args.functions...) + if tt.err != nil { + require.Error(t, err) + require.Equal(t, tt.err.Error(), err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/ignite/pkg/xast/global.go b/ignite/pkg/xast/global.go new file mode 100644 index 0000000000..4be31ff807 --- /dev/null +++ b/ignite/pkg/xast/global.go @@ -0,0 +1,181 @@ +package xast + +import ( + "bytes" + "go/ast" + "go/format" + "go/parser" + "go/token" + + "github.com/ignite/cli/v28/ignite/pkg/errors" +) + +type ( + // globalOpts represent the options for globals. + globalOpts struct { + globals []global + } + + // GlobalOptions configures code generation. + GlobalOptions func(*globalOpts) + + global struct { + name, varType, value string + } + + // GlobalType represents the global type. + GlobalType string +) + +const ( + GlobalTypeVar GlobalType = "var" + GlobalTypeConst GlobalType = "const" +) + +// WithGlobal add a new global. +func WithGlobal(name, varType, value string) GlobalOptions { + return func(c *globalOpts) { + c.globals = append(c.globals, global{ + name: name, + varType: varType, + value: value, + }) + } +} + +func newGlobalOptions() globalOpts { + return globalOpts{ + globals: make([]global, 0), + } +} + +// InsertGlobal inserts global variables or constants into the provided Go source code content after the import section. +// The function parses the provided content, locates the import section, and inserts the global declarations immediately after it. +// The type of globals (variables or constants) is specified by the globalType parameter. +// Each global declaration is defined by calling WithGlobal function with appropriate arguments. +// The function returns the modified content with the inserted global declarations. +func InsertGlobal(fileContent string, globalType GlobalType, globals ...GlobalOptions) (modifiedContent string, err error) { + // apply global options. + opts := newGlobalOptions() + for _, o := range globals { + o(&opts) + } + + fileSet := token.NewFileSet() + + // Parse the Go source code content. + f, err := parser.ParseFile(fileSet, "", fileContent, parser.ParseComments) + if err != nil { + return "", err + } + + // Find the index of the import declaration or package declaration if no imports. + var insertIndex int + for i, decl := range f.Decls { + if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.IMPORT { + insertIndex = i + 1 + break + } else if funcDecl, ok := decl.(*ast.FuncDecl); ok { + insertIndex = i + if funcDecl.Doc == nil { + insertIndex++ + } + break + } + } + + // Create global variable/constant declarations. + for _, global := range opts.globals { + // Create an identifier for the global. + ident := ast.NewIdent(global.name) + + // Create a value expression if provided. + var valueExpr ast.Expr + if global.value != "" { + valueExpr, err = parser.ParseExprFrom(fileSet, "", []byte(global.value), parser.ParseComments) + if err != nil { + return "", err + } + } + + // Create a declaration based on the global type. + var spec ast.Spec + switch globalType { + case GlobalTypeVar: + spec = &ast.ValueSpec{ + Names: []*ast.Ident{ident}, + Type: ast.NewIdent(global.varType), + Values: []ast.Expr{valueExpr}, + } + case GlobalTypeConst: + spec = &ast.ValueSpec{ + Names: []*ast.Ident{ident}, + Type: ast.NewIdent(global.varType), + Values: []ast.Expr{valueExpr}, + } + default: + return "", errors.Errorf("unsupported global type: %s", string(globalType)) + } + + // Insert the declaration after the import section or package declaration if no imports. + f.Decls = append( + f.Decls[:insertIndex], + append([]ast.Decl{ + &ast.GenDecl{ + TokPos: 1, + Tok: token.Lookup(string(globalType)), + Specs: []ast.Spec{spec}, + }, + }, f.Decls[insertIndex:]...)...) + insertIndex++ + } + + // Format the modified AST. + var buf bytes.Buffer + if err := format.Node(&buf, fileSet, f); err != nil { + return "", err + } + + // Return the modified content. + return buf.String(), nil +} + +// AppendFunction appends a new function to the end of the Go source code content. +func AppendFunction(fileContent string, function string) (modifiedContent string, err error) { + fileSet := token.NewFileSet() + + // Parse the function body as a separate file. + funcFile, err := parser.ParseFile(fileSet, "", "package main\n"+function, parser.AllErrors) + if err != nil { + return "", err + } + + // Extract the first declaration, assuming it's a function declaration. + var funcDecl *ast.FuncDecl + for _, decl := range funcFile.Decls { + if fDecl, ok := decl.(*ast.FuncDecl); ok { + funcDecl = fDecl + break + } + } + if funcDecl == nil { + return "", errors.Errorf("no function declaration found in the provided function body") + } + + // Parse the Go source code content. + f, err := parser.ParseFile(fileSet, "", fileContent, parser.ParseComments) + if err != nil { + return "", err + } + + // Append the function declaration to the file's declarations. + f.Decls = append(f.Decls, funcDecl) + + // Format the modified AST. + var buf bytes.Buffer + if err := format.Node(&buf, fileSet, f); err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/ignite/pkg/xast/global_test.go b/ignite/pkg/xast/global_test.go new file mode 100644 index 0000000000..5ea4d9f259 --- /dev/null +++ b/ignite/pkg/xast/global_test.go @@ -0,0 +1,352 @@ +package xast + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/v28/ignite/pkg/errors" +) + +func TestInsertGlobal(t *testing.T) { + type args struct { + fileContent string + globalType GlobalType + globals []GlobalOptions + } + tests := []struct { + name string + args args + want string + err error + }{ + { + name: "Insert global int var", + args: args{ + fileContent: `package main + +import ( + "fmt" +) + +`, + globalType: GlobalTypeVar, + globals: []GlobalOptions{ + WithGlobal("myIntVar", "int", "42"), + }, + }, + want: `package main + +import ( + "fmt" +) + +var myIntVar int = 42 +`, + }, + { + name: "Insert global int const", + args: args{ + fileContent: `package main + +import ( + "fmt" +) + +`, + globalType: GlobalTypeConst, + globals: []GlobalOptions{ + WithGlobal("myIntConst", "int", "42"), + }, + }, + want: `package main + +import ( + "fmt" +) + +const myIntConst int = 42 +`, + }, + { + name: "Insert string const", + args: args{ + fileContent: `package main + +import ( + "fmt" +) + +`, + globalType: GlobalTypeConst, + globals: []GlobalOptions{ + WithGlobal("myStringConst", "string", `"hello"`), + }, + }, + want: `package main + +import ( + "fmt" +) + +const myStringConst string = "hello" +`, + }, + { + name: "Insert multiples consts", + args: args{ + fileContent: `package main + +import ( + "fmt" +) + +`, + globalType: GlobalTypeConst, + globals: []GlobalOptions{ + WithGlobal("myStringConst", "string", `"hello"`), + WithGlobal("myBoolConst", "bool", "true"), + WithGlobal("myUintConst", "uint64", "40"), + }, + }, + want: `package main + +import ( + "fmt" +) + +const myStringConst string = "hello" +const myBoolConst bool = true +const myUintConst uint64 = 40 +`, + }, + { + name: "Insert global int var with not imports", + args: args{ + fileContent: `package main +`, + globalType: GlobalTypeVar, + globals: []GlobalOptions{ + WithGlobal("myIntVar", "int", "42"), + }, + }, + want: `package main + +var myIntVar int = 42 +`, + }, + { + name: "Insert global int var int an empty file", + args: args{ + fileContent: ``, + globalType: GlobalTypeVar, + globals: []GlobalOptions{ + WithGlobal("myIntVar", "int", "42"), + }, + }, + err: errors.New("1:1: expected 'package', found 'EOF'"), + }, + { + name: "Insert a custom var", + args: args{ + fileContent: `package main`, + globalType: GlobalTypeVar, + globals: []GlobalOptions{ + WithGlobal("fooVar", "foo", "42"), + }, + }, + want: `package main + +var fooVar foo = 42 +`, + }, + { + name: "Insert an invalid var", + args: args{ + fileContent: `package main`, + globalType: GlobalTypeVar, + globals: []GlobalOptions{ + WithGlobal("myInvalidVar", "invalid", "AEF#3fa."), + }, + }, + err: errors.New("1:4: illegal character U+0023 '#'"), + }, + { + name: "Insert an invalid type", + args: args{ + fileContent: `package main`, + globalType: "invalid", + globals: []GlobalOptions{ + WithGlobal("fooVar", "foo", "42"), + }, + }, + err: errors.New("unsupported global type: invalid"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := InsertGlobal(tt.args.fileContent, tt.args.globalType, tt.args.globals...) + if tt.err != nil { + require.Error(t, err) + require.Equal(t, tt.err.Error(), err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestAppendFunction(t *testing.T) { + type args struct { + fileContent string + function string + } + tests := []struct { + name string + args args + want string + err error + }{ + { + name: "Append a function after the package declaration", + args: args{ + fileContent: `package main`, + function: `func add(a, b int) int { + return a + b +}`, + }, + want: `package main + +func add(a, b int) int { + return a + b +} +`, + }, + { + name: "Append a function after a var", + args: args{ + fileContent: `package main + +import ( + "fmt" +) + +var myIntVar int = 42 +`, + function: `func add(a, b int) int { + return a + b +}`, + }, + want: `package main + +import ( + "fmt" +) + +var myIntVar int = 42 + +func add(a, b int) int { + return a + b +} +`, + }, + { + name: "Append a function after the import", + args: args{ + fileContent: `package main + +import ( + "fmt" +) +`, + function: `func add(a, b int) int { + return a + b +}`, + }, + want: `package main + +import ( + "fmt" +) + +func add(a, b int) int { + return a + b +} +`, + }, + { + name: "Append a function after another function", + args: args{ + fileContent: `package main + +import ( + "fmt" +) + +var myIntVar int = 42 + +func myFunction() int { + return 42 +} +`, + function: `func add(a, b int) int { + return a + b +}`, + }, + want: `package main + +import ( + "fmt" +) + +var myIntVar int = 42 + +func myFunction() int { + return 42 +} +func add(a, b int) int { + return a + b +} +`, + }, + { + name: "Append a function in an empty file", + args: args{ + fileContent: ``, + function: `func add(a, b int) int { + return a + b +}`, + }, + err: errors.New("1:1: expected 'package', found 'EOF'"), + }, + { + name: "Append a empty function", + args: args{ + fileContent: `package main`, + function: ``, + }, + err: errors.New("no function declaration found in the provided function body"), + }, + { + name: "Append an invalid function", + args: args{ + fileContent: `package main`, + function: `@,.l.e,`, + }, + err: errors.New("2:1: illegal character U+0040 '@'"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := AppendFunction(tt.args.fileContent, tt.args.function) + if tt.err != nil { + require.Error(t, err) + require.Equal(t, tt.err.Error(), err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/ignite/pkg/xast/import.go b/ignite/pkg/xast/import.go new file mode 100644 index 0000000000..08942c5bb5 --- /dev/null +++ b/ignite/pkg/xast/import.go @@ -0,0 +1,128 @@ +package xast + +import ( + "bytes" + "go/ast" + "go/format" + "go/parser" + "go/token" + "strconv" + + "github.com/ignite/cli/v28/ignite/pkg/errors" +) + +type ( + // importOpts represent the options for imp. + importOpts struct { + imports []imp + } + + // ImportOptions configures code generation. + ImportOptions func(*importOpts) + + imp struct { + repo string + name string + index int + } +) + +// WithImport add a new import. If the index is -1 will append in the end of the imports. +func WithImport(repo, name string, index int) ImportOptions { + return func(c *importOpts) { + c.imports = append(c.imports, imp{ + repo: repo, + name: name, + index: index, + }) + } +} + +func newImportOptions() importOpts { + return importOpts{ + imports: make([]imp, 0), + } +} + +// AppendImports appends import statements to the existing import block in Go source code content. +func AppendImports(fileContent string, imports ...ImportOptions) (string, error) { + // apply global options. + opts := newImportOptions() + for _, o := range imports { + o(&opts) + } + + fileSet := token.NewFileSet() + + // Parse the Go source code content. + f, err := parser.ParseFile(fileSet, "", fileContent, parser.ParseComments) + if err != nil { + return "", err + } + + // Find the existing import declaration. + var importDecl *ast.GenDecl + for _, decl := range f.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.IMPORT || len(genDecl.Specs) == 0 { + continue + } + importDecl = genDecl + break + } + + if importDecl == nil { + // If no existing import declaration found, create a new one. + importDecl = &ast.GenDecl{ + Tok: token.IMPORT, + Specs: make([]ast.Spec, 0), + } + f.Decls = append([]ast.Decl{importDecl}, f.Decls...) + } + + // Check existing imports to avoid duplicates. + existImports := make(map[string]struct{}) + for _, spec := range importDecl.Specs { + importSpec, ok := spec.(*ast.ImportSpec) + if !ok { + continue + } + existImports[importSpec.Path.Value] = struct{}{} + } + + // Add new import statements. + for _, importStmt := range opts.imports { + // Check if the import already exists. + path := strconv.Quote(importStmt.repo) + if _, ok := existImports[path]; ok { + continue + } + // Create a new import spec. + spec := &ast.ImportSpec{ + Name: ast.NewIdent(importStmt.name), + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: path, + }, + } + + switch { + case importStmt.index == -1: + // Append the new argument to the end + importDecl.Specs = append(importDecl.Specs, spec) + case importStmt.index >= 0 && importStmt.index <= len(importDecl.Specs): + // Insert the new argument at the specified index + importDecl.Specs = append(importDecl.Specs[:importStmt.index], append([]ast.Spec{spec}, importDecl.Specs[importStmt.index:]...)...) + default: + return "", errors.Errorf("index out of range") // Stop the inspection, an error occurred + } + } + + // Format the modified AST. + var buf bytes.Buffer + if err := format.Node(&buf, fileSet, f); err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/ignite/pkg/xast/import_test.go b/ignite/pkg/xast/import_test.go new file mode 100644 index 0000000000..e9f564fbab --- /dev/null +++ b/ignite/pkg/xast/import_test.go @@ -0,0 +1,252 @@ +package xast + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/v28/ignite/pkg/errors" +) + +func TestAppendImports(t *testing.T) { + existingContent := `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") +}` + + type args struct { + fileContent string + imports []ImportOptions + } + tests := []struct { + name string + args args + want string + err error + }{ + { + name: "add single import statement", + args: args{ + fileContent: existingContent, + imports: []ImportOptions{ + WithImport("strings", "", -1), + }, + }, + want: `package main + +import ( + "fmt" + "strings" +) + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "add multiple import statements", + args: args{ + fileContent: existingContent, + imports: []ImportOptions{ + WithImport("strings", "st", -1), + WithImport("strconv", "", -1), + WithImport("os", "", -1), + }, + }, + want: `package main + +import ( + "fmt" + "os" + "strconv" + st "strings" +) + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "add multiple import statements with an existing one", + args: args{ + fileContent: existingContent, + imports: []ImportOptions{ + WithImport("strings", "st", -1), + WithImport("strconv", "", -1), + WithImport("os", "", -1), + WithImport("fmt", "", -1), + }, + }, + want: `package main + +import ( + "fmt" + "os" + "strconv" + st "strings" +) + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "add import to specific index", + args: args{ + fileContent: `package main + +import ( + "fmt" + "os" + st "strings" +)`, + imports: []ImportOptions{ + WithImport("strconv", "", 1), + }, + }, + want: `package main + +import ( + "fmt" + "os" + "strconv" + st "strings" +) +`, + }, + { + name: "add multiple imports to specific index", + args: args{ + fileContent: `package main + +import ( + "fmt" + "os" + st "strings" +)`, + imports: []ImportOptions{ + WithImport("strconv", "", 0), + WithImport("testing", "", 3), + WithImport("bytes", "", -1), + }, + }, + want: `package main + +import ( + "bytes" + "fmt" + "os" + "strconv" + st "strings" + "testing" +) +`, + }, + { + name: "add duplicate import statement", + args: args{ + fileContent: existingContent, + imports: []ImportOptions{ + WithImport("fmt", "", -1), + }, + }, + want: existingContent + "\n", + }, + { + name: "no import statement", + args: args{ + fileContent: `package main + +func main() { + fmt.Println("Hello, world!") +}`, + imports: []ImportOptions{ + WithImport("fmt", "", -1), + }, + }, + want: `package main + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "no import statement and add two imports", + args: args{ + fileContent: `package main + +func main() { + fmt.Println("Hello, world!") +}`, + imports: []ImportOptions{ + WithImport("fmt", "", -1), + WithImport("os", "", -1), + }, + }, + want: `package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "invalid index", + args: args{ + fileContent: existingContent, + imports: []ImportOptions{ + WithImport("strings", "", 10), + }, + }, + err: errors.New("index out of range"), + }, + { + name: "add invalid import name", + args: args{ + fileContent: existingContent, + imports: []ImportOptions{ + WithImport("fmt\"", "fmt\"", -1), + }, + }, + err: errors.New("format.Node internal error (5:8: expected ';', found fmt (and 2 more errors))"), + }, + { + name: "add empty file content", + args: args{ + fileContent: "", + imports: []ImportOptions{ + WithImport("fmt", "", -1), + }, + }, + err: errors.New("1:1: expected 'package', found 'EOF'"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := AppendImports(tt.args.fileContent, tt.args.imports...) + if tt.err != nil { + require.Error(t, err) + require.Equal(t, tt.err.Error(), err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/ignite/services/chain/lint.go b/ignite/services/chain/lint.go index 7e33bd6e97..5ad44fbf71 100644 --- a/ignite/services/chain/lint.go +++ b/ignite/services/chain/lint.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/ignite/cli/v28/ignite/pkg/cmdrunner/exec" + "github.com/ignite/cli/v28/ignite/pkg/errors" ) var golangCiLintVersion = "latest" @@ -13,7 +14,7 @@ var golangCiLintVersion = "latest" // It uses golangci-lint to lint the chain's codebase. func (c *Chain) Lint(ctx context.Context) error { if err := exec.Exec(ctx, []string{"go", "install", fmt.Sprintf("github.com/golangci/golangci-lint/cmd/golangci-lint@%s", golangCiLintVersion)}); err != nil { - return fmt.Errorf("failed to install golangci-lint: %w", err) + return errors.Errorf("failed to install golangci-lint: %w", err) } return exec.Exec(ctx, []string{"golangci-lint", "run", "./...", "--out-format=tab"}, exec.IncludeStdLogsToError()) } diff --git a/ignite/services/chain/lint_test.go b/ignite/services/chain/lint_test.go index 840d12223a..dc1f90b240 100644 --- a/ignite/services/chain/lint_test.go +++ b/ignite/services/chain/lint_test.go @@ -3,5 +3,4 @@ package chain_test import "testing" func TestChainLint(t *testing.T) { - } diff --git a/ignite/services/scaffolder/configs.go b/ignite/services/scaffolder/configs.go new file mode 100644 index 0000000000..8e347765e9 --- /dev/null +++ b/ignite/services/scaffolder/configs.go @@ -0,0 +1,95 @@ +package scaffolder + +import ( + "context" + "path/filepath" + "strings" + + "github.com/gobuffalo/genny/v2" + + "github.com/ignite/cli/v28/ignite/pkg/cache" + "github.com/ignite/cli/v28/ignite/pkg/errors" + "github.com/ignite/cli/v28/ignite/pkg/goanalysis" + "github.com/ignite/cli/v28/ignite/pkg/multiformatname" + "github.com/ignite/cli/v28/ignite/pkg/placeholder" + "github.com/ignite/cli/v28/ignite/pkg/xgenny" + "github.com/ignite/cli/v28/ignite/templates/field" + modulecreate "github.com/ignite/cli/v28/ignite/templates/module/create" +) + +// CreateConfigs creates a new configs in the scaffolded module. +func (s Scaffolder) CreateConfigs( + ctx context.Context, + cacheStorage cache.Storage, + tracer *placeholder.Tracer, + moduleName string, + configs ...string, +) (sm xgenny.SourceModification, err error) { + appName := s.modpath.Package + // If no module is provided, we add the type to the app's module + if moduleName == "" { + moduleName = s.modpath.Package + } + mfName, err := multiformatname.NewName(moduleName, multiformatname.NoNumber) + if err != nil { + return sm, err + } + moduleName = mfName.LowerCase + + // Check if the module already exist + ok, err := moduleExists(s.path, moduleName) + if err != nil { + return sm, err + } + if !ok { + return sm, errors.Errorf("the module %v not exist", moduleName) + } + + if err := checkConfigCreated(s.path, appName, moduleName, configs); err != nil { + return sm, err + } + + // Parse config with the associated type + configsFields, err := field.ParseFields(configs, checkForbiddenTypeIndex) + if err != nil { + return sm, err + } + + opts := modulecreate.ConfigsOptions{ + ModuleName: moduleName, + Configs: configsFields, + AppName: s.modpath.Package, + AppPath: s.path, + } + + g, err := modulecreate.NewModuleConfigs(opts) + if err != nil { + return sm, err + } + gens := []*genny.Generator{g} + + sm, err = xgenny.RunWithValidation(tracer, gens...) + if err != nil { + return sm, err + } + + return sm, finish(ctx, cacheStorage, opts.AppPath, s.modpath.RawPath) +} + +// checkConfigCreated checks if the config has been already created. +func checkConfigCreated(appPath, appName, moduleName string, configs []string) (err error) { + path := filepath.Join(appPath, "api", appName, moduleName, "module") + ok, err := goanalysis.HasAnyStructFieldsInPkg(path, "Module", configs) + if err != nil { + return err + } + + if ok { + return errors.Errorf( + "duplicated configs (%s) module %s", + strings.Join(configs, " "), + moduleName, + ) + } + return nil +} diff --git a/ignite/services/scaffolder/init.go b/ignite/services/scaffolder/init.go index 1e018bcf35..4262c2bb2a 100644 --- a/ignite/services/scaffolder/init.go +++ b/ignite/services/scaffolder/init.go @@ -26,7 +26,7 @@ func Init( tracer *placeholder.Tracer, root, name, addressPrefix string, noDefaultModule, skipGit, minimal bool, - params []string, + params, moduleConfigs []string, ) (path string, err error) { pathInfo, err := gomodulepath.Parse(name) if err != nil { @@ -46,7 +46,7 @@ func Init( path = filepath.Join(root, appFolder) // create the project - err = generate(ctx, tracer, pathInfo, addressPrefix, path, noDefaultModule, minimal, params) + err = generate(ctx, tracer, pathInfo, addressPrefix, path, noDefaultModule, minimal, params, moduleConfigs) if err != nil { return "", err } @@ -74,7 +74,7 @@ func generate( addressPrefix, absRoot string, noDefaultModule, minimal bool, - params []string, + params, moduleConfigs []string, ) error { // Parse params with the associated type paramsFields, err := field.ParseFields(params, checkForbiddenTypeIndex) @@ -82,6 +82,12 @@ func generate( return err } + // Parse configs with the associated type + configsFields, err := field.ParseFields(moduleConfigs, checkForbiddenTypeIndex) + if err != nil { + return err + } + githubPath := gomodulepath.ExtractAppPath(pathInfo.RawPath) if !strings.Contains(githubPath, "/") { // A username must be added when the app module path has a single element @@ -129,6 +135,7 @@ func generate( AppName: pathInfo.Package, AppPath: absRoot, Params: paramsFields, + Configs: configsFields, IsIBC: false, } // Check if the module name is valid diff --git a/ignite/services/scaffolder/module.go b/ignite/services/scaffolder/module.go index 781127593b..c25941daf6 100644 --- a/ignite/services/scaffolder/module.go +++ b/ignite/services/scaffolder/module.go @@ -105,16 +105,19 @@ var ( // moduleCreationOptions holds options for creating a new module. type moduleCreationOptions struct { - // ibc true if the module is an ibc module + // ibc true if the module is an ibc module. ibc bool - // params list of parameters + // params list of parameters. params []string - // ibcChannelOrdering ibc channel ordering + // moduleConfigs list of module configs. + moduleConfigs []string + + // ibcChannelOrdering ibc channel ordering. ibcChannelOrdering string - // dependencies list of module dependencies + // dependencies list of module dependencies. dependencies []modulecreate.Dependency } @@ -135,6 +138,13 @@ func WithParams(params []string) ModuleCreationOption { } } +// WithModuleConfigs scaffolds a module with module configs. +func WithModuleConfigs(moduleConfigs []string) ModuleCreationOption { + return func(m *moduleCreationOptions) { + m.moduleConfigs = moduleConfigs + } +} + // WithIBCChannelOrdering configures channel ordering of the IBC module. func WithIBCChannelOrdering(ordering string) ModuleCreationOption { return func(m *moduleCreationOptions) { @@ -196,6 +206,12 @@ func (s Scaffolder) CreateModule( return sm, err } + // Parse configs with the associated type + configs, err := field.ParseFields(creationOpts.moduleConfigs, checkForbiddenTypeIndex) + if err != nil { + return sm, err + } + // Check dependencies if err := checkDependencies(creationOpts.dependencies, s.path); err != nil { return sm, err @@ -205,6 +221,7 @@ func (s Scaffolder) CreateModule( ModuleName: moduleName, ModulePath: s.modpath.RawPath, Params: params, + Configs: configs, AppName: s.modpath.Package, AppPath: s.path, IsIBC: creationOpts.ibc, diff --git a/ignite/services/scaffolder/params.go b/ignite/services/scaffolder/params.go new file mode 100644 index 0000000000..02407d1936 --- /dev/null +++ b/ignite/services/scaffolder/params.go @@ -0,0 +1,94 @@ +package scaffolder + +import ( + "context" + "path/filepath" + "strings" + + "github.com/gobuffalo/genny/v2" + + "github.com/ignite/cli/v28/ignite/pkg/cache" + "github.com/ignite/cli/v28/ignite/pkg/errors" + "github.com/ignite/cli/v28/ignite/pkg/goanalysis" + "github.com/ignite/cli/v28/ignite/pkg/multiformatname" + "github.com/ignite/cli/v28/ignite/pkg/placeholder" + "github.com/ignite/cli/v28/ignite/pkg/xgenny" + "github.com/ignite/cli/v28/ignite/templates/field" + modulecreate "github.com/ignite/cli/v28/ignite/templates/module/create" +) + +// CreateParams creates a new params in the scaffolded module. +func (s Scaffolder) CreateParams( + ctx context.Context, + cacheStorage cache.Storage, + tracer *placeholder.Tracer, + moduleName string, + params ...string, +) (sm xgenny.SourceModification, err error) { + // If no module is provided, we add the type to the app's module + if moduleName == "" { + moduleName = s.modpath.Package + } + mfName, err := multiformatname.NewName(moduleName, multiformatname.NoNumber) + if err != nil { + return sm, err + } + moduleName = mfName.LowerCase + + // Check if the module already exist + ok, err := moduleExists(s.path, moduleName) + if err != nil { + return sm, err + } + if !ok { + return sm, errors.Errorf("the module %v not exist", moduleName) + } + + if err := checkParamCreated(s.path, moduleName, params); err != nil { + return sm, err + } + + // Parse params with the associated type + paramsFields, err := field.ParseFields(params, checkForbiddenTypeIndex) + if err != nil { + return sm, err + } + + opts := modulecreate.ParamsOptions{ + ModuleName: moduleName, + Params: paramsFields, + AppName: s.modpath.Package, + AppPath: s.path, + } + + g, err := modulecreate.NewModuleParam(opts) + if err != nil { + return sm, err + } + gens := []*genny.Generator{g} + + sm, err = xgenny.RunWithValidation(tracer, gens...) + if err != nil { + return sm, err + } + + return sm, finish(ctx, cacheStorage, opts.AppPath, s.modpath.RawPath) +} + +// checkParamCreated checks if the parameter has been already created. +func checkParamCreated(appPath, moduleName string, params []string) error { + path := filepath.Join(appPath, "x", moduleName, "types") + ok, err := goanalysis.HasAnyStructFieldsInPkg(path, "Params", params) + if err != nil { + return err + } + + if ok { + return errors.Errorf( + "duplicated params (%s) module %s", + strings.Join(params, " "), + moduleName, + ) + } + return nil +} diff --git a/ignite/templates/field/field.go b/ignite/templates/field/field.go index 3407e6c193..f33a8ea1d1 100644 --- a/ignite/templates/field/field.go +++ b/ignite/templates/field/field.go @@ -155,3 +155,11 @@ func (f Field) ProtoImports() []string { } return dt.ProtoImports } + +// Value returns the field assign value. +func (f Field) Value() string { + if f.DataType() == "string" { + return fmt.Sprintf(`"%s"`, f.Name.Snake) + } + return f.ValueIndex() +} diff --git a/ignite/templates/module/create/base.go b/ignite/templates/module/create/base.go index ba19e4b9da..0af3c1de82 100644 --- a/ignite/templates/module/create/base.go +++ b/ignite/templates/module/create/base.go @@ -48,6 +48,7 @@ func NewGenerator(opts *CreateOptions) (*genny.Generator, error) { ctx.Set("appName", opts.AppName) ctx.Set("dependencies", opts.Dependencies) ctx.Set("params", opts.Params) + ctx.Set("configs", opts.Configs) ctx.Set("isIBC", opts.IsIBC) ctx.Set("apiPath", fmt.Sprintf("/%s/%s", appModulePath, opts.ModuleName)) ctx.Set("protoPkgName", module.ProtoPackageName(appModulePath, opts.ModuleName)) diff --git a/ignite/templates/module/create/configs.go b/ignite/templates/module/create/configs.go new file mode 100644 index 0000000000..db3ece0934 --- /dev/null +++ b/ignite/templates/module/create/configs.go @@ -0,0 +1,47 @@ +package modulecreate + +import ( + "path/filepath" + + "github.com/gobuffalo/genny/v2" + + "github.com/ignite/cli/v28/ignite/pkg/errors" + "github.com/ignite/cli/v28/ignite/pkg/protoanalysis/protoutil" +) + +// NewModuleConfigs returns the generator to scaffold a new configs inside a module. +func NewModuleConfigs(opts ConfigsOptions) (*genny.Generator, error) { + g := genny.New() + g.RunFn(configsProtoModify(opts)) + return g, nil +} + +func configsProtoModify(opts ConfigsOptions) genny.RunFn { + return func(r *genny.Runner) error { + path := filepath.Join(opts.AppPath, "proto", opts.AppName, opts.ModuleName, "module/module.proto") + f, err := r.Disk.Find(path) + if err != nil { + return err + } + protoFile, err := protoutil.ParseProtoFile(f) + if err != nil { + return err + } + + params, err := protoutil.GetMessageByName(protoFile, "Module") + if err != nil { + return errors.Errorf("couldn't find message 'Module' in %s: %w", path, err) + } + for _, paramField := range opts.Configs { + param := protoutil.NewField( + paramField.Name.LowerCamel, + paramField.DataType(), + protoutil.NextUniqueID(params), + ) + protoutil.Append(params, param) + } + + newFile := genny.NewFileS(path, protoutil.Print(protoFile)) + return r.File(newFile) + } +} diff --git a/ignite/templates/module/create/files/base/proto/{{appName}}/{{moduleName}}/module/module.proto.plush b/ignite/templates/module/create/files/base/proto/{{appName}}/{{moduleName}}/module/module.proto.plush index 4a5ffaa9c9..25ebf95e9a 100644 --- a/ignite/templates/module/create/files/base/proto/{{appName}}/{{moduleName}}/module/module.proto.plush +++ b/ignite/templates/module/create/files/base/proto/{{appName}}/{{moduleName}}/module/module.proto.plush @@ -11,4 +11,7 @@ message Module { // authority defines the custom module authority. If not set, defaults to the governance module. string authority = 1; + + <%= for (i, config) in configs { %> + <%= config.ProtoType(i+2) %>;<% } %> } \ No newline at end of file diff --git a/ignite/templates/module/create/files/base/proto/{{appName}}/{{moduleName}}/params.proto.plush b/ignite/templates/module/create/files/base/proto/{{appName}}/{{moduleName}}/params.proto.plush index 1790a2ee02..df82363d16 100644 --- a/ignite/templates/module/create/files/base/proto/{{appName}}/{{moduleName}}/params.proto.plush +++ b/ignite/templates/module/create/files/base/proto/{{appName}}/{{moduleName}}/params.proto.plush @@ -12,5 +12,5 @@ message Params { option (gogoproto.equal) = true; <%= for (i, param) in params { %> - <%= param.ProtoType(i+1) %> [(gogoproto.moretags) = "yaml:\"<%= param.Name.Snake %>\""];<% } %> + <%= param.ProtoType(i+1) %>;<% } %> } \ No newline at end of file diff --git a/ignite/templates/module/create/files/base/x/{{moduleName}}/types/params.go.plush b/ignite/templates/module/create/files/base/x/{{moduleName}}/types/params.go.plush index dbe9b6c49b..b788f28081 100644 --- a/ignite/templates/module/create/files/base/x/{{moduleName}}/types/params.go.plush +++ b/ignite/templates/module/create/files/base/x/{{moduleName}}/types/params.go.plush @@ -1,29 +1,12 @@ package types -import ( - <%= if (len(params) > 0) { %>"fmt"<% } %> - - paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" -) - -var _ paramtypes.ParamSet = (*Params)(nil) - <%= for (param) in params { %> -var ( - Key<%= param.Name.UpperCamel %> = []byte("<%= param.Name.UpperCamel %>")<%= if (param.DataType() == "string") { %> - // TODO: Determine the default value - Default<%= param.Name.UpperCamel %> <%= param.DataType() %> = "<%= param.Name.Snake %>"<% } else { %> - // TODO: Determine the default value - Default<%= param.Name.UpperCamel %> <%= param.DataType() %> = <%= param.ValueIndex() %><% } %> -) + // Default<%= param.Name.UpperCamel %> represents the <%= param.Name.UpperCamel %> default value. + // TODO: Determine the default value. + var Default<%= param.Name.UpperCamel %> <%= param.DataType() %> = <%= if (param.DataType() == "string") { %>"<%= param.Name.Snake %>"<% } else { %><%= param.ValueIndex() %><% } %> <% } %> -// ParamKeyTable the param key table for launch module -func ParamKeyTable() paramtypes.KeyTable { - return paramtypes.NewKeyTable().RegisterParamSet(&Params{}) -} - -// NewParams creates a new Params instance +// NewParams creates a new Params instance. func NewParams(<%= for (param) in params { %> <%= param.Name.LowerCamel %> <%= param.DataType() %>,<% } %> ) Params { @@ -32,40 +15,27 @@ func NewParams(<%= for (param) in params { %> } } -// DefaultParams returns a default set of parameters +// DefaultParams returns a default set of parameters. func DefaultParams() Params { return NewParams(<%= for (param) in params { %> Default<%= param.Name.UpperCamel %>,<% } %> ) } -// ParamSetPairs get the params.ParamSet -func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { - return paramtypes.ParamSetPairs{<%= for (param) in params { %> - paramtypes.NewParamSetPair(Key<%= param.Name.UpperCamel %>, &p.<%= param.Name.UpperCamel %>, validate<%= param.Name.UpperCamel %>),<% } %> - } -} - -// Validate validates the set of params +// Validate validates the set of params. func (p Params) Validate() error {<%= for (param) in params { %> if err := validate<%= param.Name.UpperCamel %>(p.<%= param.Name.UpperCamel %>); err != nil { return err } <% } %> + return nil } <%= for (param) in params { %> -// validate<%= param.Name.UpperCamel %> validates the <%= param.Name.UpperCamel %> param -func validate<%= param.Name.UpperCamel %>(v interface{}) error { - <%= param.Name.LowerCamel %>, ok := v.(<%= param.DataType() %>) - if !ok { - return fmt.Errorf("invalid parameter type: %T", v) - } - +// validate<%= param.Name.UpperCamel %> validates the <%= param.Name.UpperCamel %> parameter. +func validate<%= param.Name.UpperCamel %>(v <%= param.DataType() %>) error { // TODO implement validation - _ = <%= param.Name.LowerCamel %> - return nil } -<% } %> \ No newline at end of file +<% } %> diff --git a/ignite/templates/module/create/options.go b/ignite/templates/module/create/options.go index d74dbeab77..58d0d40347 100644 --- a/ignite/templates/module/create/options.go +++ b/ignite/templates/module/create/options.go @@ -9,6 +9,22 @@ import ( ) type ( + // ConfigsOptions represents the options to scaffold a Cosmos SDK module configs. + ConfigsOptions struct { + ModuleName string + AppName string + AppPath string + Configs field.Fields + } + + // ParamsOptions represents the options to scaffold a Cosmos SDK module parameters. + ParamsOptions struct { + ModuleName string + AppName string + AppPath string + Params field.Fields + } + // CreateOptions represents the options to scaffold a Cosmos SDK module. CreateOptions struct { ModuleName string @@ -16,6 +32,7 @@ type ( AppName string AppPath string Params field.Fields + Configs field.Fields // True if the module should implement the IBC module interface IsIBC bool diff --git a/ignite/templates/module/create/params.go b/ignite/templates/module/create/params.go new file mode 100644 index 0000000000..bdad058709 --- /dev/null +++ b/ignite/templates/module/create/params.go @@ -0,0 +1,141 @@ +package modulecreate + +import ( + "fmt" + "path/filepath" + + "github.com/gobuffalo/genny/v2" + + "github.com/ignite/cli/v28/ignite/pkg/errors" + "github.com/ignite/cli/v28/ignite/pkg/protoanalysis/protoutil" + "github.com/ignite/cli/v28/ignite/pkg/xast" +) + +// NewModuleParam returns the generator to scaffold a new parameter inside a module. +func NewModuleParam(opts ParamsOptions) (*genny.Generator, error) { + g := genny.New() + g.RunFn(paramsProtoModify(opts)) + g.RunFn(paramsTypesModify(opts)) + return g, nil +} + +func paramsProtoModify(opts ParamsOptions) genny.RunFn { + return func(r *genny.Runner) error { + path := filepath.Join(opts.AppPath, "proto", opts.AppName, opts.ModuleName, "params.proto") + f, err := r.Disk.Find(path) + if err != nil { + return err + } + protoFile, err := protoutil.ParseProtoFile(f) + if err != nil { + return err + } + + params, err := protoutil.GetMessageByName(protoFile, "Params") + if err != nil { + return errors.Errorf("couldn't find message 'Params' in %s: %w", path, err) + } + for _, paramField := range opts.Params { + param := protoutil.NewField( + paramField.Name.LowerCamel, + paramField.DataType(), + protoutil.NextUniqueID(params), + ) + protoutil.Append(params, param) + } + + newFile := genny.NewFileS(path, protoutil.Print(protoFile)) + return r.File(newFile) + } +} + +func paramsTypesModify(opts ParamsOptions) genny.RunFn { + return func(r *genny.Runner) error { + path := filepath.Join(opts.AppPath, "x", opts.ModuleName, "types/params.go") + f, err := r.Disk.Find(path) + if err != nil { + return err + } + + var ( + content = f.String() + globalOpts = make([]xast.GlobalOptions, len(opts.Params)) + newParamsModifier = make([]xast.FunctionOptions, 0) + defaultParamsModifier = make([]xast.FunctionOptions, len(opts.Params)) + validateModifier = make([]xast.FunctionOptions, len(opts.Params)) + ) + for i, param := range opts.Params { + // param key and default value. + globalOpts[i] = xast.WithGlobal( + fmt.Sprintf("Default%s", param.Name.UpperCamel), + param.DataType(), + param.Value(), + ) + + // add parameter to the struct into the new method. + newParamsModifier = append( + newParamsModifier, + xast.AppendFuncParams(param.Name.LowerCamel, param.DataType(), -1), + xast.AppendInsideFuncStruct( + "Params", + param.Name.UpperCamel, + param.Name.LowerCamel, + -1, + ), + ) + + // add default parameter. + defaultParamsModifier[i] = xast.AppendInsideFuncCall( + "NewParams", + fmt.Sprintf("Default%s", param.Name.UpperCamel), + -1, + ) + + // add param field to the validate method. + replacementValidate := fmt.Sprintf( + `if err := validate%[1]v(p.%[1]v); err != nil { return err }`, + param.Name.UpperCamel, + ) + validateModifier[i] = xast.AppendFuncCode(replacementValidate) + + // add param field to the validate method. + templateValidation := `// validate%[1]v validates the %[1]v parameter. +func validate%[1]v(v %[2]v) error { + // TODO implement validation + return nil +}` + validationFunc := fmt.Sprintf( + templateValidation, + param.Name.UpperCamel, + param.DataType(), + ) + content, err = xast.AppendFunction(content, validationFunc) + if err != nil { + return err + } + } + + content, err = xast.InsertGlobal(content, xast.GlobalTypeConst, globalOpts...) + if err != nil { + return err + } + + content, err = xast.ModifyFunction(content, "NewParams", newParamsModifier...) + if err != nil { + return err + } + + content, err = xast.ModifyFunction(content, "DefaultParams", defaultParamsModifier...) + if err != nil { + return err + } + + content, err = xast.ModifyFunction(content, "Validate", validateModifier...) + if err != nil { + return err + } + + newFile := genny.NewFileS(path, content) + return r.File(newFile) + } +} diff --git a/integration/params/cmd_configs_test.go b/integration/params/cmd_configs_test.go new file mode 100644 index 0000000000..2916467f5b --- /dev/null +++ b/integration/params/cmd_configs_test.go @@ -0,0 +1,79 @@ +//go:build !relayer + +package params_test + +import ( + "testing" + + "github.com/ignite/cli/v28/ignite/pkg/cmdrunner/step" + envtest "github.com/ignite/cli/v28/integration" +) + +func TestCreateModuleConfigs(t *testing.T) { + var ( + env = envtest.New(t) + app = env.Scaffold("github.com/test/mars") + ) + + env.Must(env.Exec("create a new module with configs", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "module", + "--yes", + "foo", + "--module-configs", + "bla,baz:uint,bar:bool", + "--require-registration"), + step.Workdir(app.SourcePath()), + )), + )) + + env.Must(env.Exec("should prevent creating configs field that already exist", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "configs", + "--yes", + "bla", + "buu:uint", + "--module", + "foo", + ), + step.Workdir(app.SourcePath()), + )), + envtest.ExecShouldError(), + )) + + env.Must(env.Exec("create a new module configs in the foo module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "configs", + "--yes", + "bol", + "buu:uint", + "plk:bool", + "--module", + "foo", + ), + step.Workdir(app.SourcePath()), + )), + )) + + env.Must(env.Exec("create a new module configs in the mars module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "configs", + "--yes", + "foo", + "bar:uint", + "baz:bool", + ), + step.Workdir(app.SourcePath()), + )), + )) + + app.EnsureSteady() +} diff --git a/integration/params/cmd_params_test.go b/integration/params/cmd_params_test.go new file mode 100644 index 0000000000..65e47ebf24 --- /dev/null +++ b/integration/params/cmd_params_test.go @@ -0,0 +1,79 @@ +//go:build !relayer + +package params_test + +import ( + "testing" + + "github.com/ignite/cli/v28/ignite/pkg/cmdrunner/step" + envtest "github.com/ignite/cli/v28/integration" +) + +func TestCreateModuleParameters(t *testing.T) { + var ( + env = envtest.New(t) + app = env.Scaffold("github.com/test/mars") + ) + + env.Must(env.Exec("create a new module with parameter", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "module", + "--yes", + "foo", + "--params", + "bla,baz:uint,bar:bool", + "--require-registration"), + step.Workdir(app.SourcePath()), + )), + )) + + env.Must(env.Exec("should prevent creating parameter field that already exist", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "params", + "--yes", + "bla", + "buu:uint", + "--module", + "foo", + ), + step.Workdir(app.SourcePath()), + )), + envtest.ExecShouldError(), + )) + + env.Must(env.Exec("create a new module parameters in the foo module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "params", + "--yes", + "bol", + "buu:uint", + "plk:bool", + "--module", + "foo", + ), + step.Workdir(app.SourcePath()), + )), + )) + + env.Must(env.Exec("create a new module parameters in the mars module", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, + "s", + "params", + "--yes", + "foo", + "bar:uint", + "baz:bool", + ), + step.Workdir(app.SourcePath()), + )), + )) + + app.EnsureSteady() +} diff --git a/integration/plugin/testdata/example-plugin/go.mod b/integration/plugin/testdata/example-plugin/go.mod index 4f000e4ab4..35e98f71d4 100644 --- a/integration/plugin/testdata/example-plugin/go.mod +++ b/integration/plugin/testdata/example-plugin/go.mod @@ -66,13 +66,13 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.etcd.io/bbolt v1.3.8 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/net v0.20.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.15.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect