Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parser Panic #148

Open
erdemtuna opened this issue Nov 25, 2022 · 5 comments
Open

Parser Panic #148

erdemtuna opened this issue Nov 25, 2022 · 5 comments

Comments

@erdemtuna
Copy link

erdemtuna commented Nov 25, 2022

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
@daniel-santos
Copy link

@erdemtuna omg, I'm so sad. 😢 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
Copy link

daniel-santos commented Feb 1, 2023

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
Copy link

package g

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

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

Becomes: a

@daniel-santos
Copy link

Interfaces are still jacked up

@erdemtuna
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants