golang源码分析:goplantuml

2023-09-06 19:23:30 浏览数 (1)

https://github.com/jfeliu007/goplantuml/是一款工具可以解析golang源码,生成plantuml类图,它的原理是通过源码静态分析,提取结构体,接口,方法,包等属性,然后按照uml语法生成puml文件,效果如下。

首先看下如何使用:

代码语言:javascript复制
go get github.com/jfeliu007/goplantuml/parser
go install github.com/jfeliu007/goplantuml/cmd/goplantuml@latest
goplantuml $GOPATH/src/github.com/jfeliu007/goplantuml/parser > ClassDiagram.puml
// Generates a file ClassDiagram.puml with the previous specifications

然后我们就可以借助vscode plantuml插件生成上面的类图。熟悉完使用后我们来分析下它的源码。它的入口函数位于:cmd/goplantuml/main.go

代码语言:javascript复制
func main() {
    recursive := flag.Bool("recursive", false, "walk all directories recursively")
    ignore := flag.String("ignore", "", "comma separated list of folders to ignore")
    showAggregations := flag.Bool("show-aggregations", false, "renders public aggregations even when -hide-connections is used (do not render by default)")
    hideFields := flag.Bool("hide-fields", false, "hides fields")
    renderingOptions := map[goplantuml.RenderingOption]interface{}{
    legend, err := getLegend(renderingOptions)
    dirs, err := getDirectories()
      dirAbs, err := filepath.Abs(dir)
    ignoredDirectories, err := getIgnoredDirectories(*ignore)
    result, err := goplantuml.NewClassDiagram(dirs, ignoredDirectories, *recursive)
    result.SetRenderingOptions(renderingOptions)
    rendered := result.Render()
    writer, err = os.Create(*output)
    fmt.Fprint(writer, rendered)

解析完参数之后,将所有选项放入map,然后获取源码目录,以及忽略的目录,接着进行源码解析,提取生成plantuml需要的元数据信息后,调用渲染函数生成plantuml文件,最后输出。如果制定了标签,也会调用下面函数获取标签。

代码语言:javascript复制
func getLegend(ro map[goplantuml.RenderingOption]interface{}) (string, error) {

它的核心解析函数定义在parser/class_parser.go

代码语言:javascript复制
func NewClassDiagram(directoryPaths []string, ignoreDirectories []string, recursive bool) (*ClassParser, error) {
      return NewClassDiagramWithOptions(options)
代码语言:javascript复制
func NewClassDiagramWithOptions(options *ClassDiagramOptions) (*ClassParser, error) {
  classParser := &ClassParser{
  renderingOptions: &RenderingOptions{
    structure:         make(map[string]map[string]*Struct),
    allInterfaces:     make(map[string]struct{}),
    allStructs:        make(map[string]struct{}),
    allImports:        make(map[string]string),
    allAliases:        make(map[string]*Alias),
    allRenamedStructs: make(map[string]map[string]string),
    
    for _, dir := range options.IgnoredDirectories {
    ignoreDirectoryMap[dir] = struct{}{}
   for _, directoryPath := range options.Directories {
    if options.Recursive {
      err := afero.Walk(options.FileSystem, directoryPath, func(path string, info os.FileInfo, err error) error {
        classParser.parseDirectory(path)
  classParser.parseDirectory(path)
  for s := range classParser.allStructs {
    st := classParser.getStruct(s)
    if st != nil {
      for i := range classParser.allInterfaces {
        inter := classParser.getStruct(i)
        if st.ImplementsInterface(inter) {
          st.AddToExtends(i)
        }
      }
    }
  }
 classParser.SetRenderingOptions(options.RenderingOptions)

可以看到在准备完可选参数后,定义了一系列map,从名字到结构体信息映射,从名字到接口信息映射。用来存放提取的结构体信息,然后调用 afero.Walk,遍历所有目录和子目录。完成需要信息的提取。该函数是著名的spf13定义的一个文件接口包的一个函数:github.com/spf13/afero

在每个目录内都会调用解析函数进行解析,解析函数如下:

代码语言:javascript复制
func (p *ClassParser) parseDirectory(directoryPath string) error {
      fs := token.NewFileSet()
      result, err := parser.ParseDir(fs, directoryPath, nil, 0)
      for _, v := range result {
         p.parsePackage(v)

它调用了golang源码的解析器,解析每个目录下的.go文件,返回包名到抽象语法树的映射。

代码语言:javascript复制
func ParseDir(fset *token.FileSet, path string, filter func(fs.FileInfo) bool, mode Mode) (pkgs map[string]*ast.Package, first error) {

得到抽象语法树以后,就会遍历所有抽象语法树,解析每个包内的信息。

代码语言:javascript复制
func (p *ClassParser) parsePackage(node ast.Node) {
    pack := node.(*ast.Package)
    p.currentPackageName = pack.Name
    for fileName := range pack.Files {
    sortedFiles = append(sortedFiles, fileName)
    for _, fileName := range sortedFiles {

    if !strings.HasSuffix(fileName, "_test.go") {
      f := pack.Files[fileName]
      for _, d := range f.Imports {
        p.parseImports(d)
      }
      for _, d := range f.Decls {
        p.parseFileDeclarations(d)
      }
    }
  }

在每个包内依次递归解析它的下一层结构的信息:

代码语言:javascript复制
func (p *ClassParser) parseImports(impt *ast.ImportSpec) {
      p.allImports[impt.Name.Name] = s

解析过程中,提取了两类信息:

代码语言:javascript复制
func (p *ClassParser) parseFileDeclarations(node ast.Decl) {
  switch decl := node.(type) {
  case *ast.GenDecl:
    p.handleGenDecl(decl)
  case *ast.FuncDecl:
    p.handleFuncDecl(decl)
  }

其中GenDecl包括下面四类信息:

代码语言:javascript复制
  //  token.IMPORT  *ImportSpec
  //  token.CONST   *ValueSpec
  //  token.TYPE    *TypeSpec
  //  token.VAR     *ValueSpec
代码语言:javascript复制
func (p *ClassParser) handleGenDecl(decl *ast.GenDecl) {
  for _, spec := range decl.Specs {
    p.processSpec(spec)
  }

对每个类型的内部每一部分也会进行处理:

代码语言:javascript复制
func (p *ClassParser) processSpec(spec ast.Spec) {
switch v := spec.(type) {
  case *ast.TypeSpec:
    typeName = v.Name.Name
    switch c := v.Type.(type) {
    case *ast.StructType:
      declarationType = "class"
      handleGenDecStructType(p, typeName, c)
    case *ast.InterfaceType:
      declarationType = "interface"
      handleGenDecInterfaceType(p, typeName, c)
    default:
      basicType, _ := getFieldType(getBasicType(c), p.allImports)


      aliasType, _ := getFieldType(c, p.allImports)
      aliasType = replacePackageConstant(aliasType, "")
      if !isPrimitiveString(typeName) {
        typeName = fmt.Sprintf("%s.%s", p.currentPackageName, typeName)
      }
      packageName := p.currentPackageName
      if isPrimitiveString(basicType) {
        packageName = builtinPackageName
      }
      alias = getNewAlias(fmt.Sprintf("%s.%s", packageName, aliasType), p.currentPackageName, typeName)


    }
  default:
      p.getOrCreateStruct(typeName).Type = declarationType
      switch declarationType {
  case "interface":
    p.allInterfaces[fullName] = struct{}{}
  case "class":
    p.allStructs[fullName] = struct{}{}
  case "alias":
    p.allAliases[typeName] = alias
    if strings.Count(alias.Name, ".") > 1 {
      pack := strings.SplitN(alias.Name, ".", 2)
      if _, ok := p.allRenamedStructs[pack[0]]; !ok {
        p.allRenamedStructs[pack[0]] = map[string]string{}
      }
      renamedClass := generateRenamedStructName(pack[1])
      p.allRenamedStructs[pack[0]][renamedClass] = pack[1]
    }
  }  

对于结构体类型,会纯粹在Struct的结构体里:

代码语言:javascript复制
func handleGenDecStructType(p *ClassParser, typeName string, c *ast.StructType) {
  for _, f := range c.Fields.List {
    p.getOrCreateStruct(typeName).AddField(f, p.allImports)
  }
}
代码语言:javascript复制
func (p *ClassParser) getOrCreateStruct(name string) *Struct {
    result = &Struct{
      PackageName:         p.currentPackageName,
      Functions:           make([]*Function, 0),
      Fields:              make([]*Field, 0),
      Type:                "",
      Composition:         make(map[string]struct{}, 0),
      Extends:             make(map[string]struct{}, 0),
      Aggregations:        make(map[string]struct{}, 0),
      PrivateAggregations: make(map[string]struct{}, 0),
    }

interface的处理是类似的:

代码语言:javascript复制
func handleGenDecInterfaceType(p *ClassParser, typeName string, c *ast.InterfaceType) {
   for _, f := range c.Methods.List {
    switch t := f.Type.(type) {
    case *ast.FuncType:
      p.getOrCreateStruct(typeName).AddMethod(f, p.allImports)
      break
    case *ast.Ident:
      f, _ := getFieldType(t, p.allImports)
      st := p.getOrCreateStruct(typeName)
      f = replacePackageConstant(f, st.PackageName)
      st.AddToComposition(f)
      break
    }
  }

函数的处理:

代码语言:javascript复制
func (p *ClassParser) handleFuncDecl(decl *ast.FuncDecl) {
      theType, _ := getFieldType(decl.Recv.List[0].Type, p.allImports)
      theType = replacePackageConstant(theType, "")
      structure := p.getOrCreateStruct(theType)
      structure.AddMethod(&ast.Field{
      Names:   []*ast.Ident{decl.Name},
      Doc:     decl.Doc,
      Type:    decl.Type,
      Tag:     nil,
      Comment: nil,
    }, p.allImports)
代码语言:javascript复制
func (p *ClassParser) getStruct(structName string) *Struct {

解析完成后设置渲染选项,为最终渲染成plantuml做准备:

代码语言:javascript复制
func (p *ClassParser) SetRenderingOptions(ro map[RenderingOption]interface{}) error {
    for option, val := range ro {
    switch option {
    case RenderAggregations:

其中解析过程中用到了go语言本性的包

代码语言:javascript复制
"go/ast"
"go/parser"
"go/token"

渲染的过程,就是将解析得到的元数据信息,渲染成plantuml语法

代码语言:javascript复制
func (p *ClassParser) Render() string {
      str.WriteLineWithDepth(0, "@startuml")
      str.WriteLineWithDepth(0, fmt.Sprintf(`title %s`, p.renderingOptions.Title))
      str.WriteLineWithDepth(0, "legend")
    str.WriteLineWithDepth(0, note)
    str.WriteLineWithDepth(0, "end legend")
        for pack := range p.structure {
    packages = append(packages, pack)
      for _, pack := range packages {
    structures := p.structure[pack]
    p.renderStructures(pack, structures, str)
        if p.renderingOptions.Aliases {
    p.renderAliases(str)
      str.WriteLineWithDepth(0, "@enduml")

对于每个具体结构也是递归进行渲染的和解析过程是类似的

代码语言:javascript复制
func (p *ClassParser) renderStructures(pack string, structures map[string]*Struct, str *LineStringBuilder) {
      str.WriteLineWithDepth(0, fmt.Sprintf(`namespace %s {`, pack))
      for _, name := range names {
      structure := structures[name]
      p.renderStructure(structure, pack, name, str, composition, extends, aggregations)
        for _, tempName := range orderedRenamedStructs {
      name := p.allRenamedStructs[pack][tempName]
      str.WriteLineWithDepth(1, fmt.Sprintf(`class "%s" as %s {`, name, tempName))
      if p.renderingOptions.Compositions {
      str.WriteLineWithDepth(0, composition.String())
    }
    if p.renderingOptions.Implementations {
      str.WriteLineWithDepth(0, extends.String())
    }
    if p.renderingOptions.Aggregations {
      str.WriteLineWithDepth(0, aggregations.String())
    }

结构体的渲染

代码语言:javascript复制
func (p *ClassParser) renderStructure(structure *Struct, pack string, name string, str *LineStringBuilder, composition *LineStringBuilder, extends *LineStringBuilder, aggregations *LineStringBuilder) {
      switch structure.Type {
  case "class":
    sType = "<< (S,Aquamarine) >>"
  case "alias":
    sType = "<< (T, #FF7700) >> "
    renderStructureType = "class"


  }
 p.renderStructFields(structure, privateFields, publicFields)
  p.renderStructMethods(structure, privateMethods, publicMethods)
  p.renderCompositions(structure, name, composition)
  p.renderExtends(structure, name, extends)
  p.renderAggregations(structure, name, aggregations)

其中渲染的目标就是string.Builder

代码语言:javascript复制
type LineStringBuilder struct {
  strings.Builder
}

如果有别名会解析到:parser/alias.go

代码语言:javascript复制
type Alias struct {
  Name        string
  PackageName string
  AliasOf     string
}

结构体被解析到:parser/struct.go

代码语言:javascript复制
type Struct struct {
  PackageName         string
  Functions           []*Function
  Fields              []*Field
  Type                string
  Composition         map[string]struct{}
  Extends             map[string]struct{}
  Aggregations        map[string]struct{}
  PrivateAggregations map[string]struct{}
}

判断一个结构体是否实现了接口,就是判断结构体的方法签名和接口所有的函数列表是否一致

代码语言:javascript复制
func (st *Struct) ImplementsInterface(inter *Struct) bool {
      for _, f1 := range inter.Functions {
      for _, f2 := range st.Functions {
      if f1.SignturesAreEqual(f2) {
代码语言:javascript复制
func (st *Struct) AddToExtends(fType string) {
      st.Extends[fType] = struct{}{}

函数的信息描述位于:parser/function.go

代码语言:javascript复制
type Function struct {
  Name                 string
  Parameters           []*Field
  ReturnValues         []string
  PackageName          string
  FullNameReturnValues []string
}

判断签名是否一致,会判断输入和返回信息:

代码语言:javascript复制
func (f *Function) SignturesAreEqual(function *Function) bool {
   result = result && (len(f.Parameters) == len(function.Parameters))

结构体字段信息描述位于:parser/field.go

代码语言:javascript复制
type Field struct {
  Name     string
  Type     string
  FullType string
}

总结起来,这类工具大同小异,都是解析源码获取元数据信息,然后将元数据信息翻译成目标语言或者协议。

0 人点赞