goplantuml icon indicating copy to clipboard operation
goplantuml copied to clipboard

Parser Panic

Open erdemtuna opened this issue 2 years ago • 5 comments

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/[email protected]/parser/class_parser.go:279 +0x3f4
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parseFileDeclarations(0x140000281e0?, {0x104fadb60?, 0x1400016e720?})
        PATH/go/pkg/mod/github.com/jfeliu007/[email protected]/parser/class_parser.go:265 +0x98
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parsePackage(0x140000281e0, {0x104fad218?, 0x1400016e390})
        PATH/go/pkg/mod/github.com/jfeliu007/[email protected]/parser/class_parser.go:233 +0x344
github.com/jfeliu007/goplantuml/parser.(*ClassParser).parseDirectory(0x0?, {0x140000242a0, 0x56})
        PATH/go/pkg/mod/github.com/jfeliu007/[email protected]/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/[email protected]/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/[email protected]/path.go:44 +0x5c
github.com/spf13/afero.Walk({0x104faea10, 0x1050e9de8}, {0x140000242a0, 0x56}, 0x1400012fa90)
        PATH/go/pkg/mod/github.com/spf13/[email protected]/path.go:105 +0x88
github.com/jfeliu007/goplantuml/parser.NewClassDiagramWithOptions(0x140001b3c48)
        PATH/go/pkg/mod/github.com/jfeliu007/[email protected]/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/[email protected]/parser/class_parser.go:209 +0x7c
main.main()
        PATH/go/pkg/mod/github.com/jfeliu007/[email protected]/cmd/goplantuml/main.go:102 +0xc10

erdemtuna avatar Nov 25 '22 09:11 erdemtuna

@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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/parser/class_parser.go:209 +0xa5
main.main()
        /home/daniel/blah/blah/.gopath/amd64-gO0/pkg/mod/github.com/jfeliu007/[email protected]/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 avatar Jan 31 '23 17:01 daniel-santos

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 avatar Feb 01 '23 01:02 daniel-santos

package g

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

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

Becomes: a

daniel-santos avatar Feb 01 '23 02:02 daniel-santos

Interfaces are still jacked up

daniel-santos avatar Feb 01 '23 02:02 daniel-santos

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

erdemtuna avatar Feb 01 '23 07:02 erdemtuna