diff --git a/integration/cmd_app_test.go b/integration/cmd_app_test.go index 8ebe7074b8..f0321cda36 100644 --- a/integration/cmd_app_test.go +++ b/integration/cmd_app_test.go @@ -110,5 +110,52 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { ExecShouldError(), )) + env.Must(env.Exec("create a module with dependencies", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "module", + "example_with_dep", + "--dep", + "account,bank,staking,slashing,example", + "--require-registration", + ), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("should prevent creating a module with invalid dependencies", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "module", + "example_with_wrong_dep", + "--dep", + "dup,dup", + "--require-registration", + ), + step.Workdir(path), + )), + ExecShouldError(), + )) + + env.Must(env.Exec("should prevent creating a module with a non registered dependency", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "module", + "example_with_no_dep", + "--dep", + "inexistent", + "--require-registration", + ), + step.Workdir(path), + )), + ExecShouldError(), + )) + env.EnsureAppIsSteady(path) } diff --git a/integration/cmd_ibc_test.go b/integration/cmd_ibc_test.go index 2534f5ee9c..c5847da2b9 100644 --- a/integration/cmd_ibc_test.go +++ b/integration/cmd_ibc_test.go @@ -31,25 +31,59 @@ func TestCreateModuleWithIBC(t *testing.T) { env.Must(env.Exec("create an IBC module with an ordered channel", step.NewSteps(step.New( - step.Exec("starport", "s", "module", "--ibc", "orderedfoo", "--ordering", "ordered", "--require-registration"), + step.Exec( + "starport", + "s", + "module", + "orderedfoo", + "--ibc", + "--ordering", + "ordered", + "--require-registration", + ), step.Workdir(path), )), )) env.Must(env.Exec("create an IBC module with an unordered channel", step.NewSteps(step.New( - step.Exec("starport", "s", "module", "--ibc", "unorderedfoo", "--ordering", "unordered", "--require-registration"), + step.Exec( + "starport", + "s", + "module", + "unorderedfoo", + "--ibc", + "--ordering", + "unordered", + "--require-registration", + ), step.Workdir(path), )), )) - env.Must(env.Exec("create an non IBC module", + env.Must(env.Exec("create a non IBC module", step.NewSteps(step.New( step.Exec("starport", "s", "module", "foobar", "--require-registration"), step.Workdir(path), )), )) + env.Must(env.Exec("create an IBC module with dependencies", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "module", + "example_with_dep", + "--ibc", + "--dep", + "account,bank,staking,slashing", + "--require-registration", + ), + step.Workdir(path), + )), + )) + env.EnsureAppIsSteady(path) } @@ -69,7 +103,17 @@ func TestCreateIBCPacket(t *testing.T) { env.Must(env.Exec("create a packet", step.NewSteps(step.New( - step.Exec("starport", "s", "packet", "bar", "text", "--module", "foo", "--ack", "foo:string,bar:int,foobar:bool"), + step.Exec( + "starport", + "s", + "packet", + "bar", + "text", + "--module", + "foo", + "--ack", + "foo:string,bar:int,foobar:bool", + ), step.Workdir(path), )), )) diff --git a/integration/cmd_message_test.go b/integration/cmd_message_test.go index 2374e5bc17..e80da5da17 100644 --- a/integration/cmd_message_test.go +++ b/integration/cmd_message_test.go @@ -16,7 +16,17 @@ func TestGenerateAnAppWithMessage(t *testing.T) { env.Must(env.Exec("create a message", step.NewSteps(step.New( - step.Exec("starport", "s", "message", "do-foo", "text", "vote:int", "like:bool", "-r", "foo,bar:int,foobar:bool"), + step.Exec( + "starport", + "s", + "message", + "do-foo", + "text", + "vote:int", + "like:bool", + "-r", + "foo,bar:int,foobar:bool", + ), step.Workdir(path), )), )) @@ -45,7 +55,19 @@ func TestGenerateAnAppWithMessage(t *testing.T) { env.Must(env.Exec("create a message in a module", step.NewSteps(step.New( - step.Exec("starport", "s", "message", "do-foo", "text", "--module", "foo", "--desc", "foo bar foobar", "--response", "foo,bar:int,foobar:bool"), + step.Exec( + "starport", + "s", + "message", + "do-foo", + "text", + "--module", + "foo", + "--desc", + "foo bar foobar", + "--response", + "foo,bar:int,foobar:bool", + ), step.Workdir(path), )), )) diff --git a/integration/cmd_query_test.go b/integration/cmd_query_test.go index 7a14f48902..293cbc0beb 100644 --- a/integration/cmd_query_test.go +++ b/integration/cmd_query_test.go @@ -14,14 +14,35 @@ func TestGenerateAnAppWithQuery(t *testing.T) { env.Must(env.Exec("create a query", step.NewSteps(step.New( - step.Exec("starport", "s", "query", "foo", "text", "vote:int", "like:bool", "-r", "foo,bar:int,foobar:bool"), + step.Exec( + "starport", + "s", + "query", + "foo", + "text", + "vote:int", + "like:bool", + "-r", + "foo,bar:int,foobar:bool", + ), step.Workdir(path), )), )) env.Must(env.Exec("create a paginated query", step.NewSteps(step.New( - step.Exec("starport", "s", "query", "bar", "text", "vote:int", "like:bool", "-r", "foo,bar:int,foobar:bool", "--paginated"), + step.Exec( + "starport", + "s", + "query", + "bar", + "text", + "vote:int", + "like:bool", + "-r", + "foo,bar:int,foobar:bool", + "--paginated", + ), step.Workdir(path), )), )) @@ -50,7 +71,19 @@ func TestGenerateAnAppWithQuery(t *testing.T) { env.Must(env.Exec("create a query in a module", step.NewSteps(step.New( - step.Exec("starport", "s", "query", "foo", "text", "--module", "foo", "--desc", "foo bar foobar", "--response", "foo,bar:int,foobar:bool"), + step.Exec( + "starport", + "s", + "query", + "foo", + "text", + "--module", + "foo", + "--desc", + "foo bar foobar", + "--response", + "foo,bar:int,foobar:bool", + ), step.Workdir(path), )), )) diff --git a/starport/cmd/scaffold_module.go b/starport/cmd/scaffold_module.go index d9710d6994..c8dacab2b3 100644 --- a/starport/cmd/scaffold_module.go +++ b/starport/cmd/scaffold_module.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "strings" "github.com/spf13/cobra" "github.com/tendermint/starport/starport/pkg/clispinner" @@ -12,9 +13,11 @@ import ( "github.com/tendermint/starport/starport/pkg/validation" "github.com/tendermint/starport/starport/services/scaffolder" "github.com/tendermint/starport/starport/templates/module" + modulecreate "github.com/tendermint/starport/starport/templates/module/create" ) const ( + flagDep = "dep" flagIBC = "ibc" flagIBCOrdering = "ordering" flagRequireRegistration = "require-registration" @@ -53,6 +56,7 @@ func NewScaffoldModule() *cobra.Command { Args: cobra.MinimumNArgs(1), RunE: scaffoldModuleHandler, } + c.Flags().StringSlice(flagDep, []string{}, "module dependencies (e.g. --dep account,bank)") c.Flags().Bool(flagIBC, false, "scaffold an IBC module") c.Flags().String(flagIBCOrdering, "none", "channel ordering of the IBC module [none|ordered|unordered]") c.Flags().Bool(flagRequireRegistration, false, "if true command will fail if module can't be registered") @@ -86,6 +90,32 @@ func scaffoldModuleHandler(cmd *cobra.Command, args []string) error { options = append(options, scaffolder.WithIBCChannelOrdering(ibcOrdering), scaffolder.WithIBC()) } + // Get module dependencies + dependencies, err := cmd.Flags().GetStringSlice(flagDep) + if err != nil { + return err + } + if len(dependencies) > 0 { + var formattedDependencies []modulecreate.Dependency + + // Parse the provided dependencies + for _, dependency := range dependencies { + var formattedDependency modulecreate.Dependency + + splitted := strings.Split(dependency, ":") + switch len(splitted) { + case 1: + formattedDependency = modulecreate.NewDependency(splitted[0], "") + case 2: + formattedDependency = modulecreate.NewDependency(splitted[0], splitted[1]) + default: + return fmt.Errorf("dependency %s is invalid, must have or .", dependency) + } + formattedDependencies = append(formattedDependencies, formattedDependency) + } + options = append(options, scaffolder.WithDependencies(formattedDependencies)) + } + sc, err := scaffolder.New(appPath) if err != nil { return err @@ -111,6 +141,30 @@ func scaffoldModuleHandler(cmd *cobra.Command, args []string) error { fmt.Println(sourceModificationToString(sm)) } + if len(dependencies) > 0 { + dependencyWarning(dependencies) + } + io.Copy(cmd.OutOrStdout(), &msg) return nil } + +// in previously scaffolded apps gov keeper is defined below the scaffolded module keeper definition +// therefore we must warn the user to manually move the definition if it's the case +// https://github.com/tendermint/starport/issues/818#issuecomment-865736052 +const govWarning = `⚠️ If your app has been scaffolded with Starport 0.16.x or below +Please make sure that your module keeper definition is defined after gov module keeper definition in app/app.go: + +app.GovKeeper = ... +... +[your module keeper definition] +` + +// dependencyWarning is used to print a warning if gov is provided as a dependency +func dependencyWarning(dependencies []string) { + for _, dep := range dependencies { + if dep == "gov" { + fmt.Print(govWarning) + } + } +} diff --git a/starport/pkg/cosmosanalysis/app/app.go b/starport/pkg/cosmosanalysis/app/app.go new file mode 100644 index 0000000000..73951e223c --- /dev/null +++ b/starport/pkg/cosmosanalysis/app/app.go @@ -0,0 +1,71 @@ +package app + +import ( + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + + "github.com/tendermint/starport/starport/pkg/cosmosanalysis" +) + +var appImplementation = []string{ + "RegisterAPIRoutes", + "RegisterTxService", + "RegisterTendermintService", +} + +// CheckKeeper checks for the existence of the keeper with the provided name in the app structure +func CheckKeeper(path, keeperName string) error { + // find app type + appImpl, err := cosmosanalysis.FindImplementation(path, appImplementation) + if err != nil { + return err + } + if len(appImpl) != 1 { + return errors.New("app.go should contain a single app") + } + appTypeName := appImpl[0] + + // Inspect the module for app struct + var found bool + fileSet := token.NewFileSet() + pkgs, err := parser.ParseDir(fileSet, path, nil, 0) + if err != nil { + return err + } + for _, pkg := range pkgs { + for _, f := range pkg.Files { + ast.Inspect(f, func(n ast.Node) bool { + // look for struct methods. + appType, ok := n.(*ast.TypeSpec) + if !ok || appType.Name.Name != appTypeName { + return true + } + + appStruct, ok := appType.Type.(*ast.StructType) + if !ok { + return true + } + + // Search for the keeper specific field + for _, field := range appStruct.Fields.List { + for _, fieldName := range field.Names { + if fieldName.Name == keeperName { + found = true + return false + } + } + } + + return false + }) + } + } + + if !found { + return fmt.Errorf("app doesn't contain %s", keeperName) + } + return nil +} diff --git a/starport/pkg/cosmosanalysis/app/app_test.go b/starport/pkg/cosmosanalysis/app/app_test.go new file mode 100644 index 0000000000..e312dccbb8 --- /dev/null +++ b/starport/pkg/cosmosanalysis/app/app_test.go @@ -0,0 +1,88 @@ +package app_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/pkg/cosmosanalysis/app" +) + +var ( + AppFile = []byte(` +package foo + +type Foo struct { + FooKeeper foo.keeper +} + +func (f Foo) RegisterAPIRoutes() {} +func (f Foo) RegisterTxService() {} +func (f Foo) RegisterTendermintService() {} +`) + + NoAppFile = []byte(` +package foo + +type Bar struct { + FooKeeper foo.keeper +} +`) + + TwoAppFile = []byte(` +package foo + +type Foo struct { + FooKeeper foo.keeper +} + +func (f Foo) RegisterAPIRoutes() {} +func (f Foo) RegisterTxService() {} +func (f Foo) RegisterTendermintService() {} + +type Bar struct { + FooKeeper foo.keeper +} + +func (f Bar) RegisterAPIRoutes() {} +func (f Bar) RegisterTxService() {} +func (f Bar) RegisterTendermintService() {} +`) +) + +func TestCheckKeeper(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "app_test") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + + // Test with a source file containing an app + tmpFile := filepath.Join(tmpDir, "app.go") + err = os.WriteFile(tmpFile, AppFile, 0644) + require.NoError(t, err) + + err = app.CheckKeeper(tmpDir, "FooKeeper") + require.NoError(t, err) + err = app.CheckKeeper(tmpDir, "BarKeeper") + require.Error(t, err) + + // No app in source must return an error + tmpDirNoApp, err := os.MkdirTemp("", "app_test") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + tmpFileNoApp := filepath.Join(tmpDirNoApp, "app.go") + err = os.WriteFile(tmpFileNoApp, NoAppFile, 0644) + require.NoError(t, err) + err = app.CheckKeeper(tmpDirNoApp, "FooKeeper") + require.Error(t, err) + + // More than one app must return an error + tmpDirTwoApp, err := os.MkdirTemp("", "app_test") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + tmpFileTwoApp := filepath.Join(tmpDirTwoApp, "app.go") + err = os.WriteFile(tmpFileTwoApp, TwoAppFile, 0644) + require.NoError(t, err) + err = app.CheckKeeper(tmpDirTwoApp, "FooKeeper") + require.Error(t, err) +} diff --git a/starport/pkg/cosmosanalysis/cosmosanalysis.go b/starport/pkg/cosmosanalysis/cosmosanalysis.go new file mode 100644 index 0000000000..b5f08c9c7d --- /dev/null +++ b/starport/pkg/cosmosanalysis/cosmosanalysis.go @@ -0,0 +1,92 @@ +// Package cosmosanalysis provides a toolset for staticly analysing Cosmos SDK's +// source code and blockchain source codes based on the Cosmos SDK +package cosmosanalysis + +import ( + "go/ast" + "go/parser" + "go/token" +) + +// implementation tracks the implementation of an interface for a given struct +type implementation map[string]bool + +// FindImplementation finds the name of all types that implement the provided interface +func FindImplementation(modulePath string, interfaceList []string) (found []string, err error) { + // parse go packages/files under path + fset := token.NewFileSet() + + // collect all structs under path to find out the ones that satisfies the implementation + structImplementations := make(map[string]implementation) + pkgs, err := parser.ParseDir(fset, modulePath, nil, 0) + if err != nil { + return nil, err + } + for _, pkg := range pkgs { + for _, f := range pkg.Files { + ast.Inspect(f, func(n ast.Node) bool { + // look for struct methods. + methodDecl, ok := n.(*ast.FuncDecl) + if !ok { + return true + } + + // not a method. + if methodDecl.Recv == nil { + return true + } + + methodName := methodDecl.Name.Name + + // find the struct name that method belongs to. + t := methodDecl.Recv.List[0].Type + ident, ok := t.(*ast.Ident) + if !ok { + sexp, ok := t.(*ast.StarExpr) + if !ok { + return true + } + ident = sexp.X.(*ast.Ident) + } + structName := ident.Name + + // mark the implementation that this struct satisfies. + if _, ok := structImplementations[structName]; !ok { + structImplementations[structName] = newImplementation(interfaceList) + } + + structImplementations[structName][methodName] = true + + return true + }) + } + } + + // append structs that satisfy the implementation + for name, impl := range structImplementations { + if checkImplementation(impl) { + found = append(found, name) + } + } + + return found, nil +} + +// newImplementation returns a new object to parse implementation of an interface +func newImplementation(interfaceList []string) implementation { + impl := make(implementation) + for _, m := range interfaceList { + impl[m] = false + } + return impl +} + +// checkImplementation checks if the entire implementation is satisfied +func checkImplementation(r implementation) bool { + for _, ok := range r { + if !ok { + return false + } + } + return true +} diff --git a/starport/pkg/cosmosanalysis/cosmosanalysis_test.go b/starport/pkg/cosmosanalysis/cosmosanalysis_test.go new file mode 100644 index 0000000000..a9d628a583 --- /dev/null +++ b/starport/pkg/cosmosanalysis/cosmosanalysis_test.go @@ -0,0 +1,70 @@ +package cosmosanalysis_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/pkg/cosmosanalysis" +) + +var ( + expectedinterface = []string{"foo", "bar", "foobar"} + + file1 = []byte(` +package foo + +type Foo struct {} +func (f Foo) foo() {} +func (f Foo) bar() {} +func (f Foo) foobar() {} + +type Bar struct {} +func (b Bar) foo() {} +func (b Bar) bar() {} +func (b Bar) barfoo() {} +`) + + file2 = []byte(` +package foo + +type Foobar struct {} +func (f Foobar) foo() {} +func (f Foobar) bar() {} +func (f Foobar) foobar() {} +func (f Foobar) barfoo() {} +`) +) + +func TestFindImplementation(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cosmosanalysis_test") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + + f1 := filepath.Join(tmpDir, "1.go") + err = os.WriteFile(f1, file1, 0644) + require.NoError(t, err) + f2 := filepath.Join(tmpDir, "2.go") + err = os.WriteFile(f2, file2, 0644) + require.NoError(t, err) + + // find in dir + found, err := cosmosanalysis.FindImplementation(tmpDir, expectedinterface) + require.NoError(t, err) + require.Len(t, found, 2) + require.Contains(t, found, "Foo") + require.Contains(t, found, "Foobar") + + // empty directory + emptyDir, err := os.MkdirTemp("", "cosmosanalysis_test") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(emptyDir) }) + found, err = cosmosanalysis.FindImplementation(emptyDir, expectedinterface) + require.NoError(t, err) + require.Empty(t, found) + + // can't provide file + _, err = cosmosanalysis.FindImplementation(filepath.Join(tmpDir, "1.go"), expectedinterface) + require.Error(t, err) +} diff --git a/starport/pkg/cosmosanalysis/docs.go b/starport/pkg/cosmosanalysis/docs.go deleted file mode 100644 index c59f3f80d1..0000000000 --- a/starport/pkg/cosmosanalysis/docs.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package cosmosanalysis provides a toolset for staticly analysing Cosmos SDK's -// source code and blockchain source codes based on the Cosmos SDK. -package cosmosanalysis diff --git a/starport/pkg/cosmosanalysis/module/message.go b/starport/pkg/cosmosanalysis/module/message.go index 0e071f3ae4..7204016d0b 100644 --- a/starport/pkg/cosmosanalysis/module/message.go +++ b/starport/pkg/cosmosanalysis/module/message.go @@ -1,84 +1,11 @@ package module -import ( - "go/ast" - "go/parser" - "go/token" -) - -// DiscoverMessages discovers sdk messages defined in a module that resides under modulePath. -func DiscoverMessages(modulePath string) (msgs []string, err error) { - // parse go packages/files under modulePath. - fset := token.NewFileSet() - - pkgs, err := parser.ParseDir(fset, modulePath, nil, 0) - if err != nil { - return nil, err - } - - // collect all structs under modulePath to find out the ones that satisfy requirements. - structs := make(map[string]requirements) - - for _, pkg := range pkgs { - for _, f := range pkg.Files { - ast.Inspect(f, func(n ast.Node) bool { - // look for struct methods. - fdecl, ok := n.(*ast.FuncDecl) - if !ok { - return true - } - - // not a method. - if fdecl.Recv == nil { - return true - } - - // fname is the name of method. - fname := fdecl.Name.Name - - // find the struct name that method belongs to. - t := fdecl.Recv.List[0].Type - sident, ok := t.(*ast.Ident) - if !ok { - sexp, ok := t.(*ast.StarExpr) - if !ok { - return true - } - sident = sexp.X.(*ast.Ident) - } - sname := sident.Name - - // mark the requirement that this struct satisfies. - if _, ok := structs[sname]; !ok { - structs[sname] = newRequirements() - } - - structs[sname][fname] = true - - return true - }) - } - } - - // checkRequirements checks if all requirements are satisfied. - checkRequirements := func(r requirements) bool { - for _, ok := range r { - if !ok { - return false - } - } - return true - } - - for name, reqs := range structs { - if checkRequirements(reqs) { - msgs = append(msgs, name) - } - } - - if len(msgs) == 0 { - return nil, ErrModuleNotFound - } - - return msgs, nil +// messageImplementation is the list of methods needed for a sdk.Msg implementation +// TODO(low priority): dynamically get these from the source code of underlying version of the sdk. +var messageImplementation = []string{ + "Route", + "Type", + "GetSigners", + "GetSignBytes", + "ValidateBasic", } diff --git a/starport/pkg/cosmosanalysis/module/module.go b/starport/pkg/cosmosanalysis/module/module.go index 6d80caf459..fffa8c9ffc 100644 --- a/starport/pkg/cosmosanalysis/module/module.go +++ b/starport/pkg/cosmosanalysis/module/module.go @@ -2,33 +2,15 @@ package module import ( "context" - "errors" "fmt" "path/filepath" "strings" + "github.com/tendermint/starport/starport/pkg/cosmosanalysis" "github.com/tendermint/starport/starport/pkg/gomodule" "github.com/tendermint/starport/starport/pkg/protoanalysis" ) -// ErrModuleNotFound error returned when an sdk module cannot be found. -var ErrModuleNotFound = errors.New("sdk module not found") - -// requirements holds a list of sdk.Msg's method names. -type requirements map[string]bool - -// newRequirements creates a new list of requirements(method names) that needed by a sdk.Msg impl. -// TODO(low priority): dynamically get these from the source code of underlying version of the sdk. -func newRequirements() requirements { - return requirements{ - "Route": false, - "Type": false, - "GetSigners": false, - "GetSignBytes": false, - "ValidateBasic": false, - } -} - // Msgs is a module import path-sdk msgs pair. type Msgs map[string][]string @@ -136,13 +118,14 @@ func (d *moduleDiscoverer) discover(pkg protoanalysis.Package) (Module, error) { pkgrelpath := strings.TrimPrefix(pkg.GoImportPath(), d.basegopath) pkgpath := filepath.Join(d.sourcePath, pkgrelpath) - msgs, err := DiscoverMessages(pkgpath) - if err == ErrModuleNotFound { - return Module{}, nil - } + msgs, err := cosmosanalysis.FindImplementation(pkgpath, messageImplementation) if err != nil { return Module{}, err } + if len(msgs) == 0 { + // No message means the module has not been found + return Module{}, nil + } namesplit := strings.Split(pkg.Name, ".") m := Module{ diff --git a/starport/pkg/placeholder/error.go b/starport/pkg/placeholder/error.go new file mode 100644 index 0000000000..9d056d6e42 --- /dev/null +++ b/starport/pkg/placeholder/error.go @@ -0,0 +1,87 @@ +package placeholder + +import ( + "fmt" + "strings" + + "github.com/tendermint/starport/starport/pkg/validation" +) + +var _ validation.Error = (*MissingPlaceholdersError)(nil) + +// MissingPlaceholdersError is used as an error when a source file is missing placeholder +type MissingPlaceholdersError struct { + missing iterableStringSet + additionalInfo string + additionalErrors error +} + +// Is true if both errors have the same list of missing placeholders. +func (e *MissingPlaceholdersError) Is(err error) bool { + other, ok := err.(*MissingPlaceholdersError) + if !ok { + return false + } + if len(other.missing) != len(e.missing) { + return false + } + for i := range e.missing { + if e.missing[i] != other.missing[i] { + return false + } + } + return true +} + +// Error implements error interface +func (e *MissingPlaceholdersError) Error() string { + var b strings.Builder + b.WriteString("missing placeholders: ") + e.missing.Iterate(func(i int, element string) bool { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(element) + return true + }) + return b.String() +} + +// ValidationInfo implements validation.Error interface +func (e *MissingPlaceholdersError) ValidationInfo() string { + var b strings.Builder + b.WriteString("Missing placeholders:\n\n") + e.missing.Iterate(func(i int, element string) bool { + if i > 0 { + b.WriteString("\n") + } + b.WriteString(element) + return true + }) + if e.additionalInfo != "" { + b.WriteString("\n\n") + b.WriteString(e.additionalInfo) + } + if e.additionalErrors != nil { + b.WriteString("\n\nAdditional errors: ") + b.WriteString(e.additionalErrors.Error()) + } + return b.String() +} + +var _ validation.Error = (*ValidationMiscError)(nil) + +// ValidationMiscError is used as a miscellaneous error related to validation +type ValidationMiscError struct { + errors []string +} + +// Error implements error interface +func (e *ValidationMiscError) Error() string { + return fmt.Sprintf("validation errors: %v", e.errors) +} + +// ValidationInfo implements validation.Error interface +func (e *ValidationMiscError) ValidationInfo() string { + return fmt.Sprintf("Validation errors:\n\n%v", strings.Join(e.errors, "\n")) +} diff --git a/starport/pkg/placeholder/tracer.go b/starport/pkg/placeholder/tracer.go index 26bee87b2d..641634a2f0 100644 --- a/starport/pkg/placeholder/tracer.go +++ b/starport/pkg/placeholder/tracer.go @@ -2,8 +2,6 @@ package placeholder import ( "strings" - - "github.com/tendermint/starport/starport/pkg/validation" ) type iterableStringSet map[string]struct{} @@ -22,60 +20,6 @@ func (set iterableStringSet) Add(item string) { set[item] = struct{}{} } -var _ validation.Error = (*MissingPlaceholdersError)(nil) - -type MissingPlaceholdersError struct { - missing iterableStringSet - additionalInfo string -} - -// Is true if both errors have the same list of missing placeholders. -func (e *MissingPlaceholdersError) Is(err error) bool { - other, ok := err.(*MissingPlaceholdersError) - if !ok { - return false - } - if len(other.missing) != len(e.missing) { - return false - } - for i := range e.missing { - if e.missing[i] != other.missing[i] { - return false - } - } - return true -} - -func (e *MissingPlaceholdersError) Error() string { - var b strings.Builder - b.WriteString("missing placeholders: ") - e.missing.Iterate(func(i int, element string) bool { - if i > 0 { - b.WriteString(", ") - } - b.WriteString(element) - return true - }) - return b.String() -} - -func (e *MissingPlaceholdersError) ValidationInfo() string { - var b strings.Builder - b.WriteString("Missing placeholders:\n\n") - e.missing.Iterate(func(i int, element string) bool { - if i > 0 { - b.WriteString("\n") - } - b.WriteString(element) - return true - }) - if e.additionalInfo != "" { - b.WriteString("\n\n") - b.WriteString(e.additionalInfo) - } - return b.String() -} - // Option for configuring session. type Option func(*Tracer) @@ -98,11 +42,13 @@ func New(opts ...Option) *Tracer { type Replacer interface { Replace(content, placeholder, replacement string) string ReplaceOnce(content, placeholder, replacement string) string + AppendMiscError(miscError string) } -// Tracer keeps track of missing placeholders. +// Tracer keeps track of missing placeholders or other issues related to file modification. type Tracer struct { missing iterableStringSet + miscErrors []string additionalInfo string } @@ -125,14 +71,33 @@ func (t *Tracer) ReplaceOnce(content, placeholder, replacement string) string { return content } +// AppendMiscError allows to track errors not related to missing placeholders during file modification +func (t *Tracer) AppendMiscError(miscError string) { + t.miscErrors = append(t.miscErrors, miscError) +} + // Err if any of the placeholders were missing during execution. func (t *Tracer) Err() error { + // miscellaneous errors represent errors preventing source modification not related to missing placeholder + var miscErrors error + if len(t.miscErrors) > 0 { + miscErrors = &ValidationMiscError{ + errors: t.miscErrors, + } + } + if len(t.missing) > 0 { missing := iterableStringSet{} for key := range t.missing { missing.Add(key) } - return &MissingPlaceholdersError{missing: missing, additionalInfo: t.additionalInfo} + return &MissingPlaceholdersError{ + missing: missing, + additionalInfo: t.additionalInfo, + additionalErrors: miscErrors, + } } - return nil + + // if not missing placeholder but still miscellaneous errors, return them + return miscErrors } diff --git a/starport/services/scaffolder/module.go b/starport/services/scaffolder/module.go index f929b85ff4..b4a3c3b7ff 100644 --- a/starport/services/scaffolder/module.go +++ b/starport/services/scaffolder/module.go @@ -14,6 +14,7 @@ import ( "github.com/gobuffalo/genny" "github.com/tendermint/starport/starport/pkg/cmdrunner" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" + appanalysis "github.com/tendermint/starport/starport/pkg/cosmosanalysis/app" "github.com/tendermint/starport/starport/pkg/cosmosver" "github.com/tendermint/starport/starport/pkg/gocmd" "github.com/tendermint/starport/starport/pkg/gomodulepath" @@ -32,7 +33,7 @@ var ( const ( wasmImport = "github.com/CosmWasm/wasmd" - apppkg = "app" + appPkg = "app" moduleDir = "x" wasmVersion = "v0.16.0" ) @@ -44,6 +45,9 @@ type moduleCreationOptions struct { // homePath of the chain's config dir. ibcChannelOrdering string + + // list of module depencies + dependencies []modulecreate.Dependency } // ModuleCreationOption configures Chain. @@ -70,6 +74,13 @@ func WithIBCChannelOrdering(ordering string) ModuleCreationOption { } } +// WithDependencies specifies the name of the modules that the module depends on +func WithDependencies(dependencies []modulecreate.Dependency) ModuleCreationOption { + return func(m *moduleCreationOptions) { + m.dependencies = dependencies + } +} + // CreateModule creates a new empty module in the scaffolded app func (s *Scaffolder) CreateModule( tracer *placeholder.Tracer, @@ -101,17 +112,24 @@ func (s *Scaffolder) CreateModule( for _, apply := range options { apply(&creationOpts) } + + // Check dependencies + if err := checkDependencies(creationOpts.dependencies); err != nil { + return sm, err + } + path, err := gomodulepath.ParseAt(s.path) if err != nil { return sm, err } opts := &modulecreate.CreateOptions{ - ModuleName: moduleName, - ModulePath: path.RawPath, - AppName: path.Package, - OwnerName: owner(path.RawPath), - IsIBC: creationOpts.ibc, - IBCOrdering: creationOpts.ibcChannelOrdering, + ModuleName: moduleName, + ModulePath: path.RawPath, + AppName: path.Package, + OwnerName: owner(path.RawPath), + IsIBC: creationOpts.ibc, + IBCOrdering: creationOpts.ibcChannelOrdering, + Dependencies: creationOpts.dependencies, } if opts.IsIBC { ibcPlaceholder, err := checkIBCRouterPlaceholder(s.path) @@ -143,6 +161,7 @@ func (s *Scaffolder) CreateModule( return sm, err } + // Modify app.go to register the module newSourceModification, runErr := xgenny.RunWithValidation(tracer, modulecreate.NewStargateAppModify(tracer, opts)) sm.Merge(newSourceModification) var validationErr validation.Error @@ -255,7 +274,7 @@ func checkModuleName(moduleName string) error { } func isWasmImported(appPath string) (bool, error) { - abspath, err := filepath.Abs(filepath.Join(appPath, apppkg)) + abspath, err := filepath.Abs(filepath.Join(appPath, appPkg)) if err != nil { return false, err } @@ -313,3 +332,27 @@ func checkIBCRouterPlaceholder(appPath string) (bool, error) { return strings.Contains(string(content), module.PlaceholderIBCAppRouter), nil } + +// checkDependencies perform checks on the dependencies +func checkDependencies(dependencies []modulecreate.Dependency) error { + depMap := make(map[string]struct{}) + for _, dep := range dependencies { + // check the dependency has been registered + if err := appanalysis.CheckKeeper(module.PathAppModule, dep.KeeperName); err != nil { + return fmt.Errorf( + "the module cannot have %s as a dependency: %s", + dep.Name, + err.Error(), + ) + } + + // check duplicated + _, ok := depMap[dep.Name] + if ok { + return fmt.Errorf("%s is a duplicated dependency", dep) + } + depMap[dep.Name] = struct{}{} + } + + return nil +} diff --git a/starport/templates/app/stargate/app/app.go.plush b/starport/templates/app/stargate/app/app.go.plush index 74329fd1fb..21da847028 100644 --- a/starport/templates/app/stargate/app/app.go.plush +++ b/starport/templates/app/stargate/app/app.go.plush @@ -142,6 +142,7 @@ var ( stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, govtypes.ModuleName: {authtypes.Burner}, ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + // this line is used by starport scaffolding # stargate/app/maccPerms } ) @@ -316,13 +317,13 @@ func New( // If evidence needs to be handled for the app, set routes in router here and seal app.EvidenceKeeper = *evidenceKeeper - // this line is used by starport scaffolding # stargate/app/keeperDefinition - app.GovKeeper = govkeeper.NewKeeper( appCodec, keys[govtypes.StoreKey], app.GetSubspace(govtypes.ModuleName), app.AccountKeeper, app.BankKeeper, &stakingKeeper, govRouter, ) + // this line is used by starport scaffolding # stargate/app/keeperDefinition + // Create static IBC router, add transfer route, then set and seal it ibcRouter := porttypes.NewRouter() ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferModule) diff --git a/starport/templates/module/const.go b/starport/templates/module/const.go index e4e144802e..c314a08aa1 100644 --- a/starport/templates/module/const.go +++ b/starport/templates/module/const.go @@ -1,6 +1,6 @@ package module const ( - PathAppGo = "app/app.go" - PathExportGo = "app/export.go" + PathAppModule = "app" + PathAppGo = "app/app.go" ) diff --git a/starport/templates/module/create/ibc.go b/starport/templates/module/create/ibc.go index 658fd0de2a..47a2ed9c71 100644 --- a/starport/templates/module/create/ibc.go +++ b/starport/templates/module/create/ibc.go @@ -34,6 +34,7 @@ func NewIBC(replacer placeholder.Replacer, opts *CreateOptions) (*genny.Generato ctx.Set("ownerName", opts.OwnerName) ctx.Set("ibcOrdering", opts.IBCOrdering) ctx.Set("title", strings.Title) + ctx.Set("dependencies", opts.Dependencies) // Used for proto package name ctx.Set("formatOwnerName", xstrings.FormatUsername) diff --git a/starport/templates/module/create/ibc/x/{{moduleName}}/keeper/keeper_test.go.plush b/starport/templates/module/create/ibc/x/{{moduleName}}/keeper/keeper_test.go.plush index 1dfbb62abb..0b5af28f9a 100644 --- a/starport/templates/module/create/ibc/x/{{moduleName}}/keeper/keeper_test.go.plush +++ b/starport/templates/module/create/ibc/x/{{moduleName}}/keeper/keeper_test.go.plush @@ -27,8 +27,13 @@ func setupKeeper(t testing.TB) (*Keeper, sdk.Context) { registry := codectypes.NewInterfaceRegistry() keeper := NewKeeper( - codec.NewProtoCodec(registry), storeKey, memStoreKey, - nil, nil, nil, + codec.NewProtoCodec(registry), + storeKey, + memStoreKey, + nil, + nil, + nil,<%= for (dependency) in dependencies { %> + nil,<% } %> ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) diff --git a/starport/templates/module/create/options.go b/starport/templates/module/create/options.go index 3403d57601..e25d3a234e 100644 --- a/starport/templates/module/create/options.go +++ b/starport/templates/module/create/options.go @@ -1,5 +1,10 @@ package modulecreate +import ( + "fmt" + "strings" +) + // CreateOptions represents the options to scaffold a Cosmos SDK module type CreateOptions struct { ModuleName string @@ -12,6 +17,9 @@ type CreateOptions struct { // Channel ordering of the IBC module: ordered, unordered or none IBCOrdering string + + // Dependencies of the module + Dependencies []Dependency } // CreateOptions defines options to add MsgServer @@ -26,3 +34,21 @@ type MsgServerOptions struct { func (opts *CreateOptions) Validate() error { return nil } + +// Dependency represents a module dependency of a module +type Dependency struct { + Name string + KeeperName string // KeeperName represents the name of the keeper for the module in app.go +} + +// NewDependency returns a new dependency object +func NewDependency(name, keeperName string) Dependency { + // Default keeper name + if keeperName == "" { + keeperName = fmt.Sprintf("%sKeeper", strings.Title(name)) + } + return Dependency{ + name, + keeperName, + } +} diff --git a/starport/templates/module/create/stargate.go b/starport/templates/module/create/stargate.go index 64c9bed158..9963392bfc 100644 --- a/starport/templates/module/create/stargate.go +++ b/starport/templates/module/create/stargate.go @@ -27,6 +27,7 @@ func NewStargate(opts *CreateOptions) (*genny.Generator, error) { ctx.Set("appName", opts.AppName) ctx.Set("ownerName", opts.OwnerName) ctx.Set("title", strings.Title) + ctx.Set("dependencies", opts.Dependencies) // Used for proto package name ctx.Set("formatOwnerName", xstrings.FormatUsername) @@ -94,6 +95,25 @@ func appModifyStargate(replacer placeholder.Replacer, opts *CreateOptions) genny replacement = fmt.Sprintf(template, module.PlaceholderSgAppStoreKey, opts.ModuleName) content = replacer.Replace(content, module.PlaceholderSgAppStoreKey, replacement) + // Module dependencies + var depArgs string + for _, dep := range opts.Dependencies { + depArgs = fmt.Sprintf("%sapp.%s,\n", depArgs, dep.KeeperName) + + // If bank is a dependency, add account permissions to the module + if dep.Name == "bank" { + template = `%[1]v + %[2]vmoduletypes.ModuleName: {authtypes.Minter, authtypes.Burner, authtypes.Staking},` + + replacement = fmt.Sprintf( + template, + module.PlaceholderSgAppMaccPerms, + opts.ModuleName, + ) + content = replacer.Replace(content, module.PlaceholderSgAppMaccPerms, replacement) + } + } + // Keeper definition var scopedKeeperDefinition string var ibcKeeperArgument string @@ -103,15 +123,16 @@ func appModifyStargate(replacer placeholder.Replacer, opts *CreateOptions) genny scopedKeeperDefinition = module.PlaceholderIBCAppScopedKeeperDefinition ibcKeeperArgument = module.PlaceholderIBCAppKeeperArgument } - template = `%[1]v - %[3]v + template = `%[3]v app.%[5]vKeeper = *%[2]vmodulekeeper.NewKeeper( appCodec, keys[%[2]vmoduletypes.StoreKey], keys[%[2]vmoduletypes.MemStoreKey], %[4]v - ) - %[2]vModule := %[2]vmodule.NewAppModule(appCodec, app.%[5]vKeeper)` + %[6]v) + %[2]vModule := %[2]vmodule.NewAppModule(appCodec, app.%[5]vKeeper) + + %[1]v` replacement = fmt.Sprintf( template, module.PlaceholderSgAppKeeperDefinition, @@ -119,6 +140,7 @@ func appModifyStargate(replacer placeholder.Replacer, opts *CreateOptions) genny scopedKeeperDefinition, ibcKeeperArgument, strings.Title(opts.ModuleName), + depArgs, ) content = replacer.Replace(content, module.PlaceholderSgAppKeeperDefinition, replacement) diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper.go.plush index 39ec79bade..4262629d82 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper.go.plush @@ -17,6 +17,8 @@ type ( storeKey sdk.StoreKey memKey sdk.StoreKey // this line is used by starport scaffolding # ibc/keeper/attribute + <%= for (dependency) in dependencies { %> + <%= dependency.Name %>Keeper types.<%= title(dependency.Name) %>Keeper<% } %> } ) @@ -25,12 +27,14 @@ func NewKeeper( storeKey, memKey sdk.StoreKey, // this line is used by starport scaffolding # ibc/keeper/parameter + <%= for (dependency) in dependencies { %><%= dependency.Name %>Keeper types.<%= title(dependency.Name) %>Keeper,<% } %> ) *Keeper { return &Keeper{ cdc: cdc, storeKey: storeKey, memKey: memKey, // this line is used by starport scaffolding # ibc/keeper/return + <%= for (dependency) in dependencies { %><%= dependency.Name %>Keeper: <%= dependency.Name %>Keeper,<% } %> } } diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper_test.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper_test.go.plush index c6fad1b860..026f54a75a 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper_test.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper_test.go.plush @@ -26,7 +26,12 @@ func setupKeeper(t testing.TB) (*Keeper, sdk.Context) { require.NoError(t, stateStore.LoadLatestVersion()) registry := codectypes.NewInterfaceRegistry() - keeper := NewKeeper(codec.NewProtoCodec(registry), storeKey, memStoreKey) + keeper := NewKeeper( + codec.NewProtoCodec(registry), + storeKey, + memStoreKey,<%= for (dependency) in dependencies { %> + nil,<% } %> + ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) return keeper, ctx diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/types/expected_keepers.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/types/expected_keepers.go.plush new file mode 100644 index 0000000000..74e14e8e3c --- /dev/null +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/types/expected_keepers.go.plush @@ -0,0 +1,7 @@ +package types + +<%= for (dependency) in dependencies { %> +type <%= title(dependency.Name) %>Keeper interface { + // Methods imported from <%= dependency.Name %> should be defined here +} +<% } %> \ No newline at end of file diff --git a/starport/templates/module/placeholders.go b/starport/templates/module/placeholders.go index 7b87829472..6883ffe064 100644 --- a/starport/templates/module/placeholders.go +++ b/starport/templates/module/placeholders.go @@ -29,6 +29,7 @@ const ( PlaceholderSgAppNewArgument = "// this line is used by starport scaffolding # stargate/app/newArgument" PlaceholderSgAppScopedKeeper = "// this line is used by starport scaffolding # stargate/app/scopedKeeper" PlaceholderSgAppBeforeInitReturn = "// this line is used by starport scaffolding # stargate/app/beforeInitReturn" + PlaceholderSgAppMaccPerms = "// this line is used by starport scaffolding # stargate/app/maccPerms" // Placeholders in Stargate app.go for wasm PlaceholderSgWasmAppEnabledProposals = "// this line is used by starport scaffolding # stargate/wasm/app/enabledProposals"