├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── class.uml ├── cmd └── gouml │ └── main.go ├── compress.go ├── compress_test.go ├── gen.go ├── go.mod ├── go.sum ├── internal └── gouml │ └── plantuml │ ├── export_test.go │ ├── field.go │ ├── ignore_test.go │ ├── kind.go │ ├── main_test.go │ ├── method.go │ ├── model.go │ ├── note.go │ ├── note_test.go │ ├── parser.go │ └── utils.go ├── parser.go ├── plantuml.go └── self-ref.png /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: github.com/kazukousen/gouml 3 | 4 | go: 5 | - 1.13.x 6 | 7 | script: 8 | go test -race ./... 9 | 10 | env: 11 | global: 12 | - GO111MODULE=on 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kazuki Nitta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/kazukousen/gouml.svg?branch=master)](https://travis-ci.org/kazukousen/gouml) 2 | 3 | Automatically generate PlantUML from Go Code. 4 | 5 | example (self-reference): 6 | ![self-ref](self-ref.png) 7 | 8 | Note that the interface of this library is still ALPHA level quality. 9 | Breaking changes will be introduced frequently. 10 | 11 | ## Usage 12 | 13 | ```console 14 | $ go get -u github.com/kazukousen/gouml/cmd/gouml 15 | $ gouml --version 16 | ``` 17 | 18 | Run `gouml init` (or `gouml i`) . This will parse `.go` files and generate the plantUML file. 19 | 20 | ```console 21 | $ gouml i -f /path/to/package/subpackage1/ -f /path/to/package/subpackage2/foo.go 22 | ``` 23 | 24 | Fire or Directory you want to parse, you can use `-f` flag. 25 | 26 | ### Ignore a target directory or file 27 | 28 | You can use `--ignore` Flag. 29 | 30 | ```console 31 | $ gouml i -f /path/to/package/ --ignore /path/to/package/ignorepackage/ 32 | ``` 33 | 34 | ## License 35 | 36 | Copyright (c) 2019-present [Kazuki Nitta](https://github.com/kazukousen) 37 | 38 | Licensed under [MIT License](./LICENSE) 39 | -------------------------------------------------------------------------------- /class.uml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package "gouml" { 4 | interface "Generator" as gouml.Generator { 5 | +Read(files: []string): error 6 | +UpdateIgnore(files: []string): error 7 | +WriteTo(buf: *bytes.Buffer): error 8 | } 9 | } 10 | 11 | 12 | 13 | package "gouml" { 14 | interface "Parser" as gouml.Parser { 15 | +Build(pkgs: types.Package) 16 | +WriteTo(buf: *bytes.Buffer) 17 | } 18 | } 19 | 20 | 21 | 22 | package "gouml" { 23 | class "generator" as gouml.generator <> { 24 | -parser: gouml.Parser 25 | -targets: []string 26 | -ignoreFiles: map[string]struct{} 27 | -fset: token.FileSet 28 | -astPkgs: ast.Package 29 | -pkgs: types.Package 30 | -isDebug: bool 31 | +WriteTo(buf: *bytes.Buffer): error 32 | +Read(files: []string): error 33 | -read(f: string): error 34 | -visit(path: string, f: os.FileInfo, err: error): error 35 | +UpdateIgnore(files: []string): error 36 | -updateIgnore(f: string): error 37 | -doUpdateIgnore(path: string, f: os.FileInfo, err: error): error 38 | -ast(): error 39 | -check(): error 40 | } 41 | } 42 | 43 | gouml.generator --> gouml.Parser 44 | 45 | 46 | package "plantuml" { 47 | class "Models" as plantuml.Models <> { 48 | -append(obj: types.TypeName) 49 | +WriteTo(buf: *bytes.Buffer, ex: plantuml.exists) 50 | -writeImplements(buf: *bytes.Buffer, depth: int) 51 | } 52 | } 53 | 54 | 55 | plantuml.Models ..> plantuml.exists : <> 56 | plantuml.Models *-- plantuml.model 57 | package "plantuml" { 58 | class "Note" as plantuml.Note <> { 59 | +WriteTo(buf: *bytes.Buffer, depth: int) 60 | } 61 | } 62 | 63 | 64 | 65 | package "plantuml" { 66 | class "Notes" as plantuml.Notes <> { 67 | -append(k: types.Named, c: types.Const) 68 | +WriteTo(buf: *bytes.Buffer) 69 | } 70 | } 71 | 72 | 73 | plantuml.Notes *-- plantuml.Note 74 | package "plantuml" { 75 | class "exists" as plantuml.exists <> 76 | } 77 | 78 | 79 | 80 | package "plantuml" { 81 | class "field" as plantuml.field <> { 82 | -st: types.Struct 83 | -size(): int 84 | +WriteTo(buf: *bytes.Buffer, depth: int) 85 | -typeString(buf: *bytes.Buffer, typ: types.Type) 86 | -writeDiagram(buf: *bytes.Buffer, ex: plantuml.exists, from: string, depth: int) 87 | } 88 | } 89 | 90 | 91 | 92 | package "plantuml" { 93 | class "method" as plantuml.method <> { 94 | -f: types.Func 95 | +WriteTo(buf: *bytes.Buffer, depth: int) 96 | -writeDiagram(buf: *bytes.Buffer, ex: plantuml.exists, from: string, depth: int) 97 | } 98 | } 99 | 100 | 101 | 102 | package "plantuml" { 103 | class "methods" as plantuml.methods <> { 104 | +WriteTo(buf: *bytes.Buffer, depth: int) 105 | -writeDiagram(buf: *bytes.Buffer, ex: plantuml.exists, from: string, depth: int) 106 | } 107 | } 108 | 109 | 110 | plantuml.methods *-- plantuml.method 111 | package "plantuml" { 112 | class "model" as plantuml.model <> { 113 | -obj: types.TypeName 114 | -id: string 115 | -kind: plantuml.modelKind 116 | -field: plantuml.field 117 | -methods: plantuml.methods 118 | -wrap: types.Named 119 | -build() 120 | -as(): string 121 | -writeClass(buf: *bytes.Buffer) 122 | -writeDiagram(buf: *bytes.Buffer, ex: plantuml.exists) 123 | } 124 | } 125 | 126 | plantuml.model --> plantuml.modelKind 127 | plantuml.model --> plantuml.field 128 | plantuml.model --> plantuml.methods 129 | 130 | 131 | package "plantuml" { 132 | class "modelKind" as plantuml.modelKind <> { 133 | +Printf(name: string, alias: string): string 134 | } 135 | } 136 | 137 | 138 | 139 | package "plantuml" { 140 | class "parser" as plantuml.parser <> { 141 | -models: plantuml.Models 142 | -notes: plantuml.Notes 143 | -ex: plantuml.exists 144 | +Build(pkgs: types.Package) 145 | +WriteTo(buf: *bytes.Buffer) 146 | } 147 | } 148 | 149 | plantuml.parser --> plantuml.Models 150 | plantuml.parser --> plantuml.Notes 151 | plantuml.parser --> plantuml.exists 152 | 153 | 154 | gouml.generator -up-|> gouml.Generator 155 | plantuml.parser -up-|> gouml.Parser 156 | 157 | package "plantuml" { 158 | note as N_plantuml_modelKind 159 | modelKind 160 | 161 | modelKindEntity 162 | modelKindInterface 163 | modelKindValueObject 164 | end note 165 | } 166 | N_plantuml_modelKind --> plantuml.modelKind 167 | 168 | @enduml 169 | -------------------------------------------------------------------------------- /cmd/gouml/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/go-kit/kit/log" 11 | "github.com/go-kit/kit/log/level" 12 | "github.com/kazukousen/gouml" 13 | "github.com/urfave/cli" 14 | ) 15 | 16 | func main() { 17 | logger := log.NewLogfmtLogger(os.Stdout) 18 | logger = log.With(logger, "ts", log.DefaultTimestamp) 19 | 20 | flags := []cli.Flag{ 21 | &cli.StringSliceFlag{ 22 | Name: "file, f", 23 | Usage: "File or Directory you want to parse", 24 | }, 25 | &cli.StringSliceFlag{ 26 | Name: "ignore, I", 27 | Usage: "File or Directory you want to ignore parsing", 28 | }, 29 | &cli.BoolFlag{ 30 | Name: "verbose", 31 | Usage: "debugging", 32 | }, 33 | } 34 | app := cli.NewApp() 35 | app.Version = "0.2" 36 | app.Usage = "Automatically generate PlantUML from Go Code." 37 | app.Commands = []cli.Command{ 38 | { 39 | Name: "init", 40 | Aliases: []string{"i"}, 41 | Usage: "Create *.puml", 42 | Action: func(c *cli.Context) error { 43 | buf := &bytes.Buffer{} 44 | buf.WriteString("@startuml\n") 45 | if err := generate(logger, buf, c.StringSlice("ignore"), c.StringSlice("file"), c.Bool("verbose")); err != nil { 46 | return err 47 | } 48 | buf.WriteString("@enduml\n") 49 | 50 | out := c.String("out") 51 | out, err := filepath.Abs(out) 52 | if err != nil { 53 | return err 54 | } 55 | if err := writeFile(out, buf); err != nil { 56 | return err 57 | } 58 | fmt.Printf("output to file: %s\n", out) 59 | return nil 60 | }, 61 | Flags: append(flags, []cli.Flag{ 62 | &cli.StringFlag{ 63 | Name: "out, o", 64 | Value: "file.puml", 65 | Usage: "File Name you want to parsed", 66 | }, 67 | }...), 68 | }, 69 | { 70 | Name: "encode", 71 | Aliases: []string{"e"}, 72 | Usage: "encode base64", 73 | Action: func(c *cli.Context) error { 74 | buf := &bytes.Buffer{} 75 | if err := generate(logger, buf, c.StringSlice("ignore"), c.StringSlice("file"), c.Bool("verbose")); err != nil { 76 | return err 77 | } 78 | 79 | fmt.Printf(gouml.Compress(buf.String())) 80 | return nil 81 | }, 82 | Flags: append(flags, []cli.Flag{}...), 83 | }, 84 | } 85 | 86 | if err := app.Run(os.Args); err != nil { 87 | level.Error(logger).Log("msg", "failed to run", "error", err) 88 | } 89 | } 90 | 91 | func generate(logger log.Logger, buf *bytes.Buffer, ignores []string, targets []string, verbose bool) error { 92 | gen := gouml.NewGenerator(logger, gouml.PlantUMLParser(logger), verbose) 93 | if len(ignores) > 0 { 94 | if err := gen.UpdateIgnore(ignores); err != nil { 95 | return err 96 | } 97 | } 98 | if len(targets) == 0 { 99 | targets = []string{"./"} 100 | } 101 | if err := gen.Read(targets); err != nil { 102 | return err 103 | } 104 | 105 | gen.WriteTo(buf) 106 | return nil 107 | } 108 | 109 | func writeFile(file string, buf io.Reader) (e error) { 110 | f, err := os.Create(file) 111 | if err != nil { 112 | return err 113 | } 114 | defer func() { 115 | if err := f.Close(); err != nil { 116 | e = err 117 | } 118 | }() 119 | io.Copy(f, buf) 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /compress.go: -------------------------------------------------------------------------------- 1 | package gouml 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | // Compress ... 11 | func Compress(src string) string { 12 | trimmed := strings.Replace(src, "\t", "", -1) 13 | compressed := compress(trimmed) 14 | converted := encode64(compressed) 15 | return converted 16 | } 17 | 18 | func compress(src string) []byte { 19 | buf := bytesPool.Get().(*bytes.Buffer) 20 | defer func() { 21 | buf.Reset() 22 | bytesPool.Put(buf) 23 | }() 24 | 25 | zw, _ := zlib.NewWriterLevel(buf, zlib.BestCompression) 26 | zw.Write([]byte(src)) 27 | zw.Close() 28 | return buf.Bytes() 29 | } 30 | 31 | var bytesPool = &sync.Pool{ 32 | New: func() interface{} { 33 | return &bytes.Buffer{} 34 | }, 35 | } 36 | 37 | func encode64(input []byte) string { 38 | var buf bytes.Buffer 39 | length := len(input) 40 | // padding 41 | for i := 0; i < 3-length%3; i++ { 42 | input = append(input, byte(0)) 43 | } 44 | 45 | for i := 0; i < length; i += 3 { 46 | cs := append3bytes(input[i], input[i+1], input[i+2]) 47 | for _, c := range cs { 48 | buf.WriteByte(byte(chars[c])) 49 | } 50 | } 51 | return buf.String() 52 | } 53 | 54 | const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_" 55 | 56 | func append3bytes(b1, b2, b3 byte) []byte { 57 | c1 := (b1 >> 2) 58 | c2 := ((b1 & 0x3) << 4) | (b2 >> 4) 59 | c3 := ((b2 & 0xF) << 2) | (b3 >> 6) 60 | c4 := b3 & 0x3F 61 | return []byte{c1, c2, c3, c4} 62 | } 63 | -------------------------------------------------------------------------------- /compress_test.go: -------------------------------------------------------------------------------- 1 | package gouml_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kazukousen/gouml" 7 | ) 8 | 9 | func TestCompress(t *testing.T) { 10 | src := ` 11 | class Foo { 12 | Bar: Bar 13 | } 14 | 15 | class Bar 16 | 17 | Foo --> Bar 18 | ` 19 | 20 | want := `UDhYIiv9B2vMSClFLwZcSaeiib9mIYpYgkM2YeCuN219NLqxC0SG003__rvv3QC0` 21 | 22 | got := gouml.Compress(src) 23 | if got != want { 24 | t.Errorf("\ngot %s\nwant %s\n", got, want) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | package gouml 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/importer" 8 | "go/parser" 9 | "go/token" 10 | "go/types" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "time" 15 | 16 | "github.com/go-kit/kit/log" 17 | "github.com/go-kit/kit/log/level" 18 | ) 19 | 20 | // Generator ... 21 | type Generator interface { 22 | UpdateIgnore(files []string) error 23 | Read(files []string) error 24 | WriteTo(buf *bytes.Buffer) error 25 | } 26 | 27 | type generator struct { 28 | logger log.Logger 29 | parser Parser 30 | targets []string 31 | ignoreFiles map[string]struct{} 32 | fset *token.FileSet 33 | astPkgs map[string]*ast.Package 34 | pkgs []*types.Package 35 | isDebug bool 36 | } 37 | 38 | // NewGenerator ... 39 | func NewGenerator(logger log.Logger, parser Parser, isDebug bool) Generator { 40 | return &generator{ 41 | logger: log.With(logger, "component", "generator"), 42 | parser: parser, 43 | targets: []string{}, 44 | ignoreFiles: map[string]struct{}{}, 45 | fset: token.NewFileSet(), 46 | astPkgs: map[string]*ast.Package{}, 47 | pkgs: []*types.Package{}, 48 | isDebug: isDebug, 49 | } 50 | } 51 | 52 | func (g generator) WriteTo(buf *bytes.Buffer) error { 53 | if err := g.ast(); err != nil { 54 | return err 55 | } 56 | if err := g.check(); err != nil { 57 | return err 58 | } 59 | g.parser.Build(g.pkgs) 60 | g.parser.WriteTo(buf) 61 | return nil 62 | } 63 | 64 | func (g *generator) Read(files []string) error { 65 | start := time.Now() 66 | defer func() { 67 | elapsed := time.Since(start) 68 | level.Debug(g.logger).Log("msg", "read .go files", "ms", elapsed.Truncate(time.Millisecond)) 69 | }() 70 | 71 | for _, f := range files { 72 | if err := g.read(f); err != nil { 73 | return err 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | func (g *generator) read(f string) error { 80 | fInfo, err := os.Stat(f) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | if fInfo.IsDir() { 86 | if err := filepath.Walk(f, g.visit); err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | 92 | if err := g.visit(f, nil, nil); err != nil { 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | func (g *generator) visit(path string, f os.FileInfo, err error) error { 99 | if ext := filepath.Ext(path); ext != ".go" { 100 | return nil 101 | } 102 | if strings.HasSuffix(path, "_test.go") { 103 | return nil 104 | } 105 | path, err = filepath.Abs(path) 106 | if err != nil { 107 | return err 108 | } 109 | if _, ok := g.ignoreFiles[path]; ok { 110 | return nil 111 | } 112 | g.targets = append(g.targets, path) 113 | return nil 114 | } 115 | 116 | func (g *generator) UpdateIgnore(files []string) error { 117 | for _, f := range files { 118 | if err := g.updateIgnore(f); err != nil { 119 | return err 120 | } 121 | } 122 | return nil 123 | } 124 | 125 | func (g *generator) updateIgnore(f string) error { 126 | fInfo, err := os.Stat(f) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | if fInfo.IsDir() { 132 | if err := filepath.Walk(f, g.doUpdateIgnore); err != nil { 133 | return err 134 | } 135 | return nil 136 | } 137 | 138 | if err := g.doUpdateIgnore(f, nil, nil); err != nil { 139 | return err 140 | } 141 | return nil 142 | } 143 | 144 | func (g *generator) doUpdateIgnore(path string, f os.FileInfo, err error) error { 145 | path, err = filepath.Abs(path) 146 | if err != nil { 147 | return err 148 | } 149 | g.ignoreFiles[path] = struct{}{} 150 | return nil 151 | } 152 | 153 | func (g generator) ast() error { 154 | start := time.Now() 155 | defer func() { 156 | elapsed := time.Since(start) 157 | level.Debug(g.logger).Log("msg", "parsed to AST", "ms", elapsed.Truncate(time.Millisecond)) 158 | }() 159 | 160 | for _, path := range g.targets { 161 | if g.isDebug { 162 | fmt.Printf("parsing AST: %s\n", path) 163 | } 164 | astFile, err := parser.ParseFile(g.fset, path, nil, parser.ParseComments) 165 | if err != nil { 166 | return fmt.Errorf("ParseFile panic: %w", err) 167 | } 168 | name := astFile.Name.Name 169 | pkg, ok := g.astPkgs[name] 170 | if !ok { 171 | pkg = &ast.Package{ 172 | Name: name, 173 | Files: make(map[string]*ast.File), 174 | } 175 | } 176 | pkg.Files[path] = astFile 177 | g.astPkgs[name] = pkg 178 | } 179 | return nil 180 | } 181 | 182 | func (g *generator) check() error { 183 | start := time.Now() 184 | defer func() { 185 | elapsed := time.Since(start) 186 | level.Debug(g.logger).Log("msg", "checked type", "ms", elapsed.Truncate(time.Millisecond)) 187 | }() 188 | 189 | conf := types.Config{ 190 | Importer: importer.For("source", nil), 191 | Error: func(err error) { 192 | if g.isDebug { 193 | fmt.Printf("error: %+v\n", err) 194 | } 195 | }, 196 | } 197 | for _, astPkg := range g.astPkgs { 198 | files := make([]*ast.File, 0, len(astPkg.Files)) 199 | for _, f := range astPkg.Files { 200 | files = append(files, f) 201 | } 202 | pkg, _ := conf.Check(astPkg.Name, g.fset, files, nil) 203 | g.pkgs = append(g.pkgs, pkg) 204 | } 205 | return nil 206 | } 207 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kazukousen/gouml 2 | 3 | require ( 4 | github.com/go-kit/kit v0.9.0 5 | github.com/go-logfmt/logfmt v0.4.0 // indirect 6 | github.com/go-stack/stack v1.8.0 // indirect 7 | github.com/urfave/cli v1.20.0 8 | ) 9 | 10 | go 1.13 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= 2 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 3 | github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= 4 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 5 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 6 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 7 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= 8 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 9 | github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= 10 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 11 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/export_test.go: -------------------------------------------------------------------------------- 1 | package plantuml 2 | 3 | var ExportTestNotesAppend = (Notes).append 4 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/field.go: -------------------------------------------------------------------------------- 1 | package plantuml 2 | 3 | import ( 4 | "bytes" 5 | "go/types" 6 | ) 7 | 8 | type field struct { 9 | st *types.Struct 10 | } 11 | 12 | func (f field) size() int { 13 | if f.st == nil { 14 | return 0 15 | } 16 | return f.st.NumFields() 17 | } 18 | 19 | func (f field) WriteTo(buf *bytes.Buffer, depth int) { 20 | if f.st == nil { 21 | return 22 | } 23 | for i := 0; i < f.st.NumFields(); i++ { 24 | newline(buf, depth) 25 | v := f.st.Field(i) 26 | buf.WriteString(exportedIcon(v.Exported())) 27 | buf.WriteString(v.Name()) 28 | buf.WriteString(": ") 29 | f.typeString(buf, v.Type()) 30 | } 31 | } 32 | 33 | func (f field) typeString(buf *bytes.Buffer, typ types.Type) { 34 | switch typ := typ.(type) { 35 | case *types.Struct: 36 | buf.WriteString("struct{") 37 | for i := 0; i < typ.NumFields(); i++ { 38 | if i > 0 { 39 | buf.WriteString("; ") 40 | } 41 | v := typ.Field(i) 42 | buf.WriteString(v.Name()) 43 | buf.WriteString(": ") 44 | f.typeString(buf, v.Type()) 45 | } 46 | buf.WriteString("}") 47 | return 48 | } 49 | buf.WriteString(extractName(typ.String())) 50 | } 51 | 52 | func (f field) writeDiagram(buf *bytes.Buffer, ex exists, from string, depth int) { 53 | if f.st == nil { 54 | return 55 | } 56 | for i := 0; i < f.st.NumFields(); i++ { 57 | typ := f.st.Field(i).Type() 58 | if ptr, ok := typ.(*types.Pointer); ok { 59 | typ = ptr.Elem() 60 | } 61 | if m, ok := typ.(*types.Map); ok { 62 | typ = m.Elem() 63 | } 64 | if sl, ok := typ.(*types.Slice); ok { 65 | typ = sl.Elem() 66 | } 67 | if _, ok := typ.(*types.Named); !ok { 68 | continue 69 | } 70 | to := extractName(typ.String()) 71 | if _, ok := ex[to]; !ok { 72 | continue 73 | } 74 | newline(buf, depth) 75 | buf.WriteString(from) 76 | buf.WriteString(" --> ") 77 | buf.WriteString(to) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/ignore_test.go: -------------------------------------------------------------------------------- 1 | package plantuml_test 2 | 3 | type foo struct { 4 | } 5 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/kind.go: -------------------------------------------------------------------------------- 1 | package plantuml 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | ) 7 | 8 | type modelKind string 9 | 10 | const ( 11 | modelKindInterface modelKind = `interface "%s" as %s` 12 | modelKindValueObject modelKind = `class "%s" as %s <>` 13 | modelKindEntity modelKind = `class "%s" as %s <>` 14 | ) 15 | 16 | func (k modelKind) Printf(name, alias string) string { 17 | return fmt.Sprintf(string(k), name, alias) 18 | } 19 | 20 | func isCommand(f *types.Func) bool { 21 | // *types.Func.Type() is always a *types.Signature 22 | sig := f.Type().(*types.Signature) 23 | if _, ok := sig.Recv().Type().(*types.Pointer); !ok { 24 | return false 25 | } 26 | if sig.Results().Len() == 0 { 27 | return true 28 | } 29 | if sig.Results().Len() == 1 { 30 | t := sig.Results().At(0).Type() 31 | errType := types.Universe.Lookup("error").Type() 32 | if types.Implements(t, errType.Underlying().(*types.Interface)) { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/main_test.go: -------------------------------------------------------------------------------- 1 | package plantuml_test 2 | 3 | func trim(src string) string { 4 | dst := make([]byte, 0, len(src)) 5 | for _, ch := range src { 6 | if ch == '\t' { 7 | continue 8 | } 9 | dst = append(dst, byte(ch)) 10 | } 11 | return string(dst) 12 | } 13 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/method.go: -------------------------------------------------------------------------------- 1 | package plantuml 2 | 3 | import ( 4 | "bytes" 5 | "go/types" 6 | ) 7 | 8 | type methods []method 9 | 10 | func (ms methods) WriteTo(buf *bytes.Buffer, depth int) { 11 | for _, m := range ms { 12 | m.WriteTo(buf, depth) 13 | } 14 | } 15 | 16 | func (ms methods) writeDiagram(buf *bytes.Buffer, ex exists, from string, depth int) { 17 | for _, m := range ms { 18 | m.writeDiagram(buf, ex, from, depth) 19 | } 20 | } 21 | 22 | type method struct { 23 | f *types.Func 24 | } 25 | 26 | func (m method) WriteTo(buf *bytes.Buffer, depth int) { 27 | if m.f == nil { 28 | return 29 | } 30 | 31 | newline(buf, depth) 32 | buf.WriteString(exportedIcon(m.f.Exported())) 33 | // Name 34 | buf.WriteString(m.f.Name()) 35 | 36 | // Signature 37 | sig, _ := m.f.Type().(*types.Signature) 38 | 39 | // parameters 40 | param := sig.Params() 41 | buf.WriteString("(") 42 | for i := 0; i < param.Len(); i++ { 43 | if i > 0 { 44 | buf.WriteString(", ") 45 | } 46 | v := param.At(i) 47 | name, typ := v.Name(), extractName(v.Type().String()) 48 | buf.WriteString(name) 49 | buf.WriteString(": ") 50 | buf.WriteString(typ) 51 | } 52 | buf.WriteString(")") 53 | 54 | // results 55 | res := sig.Results() 56 | if res.Len() > 0 { 57 | buf.WriteString(": ") 58 | } 59 | if res.Len() > 1 { 60 | buf.WriteString("(") 61 | } 62 | for i := 0; i < res.Len(); i++ { 63 | if i > 0 { 64 | buf.WriteString(", ") 65 | } 66 | v := res.At(i) 67 | name, typ := v.Name(), extractName(v.Type().String()) 68 | if name != "" { 69 | buf.WriteString(name) 70 | buf.WriteString(": ") 71 | } 72 | buf.WriteString(typ) 73 | } 74 | if res.Len() > 1 { 75 | buf.WriteString(")") 76 | } 77 | } 78 | 79 | func (m method) writeDiagram(buf *bytes.Buffer, ex exists, from string, depth int) { 80 | if m.f == nil { 81 | return 82 | } 83 | 84 | if !m.f.Exported() { 85 | // a non-exported method do not draw a diagram. 86 | return 87 | } 88 | 89 | // Signature 90 | sig, _ := m.f.Type().(*types.Signature) 91 | 92 | // parameters 93 | param := sig.Params() 94 | for i := 0; i < param.Len(); i++ { 95 | typ := param.At(i).Type() 96 | if ptr, ok := typ.(*types.Pointer); ok { 97 | typ = ptr.Elem() 98 | } 99 | if m, ok := typ.(*types.Map); ok { 100 | typ = m.Elem() 101 | } 102 | if sl, ok := typ.(*types.Slice); ok { 103 | typ = sl.Elem() 104 | } 105 | if _, ok := typ.(*types.Named); !ok { 106 | continue 107 | } 108 | to := extractName(typ.String()) 109 | if _, ok := ex[to]; !ok { 110 | continue 111 | } 112 | newline(buf, depth) 113 | buf.WriteString(from) 114 | buf.WriteString(" ..> ") 115 | buf.WriteString(to) 116 | buf.WriteString(" : <> ") 117 | } 118 | 119 | // results 120 | res := sig.Results() 121 | for i := 0; i < res.Len(); i++ { 122 | typ := res.At(i).Type() 123 | if ptr, ok := typ.(*types.Pointer); ok { 124 | typ = ptr.Elem() 125 | } 126 | if m, ok := typ.(*types.Map); ok { 127 | typ = m.Elem() 128 | } 129 | if sl, ok := typ.(*types.Slice); ok { 130 | typ = sl.Elem() 131 | } 132 | if _, ok := typ.(*types.Named); !ok { 133 | continue 134 | } 135 | to := extractName(typ.String()) 136 | if _, ok := ex[to]; !ok { 137 | continue 138 | } 139 | newline(buf, depth) 140 | buf.WriteString(from) 141 | buf.WriteString(" ..> ") 142 | buf.WriteString(to) 143 | buf.WriteString(" : <> ") 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/model.go: -------------------------------------------------------------------------------- 1 | package plantuml 2 | 3 | import ( 4 | "bytes" 5 | "go/token" 6 | "go/types" 7 | ) 8 | 9 | // Models ... 10 | type Models []model 11 | 12 | func (ms *Models) append(obj *types.TypeName) { 13 | m := model{obj: obj} 14 | m.build() 15 | *ms = append(*ms, m) 16 | } 17 | 18 | // WriteTo ... 19 | func (ms Models) WriteTo(buf *bytes.Buffer, ex exists) { 20 | for _, m := range ms { 21 | m.writeClass(buf) 22 | m.writeDiagram(buf, ex) 23 | } 24 | ms.writeImplements(buf, 1) 25 | } 26 | 27 | func (ms Models) writeImplements(buf *bytes.Buffer, depth int) { 28 | for _, t := range ms { 29 | T := t.obj.Type() 30 | for _, u := range ms { 31 | U := u.obj.Type() 32 | if T == U || !types.IsInterface(U) { 33 | continue 34 | } 35 | if types.AssignableTo(T, U) || (!types.IsInterface(T) && types.AssignableTo(types.NewPointer(T), U)) { 36 | newline(buf, depth) 37 | buf.WriteString(t.as()) 38 | buf.WriteString(" -up-|> ") 39 | buf.WriteString(u.as()) 40 | } 41 | } 42 | } 43 | } 44 | 45 | type model struct { 46 | obj *types.TypeName 47 | id string 48 | kind modelKind 49 | field field 50 | methods methods 51 | wrap *types.Named 52 | } 53 | 54 | func (m *model) build() { 55 | obj := m.obj 56 | // *types.TypeName represents ```type [typ] [underlying]``` 57 | 58 | // get type 59 | typ := obj.Type() 60 | m.id = extractName(typ.String()) 61 | // TODO: obj.IsAlias() is true 62 | 63 | // named type (means user-defined class in OOP) 64 | if named, _ := typ.(*types.Named); named != nil { 65 | 66 | // implemented methods 67 | for i := 0; i < named.NumMethods(); i++ { 68 | f := named.Method(i) 69 | if isCommand(f) { 70 | m.kind = modelKindEntity 71 | } 72 | m.methods = append(m.methods, method{f: f}) 73 | } 74 | } 75 | 76 | // underlying 77 | switch un := typ.Underlying().(type) { 78 | // struct 79 | case *types.Struct: 80 | m.field = field{st: un} 81 | 82 | // interface 83 | case *types.Interface: 84 | m.kind = modelKindInterface 85 | for i := 0; i < un.NumMethods(); i++ { 86 | m.methods = append(m.methods, method{f: un.Method(i)}) 87 | } 88 | 89 | // wrap 90 | case *types.Slice: 91 | if named, _ := un.Elem().(*types.Named); named != nil { 92 | m.wrap = named 93 | } 94 | case *types.Map: 95 | if named, _ := un.Elem().(*types.Named); named != nil { 96 | m.wrap = named 97 | } 98 | 99 | // first-class function 100 | case *types.Signature: 101 | f := types.NewFunc(token.NoPos, obj.Pkg(), obj.Name(), un) 102 | m.methods = append(m.methods, method{f: f}) 103 | } 104 | 105 | if m.kind == "" { 106 | m.kind = modelKindValueObject 107 | } 108 | } 109 | 110 | func (m model) as() string { 111 | return m.id 112 | } 113 | 114 | func (m model) writeClass(buf *bytes.Buffer) { 115 | id := m.as() 116 | 117 | newline(buf, 0) 118 | // package 119 | buf.WriteString(`package "`) 120 | buf.WriteString(extractPkgName(id)) 121 | buf.WriteString(`" {`) 122 | // class 123 | newline(buf, 1) 124 | buf.WriteString(m.kind.Printf(extractTypeName(id), id)) 125 | if m.field.size() > 0 || len(m.methods) > 0 { 126 | buf.WriteString(` {`) 127 | // fields 128 | m.field.WriteTo(buf, 2) 129 | // methods 130 | m.methods.WriteTo(buf, 2) 131 | newline(buf, 1) 132 | buf.WriteString("}") 133 | } 134 | newline(buf, 0) 135 | buf.WriteString("}") 136 | } 137 | 138 | func (m model) writeDiagram(buf *bytes.Buffer, ex exists) { 139 | from := m.as() 140 | 141 | newline(buf, 0) 142 | m.field.writeDiagram(buf, ex, from, 1) 143 | 144 | newline(buf, 0) 145 | m.methods.writeDiagram(buf, ex, from, 1) 146 | 147 | newline(buf, 0) 148 | if wrap := m.wrap; wrap != nil { 149 | to := extractName(wrap.String()) 150 | if _, ok := ex[to]; ok { 151 | buf.WriteString(from) 152 | buf.WriteString(" *-- ") 153 | buf.WriteString(to) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/note.go: -------------------------------------------------------------------------------- 1 | package plantuml 2 | 3 | import ( 4 | "bytes" 5 | "go/types" 6 | "strings" 7 | ) 8 | 9 | // Notes ... 10 | type Notes map[*types.Named]Note 11 | 12 | func (ns Notes) append(k *types.Named, c *types.Const) { 13 | note, ok := ns[k] 14 | if !ok { 15 | note = []*types.Const{} 16 | } 17 | note = append(note, c) 18 | ns[k] = note 19 | } 20 | 21 | // Note ... 22 | type Note []*types.Const 23 | 24 | // WriteTo ... 25 | func (ns Notes) WriteTo(buf *bytes.Buffer) { 26 | newline(buf, 0) 27 | for named, n := range ns { 28 | to := extractName(named.String()) 29 | from := "N_" + strings.Replace(to, ".", "_", -1) 30 | 31 | newline(buf, 0) 32 | buf.WriteString(`package "`) 33 | buf.WriteString(named.Obj().Pkg().Name()) 34 | buf.WriteString(`" {`) 35 | 36 | // write header 37 | newline(buf, 1) 38 | buf.WriteString("note as ") 39 | buf.WriteString(from) 40 | // write title 41 | newline(buf, 2) 42 | buf.WriteString("") 43 | buf.WriteString(extractTypeName(to)) 44 | buf.WriteString("\n") 45 | 46 | // write elements 47 | n.WriteTo(buf, 2) 48 | // write footer 49 | newline(buf, 1) 50 | buf.WriteString("end note") 51 | 52 | newline(buf, 0) 53 | buf.WriteString(`}`) 54 | 55 | // write relation 56 | newline(buf, 0) 57 | buf.WriteString(from) 58 | buf.WriteString(" --> ") 59 | buf.WriteString(to) 60 | } 61 | } 62 | 63 | // WriteTo ... 64 | func (n Note) WriteTo(buf *bytes.Buffer, depth int) { 65 | for _, row := range n { 66 | newline(buf, depth) 67 | buf.WriteString(row.Name()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/note_test.go: -------------------------------------------------------------------------------- 1 | package plantuml_test 2 | 3 | import ( 4 | "bytes" 5 | "go/ast" 6 | "go/importer" 7 | "go/parser" 8 | "go/token" 9 | "go/types" 10 | "testing" 11 | 12 | "github.com/kazukousen/gouml/internal/gouml/plantuml" 13 | ) 14 | 15 | func TestNote(t *testing.T) { 16 | notes := plantuml.Notes{} 17 | fset := token.NewFileSet() 18 | src := ` 19 | package time 20 | type Weekday int 21 | 22 | const ( 23 | Sunday Weekday = iota 24 | Monday 25 | Tuesday 26 | Wednesday 27 | Thursday 28 | Friday 29 | Saturday 30 | ) 31 | ` 32 | want := ` 33 | 34 | package "time" { 35 | note as N_time_Weekday 36 | Weekday 37 | 38 | Friday 39 | Monday 40 | Saturday 41 | Sunday 42 | Thursday 43 | Tuesday 44 | Wednesday 45 | end note 46 | } 47 | N_time_Weekday --> time.Weekday` 48 | file, err := parser.ParseFile(fset, "", src, parser.ParseComments) 49 | if err != nil { 50 | t.Errorf(": %+v", err) 51 | return 52 | } 53 | conf := types.Config{ 54 | Importer: importer.Default(), 55 | } 56 | pkg, err := conf.Check(file.Name.Name, fset, []*ast.File{file}, nil) 57 | if err != nil { 58 | t.Errorf(": %+v", err) 59 | return 60 | } 61 | for _, name := range pkg.Scope().Names() { 62 | obj := pkg.Scope().Lookup(name) 63 | if c, _ := obj.(*types.Const); c != nil { 64 | if named, _ := obj.Type().(*types.Named); named != nil { 65 | plantuml.ExportTestNotesAppend(notes, named, c) 66 | } 67 | } 68 | } 69 | buf := &bytes.Buffer{} 70 | notes.WriteTo(buf) 71 | if g, w := trim(buf.String()), trim(want); g != w { 72 | t.Errorf("not equal\ngot: %s\nwant: %s", g, w) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/parser.go: -------------------------------------------------------------------------------- 1 | package plantuml 2 | 3 | import ( 4 | "bytes" 5 | "go/types" 6 | "time" 7 | 8 | "github.com/go-kit/kit/log" 9 | "github.com/go-kit/kit/log/level" 10 | ) 11 | 12 | // NewParser ... 13 | func NewParser(logger log.Logger) *parser { 14 | return &parser{ 15 | logger: log.With(logger, "component", "parser"), 16 | models: Models{}, 17 | notes: Notes{}, 18 | ex: exists{}, 19 | } 20 | } 21 | 22 | type parser struct { 23 | logger log.Logger 24 | models Models 25 | notes Notes 26 | ex exists 27 | } 28 | 29 | func (p *parser) Build(pkgs []*types.Package) { 30 | start := time.Now() 31 | defer func() { 32 | elapsed := time.Since(start) 33 | level.Debug(p.logger).Log("msg", "built uml", "ms", elapsed.Truncate(time.Millisecond)) 34 | }() 35 | 36 | objects := []types.Object{} 37 | for _, pkg := range pkgs { 38 | scope := pkg.Scope() 39 | for _, name := range scope.Names() { 40 | obj := scope.Lookup(name) 41 | objects = append(objects, obj) 42 | 43 | if obj.Pkg().Name() == pkg.Name() { 44 | if named, _ := obj.Type().(*types.Named); named != nil { 45 | p.ex[extractName(named.String())] = struct{}{} 46 | } 47 | } 48 | } 49 | } 50 | 51 | for _, obj := range objects { 52 | switch obj := obj.(type) { 53 | 54 | // declared type 55 | case *types.TypeName: 56 | p.models.append(obj) 57 | 58 | // declared constant 59 | case *types.Const: 60 | if named, _ := obj.Type().(*types.Named); named != nil { 61 | p.notes.append(named, obj) 62 | } 63 | } 64 | } 65 | } 66 | 67 | func (p parser) WriteTo(buf *bytes.Buffer) { 68 | start := time.Now() 69 | defer func() { 70 | elapsed := time.Since(start) 71 | level.Debug(p.logger).Log("msg", "write to file", "ms", elapsed.Truncate(time.Millisecond)) 72 | }() 73 | 74 | p.models.WriteTo(buf, p.ex) 75 | p.notes.WriteTo(buf) 76 | newline(buf, 0) 77 | newline(buf, 0) 78 | } 79 | -------------------------------------------------------------------------------- /internal/gouml/plantuml/utils.go: -------------------------------------------------------------------------------- 1 | package plantuml 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | func newline(dst *bytes.Buffer, depth int) { 9 | dst.WriteString("\n") 10 | for i := 0; i < depth; i++ { 11 | dst.WriteString("\t") 12 | } 13 | } 14 | 15 | func exportedIcon(exported bool) string { 16 | if exported { 17 | return "+" 18 | } 19 | return "-" 20 | } 21 | 22 | func extractName(full string) string { 23 | if strings.Contains(full, "/") { 24 | parts := strings.Split(full, "/") 25 | full = parts[len(parts)-1] 26 | } 27 | return full 28 | } 29 | 30 | func extractPkgName(name string) string { 31 | if strings.Contains(name, ".") { 32 | parts := strings.Split(name, ".") 33 | name = parts[len(parts)-2] 34 | } 35 | return name 36 | } 37 | 38 | func extractTypeName(name string) string { 39 | if strings.Contains(name, ".") { 40 | parts := strings.Split(name, ".") 41 | name = parts[len(parts)-1] 42 | } 43 | return name 44 | } 45 | 46 | type exists map[string]struct{} 47 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package gouml 2 | 3 | import ( 4 | "bytes" 5 | "go/types" 6 | ) 7 | 8 | // Parser ... 9 | type Parser interface { 10 | Build(pkgs []*types.Package) 11 | WriteTo(buf *bytes.Buffer) 12 | } 13 | -------------------------------------------------------------------------------- /plantuml.go: -------------------------------------------------------------------------------- 1 | package gouml 2 | 3 | import ( 4 | "github.com/go-kit/kit/log" 5 | "github.com/kazukousen/gouml/internal/gouml/plantuml" 6 | ) 7 | 8 | // PlantUMLParser ... 9 | func PlantUMLParser(logger log.Logger) Parser { 10 | return plantuml.NewParser(logger) 11 | } 12 | -------------------------------------------------------------------------------- /self-ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazukousen/gouml/c881f0b0c32d77eb28b0f95c19ba93b19b5c29db/self-ref.png --------------------------------------------------------------------------------