Repository: goadesign/goa
Pull request #3082 (2022-06-18 14:26:58)

Fix a parsing bug for user defined imports
A bug in the recursion of the safelyGetMetaTypeImports function may cause user-defined imports to be missing.

# Example Design

```go
package design

import (
	. "goa.design/goa/v3/dsl"
)

var _ = Service("calc", func() {
	Method("foo", func() {
		Payload(func() {
			Attribute("x", MapOf(String, String, func() {
				Elem(func() {
					Meta("struct:field:type", "json.RawMessage", "encoding/json")
				})
			}))
		})
		Result(Empty)
		HTTP(func() {
			GET("/foo")
		})
	})
})

```

# Details 

The "encoding/json" must be included in the imports of `gen/calc/service.go` file, but is not.

```console
$ goa version
Goa version v3.7.6
$ goa gen calc/design
gen/calc/client.go
gen/calc/endpoints.go
gen/calc/service.go
... snip
$ cat gen/calc/service.go |head -20
// Code generated by goa v3.7.6, DO NOT EDIT.
//
// calc service
//
// Command:
// $ goa gen calc/design

package calc

import (
	"context"     // ← ★
)

// Service is the calc service interface.
type Service interface {
	// Foo implements foo.
	Foo(context.Context, *FooPayload) (err error)
}
```

raphael: This is great, thank you!

Patch:
diff --git a/codegen/import.go b/codegen/import.go
index dc0e7bf8..3fe22cda 100644
--- a/codegen/import.go
+++ b/codegen/import.go
@@ -147,23 +147,25 @@ func safelyGetMetaTypeImports(att *expr.AttributeExpr, seen map[string]struct{})
 			}
 		}
 	case *expr.Array:
