diff --git a/changelog.md b/changelog.md index 0d15025733..e5aae8d3c2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,19 @@ # Changelog +## [`v0.19.5`](https://github.com/tendermint/starport/pull/2158/commits) + +### Features + +- Enable client code and Vuex code generation for query only modules as well. +- Upgraded the Vue template to `v0.3.5`. + +### Fixes: +- Fixed snake case in code generation. +- Fixed plugin installations for Go =>v1.18. + +### Changes: +- Dropped transpilation of TS to JS. Code generation now only produces TS files. + ## `v0.19.4` ### Features diff --git a/go.mod b/go.mod index 86195dc780..84aeea9fac 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/tendermint/spn v0.1.1-0.20211210094128-4ca78a240c57 github.com/tendermint/tendermint v0.34.14 github.com/tendermint/tm-db v0.6.4 - github.com/tendermint/vue v0.3.0 + github.com/tendermint/vue v0.3.5 golang.org/x/mod v0.4.2 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf diff --git a/go.sum b/go.sum index afe76a95de..09667d6b20 100644 --- a/go.sum +++ b/go.sum @@ -1508,8 +1508,8 @@ github.com/tendermint/tm-db v0.6.2/go.mod h1:GYtQ67SUvATOcoY8/+x6ylk8Qo02BQyLrAs github.com/tendermint/tm-db v0.6.3/go.mod h1:lfA1dL9/Y/Y8wwyPp2NMLyn5P5Ptr/gvDFNWtrCWSf8= github.com/tendermint/tm-db v0.6.4 h1:3N2jlnYQkXNQclQwd/eKV/NzlqPlfK21cpRRIx80XXQ= github.com/tendermint/tm-db v0.6.4/go.mod h1:dptYhIpJ2M5kUuenLr+Yyf3zQOv1SgBZcl8/BmWlMBw= -github.com/tendermint/vue v0.3.0 h1:jllMEhdq3r7pIwLCNh7Aj7CXmOG3mqHCKeRMelohjwQ= -github.com/tendermint/vue v0.3.0/go.mod h1:Sg9MGPF+uY+SJ79sdZgtC2LnH+FDU2qWuiRxoZn5bmw= +github.com/tendermint/vue v0.3.5 h1:PTZaW0+7/4lRJyUtuZOxFzMCahNLowTadVoCZns2Wmw= +github.com/tendermint/vue v0.3.5/go.mod h1:Sg9MGPF+uY+SJ79sdZgtC2LnH+FDU2qWuiRxoZn5bmw= github.com/tetafro/godot v1.4.9/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= diff --git a/integration/cosmosgen/cosmosgen_test.go b/integration/cosmosgen/cosmosgen_test.go new file mode 100644 index 0000000000..e41882fc06 --- /dev/null +++ b/integration/cosmosgen/cosmosgen_test.go @@ -0,0 +1,153 @@ +package cosmosgen_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + envtest "github.com/tendermint/starport/integration" + "github.com/tendermint/starport/starport/pkg/cmdrunner/step" +) + +func TestCosmosGen(t *testing.T) { + var ( + env = envtest.New(t) + path = env.Scaffold("blog") + dirGenerated = filepath.Join(path, "vue/src/store/generated") + ) + + const ( + withMsgModuleName = "withmsg" + withoutMsgModuleName = "withoutmsg" + ) + + env.Must(env.Exec("add custom module with message", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "module", + withMsgModuleName, + ), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("create a message", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "message", + "mymessage", + "myfield1", + "myfield2:bool", + "--module", + withMsgModuleName, + ), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("add custom module without message", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "module", + withoutMsgModuleName, + ), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("create a type", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "type", + "mytype", + "mytypefield", + "--module", + withoutMsgModuleName, + ), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("create a query", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "query", + "myQuery", + "mytypefield", + "--module", + withoutMsgModuleName, + ), + step.Workdir(path), + )), + )) + + require.NoError(t, os.RemoveAll(dirGenerated)) + + env.Must(env.Exec("generate vuex", + step.NewSteps(step.New( + step.Exec( + "starport", + "g", + "vuex", + "--proto-all-modules", + ), + step.Workdir(path), + )), + )) + + var expectedCosmosModules = []string{ + "cosmos.auth.v1beta1", + "cosmos.bank.v1beta1", + "cosmos.base.tendermint.v1beta1", + "cosmos.crisis.v1beta1", + "cosmos.distribution.v1beta1", + "cosmos.evidence.v1beta1", + "cosmos.feegrant.v1beta1", + "cosmos.gov.v1beta1", + "cosmos.mint.v1beta1", + "cosmos.params.v1beta1", + "cosmos.slashing.v1beta1", + "cosmos.staking.v1beta1", + "cosmos.tx.v1beta1", + "cosmos.upgrade.v1beta1", + "cosmos.vesting.v1beta1", + } + + var expectedCustomModules = []string{ + "test.blog.blog", + "test.blog.withmsg", + "test.blog.withoutmsg", + } + + for _, chainModule := range expectedCustomModules { + _, statErr := os.Stat(filepath.Join(dirGenerated, "test/blog", chainModule)) + require.False(t, os.IsNotExist(statErr), fmt.Sprintf("the %s vuex store should have be generated", chainModule)) + require.NoError(t, statErr) + } + + chainDir, err := os.ReadDir(filepath.Join(dirGenerated, "test/blog")) + require.Equal(t, len(expectedCustomModules), len(chainDir), "no extra modules should have been generated for test/blog") + require.NoError(t, err) + + for _, cosmosModule := range expectedCosmosModules { + _, statErr := os.Stat(filepath.Join(dirGenerated, "cosmos/cosmos-sdk", cosmosModule)) + require.False(t, os.IsNotExist(statErr), fmt.Sprintf("the %s code generation for module should have be made", cosmosModule)) + require.NoError(t, statErr) + } + + cosmosDirs, err := os.ReadDir(filepath.Join(dirGenerated, "cosmos/cosmos-sdk")) + require.Equal(t, len(expectedCosmosModules), len(cosmosDirs), "no extra modules should have been generated for cosmos/cosmos-sdk") + require.NoError(t, err) +} diff --git a/scripts/data/gen-nodetime/nodetime b/scripts/data/gen-nodetime/nodetime index f5465f1c80..19be05b792 100755 --- a/scripts/data/gen-nodetime/nodetime +++ b/scripts/data/gen-nodetime/nodetime @@ -8,7 +8,6 @@ main(); function main() { switch (mode) { case "ts-proto": require("ts-proto/protoc-gen-ts_proto"); return; - case "tsc": require("typescript/bin/tsc"); return; case "sta": require("swagger-typescript-api/index"); return; case "swagger-combine": require("swagger-combine/bin/swagger-combine"); return; case "ibc-setup": require("@confio/relayer/build/binary/ibc-setup/index"); return; diff --git a/starport/pkg/cosmosanalysis/app/app.go b/starport/pkg/cosmosanalysis/app/app.go index 73951e223c..456de33798 100644 --- a/starport/pkg/cosmosanalysis/app/app.go +++ b/starport/pkg/cosmosanalysis/app/app.go @@ -8,6 +8,7 @@ import ( "go/token" "github.com/tendermint/starport/starport/pkg/cosmosanalysis" + "github.com/tendermint/starport/starport/pkg/goanalysis" ) var appImplementation = []string{ @@ -69,3 +70,162 @@ func CheckKeeper(path, keeperName string) error { } return nil } + +// FindRegisteredModules looks for all the registered modules in the App +// It finds activated modules by checking if imported modules are registered in the app and also checking if their query clients are registered +// It does so by: +// 1. Mapping out all the imports and named imports +// 2. Looking for the call to module.NewBasicManager and finds the modules registered there +// 3. Looking for the implementation of RegisterAPIRoutes and find the modules that call their RegisterGRPCGatewayRoutes +func FindRegisteredModules(chainRoot string) ([]string, error) { + appFilePath, err := cosmosanalysis.FindAppFilePath(chainRoot) + if err != nil { + return nil, err + } + + fileSet := token.NewFileSet() + f, err := parser.ParseFile(fileSet, appFilePath, nil, 0) + if err != nil { + return []string{}, err + } + + packages, err := goanalysis.FindImportedPackages(appFilePath) + if err != nil { + return nil, err + } + + basicManagerModule, err := findBasicManagerModule(packages) + if err != nil { + return nil, err + } + + var basicModules []string + ast.Inspect(f, func(n ast.Node) bool { + if pkgsReg := findBasicManagerRegistrations(n, basicManagerModule); pkgsReg != nil { + for _, rp := range pkgsReg { + importModule := packages[rp] + basicModules = append(basicModules, importModule) + } + + return false + } + + if pkgsReg := findRegisterAPIRoutersRegistrations(n); pkgsReg != nil { + for _, rp := range pkgsReg { + importModule := packages[rp] + if importModule == "" { + continue + } + basicModules = append(basicModules, importModule) + } + + return false + } + + return true + }) + + return basicModules, nil +} + +func findBasicManagerRegistrations(n ast.Node, basicManagerModule string) []string { + callExprType, ok := n.(*ast.CallExpr) + if !ok { + return nil + } + + selectorExprType, ok := callExprType.Fun.(*ast.SelectorExpr) + if !ok { + return nil + } + + identExprType, ok := selectorExprType.X.(*ast.Ident) + if !ok || identExprType.Name != basicManagerModule || selectorExprType.Sel.Name != "NewBasicManager" { + return nil + } + + packagesRegistered := make([]string, len(callExprType.Args)) + for i, arg := range callExprType.Args { + argAsCompositeLitType, ok := arg.(*ast.CompositeLit) + if ok { + compositeTypeSelectorExpr, ok := argAsCompositeLitType.Type.(*ast.SelectorExpr) + if !ok { + continue + } + + compositeTypeX, ok := compositeTypeSelectorExpr.X.(*ast.Ident) + if ok { + packagesRegistered[i] = compositeTypeX.Name + continue + } + } + + argAsCallType, ok := arg.(*ast.CallExpr) + if ok { + argAsFunctionType, ok := argAsCallType.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + + argX, ok := argAsFunctionType.X.(*ast.Ident) + if ok { + packagesRegistered[i] = argX.Name + } + } + } + + return packagesRegistered +} + +func findBasicManagerModule(pkgs map[string]string) (string, error) { + for mod, pkg := range pkgs { + if pkg == "github.com/cosmos/cosmos-sdk/types/module" { + return mod, nil + } + } + + return "", errors.New("no module for BasicManager was found") +} + +func findRegisterAPIRoutersRegistrations(n ast.Node) []string { + funcLitType, ok := n.(*ast.FuncDecl) + if !ok { + return nil + } + + if funcLitType.Name.Name != "RegisterAPIRoutes" { + return nil + } + + var packagesRegistered []string + for _, stmt := range funcLitType.Body.List { + exprStmt, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + + exprCall, ok := exprStmt.X.(*ast.CallExpr) + if !ok { + continue + } + + exprFun, ok := exprCall.Fun.(*ast.SelectorExpr) + if !ok || exprFun.Sel.Name != "RegisterGRPCGatewayRoutes" { + continue + } + + identType, ok := exprFun.X.(*ast.Ident) + if !ok { + continue + } + + pkgName := identType.Name + if pkgName == "" { + continue + } + + packagesRegistered = append(packagesRegistered, identType.Name) + } + + return packagesRegistered +} diff --git a/starport/pkg/cosmosanalysis/app/app_test.go b/starport/pkg/cosmosanalysis/app/app_test.go index e312dccbb8..ab921047a6 100644 --- a/starport/pkg/cosmosanalysis/app/app_test.go +++ b/starport/pkg/cosmosanalysis/app/app_test.go @@ -20,6 +20,13 @@ type Foo struct { func (f Foo) RegisterAPIRoutes() {} func (f Foo) RegisterTxService() {} func (f Foo) RegisterTendermintService() {} +func (f Foo) Name() string { return app.BaseApp.Name() } +func (f Foo) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return app.mm.BeginBlock(ctx, req) +} +func (f Foo) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + return app.mm.EndBlock(ctx, req) +} `) NoAppFile = []byte(` @@ -40,6 +47,13 @@ type Foo struct { func (f Foo) RegisterAPIRoutes() {} func (f Foo) RegisterTxService() {} func (f Foo) RegisterTendermintService() {} +func (f Foo) Name() string { return app.BaseApp.Name() } +func (f Foo) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return app.mm.BeginBlock(ctx, req) +} +func (f Foo) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + return app.mm.EndBlock(ctx, req) +} type Bar struct { FooKeeper foo.keeper @@ -48,6 +62,173 @@ type Bar struct { func (f Bar) RegisterAPIRoutes() {} func (f Bar) RegisterTxService() {} func (f Bar) RegisterTendermintService() {} +func (f Bar) Name() string { return app.BaseApp.Name() } +func (f Bar) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return app.mm.BeginBlock(ctx, req) +} +func (f Bar) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + return app.mm.EndBlock(ctx, req) +} +`) + FullAppFile = []byte(` +package app + +import ( + "io" + "net/http" + "os" + "path/filepath" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/server/api" + "github.com/cosmos/cosmos-sdk/server/config" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmodule "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authsims "github.com/cosmos/cosmos-sdk/x/auth/simulation" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + "github.com/cosmos/cosmos-sdk/x/bank" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/capability" + capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + "github.com/cosmos/cosmos-sdk/x/crisis" + crisiskeeper "github.com/cosmos/cosmos-sdk/x/crisis/keeper" + crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + distrclient "github.com/cosmos/cosmos-sdk/x/distribution/client" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/evidence" + evidencekeeper "github.com/cosmos/cosmos-sdk/x/evidence/keeper" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" + feegrantkeeper "github.com/cosmos/cosmos-sdk/x/feegrant/keeper" + feegrantmodule "github.com/cosmos/cosmos-sdk/x/feegrant/module" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/cosmos-sdk/x/gov" + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/mint" + mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/cosmos/cosmos-sdk/x/params" + paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" + paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + paramproposal "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + "github.com/cosmos/cosmos-sdk/x/slashing" + slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/cosmos-sdk/x/upgrade" + upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client" + upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/cosmos/ibc-go/v2/modules/apps/transfer" + ibctransferkeeper "github.com/cosmos/ibc-go/v2/modules/apps/transfer/keeper" + ibctransfertypes "github.com/cosmos/ibc-go/v2/modules/apps/transfer/types" + ibc "github.com/cosmos/ibc-go/v2/modules/core" + ibcclient "github.com/cosmos/ibc-go/v2/modules/core/02-client" + ibcclientclient "github.com/cosmos/ibc-go/v2/modules/core/02-client/client" + ibcclienttypes "github.com/cosmos/ibc-go/v2/modules/core/02-client/types" + ibcporttypes "github.com/cosmos/ibc-go/v2/modules/core/05-port/types" + ibchost "github.com/cosmos/ibc-go/v2/modules/core/24-host" + ibckeeper "github.com/cosmos/ibc-go/v2/modules/core/keeper" + "github.com/spf13/cast" + abci "github.com/tendermint/tendermint/abci/types" + tmjson "github.com/tendermint/tendermint/libs/json" + "github.com/tendermint/tendermint/libs/log" + tmos "github.com/tendermint/tendermint/libs/os" + dbm "github.com/tendermint/tm-db" + + "github.com/tendermint/starport/starport/pkg/cosmoscmd" + "github.com/tendermint/starport/starport/pkg/openapiconsole" + + "github.com/tendermint/testchain/docs" + + queryonlymodmodule "github.com/tendermint/testchain/x/queryonlymod" + queryonlymodmodulekeeper "github.com/tendermint/testchain/x/queryonlymod/keeper" + queryonlymodmoduletypes "github.com/tendermint/testchain/x/queryonlymod/types" + testchainmodule "github.com/tendermint/testchain/x/testchain" + testchainmodulekeeper "github.com/tendermint/testchain/x/testchain/keeper" + testchainmoduletypes "github.com/tendermint/testchain/x/testchain/types" +) + +type App struct {} +func (app *App) Name() string { return app.BaseApp.Name() } +func (app *App) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return app.mm.BeginBlock(ctx, req) +} +func (app *App) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + return app.mm.EndBlock(ctx, req) +} + +var ( + ModuleBasics = sdkmodule.NewBasicManager( + auth.AppModuleBasic{}, + genutil.AppModuleBasic{}, + bank.AppModuleBasic{}, + capability.AppModuleBasic{}, + staking.AppModuleBasic{}, + mint.AppModuleBasic{}, + distr.AppModuleBasic{}, + gov.NewAppModuleBasic(getGovProposalHandlers()...), + params.AppModuleBasic{}, + crisis.AppModuleBasic{}, + slashing.AppModuleBasic{}, + feegrantmodule.AppModuleBasic{}, + ibc.AppModuleBasic{}, + upgrade.AppModuleBasic{}, + evidence.AppModuleBasic{}, + transfer.AppModuleBasic{}, + vesting.AppModuleBasic{}, + testchainmodule.AppModuleBasic{}, + queryonlymodmodule.AppModuleBasic{}, + // this line is used by starport scaffolding # stargate/app/moduleBasic + ) +) + +func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig) { + clientCtx := apiSvr.ClientCtx + rpc.RegisterRoutes(clientCtx, apiSvr.Router) + authrest.RegisterTxRoutes(clientCtx, apiSvr.Router) + authtx.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + tmservice.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + + ModuleBasics.RegisterRESTRoutes(clientCtx, apiSvr.Router) + ModuleBasics.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + + apiSvr.Router.Handle("/static/openapi.yml", http.FileServer(http.FS(docs.Docs))) + apiSvr.Router.HandleFunc("/", openapiconsole.Handler(Name, "/static/openapi.yml")) +} + +func (app *App) RegisterTxService(clientCtx client.Context) { + authtx.RegisterTxService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.BaseApp.Simulate, app.interfaceRegistry) +} + +func (app *App) RegisterTendermintService(clientCtx client.Context) { + tmservice.RegisterTendermintService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.interfaceRegistry) +} + `) ) @@ -86,3 +267,41 @@ func TestCheckKeeper(t *testing.T) { err = app.CheckKeeper(tmpDirTwoApp, "FooKeeper") require.Error(t, err) } + +func TestGetRegisteredModules(t *testing.T) { + tmpDir := t.TempDir() + + tmpFile := filepath.Join(tmpDir, "app.go") + err := os.WriteFile(tmpFile, FullAppFile, 0644) + require.NoError(t, err) + + tmpNoAppFile := filepath.Join(tmpDir, "someOtherFile.go") + err = os.WriteFile(tmpNoAppFile, NoAppFile, 0644) + require.NoError(t, err) + + registeredModules, err := app.FindRegisteredModules(tmpDir) + require.NoError(t, err) + require.ElementsMatch(t, []string{ + "github.com/cosmos/cosmos-sdk/x/auth", + "github.com/cosmos/cosmos-sdk/x/genutil", + "github.com/cosmos/cosmos-sdk/x/bank", + "github.com/cosmos/cosmos-sdk/x/capability", + "github.com/cosmos/cosmos-sdk/x/staking", + "github.com/cosmos/cosmos-sdk/x/mint", + "github.com/cosmos/cosmos-sdk/x/distribution", + "github.com/cosmos/cosmos-sdk/x/gov", + "github.com/cosmos/cosmos-sdk/x/params", + "github.com/cosmos/cosmos-sdk/x/crisis", + "github.com/cosmos/cosmos-sdk/x/slashing", + "github.com/cosmos/cosmos-sdk/x/feegrant/module", + "github.com/cosmos/ibc-go/v2/modules/core", + "github.com/cosmos/cosmos-sdk/x/upgrade", + "github.com/cosmos/cosmos-sdk/x/evidence", + "github.com/cosmos/ibc-go/v2/modules/apps/transfer", + "github.com/cosmos/cosmos-sdk/x/auth/vesting", + "github.com/tendermint/testchain/x/testchain", + "github.com/tendermint/testchain/x/queryonlymod", + "github.com/cosmos/cosmos-sdk/x/auth/tx", + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice", + }, registeredModules) +} diff --git a/starport/pkg/cosmosanalysis/cosmosanalysis.go b/starport/pkg/cosmosanalysis/cosmosanalysis.go index 6683620851..de42c7c08a 100644 --- a/starport/pkg/cosmosanalysis/cosmosanalysis.go +++ b/starport/pkg/cosmosanalysis/cosmosanalysis.go @@ -7,77 +7,126 @@ import ( "go/ast" "go/parser" "go/token" + "os" + "path/filepath" + "github.com/pkg/errors" "golang.org/x/mod/modfile" ) const ( cosmosModulePath = "github.com/cosmos/cosmos-sdk" tendermintModulePath = "github.com/tendermint/tendermint" + appFileName = "app.go" + defaultAppFilePath = "app/" + appFileName ) +var appImplementation = []string{ + "Name", + "BeginBlocker", + "EndBlocker", +} + // implementation tracks the implementation of an interface for a given struct type implementation map[string]bool +// DeepFindImplementation does the same as FindImplementation, but walks recursively through the folder structure +// Useful if implementations might be in sub folders +func DeepFindImplementation(modulePath string, interfaceList []string) (found []string, err error) { + err = filepath.Walk(modulePath, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + return nil + } + + currFound, err := FindImplementation(path, interfaceList) + if err != nil { + return err + } + + found = append(found, currFound...) + return nil + }) + + if err != nil { + return nil, err + } + + return found, nil +} + // 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 { + var files []*ast.File 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 - } + files = append(files, f) + } + found = append(found, findImplementationInFiles(files, interfaceList)...) + } - // not a method. - if methodDecl.Recv == nil { - return true - } + return found, nil +} - methodName := methodDecl.Name.Name +func findImplementationInFiles(files []*ast.File, interfaceList []string) (found []string) { + // collect all structs under path to find out the ones that satisfies the implementation + structImplementations := make(map[string]implementation) - // find the struct name that method belongs to. - t := methodDecl.Recv.List[0].Type - ident, ok := t.(*ast.Ident) + for _, f := range 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 { - sexp, ok := t.(*ast.StarExpr) - if !ok { - return true - } - ident = sexp.X.(*ast.Ident) + return true } - structName := ident.Name + ident = sexp.X.(*ast.Ident) + } + structName := ident.Name - // mark the implementation that this struct satisfies. - if _, ok := structImplementations[structName]; !ok { - structImplementations[structName] = newImplementation(interfaceList) - } + // mark the implementation that this struct satisfies. + if _, ok := structImplementations[structName]; !ok { + structImplementations[structName] = newImplementation(interfaceList) + } - structImplementations[structName][methodName] = true + structImplementations[structName][methodName] = true - return true - }) - } + return true + }) } - // append structs that satisfy the implementation for name, impl := range structImplementations { if checkImplementation(impl) { found = append(found, name) } } - return found, nil + return found } // newImplementation returns a new object to parse implementation of an interface @@ -113,3 +162,69 @@ func ValidateGoMod(module *modfile.File) error { } return nil } + +// FindAppFilePath looks for the app file that implements the interfaces listed in appImplementation +func FindAppFilePath(chainRoot string) (path string, err error) { + var found []string + + err = filepath.Walk(chainRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() || filepath.Ext(info.Name()) != ".go" { + return nil + } + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + return err + } + + currFound := findImplementationInFiles([]*ast.File{f}, appImplementation) + + if len(currFound) > 0 { + found = append(found, path) + } + + return nil + }) + + if err != nil { + return "", err + } + + numFound := len(found) + if numFound == 0 { + return "", errors.New("app.go file cannot be found") + } + + if numFound == 1 { + return found[0], nil + } + + appFilePath := "" + for _, p := range found { + if filepath.Base(p) == appFileName { + if appFilePath != "" { + // multiple app.go found, fallback to app/app.go + return getDefaultAppFile(chainRoot) + } + + appFilePath = p + } + } + + if appFilePath != "" { + return appFilePath, nil + } + + return getDefaultAppFile(chainRoot) +} + +// getDefaultAppFile returns the default app.go file path for a chain. +func getDefaultAppFile(chainRoot string) (string, error) { + path := filepath.Join(chainRoot, defaultAppFilePath) + _, err := os.Stat(path) + return path, errors.Wrap(err, "cannot locate your app.go") +} diff --git a/starport/pkg/cosmosanalysis/cosmosanalysis_test.go b/starport/pkg/cosmosanalysis/cosmosanalysis_test.go index a9d628a583..30f40b5089 100644 --- a/starport/pkg/cosmosanalysis/cosmosanalysis_test.go +++ b/starport/pkg/cosmosanalysis/cosmosanalysis_test.go @@ -34,6 +34,47 @@ func (f Foobar) foo() {} func (f Foobar) bar() {} func (f Foobar) foobar() {} func (f Foobar) barfoo() {} +`) + noImplementation = []byte(` +package foo +type Foo struct {} +func (f Foo) nofoo() {} +func (f Foo) nobar() {} +func (f Foo) nofoobar() {} +`) + + partialImplementation = []byte(` +package foo +type Foo struct {} +func (f Foo) foo() {} +func (f Foo) bar() {} +`) + restOfImplementation = []byte(` +package foo +func (f Foo) foobar() {} +`) + + appFile = []byte(` +package app +type App struct {} +func (app *App) Name() string { return app.BaseApp.Name() } +func (app *App) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return app.mm.BeginBlock(ctx, req) +} +func (app *App) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + return app.mm.EndBlock(ctx, req) +} +`) + appTestFile = []byte(` +package app_test +type App struct {} +func (app *App) Name() string { return app.BaseApp.Name() } +func (app *App) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return app.mm.BeginBlock(ctx, req) +} +func (app *App) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + return app.mm.EndBlock(ctx, req) +} `) ) @@ -45,6 +86,7 @@ func TestFindImplementation(t *testing.T) { 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) @@ -68,3 +110,85 @@ func TestFindImplementation(t *testing.T) { _, err = cosmosanalysis.FindImplementation(filepath.Join(tmpDir, "1.go"), expectedinterface) require.Error(t, err) } + +func TestFindImplementationInSpreadInMultipleFiles(t *testing.T) { + tmpDir := t.TempDir() + + f1 := filepath.Join(tmpDir, "1.go") + err := os.WriteFile(f1, partialImplementation, 0644) + require.NoError(t, err) + f2 := filepath.Join(tmpDir, "2.go") + err = os.WriteFile(f2, restOfImplementation, 0644) + require.NoError(t, err) + + found, err := cosmosanalysis.FindImplementation(tmpDir, expectedinterface) + require.NoError(t, err) + require.Len(t, found, 1) + require.Contains(t, found, "Foo") +} + +func TestFindImplementationNotFound(t *testing.T) { + tmpDir1 := t.TempDir() + tmpDir2 := t.TempDir() + + noImplFile := filepath.Join(tmpDir1, "1.go") + err := os.WriteFile(noImplFile, noImplementation, 0644) + require.NoError(t, err) + partialImplFile := filepath.Join(tmpDir2, "2.go") + err = os.WriteFile(partialImplFile, partialImplementation, 0644) + require.NoError(t, err) + + // No implementation + found, err := cosmosanalysis.FindImplementation(tmpDir1, expectedinterface) + require.Len(t, found, 0) + + // Partial implementation + found, err = cosmosanalysis.FindImplementation(tmpDir2, expectedinterface) + require.Len(t, found, 0) +} + +func TestFindAppFilePath(t *testing.T) { + tmpDir := t.TempDir() + + appFolder := filepath.Join(tmpDir, "app") + secondaryAppFolder := filepath.Join(tmpDir, "myOwnAppDir") + err := os.Mkdir(appFolder, 0700) + require.NoError(t, err) + err = os.Mkdir(secondaryAppFolder, 0700) + require.NoError(t, err) + + // No file + _, err = cosmosanalysis.FindAppFilePath(tmpDir) + require.Equal(t, "app.go file cannot be found", err.Error()) + + // Only one file with app implementation + myOwnAppFilePath := filepath.Join(secondaryAppFolder, "my_own_app.go") + err = os.WriteFile(myOwnAppFilePath, appFile, 0644) + require.NoError(t, err) + pathFound, err := cosmosanalysis.FindAppFilePath(tmpDir) + require.NoError(t, err) + require.Equal(t, myOwnAppFilePath, pathFound) + + // With a test file added + appTestFilePath := filepath.Join(secondaryAppFolder, "my_own_app_test.go") + err = os.WriteFile(appTestFilePath, appTestFile, 0644) + require.NoError(t, err) + pathFound, err = cosmosanalysis.FindAppFilePath(tmpDir) + require.Contains(t, err.Error(), "cannot locate your app.go") + + // With an additional app file (that is app.go) + appFilePath := filepath.Join(appFolder, "app.go") + err = os.WriteFile(appFilePath, appFile, 0644) + require.NoError(t, err) + pathFound, err = cosmosanalysis.FindAppFilePath(tmpDir) + require.NoError(t, err) + require.Equal(t, appFilePath, pathFound) + + // With two app.go files + extraAppFilePath := filepath.Join(secondaryAppFolder, "app.go") + err = os.WriteFile(extraAppFilePath, appFile, 0644) + require.NoError(t, err) + pathFound, err = cosmosanalysis.FindAppFilePath(tmpDir) + require.NoError(t, err) + require.Equal(t, filepath.Join(appFolder, "app.go"), pathFound) +} diff --git a/starport/pkg/cosmosanalysis/module/module.go b/starport/pkg/cosmosanalysis/module/module.go index 5d5bdfad01..c9562b064d 100644 --- a/starport/pkg/cosmosanalysis/module/module.go +++ b/starport/pkg/cosmosanalysis/module/module.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/tendermint/starport/starport/pkg/cosmosanalysis" + "github.com/tendermint/starport/starport/pkg/cosmosanalysis/app" "github.com/tendermint/starport/starport/pkg/gomodule" "github.com/tendermint/starport/starport/pkg/protoanalysis" ) @@ -65,22 +66,22 @@ type Type struct { } type moduleDiscoverer struct { - sourcePath string - protoPath string - basegopath string + sourcePath string + protoPath string + basegopath string + registeredModules []string } -// Discover discovers and returns modules and their types that implements sdk.Msg. -// sourcePath is the root path of an sdk blockchain. +// Discover discovers and returns modules and their types that are registered in the app +// chainRoot is the root path of the chain +// sourcePath is the root path of the go module which the proto dir is from // -// discovery algorithm make use of proto definitions to discover modules inside the blockchain. -// -// checking whether a type implements sdk.Msg is done by running a simple algorithm of comparing method names -// of each type in a package with sdk.Msg's, which satisfies our needs for the time being. -// for a more opinionated check: -// - go/types.Implements() might be utilized and as needed. -// - instead of just comparing method names, their full signatures can be compared. -func Discover(ctx context.Context, sourcePath, protoDir string) ([]Module, error) { +// discovery algorithm make use of registered modules and proto definitions to find relevant registered modules +// It does so by: +// 1. Getting all the registered go modules from the app +// 2. Parsing the proto files to find services and messages +// 3. Check if the proto services are implemented in any of the registered modules +func Discover(ctx context.Context, chainRoot, sourcePath, protoDir string) ([]Module, error) { // find out base Go import path of the blockchain. gm, err := gomodule.ParseAt(sourcePath) if err != nil { @@ -90,10 +91,29 @@ func Discover(ctx context.Context, sourcePath, protoDir string) ([]Module, error return nil, err } + registeredModules, err := app.FindRegisteredModules(chainRoot) + if err != nil { + return nil, err + } + + basegopath := gm.Module.Mod.Path + + // Just filter out the registered modules that are not possibly relevant here + potentialModules := make([]string, 0) + for _, m := range registeredModules { + if strings.HasPrefix(m, basegopath) { + potentialModules = append(potentialModules, m) + } + } + if len(potentialModules) == 0 { + return []Module{}, nil + } + md := &moduleDiscoverer{ - protoPath: filepath.Join(sourcePath, protoDir), - sourcePath: sourcePath, - basegopath: gm.Module.Mod.Path, + protoPath: filepath.Join(sourcePath, protoDir), + sourcePath: sourcePath, + basegopath: basegopath, + registeredModules: potentialModules, } // find proto packages that belong to modules under x/. @@ -101,6 +121,9 @@ func Discover(ctx context.Context, sourcePath, protoDir string) ([]Module, error if err != nil { return nil, err } + if len(pkgs) == 0 { + return []Module{}, nil + } var modules []Module @@ -121,12 +144,20 @@ func (d *moduleDiscoverer) discover(pkg protoanalysis.Package) (Module, error) { pkgrelpath := strings.TrimPrefix(pkg.GoImportPath(), d.basegopath) pkgpath := filepath.Join(d.sourcePath, pkgrelpath) + found, err := d.pkgIsFromRegisteredModule(pkg) + if err != nil { + return Module{}, err + } + if !found { + 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 + + if len(pkg.Services)+len(msgs) == 0 { return Module{}, nil } @@ -227,3 +258,41 @@ func (d *moduleDiscoverer) findModuleProtoPkgs(ctx context.Context) ([]protoanal return xprotopkgs, nil } + +// Checks if the pkg is implemented in any of the registered modules +func (d *moduleDiscoverer) pkgIsFromRegisteredModule(pkg protoanalysis.Package) (bool, error) { + for _, rm := range d.registeredModules { + implRelPath := strings.TrimPrefix(rm, d.basegopath) + implPath := filepath.Join(d.sourcePath, implRelPath) + + for _, s := range pkg.Services { + methods := make([]string, len(s.RPCFuncs)) + for i, rpcFunc := range s.RPCFuncs { + methods[i] = rpcFunc.Name + } + + found, err := cosmosanalysis.DeepFindImplementation(implPath, methods) + if err != nil { + return false, err + } + + // In some cases, the module registration is in another level of sub dir in the module. + // TODO: find the closest sub dir among proto packages. + if len(found) == 0 && strings.HasPrefix(rm, pkg.GoImportName) { + altImplRelPath := strings.TrimPrefix(pkg.GoImportName, d.basegopath) + altImplPath := filepath.Join(d.sourcePath, altImplRelPath) + found, err = cosmosanalysis.DeepFindImplementation(altImplPath, methods) + if err != nil { + return false, err + } + } + + if len(found) > 0 { + return true, nil + } + } + + } + + return false, nil +} diff --git a/starport/pkg/cosmosanalysis/module/module_test.go b/starport/pkg/cosmosanalysis/module/module_test.go index 86fdad4c61..7c15ddd0e7 100644 --- a/starport/pkg/cosmosanalysis/module/module_test.go +++ b/starport/pkg/cosmosanalysis/module/module_test.go @@ -8,6 +8,51 @@ import ( "github.com/tendermint/starport/starport/pkg/protoanalysis" ) +var testModule = Module{ + Name: "planet", + Pkg: protoanalysis.Package{ + Name: "tendermint.planet.planet", + Path: "testdata/planet/proto/planet", + Files: protoanalysis.Files{protoanalysis.File{Path: "testdata/planet/proto/planet/planet.proto", Dependencies: []string{"google/api/annotations.proto"}}}, + GoImportName: "github.com/tendermint/planet/x/planet/types", + Messages: []protoanalysis.Message{ + protoanalysis.Message{Name: "QueryMyQueryRequest", Path: "testdata/planet/proto/planet/planet.proto", HighestFieldNumber: 1}, + protoanalysis.Message{Name: "QueryMyQueryResponse", Path: "testdata/planet/proto/planet/planet.proto", HighestFieldNumber: 0}, + }, + Services: []protoanalysis.Service{ + protoanalysis.Service{ + Name: "Query", + RPCFuncs: []protoanalysis.RPCFunc{ + protoanalysis.RPCFunc{ + Name: "MyQuery", + RequestType: "QueryMyQueryRequest", + ReturnsType: "QueryMyQueryResponse", + HTTPRules: []protoanalysis.HTTPRule{ + protoanalysis.HTTPRule{ + Params: []string{"mytypefield"}, + HasQuery: false, HasBody: false}, + }, + }, + }, + }, + }, + }, + Msgs: []Msg(nil), + HTTPQueries: []HTTPQuery{ + HTTPQuery{ + Name: "MyQuery", + FullName: "QueryMyQuery", + Rules: []protoanalysis.HTTPRule{ + protoanalysis.HTTPRule{ + Params: []string{"mytypefield"}, + HasQuery: false, + HasBody: false}, + }, + }, + }, + Types: []Type(nil), +} + func TestDiscover(t *testing.T) { type args struct { sourcePath string @@ -24,24 +69,21 @@ func TestDiscover(t *testing.T) { sourcePath: "testdata/planet", protoDir: "proto", }, - want: []Module{ - {Pkg: protoanalysis.Package{}}, - }, + want: []Module{testModule}, }, { name: "test no proto folder", args: args{ sourcePath: "testdata/planet", protoDir: "", }, - want: []Module{ - {Pkg: protoanalysis.Package{}}, - }, + want: []Module{testModule}, }, { name: "test invalid proto folder", args: args{ sourcePath: "testdata/planet", protoDir: "invalid", }, + want: []Module{}, }, { name: "test invalid folder", args: args{ @@ -60,7 +102,7 @@ func TestDiscover(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Discover(context.Background(), tt.args.sourcePath, tt.args.protoDir) + got, err := Discover(context.Background(), "testdata/planet", tt.args.sourcePath, tt.args.protoDir) require.NoError(t, err) require.Equal(t, tt.want, got) }) diff --git a/starport/pkg/cosmosanalysis/module/testdata/planet/app/app.go b/starport/pkg/cosmosanalysis/module/testdata/planet/app/app.go new file mode 100644 index 0000000000..f59a45c617 --- /dev/null +++ b/starport/pkg/cosmosanalysis/module/testdata/planet/app/app.go @@ -0,0 +1,23 @@ +package app + +import ( + "github.com/cosmos/cosmos-sdk/types/module" + planet "github.com/tendermint/planet/x/planet" +) + +type Foo struct { + FooKeeper foo.keeper +} + +var ModuleBasics = module.NewBasicManager(planet.AppModuleBasic{}) + +func (f Foo) Name() string { return app.BaseApp.Name() } +func (f Foo) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return app.mm.BeginBlock(ctx, req) +} +func (f Foo) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + return app.mm.EndBlock(ctx, req) +} +func (f Foo) RegisterAPIRoutes() {} +func (f Foo) RegisterTxService() {} +func (f Foo) RegisterTendermintService() {} diff --git a/starport/pkg/cosmosanalysis/module/testdata/planet/proto/planet/planet.proto b/starport/pkg/cosmosanalysis/module/testdata/planet/proto/planet/planet.proto index 4e2a1d1837..9a464ab23c 100644 --- a/starport/pkg/cosmosanalysis/module/testdata/planet/proto/planet/planet.proto +++ b/starport/pkg/cosmosanalysis/module/testdata/planet/proto/planet/planet.proto @@ -1,4 +1,17 @@ syntax = "proto3"; package tendermint.planet.planet; +import "google/api/annotations.proto"; option go_package = "github.com/tendermint/planet/x/planet/types"; +service Query { + rpc MyQuery(QueryMyQueryRequest) returns (QueryMyQueryResponse) { + option (google.api.http).get = "/tendermint/planet/withoutmsg/my_query/{mytypefield}"; + } +} + +message QueryMyQueryRequest { + string mytypefield = 1; +} + +message QueryMyQueryResponse { +} \ No newline at end of file diff --git a/starport/pkg/cosmosanalysis/module/testdata/planet/x/planet/keeper/grpc_query_my_query.go b/starport/pkg/cosmosanalysis/module/testdata/planet/x/planet/keeper/grpc_query_my_query.go new file mode 100644 index 0000000000..cea2898e53 --- /dev/null +++ b/starport/pkg/cosmosanalysis/module/testdata/planet/x/planet/keeper/grpc_query_my_query.go @@ -0,0 +1,13 @@ +package keeper + +import ( + "context" + + "github.com/tendermint/planet/x/planet/types" +) + +type Keeper struct{} + +func (k Keeper) MyQuery(goCtx context.Context, req *types.QueryMyQueryRequest) (*types.QueryMyQueryResponse, error) { + return nil, nil +} diff --git a/starport/pkg/cosmosanalysis/module/testdata/planet/x/planet/types/types.go b/starport/pkg/cosmosanalysis/module/testdata/planet/x/planet/types/types.go index ab1254f4c2..cecbfe04c3 100644 --- a/starport/pkg/cosmosanalysis/module/testdata/planet/x/planet/types/types.go +++ b/starport/pkg/cosmosanalysis/module/testdata/planet/x/planet/types/types.go @@ -1 +1,4 @@ package types + +type QueryMyQueryRequest struct{} +type QueryMyQueryResponse struct{} diff --git a/starport/pkg/cosmosgen/generate.go b/starport/pkg/cosmosgen/generate.go index 0913b83de7..70924bb35e 100644 --- a/starport/pkg/cosmosgen/generate.go +++ b/starport/pkg/cosmosgen/generate.go @@ -101,7 +101,7 @@ func (g *generator) resolveInclude(path string) (paths []string, err error) { func (g *generator) discoverModules(path, protoDir string) ([]module.Module, error) { var filteredModules []module.Module - modules, err := module.Discover(g.ctx, path, protoDir) + modules, err := module.Discover(g.ctx, g.appPath, path, protoDir) if err != nil { return nil, err } diff --git a/starport/pkg/cosmosgen/generate_javascript.go b/starport/pkg/cosmosgen/generate_javascript.go index 2520b6d308..2d2e3ddcc6 100644 --- a/starport/pkg/cosmosgen/generate_javascript.go +++ b/starport/pkg/cosmosgen/generate_javascript.go @@ -13,7 +13,6 @@ import ( "github.com/tendermint/starport/starport/pkg/localfs" "github.com/tendermint/starport/starport/pkg/nodetime/programs/sta" tsproto "github.com/tendermint/starport/starport/pkg/nodetime/programs/ts-proto" - "github.com/tendermint/starport/starport/pkg/nodetime/programs/tsc" "github.com/tendermint/starport/starport/pkg/protoc" "github.com/tendermint/starport/starport/pkg/xstrings" "golang.org/x/sync/errgroup" @@ -25,7 +24,7 @@ var ( } jsOpenAPIOut = []string{ - "--openapiv2_out=logtostderr=true,allow_merge=true,Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types:.", + "--openapiv2_out=logtostderr=true,allow_merge=true,json_names_for_fields=false,Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types:.", } ) @@ -106,7 +105,7 @@ func (g *jsGenerator) generateModule(ctx context.Context, tsprotoPluginPath, app m.Pkg.Path, includePaths, tsOut, - protoc.Plugin(tsprotoPluginPath), + protoc.Plugin(tsprotoPluginPath, "--ts_proto_opt=snakeToCamel=false"), ) if err != nil { return err @@ -153,8 +152,8 @@ func (g *jsGenerator) generateModule(ctx context.Context, tsprotoPluginPath, app return err } } - // generate .js and .d.ts files for all ts files. - return tsc.Generate(g.g.ctx, tscConfig(storeDirPath+"/**/*.ts")) + + return nil } func (g *jsGenerator) generateVuexModuleLoader() error { @@ -209,20 +208,9 @@ func (g *jsGenerator) generateVuexModuleLoader() error { }) } - loaderPath := filepath.Join(g.g.o.vuexStoreRootPath, "index.ts") - if err := templateVuexRoot.Write(g.g.o.vuexStoreRootPath, "", data); err != nil { return err } - return tsc.Generate(g.g.ctx, tscConfig(loaderPath)) -} - -func tscConfig(include ...string) tsc.Config { - return tsc.Config{ - Include: include, - CompilerOptions: tsc.CompilerOptions{ - Declaration: true, - }, - } + return nil } diff --git a/starport/pkg/cosmosgen/generate_openapi.go b/starport/pkg/cosmosgen/generate_openapi.go index 77c663ad79..863839c19d 100644 --- a/starport/pkg/cosmosgen/generate_openapi.go +++ b/starport/pkg/cosmosgen/generate_openapi.go @@ -12,7 +12,7 @@ import ( ) var openAPIOut = []string{ - "--openapiv2_out=logtostderr=true,allow_merge=true,fqn_for_openapi_name=true,simple_operation_ids=true,Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types:.", + "--openapiv2_out=logtostderr=true,allow_merge=true,json_names_for_fields=false,fqn_for_openapi_name=true,simple_operation_ids=true,Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types:.", } func generateOpenAPISpec(g *generator) error { diff --git a/starport/pkg/cosmosgen/install.go b/starport/pkg/cosmosgen/install.go index 8b45e14978..f0f4e5f495 100644 --- a/starport/pkg/cosmosgen/install.go +++ b/starport/pkg/cosmosgen/install.go @@ -11,6 +11,19 @@ import ( // InstallDependencies installs protoc dependencies needed by Cosmos ecosystem. func InstallDependencies(ctx context.Context, appPath string) error { + plugins := []string{ + // installs the gocosmos plugin. + "github.com/regen-network/cosmos-proto/protoc-gen-gocosmos", + + // install Go code generation plugin. + "github.com/golang/protobuf/protoc-gen-go", + + // install grpc-gateway plugins. + "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway", + "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger", + "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2", + } + errb := &bytes.Buffer{} err := cmdrunner. New( @@ -18,22 +31,8 @@ func InstallDependencies(ctx context.Context, appPath string) error { cmdrunner.DefaultWorkdir(appPath), ). Run(ctx, - step.New( - step.Exec( - "go", - "get", - // installs the gocosmos plugin. - "github.com/regen-network/cosmos-proto/protoc-gen-gocosmos", - - // install Go code generation plugin. - "github.com/golang/protobuf/protoc-gen-go", - - // install grpc-gateway plugins. - "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway", - "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger", - "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2", - ), - ), + step.New(step.Exec("go", append([]string{"get"}, plugins...)...)), + step.New(step.Exec("go", append([]string{"install"}, plugins...)...)), ) return errors.Wrap(err, errb.String()) } diff --git a/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl b/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl index 6ddd8004f7..fbab80788a 100644 --- a/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl +++ b/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl @@ -1,6 +1,4 @@ import { txClient, queryClient, MissingWalletError , registry} from './module' -// @ts-ignore -import { SpVuexError } from '@starport/vuex' {{ range .Module.Types }}import { {{ .Name }} } from "./module/types/{{ resolveFile .FilePath }}" {{ end }} @@ -110,7 +108,7 @@ export default { const sub=JSON.parse(subscription) await dispatch(sub.action, sub.payload) }catch(e) { - throw new SpVuexError('Subscriptions: ' + e.message) + throw new Error('Subscriptions: ' + e.message) } }) }, @@ -156,7 +154,7 @@ export default { if (subscribe) commit('SUBSCRIBE', { action: '{{ $FullName }}{{ $n }}', payload: { options: { all }, params: {...key},query }}) return getters['get{{ $Name }}']( { params: {...key}, query}) ?? {} } catch (e) { - throw new SpVuexError('QueryClient:{{ $FullName }}{{ $n }}', 'API Node Unavailable. Could not perform query: ' + e.message) + throw new Error('QueryClient:{{ $FullName }}{{ $n }} API Node Unavailable. Could not perform query: ' + e.message) } }, @@ -171,9 +169,9 @@ export default { return result } catch (e) { if (e == MissingWalletError) { - throw new SpVuexError('TxClient:{{ .Name }}:Init', 'Could not initialize signing client. Wallet is required.') + throw new Error('TxClient:{{ .Name }}:Init Could not initialize signing client. Wallet is required.') }else{ - throw new SpVuexError('TxClient:{{ .Name }}:Send', 'Could not broadcast Tx: '+ e.message) + throw new Error('TxClient:{{ .Name }}:Send Could not broadcast Tx: '+ e.message) } } }, @@ -185,10 +183,9 @@ export default { return msg } catch (e) { if (e == MissingWalletError) { - throw new SpVuexError('TxClient:{{ .Name }}:Init', 'Could not initialize signing client. Wallet is required.') + throw new Error('TxClient:{{ .Name }}:Init Could not initialize signing client. Wallet is required.') }else{ - throw new SpVuexError('TxClient:{{ .Name }}:Create', 'Could not create message: ' + e.message) - + throw new Error('TxClient:{{ .Name }}:Create Could not create message: ' + e.message) } } }, diff --git a/starport/pkg/goanalysis/goanalysis.go b/starport/pkg/goanalysis/goanalysis.go index f1f3d6f254..da519a9ee8 100644 --- a/starport/pkg/goanalysis/goanalysis.go +++ b/starport/pkg/goanalysis/goanalysis.go @@ -70,3 +70,29 @@ func DiscoverOneMain(path string) (pkgPath string, err error) { return pkgPaths[0], nil } + +// FindImportedPackages finds the imported packages in a Go file and returns a map +// with package name, import path pair. +func FindImportedPackages(name string) (map[string]string, error) { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, name, nil, 0) + if err != nil { + return nil, err + } + + packages := make(map[string]string) // name -> import + for _, imp := range f.Imports { + var importName string + if imp.Name != nil { + importName = imp.Name.Name + } else { + importParts := strings.Split(imp.Path.Value, "/") + importName = importParts[len(importParts)-1] + } + + name := strings.Trim(importName, "\"") + packages[name] = strings.Trim(imp.Path.Value, "\"") + } + + return packages, nil +} diff --git a/starport/pkg/goanalysis/goanalysis_test.go b/starport/pkg/goanalysis/goanalysis_test.go new file mode 100644 index 0000000000..53ac0401f2 --- /dev/null +++ b/starport/pkg/goanalysis/goanalysis_test.go @@ -0,0 +1,181 @@ +package goanalysis_test + +import ( + "errors" + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/pkg/goanalysis" + "os" + "path/filepath" + "testing" +) + +var ( + MainFile = []byte(`package main`) + ImportFile = []byte(` +package app + +import ( + "io" + "net/http" + "path/filepath" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/server/api" + "github.com/cosmos/cosmos-sdk/server/config" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/cosmos/cosmos-sdk/simapp" + queryonlymodmodule "github.com/tendermint/testchain/x/queryonlymod" + queryonlymodmodulekeeper "github.com/tendermint/testchain/x/queryonlymod/keeper" + queryonlymodmoduletypes "github.com/tendermint/testchain/x/queryonlymod/types" +) +`) +) + +func TestDiscoverMain(t *testing.T) { + tests := []struct { + name string + mainFiles []string + expectFind bool + }{ + { + name: "single main", + mainFiles: []string{"main.go"}, + expectFind: true, + }, + { + name: "no mains", + mainFiles: []string{}, + expectFind: false, + }, + { + name: "single main in sub-folder", + mainFiles: []string{"sub/main.go"}, + expectFind: true, + }, + { + name: "single main with different name", + mainFiles: []string{"sub/somethingelse.go"}, + expectFind: true, + }, + { + name: "multiple mains", + mainFiles: []string{ + "main.go", + "sub/main.go", + "diffSub/alsomain.go", + }, + expectFind: true, + }, + { + name: "single main with wrong extension", + mainFiles: []string{"main.ogg"}, + expectFind: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + want, err := createMainFiles(tmpDir, tt.mainFiles) + require.NoError(t, err) + + actual, err := goanalysis.DiscoverMain(tmpDir) + require.NoError(t, err) + if !tt.expectFind { + want = []string{} + } + require.ElementsMatch(t, actual, want) + }) + } +} + +func TestDiscoverOneMain(t *testing.T) { + tests := []struct { + name string + mainFiles []string + err error + }{ + { + name: "single main", + mainFiles: []string{"main.go"}, + err: nil, + }, + { + name: "multiple mains", + mainFiles: []string{ + "main.go", + "sub/main.go", + }, + err: goanalysis.ErrMultipleMainPackagesFound, + }, + { + name: "no mains", + mainFiles: []string{}, + err: errors.New("main package cannot be found"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + want, err := createMainFiles(tmpDir, tt.mainFiles) + require.NoError(t, err) + + actual, err := goanalysis.DiscoverOneMain(tmpDir) + + require.Equal(t, tt.err, err) + + if tt.err == nil { + require.Equal(t, 1, len(want)) + require.Equal(t, want[0], actual) + } + }) + } +} + +func createMainFiles(tmpDir string, mainFiles []string) (pathsWithMain []string, err error) { + for _, mf := range mainFiles { + mainFile := filepath.Join(tmpDir, mf) + dir := filepath.Dir(mainFile) + + if err = os.MkdirAll(dir, 0770); err != nil { + return nil, err + } + + if err = os.WriteFile(mainFile, MainFile, 0644); err != nil { + return nil, err + } + + pathsWithMain = append(pathsWithMain, dir) + } + + return pathsWithMain, nil +} + +func TestFindImportedPackages(t *testing.T) { + tmpDir := t.TempDir() + + tmpFile := filepath.Join(tmpDir, "app.go") + err := os.WriteFile(tmpFile, ImportFile, 0644) + require.NoError(t, err) + + packages, err := goanalysis.FindImportedPackages(tmpFile) + require.NoError(t, err) + require.EqualValues(t, packages, map[string]string{ + "io": "io", + "http": "net/http", + "filepath": "path/filepath", + "baseapp": "github.com/cosmos/cosmos-sdk/baseapp", + "client": "github.com/cosmos/cosmos-sdk/client", + "types": "github.com/cosmos/cosmos-sdk/codec/types", + "api": "github.com/cosmos/cosmos-sdk/server/api", + "config": "github.com/cosmos/cosmos-sdk/server/config", + "servertypes": "github.com/cosmos/cosmos-sdk/server/types", + "simapp": "github.com/cosmos/cosmos-sdk/simapp", + "queryonlymodmodule": "github.com/tendermint/testchain/x/queryonlymod", + "queryonlymodmodulekeeper": "github.com/tendermint/testchain/x/queryonlymod/keeper", + "queryonlymodmoduletypes": "github.com/tendermint/testchain/x/queryonlymod/types", + }) +} diff --git a/starport/pkg/nodetime/nodetime.go b/starport/pkg/nodetime/nodetime.go index 960cb04d41..746fb9180a 100644 --- a/starport/pkg/nodetime/nodetime.go +++ b/starport/pkg/nodetime/nodetime.go @@ -19,9 +19,6 @@ const ( // CommandTSProto is https://github.com/stephenh/ts-proto. CommandTSProto CommandName = "ts-proto" - // CommandTSC is https://github.com/microsoft/TypeScript. - CommandTSC CommandName = "tsc" - // CommandSTA is https://github.com/acacode/swagger-typescript-api. CommandSTA CommandName = "sta" diff --git a/starport/pkg/nodetime/programs/swagger-combine/swagger-combine.go b/starport/pkg/nodetime/programs/swagger-combine/swagger-combine.go index f6f462e1ee..d9f7ad77e2 100644 --- a/starport/pkg/nodetime/programs/swagger-combine/swagger-combine.go +++ b/starport/pkg/nodetime/programs/swagger-combine/swagger-combine.go @@ -28,6 +28,9 @@ type API struct { ID string `json:"-"` URL string `json:"url"` OperationIDs OperationIDs `json:"operationIds"` + Dereference struct { + Circular string `json:"circular"` + } `json:"dereference"` } type OperationIDs struct { @@ -63,6 +66,10 @@ func (c *Config) AddSpec(id, path string) error { ID: id, URL: path, OperationIDs: OperationIDs{Rename: rename}, + // Added due to https://github.com/maxdome/swagger-combine/pull/110 after enabling more services to be generated in #835 + Dereference: struct { + Circular string `json:"circular"` + }(struct{ Circular string }{Circular: "ignore"}), }) return nil diff --git a/starport/pkg/nodetime/programs/tsc/tsc.go b/starport/pkg/nodetime/programs/tsc/tsc.go deleted file mode 100644 index 398b9640b3..0000000000 --- a/starport/pkg/nodetime/programs/tsc/tsc.go +++ /dev/null @@ -1,86 +0,0 @@ -package tsc - -import ( - "context" - "os" - "path/filepath" - - "github.com/imdario/mergo" - "github.com/tendermint/starport/starport/pkg/cmdrunner/exec" - "github.com/tendermint/starport/starport/pkg/confile" - "github.com/tendermint/starport/starport/pkg/nodetime" -) - -const nodeModulesPath = "/snapshot/gen-nodetime/node_modules" - -var ( - defaultConfig = func() Config { - return Config{ - CompilerOptions: CompilerOptions{ - BaseURL: nodeModulesPath, - ModuleResolution: "node", - Target: "es2020", - Module: "es2020", - TypeRoots: []string{filepath.Join(nodeModulesPath, "@types")}, - SkipLibCheck: true, - }, - } - } - tsconfig = func(dir string) string { return filepath.Join(dir, "tsconfig.json") } -) - -// Config represents tsconfig.json. -type Config struct { - Include []string `json:"include"` - CompilerOptions CompilerOptions `json:"compilerOptions"` -} - -// CompilerOptions section of tsconfig.json. -type CompilerOptions struct { - BaseURL string `json:"baseUrl"` - ModuleResolution string `json:"moduleResolution"` - Target string `json:"target"` - Module string `json:"module"` - TypeRoots []string `json:"typeRoots"` - Declaration bool `json:"declaration"` - SkipLibCheck bool `json:"skipLibCheck"` -} - -// Generate transpiles TS into JS by given TS config. -func Generate(ctx context.Context, config Config) error { - command, cleanup, err := nodetime.Command(nodetime.CommandTSC) - if err != nil { - return err - } - defer cleanup() - - dconfig := defaultConfig() - - if err := mergo.Merge(&dconfig, config, mergo.WithOverride); err != nil { - return err - } - - // save the config into a temp file in the fs. - dir, err := os.MkdirTemp("", "") - if err != nil { - return err - } - defer os.RemoveAll(dir) - - path := tsconfig(dir) - - if err := confile. - New(confile.DefaultJSONEncodingCreator, path). - Save(dconfig); err != nil { - return err - } - - // command constructs the tsc command. - command = append(command, []string{ - "-b", - path, - }...) - - // execute the command. - return exec.Exec(ctx, command, exec.IncludeStdLogsToError()) -} diff --git a/starport/pkg/protoc/protoc.go b/starport/pkg/protoc/protoc.go index 06b5e3d018..f2b6705131 100644 --- a/starport/pkg/protoc/protoc.go +++ b/starport/pkg/protoc/protoc.go @@ -22,12 +22,14 @@ type Option func(*configs) type configs struct { pluginPath string isGeneratedDepsEnabled bool + pluginOptions []string } // Plugin configures a plugin for code generation. -func Plugin(path string) Option { +func Plugin(path string, options ...string) Option { return func(c *configs) { c.pluginPath = path + c.pluginOptions = options } } @@ -73,6 +75,7 @@ func Command() (command Cmd, cleanup func(), err error) { // Generate generates code into outDir from protoPath and its includePaths by using plugins provided with protocOuts. func Generate(ctx context.Context, outDir, protoPath string, includePaths, protocOuts []string, options ...Option) error { c := configs{} + for _, o := range options { o(&c) } @@ -89,7 +92,6 @@ func Generate(ctx context.Context, outDir, protoPath string, includePaths, proto if c.pluginPath != "" { command = append(command, "--plugin", c.pluginPath) } - var existentIncludePaths []string // skip if a third party proto source actually doesn't exist on the filesystem. @@ -115,6 +117,7 @@ func Generate(ctx context.Context, outDir, protoPath string, includePaths, proto for _, out := range protocOuts { command := append(command, out) command = append(command, files...) + command = append(command, c.pluginOptions...) if err := exec.Exec(ctx, command, exec.StepOption(step.Workdir(outDir)),