jfeliu007 / goplantuml

PlantUML Class Diagram Generator for golang projects
MIT License
1.77k stars 166 forks source link

Parser Panic #148

Open erdemtuna opened 1 year ago

erdemtuna commented 1 year ago

Hello, when I run the tool, the parser raises the following panic and error. A blank puml file is generated at the end.

% goplantuml -show-aggregations -show-implementations -show-compositions -recursive . > diagram_file_name.puml
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
github.com/jfeliu007/goplantuml/parser.(*ClassParser).handleFuncDecl(0x140000281e0, 0x1400016e720)
        PATH/go/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:279 +0x3f4
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parseFileDeclarations(0x140000281e0?, {0x104fadb60?, 0x1400016e720?})
        PATH/go/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:265 +0x98
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parsePackage(0x140000281e0, {0x104fad218?, 0x1400016e390})
        PATH/go/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:233 +0x344
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parseDirectory(0x0?, {0x140000242a0, 0x56})
        PATH/go/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:254 +0xac
github.com/jfeliu007/goplantuml/parser.NewClassDiagramWithOptions.func1({0x140000242a0, 0x56}, {0x104fae7f8, 0x1400007fba0}, {0x0?, 0x0?})
        PATH/go/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:169 +0x11c
github.com/spf13/afero.walk({0x104faea10, 0x1050e9de8}, {0x140000242a0, 0x56}, {0x104fae7f8, 0x1400007fba0}, 0x140001b3a90)
        PATH/go/pkg/mod/github.com/spf13/afero@v1.8.2/path.go:44 +0x5c
github.com/spf13/afero.Walk({0x104faea10, 0x1050e9de8}, {0x140000242a0, 0x56}, 0x1400012fa90)
        PATH/go/pkg/mod/github.com/spf13/afero@v1.8.2/path.go:105 +0x88
github.com/jfeliu007/goplantuml/parser.NewClassDiagramWithOptions(0x140001b3c48)
        PATH/go/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:158 +0x208
github.com/jfeliu007/goplantuml/parser.NewClassDiagram({0x14000060e30?, 0x1400012fe68?, 0x8?}, {0x1050e9de8?, 0x0?, 0x1f?}, 0x47?)
        PATH/go/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:209 +0x7c
main.main()
        PATH/go/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/cmd/goplantuml/main.go:102 +0xc10
daniel-santos commented 1 year ago

@erdemtuna omg, I'm so sad. :cry: You beat me to it! At least mine has a different backtrace.

panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
github.com/jfeliu007/goplantuml/parser.(*Struct).AddField(0xc0001eca10, 0xc0001c6f00, 0xc0001c8104?)
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/struct.go:99 +0x28b
github.com/jfeliu007/goplantuml/parser.handleGenDecStructType(0xc00009a190, {0xc0001c8104, 0x4}, 0xe?)
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:301 +0x65
github.com/jfeliu007/goplantuml/parser.(*ClassParser).processSpec(0xc00009a190, {0x627788?, 0xc0001c6e80?})
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:341 +0xfd
github.com/jfeliu007/goplantuml/parser.(*ClassParser).handleGenDecl(...)
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:327
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parseFileDeclarations(0xc00009a190?, {0x627338?, 0xc0001c7080?})
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:263 +0xb3
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parsePackage(0xc00009a190, {0x626938?, 0xc000182db0})
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:233 +0x3b2
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parseDirectory(0x7f1a1bad76e8?, {0xc000026420, 0x2f})
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:254 +0xd8
github.com/jfeliu007/goplantuml/parser.NewClassDiagramWithOptions(0xc00018dc58)
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:177 +0x29b
github.com/jfeliu007/goplantuml/parser.NewClassDiagram({0xc00007ad80?, 0xc0000c9e70?, 0x8?}, {0x772378?, 0x0?, 0x1f?}, 0x47?)
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/parser/class_parser.go:209 +0xa5
main.main()
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/goplantuml@v1.6.1/cmd/goplantuml/main.go:102 +0xcb6

While I doubt my environment matters for this, here it is anyway. It's non-standard because I have to cross-compile, but this is the normal (target=host) build:

+ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/daniel/blah/blah/.gopath/amd64-gO0/.cache/go-build"
GOENV="/home/daniel/blah/blah/.gopath/amd64-gO0/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/daniel/blah/blah/.gopath/amd64-gO0"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/home/daniel/blah/blah/install/go-amd64-1.19.y"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/home/daniel/blah/blah/install/go-amd64-1.19.y/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.19.4"
GCCGO="/usr/bin/gccgo"
GOAMD64="v1"
AR="x86_64-pc-linux-gnu-gcc-ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/daniel/blah/blah/src/go/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O0"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O0"
CGO_FFLAGS="-g -O0"
CGO_LDFLAGS="-g -O0"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3860847493=/tmp/go-build -gno-record-gcc-switches"
daniel-santos commented 1 year ago

OK, here's a patch that works, but hasn't been cleaned up or anything. I'll get around to forking and submitting a proper pull request. This adds at least partial support for Generics. Where you see &Type{theType, nil}, it might not properly support Generics.

diff --git a/go.mod b/go.mod
index 63a30d2..670f08a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/jfeliu007/goplantuml

-go 1.17
+go 1.18

 require (
    github.com/spf13/afero v1.8.2
diff --git a/parser/class_parser.go b/parser/class_parser.go
index 7526b00..9c5a907 100644
--- a/parser/class_parser.go
+++ b/parser/class_parser.go
@@ -264,7 +264,7 @@ func (p *ClassParser) parseFileDeclarations(node ast.Decl) {
    case *ast.FuncDecl:
        p.handleFuncDecl(decl)
    }
-}
+}//p result["g"].Files["/home/daniel/proj/uml/goplantuml/g/a.go"].Decls[0].Specs[0].TypeParams.List[0].Names

 func (p *ClassParser) handleFuncDecl(decl *ast.FuncDecl) {

@@ -279,7 +279,7 @@ func (p *ClassParser) handleFuncDecl(decl *ast.FuncDecl) {
        if theType[0] == "*"[0] {
            theType = theType[1:]
        }
-       structure := p.getOrCreateStruct(theType)
+       structure := p.getOrCreateStruct(&Type{theType, nil})
        if structure.Type == "" {
            structure.Type = "class"
        }
@@ -296,21 +296,21 @@ func (p *ClassParser) handleFuncDecl(decl *ast.FuncDecl) {
    }
 }

-func handleGenDecStructType(p *ClassParser, typeName string, c *ast.StructType) {
+func handleGenDecStructType(p *ClassParser, t *Type, c *ast.StructType) {
    for _, f := range c.Fields.List {
-       p.getOrCreateStruct(typeName).AddField(f, p.allImports)
+       p.getOrCreateStruct(t).AddField(f, p.allImports)
    }
 }

-func handleGenDecInterfaceType(p *ClassParser, typeName string, c *ast.InterfaceType) {
+func handleGenDecInterfaceType(p *ClassParser, typ *Type, c *ast.InterfaceType) {
    for _, f := range c.Methods.List {
        switch t := f.Type.(type) {
        case *ast.FuncType:
-           p.getOrCreateStruct(typeName).AddMethod(f, p.allImports)
+           p.getOrCreateStruct(typ).AddMethod(f, p.allImports)
            break
        case *ast.Ident:
            f, _ := getFieldType(t, p.allImports)
-           st := p.getOrCreateStruct(typeName)
+           st := p.getOrCreateStruct(typ)
            f = replacePackageConstant(f, st.PackageName)
            st.AddToComposition(f)
            break
@@ -329,47 +329,47 @@ func (p *ClassParser) handleGenDecl(decl *ast.GenDecl) {
 }

 func (p *ClassParser) processSpec(spec ast.Spec) {
-   var typeName string
+   var t Type
    var alias *Alias
    declarationType := "alias"
    switch v := spec.(type) {
    case *ast.TypeSpec:
-       typeName = v.Name.Name
+       t = MakeType(v)
        switch c := v.Type.(type) {
        case *ast.StructType:
            declarationType = "class"
-           handleGenDecStructType(p, typeName, c)
+           handleGenDecStructType(p, &t, c)
        case *ast.InterfaceType:
            declarationType = "interface"
-           handleGenDecInterfaceType(p, typeName, c)
+           handleGenDecInterfaceType(p, &t, 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)
+           if !isPrimitiveString(t.Name) {
+               t.Name = fmt.Sprintf("%s.%s", p.currentPackageName, t.Name)
            }
            packageName := p.currentPackageName
            if isPrimitiveString(basicType) {
                packageName = builtinPackageName
            }
-           alias = getNewAlias(fmt.Sprintf("%s.%s", packageName, aliasType), p.currentPackageName, typeName)
+           alias = getNewAlias(fmt.Sprintf("%s.%s", packageName, aliasType), p.currentPackageName, t.Name)

        }
    default:
        // Not needed for class diagrams (Imports, global variables, regular functions, etc)
        return
    }
-   p.getOrCreateStruct(typeName).Type = declarationType
-   fullName := fmt.Sprintf("%s.%s", p.currentPackageName, typeName)
+   p.getOrCreateStruct(&t).Type = declarationType
+   fullName := fmt.Sprintf("%s.%s", p.currentPackageName, t.ToString(false, false))
    switch declarationType {
    case "interface":
        p.allInterfaces[fullName] = struct{}{}
    case "class":
        p.allStructs[fullName] = struct{}{}
    case "alias":
-       p.allAliases[typeName] = alias
+       p.allAliases[t.Name] = alias
        if strings.Count(alias.Name, ".") > 1 {
            pack := strings.SplitN(alias.Name, ".", 2)
            if _, ok := p.allRenamedStructs[pack[0]]; !ok {
@@ -520,7 +520,7 @@ func (p *ClassParser) renderStructure(structure *Struct, pack string, name strin
        renderStructureType = "class"

    }
-   str.WriteLineWithDepth(1, fmt.Sprintf(`%s %s %s {`, renderStructureType, name, sType))
+   str.WriteLineWithDepth(1, fmt.Sprintf(`%s %s %s {`, renderStructureType, structure.TypeData.ToString(true, false), sType))
    p.renderStructFields(structure, privateFields, publicFields)
    p.renderStructMethods(structure, privateMethods, publicMethods)
    p.renderCompositions(structure, name, composition)
@@ -677,20 +677,21 @@ func (p *ClassParser) renderStructFields(structure *Struct, privateFields *LineS
 }

 // Returns an initialized struct of the given name or returns the existing one if it was already created
-func (p *ClassParser) getOrCreateStruct(name string) *Struct {
-   result, ok := p.structure[p.currentPackageName][name]
+func (p *ClassParser) getOrCreateStruct(t *Type) *Struct {
+   result, ok := p.structure[p.currentPackageName][t.Name]
    if !ok {
        result = &Struct{
            PackageName:         p.currentPackageName,
            Functions:           make([]*Function, 0),
            Fields:              make([]*Field, 0),
            Type:                "",
+           TypeData:            *t,
            Composition:         make(map[string]struct{}, 0),
            Extends:             make(map[string]struct{}, 0),
            Aggregations:        make(map[string]struct{}, 0),
            PrivateAggregations: make(map[string]struct{}, 0),
        }
-       p.structure[p.currentPackageName][name] = result
+       p.structure[p.currentPackageName][t.Name] = result
    }
    return result
 }
diff --git a/parser/field.go b/parser/field.go
index fb25a88..a7bf893 100644
--- a/parser/field.go
+++ b/parser/field.go
@@ -3,6 +3,7 @@ package parser
 import (
    "fmt"
    "strings"
+   "log"

    "go/ast"
 )
@@ -40,7 +41,14 @@ func getFieldType(exp ast.Expr, aliases map[string]string) (string, []string) {
        return getFuncType(v, aliases)
    case *ast.Ellipsis:
        return getEllipsis(v, aliases)
+   case *ast.IndexExpr:
+       return getGenericType(v, aliases)
+   case *ast.IndexListExpr:
+       // Functions will have v.Indicies populated with the paraemter names,
+       // but we need the type with parameter names and types.
+       return getFieldType(v.X, aliases)
    }
+   log.Panicf("getFieldType doesn't know what this is %#+v")
    return "", []string{}
 }

@@ -136,6 +144,37 @@ func getEllipsis(v *ast.Ellipsis, aliases map[string]string) (string, []string)
    return fmt.Sprintf("...%s", t), []string{}
 }

+func getGenericType(v *ast.IndexExpr, aliases map[string]string) (string, []string) {
+   t, _ := getFieldType(v.X, aliases)
+   if p, ok := v.Index.(*ast.Ident); ok {
+       log.Printf("getGenericType: %v, %s, %s\n", v, t, p.Name)
+       return fmt.Sprintf("%s<%s>", t, p.Name), []string{}
+   }
+   panic("oops bug")
+   return fmt.Sprintf("%s<%s>", t, "not_parsed"), []string{}
+}
+
+/*
+func getGenericTypeList(v *ast.IndexListExpr, aliases map[string]string) (string, []string) {
+   t, _ := getFieldType(v.X, aliases)
+   t += "["
+   first := true
+   for _, i := range v.Indices {
+       if p, ok := i.(*ast.Ident); ok {
+           log.Printf("getGenericTypeList: %v, %s, %s\n", v, t, p.Name)
+           if first {
+               first = false
+           } else {
+               t += ", "
+           }
+           t += p.Name
+       } else {
+           log.Panicf("index is %+v", i)
+       }
+   }
+   return t + "]", []string{}
+}*/
+
 var globalPrimitives = map[string]struct{}{
    "bool":        {},
    "string":      {},
diff --git a/parser/function.go b/parser/function.go
index 994325c..3a202b6 100644
--- a/parser/function.go
+++ b/parser/function.go
@@ -8,6 +8,7 @@ import (
 //Function holds the signature of a function with name, Parameters and Return values
 type Function struct {
    Name                 string
+   TypeParams           []TypeParam
    Parameters           []*Field
    ReturnValues         []string
    PackageName          string
diff --git a/parser/struct.go b/parser/struct.go
index 1383775..9920de9 100644
--- a/parser/struct.go
+++ b/parser/struct.go
@@ -12,6 +12,7 @@ type Struct struct {
    Functions           []*Function
    Fields              []*Field
    Type                string
+   TypeData            Type
    Composition         map[string]struct{}
    Extends             map[string]struct{}
    Aggregations        map[string]struct{}
diff --git a/parser/type.go b/parser/type.go
new file mode 100644
index 0000000..e676fa9
--- /dev/null
+++ b/parser/type.go
@@ -0,0 +1,120 @@
+package parser
+
+import (
+// "fmt"
+
+   "go/ast"
+)
+
+type TypeParam struct {
+   Name        string
+   Constraint  string
+}
+
+type TypeParams []TypeParam
+
+func typeParamExprToString(exp ast.Expr) string {
+   switch v := exp.(type) {
+   case *ast.Ident:
+       return v.Name
+   case *ast.BinaryExpr:
+       return typeParamExprToString(v.X) + " " + v.Op.String() + " " + typeParamExprToString(v.Y)
+   }
+   return ""
+}
+
+func MakeTypeParams(typeParams []*ast.Field) (ret TypeParams) {
+   var count = 0
+   var i = 0
+
+   if typeParams == nil && len(typeParams) == 0 {
+       return make(TypeParams, 0)
+   }
+
+   for _, tp := range typeParams {
+       count += len(tp.Names)
+   }
+
+   ret = make(TypeParams, count)
+
+   for _, tp := range typeParams {
+       c := typeParamExprToString(tp.Type)
+       for _, n := range tp.Names {
+           ret[i] = TypeParam {
+               Name:       n.Name,
+               Constraint: c,
+           }
+           i += 1
+       }
+   }
+
+   return
+}
+
+func (this TypeParams) toGoDecl() string {
+   ret := ""
+   c := ""
+   for _, i := range this {
+       if len(ret) > 0 {
+           if c == i.Constraint {
+               ret += ", "
+           } else {
+               ret += " " + c + ", "
+           }
+       }
+       ret += i.Name
+       c = i.Constraint
+   }
+   if len(c) > 0 {
+       ret += " " + c
+   }
+
+   return ret
+}
+
+func (this TypeParams) toPumlDecl() string {
+   ret := ""
+   for _, i := range this {
+       if len(ret) > 0 {
+           ret += ", "
+       }
+       ret += i.Name + " " + i.Constraint
+   }
+   return ret
+}
+
+func (this TypeParams) ToString(asGo bool) string {
+   if this == nil {
+       return ""
+   }
+   if (asGo) {
+       return this.toGoDecl()
+   } else {
+       return this.toPumlDecl()
+   }
+}
+
+type Type struct {
+   Name        string
+   Params      TypeParams
+}
+
+func MakeType(ts *ast.TypeSpec) Type {
+   return Type {
+       Name:   ts.Name.Name,
+       Params: MakeTypeParams(ts.TypeParams.List),
+   }
+}
+
+func (this *Type) ToString(asGoType bool, a bool) string {
+   if len(this.Params) == 0 {
+       return this.Name;
+   }
+
+   decl := this.Params.ToString(asGoType)
+   if a {
+       return this.Name + "[" + decl + "]"
+   } else {
+       return this.Name + "<" + decl + ">"
+   }
+}
\ No newline at end of file
daniel-santos commented 1 year ago
package g

type B[T, V int | uint] struct {
    a T
    b V
}

func (this *B[T, V]) Func() {
}

Becomes: a

daniel-santos commented 1 year ago

Interfaces are still jacked up

erdemtuna commented 1 year ago

Thank you for the fix @daniel-santos. I will try it once the fix is merged and released.