Repository: yqylovy/goimportdot Branch: master Commit: eb181a7eeabe Files: 14 Total size: 16.9 KB Directory structure: gitextract_o0n_csr4/ ├── README.md ├── core/ │ ├── file_filter.go │ ├── imps.go │ ├── imps_test.go │ ├── pkg_filter.go │ ├── pkg_filter_test.go │ └── util.go ├── docs/ │ ├── examples-cn/ │ │ ├── cronsun.dot │ │ ├── cronsun.node.core.dot │ │ ├── cronsun.node.dot │ │ └── goimportdot_guide.md │ ├── zap.dot │ └── zapcore.dot └── goimportdot.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ # GoImportDot ## What is GoImportDot ? GoImportDot is a tiny tool to generate a `dot` file (used for Graphviz) of imports of golang package.It has two purpose. * Help people quickly understand how a package organization without going into details of code. * Help people find out whether a package is too confusing and needs to be refactored. ## Quick Start ``` go get -u github.com/yqylovy/goimportdot goimportdot -pkg=yourpackagename > pkg.dot dot -Tsvg pkg.dot >pkg.svg ``` ## Example Install `go.uber.org/zap` ``` go get go.uber.org/zap ``` Get a graph of `go.uber.org/zap` ``` goimportdot -pkg=go.uber.org/zap > zap.dot dot -Tpng zap.dot > zap.png ``` ![zap](./docs/zap.png) Only get a graph of zapcore in `go.uber.org/zap` ``` goimportdot -pkg=go.uber.org/zap -root=go.uber.org/zap/zapcore > zapcore.dot dot -Tpng zapcore.dot > zapcore.png ``` ![zap](./docs/zapcore.png) ================================================ FILE: core/file_filter.go ================================================ package core import ( "os" "strings" ) type FileFilter struct { IsBlack bool Func func(fp string, info os.FileInfo, err error) bool } func NameContains(isblack bool, str string) FileFilter { return FileFilter{ IsBlack: isblack, Func: func(fp string, _ os.FileInfo, _ error) bool { return strings.Contains(fp, str) }, } } func HasSuffix(isblack bool, suffixs ...string) FileFilter { return FileFilter{ IsBlack: isblack, Func: func(fp string, _ os.FileInfo, _ error) bool { for _, sf := range suffixs { if sf == "" { continue } if sf[0] != '.' { sf = "." + sf } if strings.HasSuffix(fp, sf) { return true } } return false }, } } ================================================ FILE: core/imps.go ================================================ package core import ( "bytes" "fmt" "io" "os" "path/filepath" "strings" ) func GetImports(pkg string, filters ...FileFilter) (pkgimports map[string]StrSet, err error) { fullpath := "" goPath := os.Getenv("GOPATH") gopaths := strings.Split(goPath, ":") for _, gp := range gopaths { fp := filepath.Join(gp, "src", pkg) if _, err := os.Stat(fp); err == nil { fullpath = fp } } if fullpath == "" { err = fmt.Errorf("Can not find package [%s] in GOPATH [%s]", pkg, goPath) return } pkgimports = make(map[string]StrSet) filepath.Walk(fullpath, func(fp string, info os.FileInfo, err error) error { if info.IsDir() { return nil } for _, filter := range filters { if !filter.IsBlack { continue } if filter.Func(fp, info, err) { return nil } } for _, filter := range filters { if filter.IsBlack { continue } if !filter.Func(fp, info, err) { return nil } } pkg := PkgOfFile(fp) if _, ok := pkgimports[pkg]; !ok { pkgimports[pkg] = NewStrSet() } ss, err := ParseGoImport(fp) if err != nil { // TODO: better err panic(err) } pkgimports[pkg].Merge(ss) return nil }) return } func WriteDot(pkgimports map[string]StrSet, writer io.Writer) (err error) { nodes := NewStrSet() edges := [][2]string{} for pkg, imps := range pkgimports { nodes.Put(pkg) for imp := range imps { nodes.Put(imp) edges = append(edges, [2]string{pkg, imp}) } } buf := bytes.NewBuffer([]byte{}) buf.WriteString("digraph G {\n") for _, edge := range edges { buf.WriteString(fmt.Sprintf(`"%s"->"%s";`, edge[0], edge[1])) buf.WriteByte('\n') } for pkg, _ := range nodes { buf.WriteString(fmt.Sprintf(`"%s";`, pkg)) buf.WriteByte('\n') } buf.WriteString("}\n") _, err = writer.Write(buf.Bytes()) return } ================================================ FILE: core/imps_test.go ================================================ package core import ( "testing" . "github.com/smartystreets/goconvey/convey" ) func TestGetImports(t *testing.T) { Convey("Test GetImports", t, func() { _, err := GetImports("go.uber.org/zap", NameContains(true, ".git"), NameContains(true, "_test.go"), HasSuffix(false, ".go")) So(err, ShouldBeNil) }) } ================================================ FILE: core/pkg_filter.go ================================================ package core import ( "regexp" "strings" ) type PkgFilter func(map[string]StrSet) map[string]StrSet func RootFilter(root string) PkgFilter { return func(imps map[string]StrSet) (ret map[string]StrSet) { ret = make(map[string]StrSet) cur := []string{root} for len(cur) > 0 { newcur := NewStrSet() for _, pkg := range cur { if pkgimp, ok := imps[pkg]; ok { ret[pkg] = pkgimp newcur.Merge(pkgimp) } } cur = newcur.Array() } return ret } } func PkgWildcardFilter(isBlack bool, pkgs ...string) PkgFilter { regs := []*regexp.Regexp{} for _, pkg := range pkgs { rgp := regexp.MustCompile("^" + strings.Replace(pkg, "*", ".*", -1) + "$") regs = append(regs, rgp) } return func(imps map[string]StrSet) (ret map[string]StrSet) { ret = make(map[string]StrSet) BIG: for pkg, imps := range imps { for _, rgp := range regs { if isBlack == rgp.MatchString(pkg) { continue BIG } } for k := range imps { for _, rgp := range regs { if isBlack == rgp.MatchString(k) { imps.Del(k) } } } ret[pkg] = imps } return ret } } func ParsePkgWildcardStr(str string) (fs []PkgFilter, err error) { if str == "" { return } strArr := strings.Split(str, ";") for _, str := range strArr { str = strings.TrimSpace(str) wb_pkgs := strings.SplitN(str, ":", 2) fs = append(fs, PkgWildcardFilter(wb_pkgs[0] == "b", strings.Split(wb_pkgs[1], ",")...)) } return } func PkgLevelFilter(level int) PkgFilter { return func(imps map[string]StrSet) (ret map[string]StrSet) { if level < 0 { return imps } // find the root // which are not pointed to allTarget := NewStrSet() for _, targets := range imps { allTarget.Merge(targets) } levelMap := map[string]int{} for pkg := range imps { if !allTarget.Contains(pkg) { levelMap[pkg] = 0 } } for i := 0; i < level; i++ { nextLevel := NewStrSet() for pkg, pkgLevel := range levelMap { if pkgLevel != i { continue } for target := range imps[pkg] { nextLevel.Put(target) } } for next := range nextLevel { levelMap[next] = i + 1 } } ret = make(map[string]StrSet, len(levelMap)) for pkg, lvl := range levelMap { ret[pkg] = imps[pkg] if lvl == level { continue } } return ret } } ================================================ FILE: core/pkg_filter_test.go ================================================ package core import ( "testing" . "github.com/smartystreets/goconvey/convey" ) func TestPkgRegex(t *testing.T) { Convey("Test PkgRegex", t, func() { imps := map[string]StrSet{ "a/b/c": NewStrSet( "test/subt", ), } filter := PkgWildcardFilter(true, "test*") imps = filter(imps) So(imps["a/b/c"], ShouldBeEmpty) }) } func TestParsePkgWildcardStr(t *testing.T) { Convey("Test ParsePkgWildcardStr", t, func() { str := "w:a*,*b;b:c" fs, err := ParsePkgWildcardStr(str) So(err, ShouldBeNil) So(len(fs), ShouldEqual, 2) }) } ================================================ FILE: core/util.go ================================================ package core import ( "go/parser" "go/token" "path/filepath" "strings" ) func ParseGoImport(gofile string) (ss StrSet, err error) { fset := token.NewFileSet() // positions are relative to fset f, err := parser.ParseFile(fset, gofile, nil, parser.ImportsOnly) if err != nil { return } ss = NewStrSet() for _, s := range f.Imports { ss.Put(strings.Trim(s.Path.Value, `"`)) } return } func PkgOfFile(gofile string) (pkg string) { return strings.SplitN(filepath.Dir(gofile), "/src/", 2)[1] } type StrSet map[string]bool func NewStrSet(strs ...string) StrSet { ss := StrSet(make(map[string]bool)) for _, str := range strs { ss.Put(str) } return ss } func (this StrSet) Put(str string) { this[str] = true } func (this StrSet) Del(str string) { delete(this, str) } func (this StrSet) Contains(str string) (ok bool) { _, ok = this[str]; return ok } func (this StrSet) Merge(that StrSet) { for str := range that { this[str] = true } } func (this StrSet) Array() []string { ret := make([]string, 0, len(this)) for str := range this { ret = append(ret, str) } return ret } ================================================ FILE: docs/examples-cn/cronsun.dot ================================================ digraph G { "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/utils"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/bin/web"->"github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/bin/web"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun/bin/web"->"github.com/shunfei/cronsun/event"; "github.com/shunfei/cronsun/bin/web"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/bin/web"->"github.com/shunfei/cronsun/web"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/db"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/web"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun/web"->"github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/web"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/bin/node"->"github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/bin/node"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun/bin/node"->"github.com/shunfei/cronsun/event"; "github.com/shunfei/cronsun/bin/node"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/bin/node"->"github.com/shunfei/cronsun/node"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/db"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/utils"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/event"; "github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/bin/web"; "github.com/shunfei/cronsun/bin/node"; "github.com/shunfei/cronsun/db/mid"; "github.com/shunfei/cronsun/node"; "github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun/utils"; "github.com/shunfei/cronsun/event"; "github.com/shunfei/cronsun/web"; "github.com/shunfei/cronsun/db"; } ================================================ FILE: docs/examples-cn/cronsun.node.core.dot ================================================ digraph G { "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/db"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/db"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/event"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/db"; "github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun/event"; "github.com/shunfei/cronsun/node"; } ================================================ FILE: docs/examples-cn/cronsun.node.dot ================================================ digraph G { "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/db"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/db"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/event"; "github.com/shunfei/cronsun/conf"->"github.com/shunfei/cronsun/utils"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/utils"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/conf"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/node"->"github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun/node/cron"; "github.com/shunfei/cronsun/log"; "github.com/shunfei/cronsun/event"; "github.com/shunfei/cronsun/utils"; "github.com/shunfei/cronsun/node"; "github.com/shunfei/cronsun"; "github.com/shunfei/cronsun/db"; "github.com/shunfei/cronsun/conf"; } ================================================ FILE: docs/examples-cn/goimportdot_guide.md ================================================ # goimportdot : 一个帮你迅速了解 golang 项目结构的工具 ## 简介 很多时候,当我们想熟悉一个 `golang` 项目时,都需要能快速地对代码的整体结构有个宏观了解,初步明白项目是如何组织构成的。在有了大体的概念后,再选择适当的切入点,专注于代码的核心部分进行研究,达到熟悉项目的目的。 `goimportdot` 就是一个根据 `golang` 中 `import` 生成调用关系,再配合 `Graphviz` 生成调用图的工具。 ## 安装 ``` go get -u github.com/yqylovy/goimportdot ``` 在安装后会在 `$GOPATH/bin` 路径中生成 `goimportdot` 文件。 --- ## 使用示例 以 `github.com/shunfei/cronsun` 作为示例,这是一个分布式任务系统,类似于 `crontab`。首先我们把代码下载下来。 ``` go get github.com/shunfei/cronsun ``` 使用 `goimportdot` 对项目结构进行解析。再通过 dot 把 解析结构转化为 `png` 图片。 ``` goimportdot -pkg=github.com/shunfei/cronsun > cronsun.dot dot -Tpng cronsun.dot > cronsun.png ``` 打开 `cronsun.png`,图片如下: ![cronsun](./cronsun.png) 可以看到现在项目整体结构一目了然,存在两个入口 `github.com/shunfei/cronsun/bin/node` 和 `github.com/shunfei/cronsun/bin/web`。 `cronsun` 还是个轻量级、整洁的项目,可以一目了然。作者在分析更复杂的项目的时候,发现生成的调用图非常复杂,看上去像一团乱麻,难以入手。这时候需要减少信息量,逐步分析。依旧以 `cronsun` 为例。如果我们只想分析 `cronsun` 的 `node` 部分: ``` goimportdot -pkg=github.com/shunfei/cronsun -root=github.com/shunfei/cronsun/node > cronsun.node.dot dot -Tpng cronsun.node.dot > cronsun.node.png ``` 图片结果: ![cronsun.node](./cronsun.node.png) 项目中通常会存在一些辅助工具,如 `log` 包,被大量地引用,在分析时可以通过指定黑名单来达到忽略某些包的目的: ``` # 忽略其中的 log、utils goimportdot -pkg=github.com/shunfei/cronsun -root=github.com/shunfei/cronsun/node -filter=b:*utils,*log > cronsun.node.core.dot dot -Tpng cronsun.node.core.dot > cronsun.node.core.png ``` 图片结果: ![cronsun.node.core](./cronsun.node.core.png) 通过指定 `root` 和 `filter`,可以有效地减少输出,快速地把握核心。 ## 结语 `goimportdot`是个刚推出的小工具,还有很多不成熟的地方,欢迎提出建议。 项目地址: [https://github.com/yqylovy/goimportdot](https://github.com/yqylovy/goimportdot) ================================================ FILE: docs/zap.dot ================================================ digraph G { "go.uber.org/zap/internal/multierror"->"go.uber.org/zap/internal/bufferpool"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/internal/bufferpool"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/internal/exit"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/internal/color"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/buffer"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/internal/multierror"; "go.uber.org/zap/internal/bufferpool"->"go.uber.org/zap/buffer"; "go.uber.org/zap/zapgrpc"->"go.uber.org/zap"; "go.uber.org/zap/zaptest/observer"->"go.uber.org/zap/zapcore"; "go.uber.org/zap"->"go.uber.org/zap/zapcore"; "go.uber.org/zap"->"go.uber.org/zap/internal/bufferpool"; "go.uber.org/zap"->"go.uber.org/zap/internal/multierror"; "go.uber.org/zap/internal/readme"; "go.uber.org/zap/buffer"; "go.uber.org/zap"; "go.uber.org/zap/zaptest/observer"; "go.uber.org/zap/benchmarks"; "go.uber.org/zap/internal/multierror"; "go.uber.org/zap/zaptest"; "go.uber.org/zap/internal/exit"; "go.uber.org/zap/zapgrpc"; "go.uber.org/zap/internal/bufferpool"; "go.uber.org/zap/internal/color"; "go.uber.org/zap/zapcore"; } ================================================ FILE: docs/zapcore.dot ================================================ digraph G { "go.uber.org/zap/internal/multierror"->"go.uber.org/zap/internal/bufferpool"; "go.uber.org/zap/internal/bufferpool"->"go.uber.org/zap/buffer"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/buffer"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/internal/multierror"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/internal/exit"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/internal/bufferpool"; "go.uber.org/zap/zapcore"->"go.uber.org/zap/internal/color"; "go.uber.org/zap/internal/exit"; "go.uber.org/zap/zapcore"; "go.uber.org/zap/internal/color"; "go.uber.org/zap/buffer"; "go.uber.org/zap/internal/multierror"; "go.uber.org/zap/internal/bufferpool"; } ================================================ FILE: goimportdot.go ================================================ package main import ( "flag" "fmt" "os" "github.com/yqylovy/goimportdot/core" ) func main() { var ignoreGit = true var ignoreTest = true var onlySelfPkg = true var packageName = "" var root = "" var filters = "" var level = -1 flag.BoolVar(&ignoreGit, "ignoregit", ignoreGit, "ignore files in git") flag.BoolVar(&ignoreTest, "ignoretest", ignoreTest, "ignore test files") flag.BoolVar(&onlySelfPkg, "only", onlySelfPkg, "only to draw the input package") flag.StringVar(&filters, "filter", "", "filter to (ignore/only include) package match wildcard,example: -filter=w:a*,*b;b:c means only include package start with a and ends with b, ignore package named c") flag.StringVar(&root, "root", root, "only draw package with the graph start from root") flag.IntVar(&level, "level", level, "show how many level , -1 for all") flag.StringVar(&packageName, "pkg", packageName, "the package to draw") flag.Parse() if packageName == "" { fmt.Println("You must specify the packge name with -pkg ") return } fileFilter := []core.FileFilter{ core.HasSuffix(false, ".go"), } if ignoreGit { fileFilter = append(fileFilter, core.NameContains(true, ".git")) } if ignoreTest { fileFilter = append(fileFilter, core.NameContains(true, "_test.go")) } pkgAndImports, err := core.GetImports(packageName, fileFilter...) if err != nil { panic(err) } pkgFilters := []core.PkgFilter{} if onlySelfPkg { pkgFilters = append(pkgFilters, core.PkgWildcardFilter(false, packageName+"*")) } if root != "" { pkgFilters = append(pkgFilters, core.RootFilter(root)) } moreFilters, err := core.ParsePkgWildcardStr(filters) if err != nil { fmt.Printf("No right filter [%s], please check!", filters) return } pkgFilters = append(pkgFilters, moreFilters...) if level >= 0 { pkgFilters = append(pkgFilters, core.PkgLevelFilter(level)) } for _, f := range pkgFilters { pkgAndImports = f(pkgAndImports) } core.WriteDot(pkgAndImports, os.Stdout) }