Skip to content

Commit 8a85000

Browse files
committed
feat: sdk msg discovery from app source
* `pkg/cosmosanalysis/msg.Discover()` discovers types that implements sdk.Msg. * added `pkg/protoanalysis` for proto file analysis.
1 parent 04d84c4 commit 8a85000

File tree

7 files changed

+288
-4
lines changed

7 files changed

+288
-4
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/cosmos/go-bip39 v0.0.0-20200817134856-d632e0d11689
1212
github.com/dariubs/percent v0.0.0-20200128140941-b7801cf1c7e2
1313
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
14+
github.com/emicklei/proto v1.9.0
1415
github.com/fatih/color v1.9.0
1516
github.com/gertd/go-pluralize v0.1.7
1617
github.com/go-bindata/go-bindata v3.1.2+incompatible
@@ -20,13 +21,15 @@ require (
2021
github.com/gobuffalo/plush v3.8.3+incompatible
2122
github.com/gobuffalo/plushgen v0.1.2
2223
github.com/goccy/go-yaml v1.8.0
24+
github.com/gogo/protobuf v1.3.2 // indirect
2325
github.com/google/go-cmp v0.5.2 // indirect
2426
github.com/gookit/color v1.2.7
2527
github.com/gorilla/mux v1.8.0
2628
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
2729
github.com/imdario/mergo v0.3.11
2830
github.com/improbable-eng/grpc-web v0.13.0
2931
github.com/jpillora/chisel v1.7.3
32+
github.com/kr/pretty v0.1.0
3033
github.com/manifoldco/promptui v0.8.0
3134
github.com/mattn/go-zglob v0.0.3
3235
github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
173173
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
174174
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
175175
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
176+
github.com/emicklei/proto v1.9.0 h1:l0QiNT6Qs7Yj0Mb4X6dnWBQer4ebei2BFcgQLbGqUDc=
177+
github.com/emicklei/proto v1.9.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
176178
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
177179
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
178180
github.com/enigmampc/btcutil v1.0.3-0.20200723161021-e2fb6adb2a25 h1:2vLKys4RBU4pn2T/hjXMbvwTr1Cvy5THHrQkbeY9HRk=
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package cosmosanalysis provides a toolset for staticly analysing Cosmos SDK's and blockchains
2+
// based on the Cosmos SDK.
3+
package cosmosanalysis
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package msgs
2+
3+
import (
4+
"go/ast"
5+
"go/parser"
6+
"go/token"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/tendermint/starport/starport/pkg/gomodule"
11+
"github.com/tendermint/starport/starport/pkg/protoanalysis"
12+
)
13+
14+
// requirements holds a list of sdk.Msg's method names.
15+
type requirements map[string]bool
16+
17+
func newRequirements() requirements {
18+
return requirements{
19+
"Reset": false,
20+
"String": false,
21+
"ProtoMessage": false,
22+
"Route": false,
23+
"Type": false,
24+
"GetSigners": false,
25+
"GetSignBytes": false,
26+
"ValidateBasic": false,
27+
}
28+
}
29+
30+
// Msgs is a module import path-sdk msgs pair.
31+
type Msgs map[string][]string
32+
33+
// Discover discovers and returns pairs of module import path and their types that implements sdk.Msg.
34+
// sourcePath is the root path of an sdk blockchain.
35+
//
36+
// discovery algorithm make use of proto definitions to discover modules inside the blockchain.
37+
//
38+
// checking whether a type implements sdk.Msg is done by running a simple algorithm of comparing method names
39+
// of each type in a package with sdk.Msg's, which satisfies our needs for the time being.
40+
// for a more opinionated check, go/types.Implements() might be utilized as needed.
41+
func Discover(sourcePath string) (Msgs, error) {
42+
// find out base Go import path of the blockchain.
43+
gm, err := gomodule.ParseAt(sourcePath)
44+
if err != nil {
45+
return nil, err
46+
}
47+
bpath := gm.Module.Mod.Path
48+
49+
// find proto packages that belongs to modules under x/.
50+
xprotopkgs, err := findModuleProtoPkgs(sourcePath, bpath)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
msgs := make(Msgs)
56+
57+
for _, xproto := range xprotopkgs {
58+
rxpath := strings.TrimPrefix(xproto.GoImportName, bpath)
59+
xpath := filepath.Join(sourcePath, rxpath)
60+
61+
xmsgs, err := DiscoverModule(xpath)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
msgs[xproto.GoImportName] = xmsgs
67+
}
68+
69+
return msgs, nil
70+
}
71+
72+
// DiscoverModule discovers sdk messages defined in a module that resides under modulePath.
73+
func DiscoverModule(modulePath string) (msgs []string, err error) {
74+
// parse go packages/files under modulePath.
75+
fset := token.NewFileSet()
76+
77+
pkgs, err := parser.ParseDir(fset, modulePath, nil, 0)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
// collect all structs under modulePath to find out the ones that satisfy requirements.
83+
structs := make(map[string]requirements)
84+
85+
for _, pkg := range pkgs {
86+
for _, f := range pkg.Files {
87+
ast.Inspect(f, func(n ast.Node) bool {
88+
// look for struct methods.
89+
fdecl, ok := n.(*ast.FuncDecl)
90+
if !ok {
91+
return true
92+
}
93+
94+
// not a method.
95+
if fdecl.Recv == nil {
96+
return true
97+
}
98+
99+
// fname is the name of method.
100+
fname := fdecl.Name.Name
101+
102+
// find the struct name that method belongs to.
103+
sexp, ok := fdecl.Recv.List[0].Type.(*ast.StarExpr)
104+
if !ok {
105+
return true
106+
}
107+
108+
sname := sexp.X.(*ast.Ident).Name
109+
110+
// mark the requirement that this struct satisfies.
111+
if _, ok := structs[sname]; !ok {
112+
structs[sname] = newRequirements()
113+
}
114+
115+
structs[sname][fname] = true
116+
117+
return true
118+
})
119+
}
120+
}
121+
122+
// checkRequirements checks if all requirements are satisfied.
123+
checkRequirements := func(r requirements) bool {
124+
for _, name := range r {
125+
if name == false {
126+
return false
127+
}
128+
}
129+
return true
130+
}
131+
132+
for name, reqs := range structs {
133+
if checkRequirements(reqs) {
134+
msgs = append(msgs, name)
135+
}
136+
}
137+
138+
return msgs, nil
139+
}
140+
141+
func findModuleProtoPkgs(sourcePath, bpath string) ([]protoanalysis.Package, error) {
142+
// find out all proto packages inside blockchain.
143+
allprotopkgs, err := protoanalysis.DiscoverPackages(sourcePath)
144+
if err != nil {
145+
return nil, err
146+
}
147+
148+
// filter out proto packages that does not reprents x/ modules of blockchain.
149+
var xprotopkgs []protoanalysis.Package
150+
for _, pkg := range allprotopkgs {
151+
if !strings.HasPrefix(pkg.GoImportName, bpath) {
152+
continue
153+
}
154+
155+
xprotopkgs = append(xprotopkgs, pkg)
156+
}
157+
158+
return xprotopkgs, nil
159+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package msgs
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestA(t *testing.T) {
9+
fmt.Println(Discover("/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/test"))
10+
// outputs:
11+
// map[github.com/test/test/x/test/types:[MsgUpdateUser MsgUpdateHello MsgCreateUser MsgCreateHello MsgDeleteHello MsgDeleteUser]] <nil>
12+
}

starport/pkg/cosmosprotoc/cosmosprotoc.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import (
88
"path/filepath"
99
"strings"
1010

11-
"github.com/mattn/go-zglob"
1211
"github.com/otiai10/copy"
1312
"github.com/pkg/errors"
1413
"github.com/tendermint/starport/starport/pkg/cmdrunner"
1514
"github.com/tendermint/starport/starport/pkg/cmdrunner/step"
1615
"github.com/tendermint/starport/starport/pkg/gomodule"
16+
"github.com/tendermint/starport/starport/pkg/protoanalysis"
1717
"github.com/tendermint/starport/starport/pkg/xexec"
1818
)
1919

@@ -136,7 +136,7 @@ func Generate(
136136
}
137137

138138
// find out the list of proto files under the app and generate code for them.
139-
files, err := zglob.Glob(globProto(protoPath))
139+
files, err := protoanalysis.SearchProto(protoPath)
140140
if err != nil {
141141
return err
142142
}
@@ -183,5 +183,3 @@ func Generate(
183183

184184
return nil
185185
}
186-
187-
func globProto(path string) string { return path + "/**/*.proto" }
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package protoanalysis
2+
3+
import (
4+
"os"
5+
"sync"
6+
7+
"github.com/emicklei/proto"
8+
"github.com/mattn/go-zglob"
9+
"golang.org/x/sync/errgroup"
10+
)
11+
12+
const (
13+
optionGoPkg = "go_package"
14+
)
15+
16+
// Package represents a proto pkg.
17+
type Package struct {
18+
// Name of the proto pkg.
19+
Name string
20+
21+
// GoImportName is the go package name of proto package.
22+
GoImportName string
23+
}
24+
25+
// DiscoverPackages recursively discovers proto files, parses them returns info about
26+
// each found package.
27+
func DiscoverPackages(path string) ([]Package, error) {
28+
files, err := SearchProto(path)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
var (
34+
// m protects pkgs.
35+
m sync.Mutex
36+
pkgs []Package
37+
38+
isPkgExists = func(pkg Package) bool {
39+
for _, epkg := range pkgs {
40+
if pkg == epkg {
41+
return true
42+
}
43+
}
44+
return false
45+
}
46+
)
47+
48+
g := &errgroup.Group{}
49+
50+
for _, path := range files {
51+
path := path
52+
53+
g.Go(func() error {
54+
pkg, err := Parse(path)
55+
if err != nil {
56+
return err
57+
}
58+
59+
m.Lock()
60+
defer m.Unlock()
61+
62+
if !isPkgExists(pkg) {
63+
pkgs = append(pkgs, pkg)
64+
}
65+
66+
return nil
67+
})
68+
}
69+
70+
return pkgs, g.Wait()
71+
}
72+
73+
// Parse parses a proto file residing at path.
74+
func Parse(path string) (Package, error) {
75+
f, err := os.Open(path)
76+
if err != nil {
77+
return Package{}, err
78+
}
79+
defer f.Close()
80+
81+
def, err := proto.NewParser(f).Parse()
82+
if err != nil {
83+
return Package{}, err
84+
}
85+
86+
var pkg Package
87+
88+
proto.Walk(
89+
def,
90+
proto.WithPackage(func(p *proto.Package) { pkg.Name = p.Name }),
91+
proto.WithOption(func(o *proto.Option) {
92+
if o.Name != optionGoPkg {
93+
return
94+
}
95+
pkg.GoImportName = o.Constant.Source
96+
}))
97+
98+
return pkg, nil
99+
}
100+
101+
// SearchProto recursively finds all proto files under path.
102+
func SearchProto(path string) ([]string, error) {
103+
return zglob.Glob(GlobPattern(path))
104+
}
105+
106+
// GlobPattern returns a recursive glob search pattern to find all proto files under path.
107+
func GlobPattern(path string) string { return path + "/**/*.proto" }

0 commit comments

Comments
 (0)