diff --git a/changelog.md b/changelog.md index 199548e5db..5bb9ed92ac 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ - [#4689](https://github.com/ignite/cli/pull/4689) Revert `HasGenesis` implementation from retracted `core` v1 to SDK `HasGenesis` interface. - [#4701](https://github.com/ignite/cli/pull/4701) Improve `ignite doctor` by removing manual migration step. Additionally, remove protoc to buf migrations logic. +- [#4702](https://github.com/ignite/cli/pull/4702) Improve app detection by checking for inheritance instead of interface implementation. ### Bug Fixes diff --git a/docs/docs/06-migration/v29.0.0.md b/docs/docs/06-migration/v29.0.0.md index 5b3984e13a..8e497f2b76 100644 --- a/docs/docs/06-migration/v29.0.0.md +++ b/docs/docs/06-migration/v29.0.0.md @@ -95,16 +95,4 @@ Now start your chain. ignite chain serve ``` -:::tip -If Ignite is unable to detect the chain `app.go`, make sure you have the following methods on the `App` struct: - -```go -func (app *App) AppCodec() codec.Codec -func (app *App) TxConfig() client.TxConfig -func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig) -``` - -All recent Ignite v28 scaffolded chains should have these methods. -::: - If you need our help and support, do not hesitate to visit our [Discord](https://discord.com/invite/ignite). diff --git a/ignite/pkg/cosmosanalysis/app/app.go b/ignite/pkg/cosmosanalysis/app/app.go index cac4ba3036..c6b519d38a 100644 --- a/ignite/pkg/cosmosanalysis/app/app.go +++ b/ignite/pkg/cosmosanalysis/app/app.go @@ -22,7 +22,7 @@ const registerRoutesMethod = "RegisterAPIRoutes" // 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, cosmosanalysis.AppImplementation) + appImpl, err := cosmosanalysis.FindEmbed(path, cosmosanalysis.AppEmbeddedTypes) if err != nil { return err } @@ -119,7 +119,7 @@ func FindRegisteredModules(chainRoot string) ([]string, error) { // DiscoverModules find a map of import modules based on the configured app. func DiscoverModules(file *ast.File, chainRoot string, fileImports map[string]string) ([]string, error) { // find app type - appImpl := cosmosanalysis.FindImplementationInFile(file, cosmosanalysis.AppImplementation) + appImpl := cosmosanalysis.FindEmbedInFile(file, cosmosanalysis.AppEmbeddedTypes) appTypeName := "App" switch { case len(appImpl) > 1: diff --git a/ignite/pkg/cosmosanalysis/app/testdata/app_generic.go b/ignite/pkg/cosmosanalysis/app/testdata/app_generic.go index 8a1d861d9c..cef076a743 100644 --- a/ignite/pkg/cosmosanalysis/app/testdata/app_generic.go +++ b/ignite/pkg/cosmosanalysis/app/testdata/app_generic.go @@ -1,35 +1,22 @@ package foo import ( - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" - storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/runtime" sdk "github.com/cosmos/cosmos-sdk/types" - paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" abci "github.com/tendermint/tendermint/abci/types" - - app "github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis/app/testdata/modules/registration_not_in_app_go" ) type Foo[T any] struct { + *runtime.App + FooKeeper foo.keeper i T } -func (f Foo[T]) TxConfig() client.TxConfig { return nil } -func (f Foo[T]) RegisterAPIRoutes() {} -func (f Foo[T]) RegisterTxService() {} -func (f Foo[T]) RegisterTendermintService() {} -func (f Foo[T]) AppCodec() codec.Codec { return app.appCodec } -func (f Foo[T]) Name() string { return app.BaseApp.Name() } -func (f Foo[T]) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (f Foo[T]) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (f Foo[T]) kvStoreKeys() map[string]*storetypes.KVStoreKey { return nil } -func (f Foo[T]) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } func (f Foo[T]) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { - return app.mm.BeginBlock(ctx, req) + return f.App.BeginBlocker(ctx, req) } func (f Foo[T]) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - return app.mm.EndBlock(ctx, req) + return f.App.EndBlocker(ctx, req) } diff --git a/ignite/pkg/cosmosanalysis/app/testdata/app_minimal.go b/ignite/pkg/cosmosanalysis/app/testdata/app_minimal.go index b7fadb975c..10aeee2980 100644 --- a/ignite/pkg/cosmosanalysis/app/testdata/app_minimal.go +++ b/ignite/pkg/cosmosanalysis/app/testdata/app_minimal.go @@ -1,34 +1,21 @@ package foo import ( - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" - storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" - paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" abci "github.com/tendermint/tendermint/abci/types" - - app "github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis/app/testdata/modules/registration_not_in_app_go" ) type Foo struct { + *baseapp.BaseApp + FooKeeper foo.keeper } -func (f Foo) TxConfig() client.TxConfig { return nil } -func (f Foo) RegisterAPIRoutes() {} -func (f Foo) RegisterTxService() {} -func (f Foo) RegisterTendermintService() {} -func (f Foo) Name() string { return app.BaseApp.Name() } -func (f Foo) AppCodec() codec.Codec { return app.appCodec } -func (F Foo) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (F Foo) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (F Foo) kvStoreKeys() map[string]*storetypes.KVStoreKey { return nil } -func (F Foo) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } func (f Foo) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { - return app.mm.BeginBlock(ctx, req) + return f.BaseApp.BeginBlocker(ctx, req) } func (f Foo) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - return app.mm.EndBlock(ctx, req) + return f.BaseApp.EndBlocker(ctx, req) } diff --git a/ignite/pkg/cosmosanalysis/app/testdata/modules/app_config/app.go b/ignite/pkg/cosmosanalysis/app/testdata/modules/app_config/app.go index c26b0eef3d..1ea7617fd3 100644 --- a/ignite/pkg/cosmosanalysis/app/testdata/modules/app_config/app.go +++ b/ignite/pkg/cosmosanalysis/app/testdata/modules/app_config/app.go @@ -2,6 +2,7 @@ package app import ( "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/runtime" "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/server/api" "github.com/cosmos/cosmos-sdk/server/config" @@ -19,6 +20,8 @@ import ( ) type Foo struct { + runtime.App + AuthKeeper authkeeper.Keeper BankKeeper bankkeeper.Keeper StakingKeeper stakingkeeper.Keeper diff --git a/ignite/pkg/cosmosanalysis/app/testdata/modules/single_app/app.go b/ignite/pkg/cosmosanalysis/app/testdata/modules/single_app/app.go index b8d6bf38e8..51c8b14117 100644 --- a/ignite/pkg/cosmosanalysis/app/testdata/modules/single_app/app.go +++ b/ignite/pkg/cosmosanalysis/app/testdata/modules/single_app/app.go @@ -1,6 +1,7 @@ package app import ( + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/server/api" @@ -20,6 +21,8 @@ import ( ) type Foo struct { + baseapp.BaseApp + AuthKeeper authkeeper.Keeper BankKeeper bankkeeper.Keeper StakingKeeper stakingkeeper.Keeper diff --git a/ignite/pkg/cosmosanalysis/app/testdata/two_app.go b/ignite/pkg/cosmosanalysis/app/testdata/two_app.go index 31ac29af1a..b8d3e7b8ef 100644 --- a/ignite/pkg/cosmosanalysis/app/testdata/two_app.go +++ b/ignite/pkg/cosmosanalysis/app/testdata/two_app.go @@ -1,52 +1,32 @@ package foo import ( - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" - storetypes "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/runtime" sdk "github.com/cosmos/cosmos-sdk/types" - paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" abci "github.com/tendermint/tendermint/abci/types" - - app "github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis/app/testdata/modules/registration_not_in_app_go" ) type Foo struct { + *runtime.App + FooKeeper foo.keeper } -func (f Foo) TxConfig() client.TxConfig { return nil } -func (f Foo) RegisterAPIRoutes() {} -func (f Foo) RegisterTxService() {} -func (f Foo) RegisterTendermintService() {} -func (f Foo) Name() string { return app.BaseApp.Name() } -func (f Foo) AppCodec() codec.Codec { return app.appCodec } -func (F Foo) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (F Foo) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (F Foo) kvStoreKeys() map[string]*storetypes.KVStoreKey { return nil } -func (F Foo) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } func (f Foo) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { - return app.mm.BeginBlock(ctx, req) + return f.App.BeginBlocker(ctx, req) } func (f Foo) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - return app.mm.EndBlock(ctx, req) + return f.App.EndBlocker(ctx, req) } type Bar struct { + *baseapp.BaseApp + FooKeeper foo.keeper } -func (f Bar) TxConfig() client.TxConfig { return nil } -func (f Bar) RegisterAPIRoutes() {} -func (f Bar) RegisterTxService() {} -func (f Bar) RegisterTendermintService() {} -func (f Bar) Name() string { return app.BaseApp.Name() } -func (f Bar) AppCodec() codec.Codec { return app.appCodec } -func (f Bar) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (f Bar) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (f Bar) kvStoreKeys() map[string]*storetypes.KVStoreKey { return nil } -func (f Bar) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } func (f Bar) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { return app.mm.BeginBlock(ctx, req) } diff --git a/ignite/pkg/cosmosanalysis/cosmosanalysis.go b/ignite/pkg/cosmosanalysis/cosmosanalysis.go index c33fd74497..c3cf9fca62 100644 --- a/ignite/pkg/cosmosanalysis/cosmosanalysis.go +++ b/ignite/pkg/cosmosanalysis/cosmosanalysis.go @@ -9,6 +9,7 @@ import ( "go/token" "os" "path/filepath" + "strings" "golang.org/x/mod/modfile" @@ -23,10 +24,9 @@ const ( defaultAppFilePath = "app/" + appFileName ) -var AppImplementation = []string{ - "AppCodec", - "TxConfig", - "RegisterAPIRoutes", +var AppEmbeddedTypes = []string{ + "github.com/cosmos/cosmos-sdk/runtime.App", + "github.com/cosmos/cosmos-sdk/baseapp.BaseApp", } // implementation tracks the implementation of an interface for a given struct. @@ -199,6 +199,153 @@ func checkImplementation(r implementation) bool { return true } +// FindEmbed finds the name of all types that embed one of the target types in a given module path. +// targetEmbeddedTypes should be a list of fully qualified type names (e.g., "package/path.TypeName"). +func FindEmbed(modulePath string, targetEmbeddedTypes []string) (found []string, err error) { + // parse go packages/files under path + fset := token.NewFileSet() + + pkgs, err := parser.ParseDir(fset, modulePath, nil, 0) + if err != nil { + return nil, err + } + + for _, pkg := range pkgs { + for _, fileNode := range pkg.Files { + foundStructs := findStructsEmbeddingInFile(fileNode, targetEmbeddedTypes) + found = append(found, foundStructs...) + } + } + + // Deduplicate results as a struct might be found in multiple files of the same package (though unlikely for structs) + // or if the same struct name exists in different packages (FindEmbed currently doesn't qualify by package). + if len(found) > 0 { + uniqueNamesMap := make(map[string]struct{}) + var uniqueResult []string + for _, name := range found { + if _, exists := uniqueNamesMap[name]; !exists { + uniqueNamesMap[name] = struct{}{} + uniqueResult = append(uniqueResult, name) + } + } + return uniqueResult, nil + } + + return found, nil +} + +// FindEmbedInFile finds all struct names in a given AST node that embed one of the target types. +// The AST node is expected to be an *ast.File. +// targetEmbeddedTypes should be a list of fully qualified type names (e.g., "package/path.TypeName"). +func FindEmbedInFile(n ast.Node, targetEmbeddedTypes []string) (found []string) { + fileNode, ok := n.(*ast.File) + if !ok { + return nil + } + + return findStructsEmbeddingInFile(fileNode, targetEmbeddedTypes) +} + +// findStructsEmbeddingInFile checks if any struct in the given AST file embeds one of the target types. +// targetTypes should be fully qualified (e.g., "package/path.TypeName"). +func findStructsEmbeddingInFile(fileNode *ast.File, targetEmbeddedTypes []string) (foundStructNames []string) { + // activeTargets maps local package name to a set of expected TypeNames from that package + activeTargets := make(map[string]map[string]struct{}) + + for _, targetFQN := range targetEmbeddedTypes { + dotIndex := strings.LastIndex(targetFQN, ".") + if dotIndex == -1 || dotIndex == 0 || dotIndex == len(targetFQN)-1 { + continue // invalid format + } + expectedImportPath := targetFQN[:dotIndex] + expectedTypeName := targetFQN[dotIndex+1:] + + for _, imp := range fileNode.Imports { + importPath := strings.Trim(imp.Path.Value, `"`) + if importPath == expectedImportPath { + localPkgName := "" + if imp.Name != nil { // alias used + localPkgName = imp.Name.Name + } else { + // default name (last part of the path) + // this is a common heuristic, e.g. "github.com/cosmos/cosmos-sdk/runtime" -> "runtime" + pathParts := strings.Split(importPath, "/") + localPkgName = pathParts[len(pathParts)-1] + } + + if _, ok := activeTargets[localPkgName]; !ok { + activeTargets[localPkgName] = make(map[string]struct{}) + } + activeTargets[localPkgName][expectedTypeName] = struct{}{} + break // found the import for this target, move to next targetFQN + } + } + } + + if len(activeTargets) == 0 { + return nil // none of the target packages are imported in this file + } + + ast.Inspect(fileNode, func(n ast.Node) bool { + typeSpec, ok := n.(*ast.TypeSpec) + if !ok { + return true + } + + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + return true + } + + for _, field := range structType.Fields.List { + if len(field.Names) == 0 { // embedded field + var selExpr *ast.SelectorExpr + fieldType := field.Type + + if starExpr, isStar := fieldType.(*ast.StarExpr); isStar { + fieldType = starExpr.X // unwrap pointer + } + + if se, isSel := fieldType.(*ast.SelectorExpr); isSel { + selExpr = se + } else { + continue + } + + pkgIdent, okIdent := selExpr.X.(*ast.Ident) + if !okIdent { + continue + } + + pkgNameInCode := pkgIdent.Name + typeNameInCode := selExpr.Sel.Name + + if expectedTypeNamesSet, pkgFound := activeTargets[pkgNameInCode]; pkgFound { + if _, typeFound := expectedTypeNamesSet[typeNameInCode]; typeFound { + foundStructNames = append(foundStructNames, typeSpec.Name.Name) + } + } + } + } + return true + }) + + // deduplicate if a struct somehow embeds multiple (or the same) target type + if len(foundStructNames) > 0 { + uniqueNamesMap := make(map[string]struct{}) + var uniqueResult []string + for _, name := range foundStructNames { + if _, exists := uniqueNamesMap[name]; !exists { + uniqueNamesMap[name] = struct{}{} + uniqueResult = append(uniqueResult, name) + } + } + return uniqueResult + } + + return foundStructNames +} + // ErrPathNotChain is returned by IsChainPath() when path is not a chain path. type ErrPathNotChain struct { path string @@ -240,11 +387,10 @@ func ValidateGoMod(module *modfile.File) error { return nil } -// FindAppFilePath looks for the app file that implements the interfaces listed in AppImplementation. +// FindAppFilePath Looks for the app file that embeds the runtime.App or baseapp.BaseApp types. func FindAppFilePath(chainRoot string) (path string, err error) { - var found []string - - err = filepath.Walk(chainRoot, func(path string, info os.FileInfo, err error) error { + var foundAppStructFiles []string + err = filepath.Walk(chainRoot, func(currentPath string, info os.FileInfo, err error) error { if err != nil { return err } @@ -253,39 +399,39 @@ func FindAppFilePath(chainRoot string) (path string, err error) { } fset := token.NewFileSet() - f, err := parser.ParseFile(fset, path, nil, 0) + f, err := parser.ParseFile(fset, currentPath, nil, 0) if err != nil { - return err + // log or handle error, e.g. by returning nil to continue walking + return nil } - currFound := findImplementationInFiles([]*ast.File{f}, AppImplementation) - if len(currFound) > 0 { - found = append(found, path) + structNames := findStructsEmbeddingInFile(f, AppEmbeddedTypes) + if len(structNames) > 0 { + foundAppStructFiles = append(foundAppStructFiles, currentPath) } - return nil }) if err != nil { return "", err } - numFound := len(found) + numFound := len(foundAppStructFiles) if numFound == 0 { return "", errors.New("app.go file cannot be found") } if numFound == 1 { - return found[0], nil + return foundAppStructFiles[0], nil } + // multiple files found, prefer one named appFileName ("app.go") appFilePath := "" - for _, p := range found { + for _, p := range foundAppStructFiles { if filepath.Base(p) == appFileName { if appFilePath != "" { - // multiple app.go found, fallback to app/app.go + // more than one app.go found among candidates, fallback to default return getDefaultAppFile(chainRoot) } - appFilePath = p } } @@ -294,6 +440,8 @@ func FindAppFilePath(chainRoot string) (path string, err error) { return appFilePath, nil } + // no app.go found among the candidates, or multiple candidates and none are app.go, + // fallback to default app path logic return getDefaultAppFile(chainRoot) } diff --git a/ignite/pkg/cosmosanalysis/cosmosanalysis_test.go b/ignite/pkg/cosmosanalysis/cosmosanalysis_test.go index 9e61954c77..b94215189c 100644 --- a/ignite/pkg/cosmosanalysis/cosmosanalysis_test.go +++ b/ignite/pkg/cosmosanalysis/cosmosanalysis_test.go @@ -1,6 +1,9 @@ package cosmosanalysis_test import ( + "go/ast" + "go/parser" + "go/token" "os" "path/filepath" "testing" @@ -73,60 +76,75 @@ func (f Foo) foobar() {} appFile = []byte(` package app -type App struct {} -func (app *App) Name() string { return app.BaseApp.Name() } -func (app *App) RegisterAPIRoutes() {} -func (app *App) RegisterTxService() {} -func (app *App) AppCodec() codec.Codec { return app.appCodec } -func (app *App) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (app *App) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (app *App) kvStoreKeys() map[string]*storetypes.KVStoreKey { return nil } -func (app *App) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } -func (app *App) TxConfig() client.TxConfig { return nil } -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) -} -`) + +import ( + runtime "github.com/cosmos/cosmos-sdk/runtime" +) + +type App struct { + *runtime.App +}`) appTestFile = []byte(` package app_test -type App struct {} -func (app *App) Name() string { return app.BaseApp.Name() } -func (app *App) RegisterAPIRoutes() {} -func (app *App) RegisterTxService() {} -func (app *App) AppCodec() codec.Codec { return app.appCodec } -func (app *App) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (app *App) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (app *App) kvStoreKeys() map[string]*storetypes.KVStoreKey { return nil } -func (app *App) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } -func (app *App) TxConfig() client.TxConfig { return nil } -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) -} -`) + +import ( + runtime "github.com/cosmos/cosmos-sdk/runtime" +) + +type App struct { + *runtime.App +}`) + appFileSDKv47 = []byte(` package app -type App struct {} -func (app *App) Name() string { return app.BaseApp.Name() } -func (app *App) RegisterAPIRoutes() {} -func (app *App) RegisterTxService() {} -func (app *App) AppCodec() codec.Codec { return app.appCodec } -func (app *App) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (app *App) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (app *App) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } -func (app *App) TxConfig() client.TxConfig { return nil } -func (app *App) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { - return app.mm.BeginBlock(ctx, req) + +import "github.com/cosmos/cosmos-sdk/baseapp" + +type App struct { + baseapp.BaseApp +}`) + + embeddedTypeFile = []byte(` +package foo + +import ( + "github.com/cosmos/cosmos-sdk/baseapp" + runtime "github.com/cosmos/cosmos-sdk/runtime" +) + +type App1 struct { + *runtime.App } -func (app *App) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - return app.mm.EndBlock(ctx, req) + +type App2 struct { + baseapp.BaseApp +} + +type NotApp struct{} + +// AppPointer uses a pointer to an embedded type +type AppPointer struct { + *runtime.App +} + +// AppNoEmbed has the type but doesn't embed it +type AppNoEmbed struct { + a runtime.App +} + +// OtherEmbed embeds a different type from a target package +type OtherEmbed struct { + *runtime.Server } `) + appModuleGoMod = []byte(` +module example.com/foo + +go 1.19 + +require ( + github.com/cosmos/cosmos-sdk v0.47.0 +)`) ) func TestFindImplementation(t *testing.T) { @@ -278,3 +296,79 @@ func TestValidateGoMod(t *testing.T) { err = cosmosanalysis.ValidateGoMod(modFile) require.NoError(t, err) } + +func TestFindEmbed(t *testing.T) { + tmpDir := t.TempDir() + + // Create a dummy go.mod for the test package + modPath := filepath.Join(tmpDir, "go.mod") + err := os.WriteFile(modPath, appModuleGoMod, 0o644) + require.NoError(t, err) + + // Create the test file + filePath := filepath.Join(tmpDir, "app.go") + err = os.WriteFile(filePath, embeddedTypeFile, 0o644) + require.NoError(t, err) + + targets := []string{ + "github.com/cosmos/cosmos-sdk/runtime.App", + "github.com/cosmos/cosmos-sdk/baseapp.BaseApp", + } + + found, err := cosmosanalysis.FindEmbed(tmpDir, targets) + require.NoError(t, err) + require.ElementsMatch(t, []string{"App1", "App2", "AppPointer"}, found) + + // Test with a directory that doesn't contain the target embeds + emptyDir := t.TempDir() + modPathEmpty := filepath.Join(emptyDir, "go.mod") + err = os.WriteFile(modPathEmpty, appModuleGoMod, 0o644) + require.NoError(t, err) + otherFilePath := filepath.Join(emptyDir, "other.go") + err = os.WriteFile(otherFilePath, []byte(`package foo; type Bar struct{}`), 0o644) + require.NoError(t, err) + + foundEmpty, err := cosmosanalysis.FindEmbed(emptyDir, targets) + require.NoError(t, err) + require.Empty(t, foundEmpty) + + // Test with non-existent directory + _, err = cosmosanalysis.FindEmbed(filepath.Join(tmpDir, "nonexistent"), targets) + require.Error(t, err) // Expect an error because parser.ParseDir will fail +} + +func TestFindEmbedInFile(t *testing.T) { + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "app.go") + err := os.WriteFile(filePath, embeddedTypeFile, 0o644) + require.NoError(t, err) + + fset := token.NewFileSet() + fileNode, err := parser.ParseFile(fset, filePath, nil, 0) + require.NoError(t, err) + + targets := []string{ + "github.com/cosmos/cosmos-sdk/runtime.App", + "github.com/cosmos/cosmos-sdk/baseapp.BaseApp", + "github.com/cosmos/cosmos-sdk/runtime.Server", // To test OtherEmbed + } + + found := cosmosanalysis.FindEmbedInFile(fileNode, targets) + require.ElementsMatch(t, []string{"App1", "App2", "AppPointer", "OtherEmbed"}, found) + + // Test with a node that is not an *ast.File (though the function expects it) + // Create a simple ident node + identNode := ast.NewIdent("SomeIdent") + foundNotFile := cosmosanalysis.FindEmbedInFile(identNode, targets) + require.Empty(t, foundNotFile) // Expect empty as it's not a file node + + // Test with a file that doesn't import/embed the target types + otherContent := `package bar; type Bar struct{}` + otherFilePath := filepath.Join(tmpDir, "other.go") + err = os.WriteFile(otherFilePath, []byte(otherContent), 0o644) + require.NoError(t, err) + otherFileNode, err := parser.ParseFile(fset, otherFilePath, nil, 0) + require.NoError(t, err) + foundOther := cosmosanalysis.FindEmbedInFile(otherFileNode, targets) + require.Empty(t, foundOther) +} diff --git a/ignite/pkg/cosmosanalysis/module/testdata/earth/app/app.go b/ignite/pkg/cosmosanalysis/module/testdata/earth/app/app.go index 68958e9f09..a7fc8a1792 100644 --- a/ignite/pkg/cosmosanalysis/module/testdata/earth/app/app.go +++ b/ignite/pkg/cosmosanalysis/module/testdata/earth/app/app.go @@ -1,24 +1,18 @@ package app import ( - "cosmossdk.io/client/v2/autocli" - "github.com/cosmos/cosmos-sdk/api/tendermint/abci" - "github.com/cosmos/cosmos-sdk/client" - storetypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/runtime" "github.com/cosmos/cosmos-sdk/types/module" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" - "github.com/gogo/protobuf/codec" marskeeper "github.com/tendermint/planet/x/mars/keeper" - abci "github.com/tendermint/tendermint/abci/types" - - app "github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis/app/testdata/modules/registration_not_in_app_go" ) type Foo struct { + *runtime.App + AuthKeeper authkeeper.Keeper BankKeeper bankkeeper.Keeper StakingKeeper stakingkeeper.Keeper @@ -27,23 +21,3 @@ type Foo struct { } var ModuleBasics = module.NewBasicManager(foo.AppModuleBasic{}) - -func (Foo) Name() string { return app.BaseApp.Name() } -func (Foo) RegisterAPIRoutes() {} -func (Foo) TxConfig() client.TxConfig { return nil } -func (Foo) RegisterTxService() {} -func (Foo) RegisterTendermintService() {} -func (Foo) InterfaceRegistry() codectypes.InterfaceRegistry { return nil } -func (Foo) AppCodec() codec.Codec { return app.appCodec } -func (Foo) AutoCliOpts() autocli.AppOptions { return autocli.AppOptions{} } -func (Foo) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (Foo) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (Foo) kvStoreKeys() map[string]*storetypes.KVStoreKey { return nil } -func (Foo) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } -func (Foo) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { - return app.mm.BeginBlock(ctx, req) -} - -func (Foo) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - return app.mm.EndBlock(ctx, req) -} diff --git a/ignite/pkg/cosmosanalysis/module/testdata/planet/app/app.go b/ignite/pkg/cosmosanalysis/module/testdata/planet/app/app.go index 3bd6c166a5..10f40bdca0 100644 --- a/ignite/pkg/cosmosanalysis/module/testdata/planet/app/app.go +++ b/ignite/pkg/cosmosanalysis/module/testdata/planet/app/app.go @@ -1,24 +1,18 @@ package app import ( - "cosmossdk.io/client/v2/autocli" - "github.com/cosmos/cosmos-sdk/api/tendermint/abci" - "github.com/cosmos/cosmos-sdk/client" - storetypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/types/module" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" - "github.com/gogo/protobuf/codec" marskeeper "github.com/tendermint/planet/x/mars/keeper" - abci "github.com/tendermint/tendermint/abci/types" - - app "github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis/app/testdata/modules/registration_not_in_app_go" ) type Foo struct { + baseapp.BaseApp + AuthKeeper authkeeper.Keeper BankKeeper bankkeeper.Keeper StakingKeeper stakingkeeper.Keeper @@ -27,23 +21,3 @@ type Foo struct { } var ModuleBasics = module.NewBasicManager(mars.AppModuleBasic{}) - -func (Foo) Name() string { return app.BaseApp.Name() } -func (Foo) RegisterAPIRoutes() {} -func (Foo) RegisterTxService() {} -func (Foo) RegisterTendermintService() {} -func (Foo) InterfaceRegistry() codectypes.InterfaceRegistry { return nil } -func (Foo) TxConfig() client.TxConfig { return nil } -func (Foo) AppCodec() codec.Codec { return app.appCodec } -func (Foo) AutoCliOpts() autocli.AppOptions { return autocli.AppOptions{} } -func (Foo) GetKey(storeKey string) *storetypes.KVStoreKey { return nil } -func (Foo) GetMemKey(storeKey string) *storetypes.MemoryStoreKey { return nil } -func (Foo) kvStoreKeys() map[string]*storetypes.KVStoreKey { return nil } -func (Foo) GetSubspace(moduleName string) paramstypes.Subspace { return subspace } -func (Foo) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { - return app.mm.BeginBlock(ctx, req) -} - -func (Foo) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - return app.mm.EndBlock(ctx, req) -}