From 0be3f0c913dd96e99c3284ecaf87cc4751f8e762 Mon Sep 17 00:00:00 2001 From: Pantani Date: Mon, 23 Mar 2026 21:52:21 -0300 Subject: [PATCH 1/2] fix(protoanalysis): resolve qualified and nested RPC request messages --- ignite/pkg/cosmosanalysis/module/module.go | 4 +- ignite/pkg/protoanalysis/builder.go | 42 ++++++++--- ignite/pkg/protoanalysis/package.go | 72 +++++++++++++++++-- ignite/pkg/protoanalysis/package_test.go | 56 +++++++++++++++ .../pkg/protoanalysis/protoanalysis_test.go | 15 ++++ .../testdata/qualified_service/service.proto | 22 ++++++ .../templates/module/create/app_config_ast.go | 3 +- 7 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 ignite/pkg/protoanalysis/testdata/qualified_service/service.proto diff --git a/ignite/pkg/cosmosanalysis/module/module.go b/ignite/pkg/cosmosanalysis/module/module.go index b697e2f3dc..a9cbb9fcee 100644 --- a/ignite/pkg/cosmosanalysis/module/module.go +++ b/ignite/pkg/cosmosanalysis/module/module.go @@ -269,8 +269,8 @@ func (d *moduleDiscoverer) discover(pkg protoanalysis.Package) (Module, error) { // fill queries & messages. for _, s := range pkg.Services { for _, q := range s.RPCFuncs { - pkgmsg, err := pkg.MessageByName(q.RequestType) - if err != nil { + pkgmsg, ok := pkg.FindMessageByName(q.RequestType) + if !ok { // no msg found in the proto defs corresponds to discovered sdk message. // if it cannot be found, nothing to worry about, this means that it is used // only internally and not open for actual use. diff --git a/ignite/pkg/protoanalysis/builder.go b/ignite/pkg/protoanalysis/builder.go index 1589658e2c..8e28962490 100644 --- a/ignite/pkg/protoanalysis/builder.go +++ b/ignite/pkg/protoanalysis/builder.go @@ -117,15 +117,7 @@ func (b builder) elementsToRPCFunc(elems []proto.Visitee) (rpcFuncs []RPCFunc) { continue } - var requestMessage *proto.Message - - for _, message := range b.p.messages() { - if message.Name != rpc.RequestType { - continue - } - requestMessage = message - } - + requestMessage := findProtoMessageByTypeName(b.p.name, b.p.messages(), rpc.RequestType) if requestMessage == nil { continue } @@ -159,6 +151,38 @@ func (b builder) elementsToHTTPRules(requestMessage *proto.Message, elems []prot return } +func findProtoMessageByTypeName(pkgName string, messages []*proto.Message, typeName string) *proto.Message { + var exactMatch *proto.Message + + canonicalTypeName := canonicalMessageName(pkgName, typeName) + for _, message := range messages { + if message.Name == typeName { + exactMatch = message + } + + if flattenProtoMessageName(message) == canonicalTypeName { + return message + } + } + + return exactMatch +} + +func flattenProtoMessageName(message *proto.Message) string { + name := message.Name + for parent := message.Parent; parent != nil; { + parentMessage, ok := parent.(*proto.Message) + if !ok { + break + } + + name = fmt.Sprintf("%s_%s", parentMessage.Name, name) + parent = parentMessage.Parent + } + + return name +} + // Regexp to extract HTTP rule URL parameter names. // The expression extracts parameter names defined within "{}". // Extra parameter arguments are ignored. These arguments are normally diff --git a/ignite/pkg/protoanalysis/package.go b/ignite/pkg/protoanalysis/package.go index 6479b8b8ff..acb3a02743 100644 --- a/ignite/pkg/protoanalysis/package.go +++ b/ignite/pkg/protoanalysis/package.go @@ -39,6 +39,9 @@ type ( var regexBetaVersion = regexp.MustCompile("^v[0-9]+(beta|alpha)[0-9]+") +// ErrMessageNotFound is returned when a proto message cannot be found in a package. +var ErrMessageNotFound = errors.New("no message found") + // ModuleName retrieves the single module name of the package. func (p Package) ModuleName() (name string) { names := strings.Split(p.Name, ".") @@ -53,12 +56,41 @@ func (p Package) ModuleName() (name string) { // MessageByName finds a message by its name inside Package. func (p Package) MessageByName(name string) (Message, error) { - for _, message := range p.Messages { - if message.Name == name { - return message, nil + message, ok := p.FindMessageByName(name) + if !ok { + return Message{}, ErrMessageNotFound + } + + return message, nil +} + +// FindMessageByName finds a message by its name inside Package. +// It accepts plain message names, current-package qualified names and nested message names. +func (p Package) FindMessageByName(name string) (Message, bool) { + for _, candidate := range candidateMessageNames(p.Name, name) { + for _, message := range p.Messages { + if message.Name == candidate { + return message, true + } + } + } + + if !strings.Contains(strings.TrimPrefix(name, "."), ".") { + leafName := leafMessageName(name) + + var leafMatches []Message + for _, message := range p.Messages { + if leafMessageName(message.Name) == leafName { + leafMatches = append(leafMatches, message) + } + } + + if len(leafMatches) == 1 { + return leafMatches[0], true } } - return Message{}, errors.New("no message found") + + return Message{}, false } // GoImportPath retrieves the Go import path. @@ -87,6 +119,38 @@ type ( } ) +func candidateMessageNames(pkgName, name string) []string { + candidates := []string{name} + + canonical := canonicalMessageName(pkgName, name) + if canonical != "" && canonical != name { + candidates = append(candidates, canonical) + } + + return candidates +} + +func canonicalMessageName(pkgName, name string) string { + name = strings.TrimPrefix(name, ".") + if pkgName != "" { + name = strings.TrimPrefix(name, pkgName+".") + } + + return strings.ReplaceAll(name, ".", "_") +} + +func leafMessageName(name string) string { + if index := strings.LastIndex(name, "_"); index >= 0 { + return name[index+1:] + } + + if index := strings.LastIndex(name, "."); index >= 0 { + return name[index+1:] + } + + return name +} + // Paths retrieves the list of paths from the files. func (f Files) Paths() []string { var paths []string diff --git a/ignite/pkg/protoanalysis/package_test.go b/ignite/pkg/protoanalysis/package_test.go index e21c597f65..a1f4658fc5 100644 --- a/ignite/pkg/protoanalysis/package_test.go +++ b/ignite/pkg/protoanalysis/package_test.go @@ -80,3 +80,59 @@ func TestPackage_ModuleName(t *testing.T) { }) } } + +func TestPackage_MessageByName(t *testing.T) { + pkg := Package{ + Name: "foo.bar", + Messages: []Message{ + {Name: "Request"}, + {Name: "Outer_Inner"}, + }, + } + + tests := []struct { + name string + messageName string + want string + wantErr error + }{ + { + name: "plain name", + messageName: "Request", + want: "Request", + }, + { + name: "qualified name", + messageName: ".foo.bar.Request", + want: "Request", + }, + { + name: "nested qualified name", + messageName: ".foo.bar.Outer.Inner", + want: "Outer_Inner", + }, + { + name: "nested leaf name", + messageName: "Inner", + want: "Outer_Inner", + }, + { + name: "missing name", + messageName: "Missing", + wantErr: ErrMessageNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + message, err := pkg.MessageByName(tt.messageName) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + require.Equal(t, tt.want, message.Name) + }) + } +} diff --git a/ignite/pkg/protoanalysis/protoanalysis_test.go b/ignite/pkg/protoanalysis/protoanalysis_test.go index 592164299e..f51b7c88e0 100644 --- a/ignite/pkg/protoanalysis/protoanalysis_test.go +++ b/ignite/pkg/protoanalysis/protoanalysis_test.go @@ -17,6 +17,21 @@ func TestNestedMessages(t *testing.T) { require.Equal(t, "A_B_C", pkg.Messages[2].Name) } +func TestQualifiedServiceTypes(t *testing.T) { + packages, err := Parse(context.Background(), nil, "testdata/qualified_service") + require.NoError(t, err) + + require.Len(t, packages, 1) + require.Len(t, packages[0].Services, 1) + require.Len(t, packages[0].Services[0].RPCFuncs, 2) + require.Equal(t, ".qualified_service.PingRequest", packages[0].Services[0].RPCFuncs[0].RequestType) + require.Equal(t, ".qualified_service.Outer.NestedRequest", packages[0].Services[0].RPCFuncs[1].RequestType) + + message, err := packages[0].MessageByName(".qualified_service.Outer.NestedRequest") + require.NoError(t, err) + require.Equal(t, "Outer_NestedRequest", message.Name) +} + func TestLiquidity(t *testing.T) { packages, err := Parse(context.Background(), nil, "testdata/liquidity") require.NoError(t, err) diff --git a/ignite/pkg/protoanalysis/testdata/qualified_service/service.proto b/ignite/pkg/protoanalysis/testdata/qualified_service/service.proto new file mode 100644 index 0000000000..ae0ec0114e --- /dev/null +++ b/ignite/pkg/protoanalysis/testdata/qualified_service/service.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package qualified_service; + +option go_package = "github.com/ignite/qualified_service"; + +service Query { + rpc Ping(.qualified_service.PingRequest) returns (.qualified_service.PingResponse); + rpc Nested(.qualified_service.Outer.NestedRequest) returns (.qualified_service.PingResponse); +} + +message PingRequest { + string id = 1; +} + +message PingResponse {} + +message Outer { + message NestedRequest { + string id = 1; + } +} diff --git a/ignite/templates/module/create/app_config_ast.go b/ignite/templates/module/create/app_config_ast.go index 43ed2915b3..f1b87d105e 100644 --- a/ignite/templates/module/create/app_config_ast.go +++ b/ignite/templates/module/create/app_config_ast.go @@ -25,8 +25,7 @@ func SkipConfigEntry() AddModuleAppConfigOption { } } -// SpecifyModuleEntry allows to define to which field the module should be added in the app config. -// E.g. "PreBlockers", "InitGenesis", "BeginBlockers", "EndBlockers" +// E.g. "PreBlockers", "InitGenesis", "BeginBlockers", "EndBlockers". func SpecifyModuleEntry(fields ...string) AddModuleAppConfigOption { return func(opts *addModuleAppConfigOptions) { opts.runtimeFields = fields From 4b7ce81ec2385c70de9baeedeec4eaedecd514c7 Mon Sep 17 00:00:00 2001 From: Pantani Date: Mon, 23 Mar 2026 21:54:12 -0300 Subject: [PATCH 2/2] add changelog --- changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.md b/changelog.md index 0415f93ea9..a480b75365 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- [#4910](https://github.com/ignite/cli/pull/4910) Resolve qualified and nested RPC request messages. + ## [`v29.9.2`](https://github.com/ignite/cli/releases/tag/v29.9.2) - [#4904](https://github.com/ignite/cli/pull/4904) Add variadic options in `modulecreate.AddModuleToAppConfig`.