-		_, im := GetMetaType(t.ElemType)
-		if im != nil {
-			uniqueImports[*im] = struct{}{}
+		for _, im := range safelyGetMetaTypeImports(t.ElemType, seen) {
+			if im != nil {
+				uniqueImports[*im] = struct{}{}
+			}
 		}
 	case *expr.Map:
-		_, im := GetMetaType(t.ElemType)
-		if im != nil {
-			uniqueImports[*im] = struct{}{}
+		for _, im := range safelyGetMetaTypeImports(t.ElemType, seen) {
+			if im != nil {
+				uniqueImports[*im] = struct{}{}
+			}
 		}
-		_, im = GetMetaType(t.KeyType)
-		if im != nil {
-			uniqueImports[*im] = struct{}{}
+		for _, im := range safelyGetMetaTypeImports(t.KeyType, seen) {
+			if im != nil {
+				uniqueImports[*im] = struct{}{}
+			}
 		}
 	case *expr.Object:
-		for _, key := range *t {
-			if key != nil {
-				_, im := GetMetaType(key.Attribute)
+		for _, na := range *t {
+			for _, im := range safelyGetMetaTypeImports(na.Attribute, seen) {
 				if im != nil {
 					uniqueImports[*im] = struct{}{}
 				}
@@ -175,7 +177,7 @@ func safelyGetMetaTypeImports(att *expr.AttributeExpr, seen map[string]struct{})
 		uniqueImports[*im] = struct{}{}
 	}
 	for imp := range uniqueImports {
-		// Copy loop variable into body so next iteration doesnt overwrite its address https://stackoverflow.com/questions/27610039/golang-appending-leaves-only-last-element
+		// Copy loop variable into body so next iteration doesn't overwrite its address https://stackoverflow.com/questions/27610039/golang-appending-leaves-only-last-element
 		copy := imp
 		imports = append(imports, &copy)
 	}
diff --git a/expr/http_body_types.go b/expr/http_body_types.go
index 9a624df9..a981a458 100644
--- a/expr/http_body_types.go
+++ b/expr/http_body_types.go
@@ -85,7 +85,7 @@ func httpRequestBody(a *HTTPEndpointExpr) *AttributeExpr {
 		return unionToObject(payload, name, suffix, a.Service.Name())
 	}
 
-	// 2. If Payload is not an objectthen check whether there are
+	// 2. If Payload is not an object then check whether there are
 	// params, cookies or headers defined and if so return empty type
 	// (payload encoded in request params or headers) otherwise return
 	// payload type (payload encoded in request body).


Test patch:
diff --git a/codegen/import_test.go b/codegen/import_test.go
new file mode 100644
index 00000000..92856fed
--- /dev/null
+++ b/codegen/import_test.go
@@ -0,0 +1,170 @@
+package codegen
+
+import (
+	"reflect"
+	"sort"
+	"testing"
+
+	"goa.design/goa/v3/dsl"
+	"goa.design/goa/v3/eval"
+	"goa.design/goa/v3/expr"
+)
+
+func TestGetMetaTypeImports(t *testing.T) {
+	testdata := []struct {
+		name string
+		dsl  func()
+		want []string
+	}{
+		{
+			name: "payload-primitive",
+			dsl: func() {
+				dsl.Method("m", func() {
+					dsl.Payload(func() {
+						dsl.Attribute("a", dsl.String, func() {
+							dsl.Meta("struct:field:type", "CustomTypeString", "package/string")
+						})
+						dsl.Attribute("b", dsl.Int, func() {
+							dsl.Meta("struct:field:type", "CustomTypeInt", "package/int")
+						})
+					})
+				})
+			},
+			want: []string{
+				"package/string",
+				"package/int",
+			},
+		},
+		{
+			name: "payload-map",
+			dsl: func() {
+				dsl.Method("m", func() {
+					dsl.Payload(func() {
+						dsl.Attribute("a", dsl.MapOf(dsl.String, dsl.String, func() {
+							dsl.Key(func() {
+								dsl.Meta("struct:field:type", "CustomTypeMapKey", "package/map-key")
+							})
+							dsl.Elem(func() {
+								dsl.Meta("struct:field:type", "CustomTypeMapElem", "package/map-elem")
+							})
+						}))
+					})
+				})
+			},
+			want: []string{
+				"package/map-elem",
+				"package/map-key",
+			},
+		},
+		{
+			name: "payload-map-map",
+			dsl: func() {
+				dsl.Method("m", func() {
+					dsl.Payload(func() {
+						dsl.Attribute("a", dsl.MapOf(dsl.String, dsl.MapOf(dsl.String, dsl.String, func() {
+							dsl.Key(func() {
+								dsl.Meta("struct:field:type", "CustomTypeMapKey", "package/map-map-key")
+							})
+							dsl.Elem(func() {
+								dsl.Meta("struct:field:type", "CustomTypeMapElem", "package/map-map-elem")
+							})
+						}), func() {
+							dsl.Key(func() {
+								dsl.Meta("struct:field:type", "CustomTypeMapKey", "package/map-key")
+							})
+							dsl.Elem(func() {
+								dsl.Meta("struct:field:type", "CustomTypeMapElem", "package/map-elem")
+							})
+						}))
+					})
+				})
+			},
+			want: []string{
+				"package/map-key",
+				"package/map-map-elem",
+				"package/map-map-key",
+				"package/map-elem",
+			},
+		},
+		{
+			name: "payload-array",
+			dsl: func() {
+				dsl.Method("m", func() {
+					dsl.Payload(func() {
+						dsl.Attribute("a", dsl.ArrayOf(dsl.String, func() {
+							dsl.Meta("struct:field:type", "SomeCustomTypeArrayElem", "package/array-elem")
+						}), func() {
+							dsl.Meta("struct:field:type", "SomeCustomTypeArray", "package/array")
+						})
+					})
+				})
+			},
+			want: []string{
+				"package/array-elem",
+				"package/array",
+			},
+		},
+		{
+			name: "result",
+			dsl: func() {
+				dsl.Method("m", func() {
+					dsl.Result(func() {
+						dsl.Attribute("a", dsl.String, func() {
+							dsl.Meta("struct:field:type", "CustomTypeString", "package/result-string")
+						})
+						dsl.Attribute("b", dsl.ArrayOf(dsl.String, func() {
+							dsl.Meta("struct:field:type", "SomeCustomTypeArrayElem", "package/result-array-elem")
+						}), func() {
+							dsl.Meta("struct:field:type", "SomeCustomTypeArray", "package/result-array")
+						})
+						dsl.Attribute("a", dsl.MapOf(dsl.String, dsl.String, func() {
+							dsl.Key(func() {
+								dsl.Meta("struct:field:type", "CustomTypeMapKey", "package/result-map-key")
+							})
+							dsl.Elem(func() {
+								dsl.Meta("struct:field:type", "CustomTypeMapElem", "package/result-map-elem")
+							})
+						}))
+					})
+				})
+			},
+			want: []string{
+				"package/result-string",
+				"package/result-array",
+				"package/result-array-elem",
+				"package/result-map-key",
+				"package/result-map-elem",
+			},
+		},
+	}
+	for _, tt := range testdata {
+		t.Run(tt.name, func(t *testing.T) {
+			eval.Context = &eval.DSLContext{}
+			serviceExpr := &expr.ServiceExpr{}
+			eval.Execute(tt.dsl, serviceExpr)
+			if eval.Context.Errors != nil {
+				t.Fatalf("%s: Service DSL failed unexpectedly with %s", tt.name, eval.Context.Errors)
+			}
+			for _, methodExpr := range serviceExpr.Methods {
+				eval.Execute(methodExpr.DSLFunc, methodExpr)
+				if eval.Context.Errors != nil {
+					t.Fatalf("%s: Method DSL failed unexpectedly with %s", tt.name, eval.Context.Errors)
+				}
+			}
+			for _, methodExpr := range serviceExpr.Methods {
+				var got []string
+				for _, v := range GetMetaTypeImports(methodExpr.Payload) {
+					got = append(got, v.Path)
+				}
+				for _, v := range GetMetaTypeImports(methodExpr.Result) {
+					got = append(got, v.Path)
+				}
+				sort.Strings(got)
+				sort.Strings(tt.want)
+				if !reflect.DeepEqual(tt.want, got) {
+					t.Errorf("want %+v, got %+v", tt.want, got)
+				}
+			}
+		})
+	}
+}