Skip to content

Commit de7a74a

Browse files
authored
codegen: avoid name collisions for wrapped object payloads (#3907)
Service codegen wraps raw object payloads/results in synthetic user types named from the method (e.g. Foo -> FooPayload). When NameScope counters have already reserved FooPayload and FooPayload2, using NameScope.Name can emit a colliding identifier and produce invalid generated code. Introduce NameScope.PeekUnique to compute the Unique result without mutating the scope and use it when synthesizing wrapper type names. Add a service golden that locks the collision case. Also fix generated error name docs to reflect the returned value.
1 parent 82daa90 commit de7a74a

10 files changed

Lines changed: 170 additions & 12 deletions

codegen/scope.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,32 @@ func (s *NameScope) Unique(name string, suffix ...string) string {
7676
}
7777
}
7878

79+
// PeekUnique returns the name that Unique would return for the same inputs,
80+
// without mutating the scope.
81+
//
82+
// This is useful when synthesizing type names or identifiers that are later
83+
// reserved via Hash-based naming (e.g., GoTypeName/HashedUnique) and therefore
84+
// must not increment the scope counters twice.
85+
func (s *NameScope) PeekUnique(name string, suffix ...string) string {
86+
c, ok := s.counts[name]
87+
if !ok {
88+
return name
89+
}
90+
if len(suffix) > 0 {
91+
name += suffix[0]
92+
c, ok = s.counts[name]
93+
if !ok {
94+
return name
95+
}
96+
}
97+
for i := c; ; i++ {
98+
ret := name + strconv.Itoa(i+1)
99+
if _, ok := s.counts[ret]; !ok {
100+
return ret
101+
}
102+
}
103+
}
104+
79105
// Name returns a unique name for the given name by adding a counter value to
80106
// the name until unique. It returns the same value when called multiple times
81107
// for the same given name.

codegen/scope_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,34 @@ func TestNameScope_GoFullTypeName_UsesScopedNameWhenQualified(t *testing.T) {
6565
t.Fatalf("expected qualified base name %q with fresh scope, got %q", want, got)
6666
}
6767
}
68+
69+
func TestNameScope_PeekUnique_MatchesUniqueWithoutMutation(t *testing.T) {
70+
seed := func(scope *NameScope) {
71+
scope.Unique("a")
72+
scope.Unique("a")
73+
scope.Unique("a2")
74+
scope.Unique("hello")
75+
scope.Unique("hello1")
76+
}
77+
78+
peek := NewNameScope()
79+
seed(peek)
80+
81+
mutating := NewNameScope()
82+
seed(mutating)
83+
84+
if got, want := peek.PeekUnique("a"), mutating.Unique("a"); got != want {
85+
t.Fatalf("expected peek %q, got %q", want, got)
86+
}
87+
if got, want := peek.PeekUnique("hel", "lo"), mutating.Unique("hel", "lo"); got != want {
88+
t.Fatalf("expected peek %q, got %q", want, got)
89+
}
90+
if got, want := peek.PeekUnique("hello", "1"), mutating.Unique("hello", "1"); got != want {
91+
t.Fatalf("expected peek %q, got %q", want, got)
92+
}
93+
94+
// PeekUnique must not mutate the scope.
95+
if got, want := peek.Unique("a"), "a3"; got != want {
96+
t.Fatalf("expected scope unchanged, got %q", got)
97+
}
98+
}

codegen/service/service_data.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ func (d *ServicesData) analyze(service *expr.ServiceExpr) *Data {
804804
if _, ok := att.Type.(*expr.Object); ok {
805805
att.Type = &expr.UserTypeExpr{
806806
AttributeExpr: expr.DupAtt(att),
807-
TypeName: scope.Name(name),
807+
TypeName: scope.PeekUnique(name),
808808
UID: id,
809809
}
810810
}

codegen/service/service_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func TestService(t *testing.T) {
5858
{"service-bidirectional-streaming-result-with-explicit-view", testdata.BidirectionalStreamingResultWithExplicitViewMethodDSL},
5959
{"service-multiple-api-key-security", testdata.MultipleAPIKeySecurityDSL},
6060
{"service-mixed-and-multiple-api-key-security", testdata.MixedAndMultipleAPIKeySecurityDSL},
61+
{"service-raw-object-payload-type-name-collision", testdata.RawObjectPayloadTypeNameCollisionDSL},
6162
}
6263
for _, c := range cases {
6364
t.Run(c.Name, func(t *testing.T) {

codegen/service/templates/error.go.tpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ func (e {{ .Ref }}) Error() string {
33
return {{ printf "%q" .Description }}
44
}
55

6-
// ErrorName returns {{ printf "%q" .Name }}.
6+
// ErrorName returns the error name.
77
//
88
// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105
99
func (e {{ .Ref }}) ErrorName() string {
1010
return e.GoaErrorName()
1111
}
1212

13-
// GoaErrorName returns {{ printf "%q" .Name }}.
13+
// GoaErrorName returns the error name.
1414
func (e {{ .Ref }}) GoaErrorName() string {
1515
return {{ errorName . }}
1616
}

codegen/service/testdata/golden/service_service-custom-errors-custom-field.go.golden

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ func (e *GoaError) Error() string {
3030
return ""
3131
}
3232

33-
// ErrorName returns "GoaError".
33+
// ErrorName returns the error name.
3434
//
3535
// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105
3636
func (e *GoaError) ErrorName() string {
3737
return e.GoaErrorName()
3838
}
3939

40-
// GoaErrorName returns "GoaError".
40+
// GoaErrorName returns the error name.
4141
func (e *GoaError) GoaErrorName() string {
4242
return e.ErrorCode
4343
}

codegen/service/testdata/golden/service_service-custom-errors.go.golden

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ func (e *APayload) Error() string {
4242
return ""
4343
}
4444

45-
// ErrorName returns "APayload".
45+
// ErrorName returns the error name.
4646
//
4747
// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105
4848
func (e *APayload) ErrorName() string {
4949
return e.GoaErrorName()
5050
}
5151

52-
// GoaErrorName returns "APayload".
52+
// GoaErrorName returns the error name.
5353
func (e *APayload) GoaErrorName() string {
5454
return "user_type"
5555
}
@@ -59,14 +59,14 @@ func (e *Result) Error() string {
5959
return ""
6060
}
6161

62-
// ErrorName returns "Result".
62+
// ErrorName returns the error name.
6363
//
6464
// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105
6565
func (e *Result) ErrorName() string {
6666
return e.GoaErrorName()
6767
}
6868

69-
// GoaErrorName returns "Result".
69+
// GoaErrorName returns the error name.
7070
func (e *Result) GoaErrorName() string {
7171
return e.B
7272
}
@@ -76,14 +76,14 @@ func (e Primitive) Error() string {
7676
return "primitive error description"
7777
}
7878

79-
// ErrorName returns "primitive".
79+
// ErrorName returns the error name.
8080
//
8181
// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105
8282
func (e Primitive) ErrorName() string {
8383
return e.GoaErrorName()
8484
}
8585

86-
// GoaErrorName returns "primitive".
86+
// GoaErrorName returns the error name.
8787
func (e Primitive) GoaErrorName() string {
8888
return "primitive"
8989
}

codegen/service/testdata/golden/service_service-mixed-results.go.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ type Payload struct {
5757
// MixedResultsMethod method.
5858
type ResultType struct {
5959
OK bool
60-
}
60+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
2+
// Service is the RawObjectPayloadTypeNameCollision service interface.
3+
type Service interface {
4+
// Foo implements Foo.
5+
Foo(context.Context, *FooPayload3) (err error)
6+
}
7+
8+
// APIName is the name of the API as defined in the design.
9+
const APIName = "test api"
10+
11+
// APIVersion is the version of the API as defined in the design.
12+
const APIVersion = "0.0.1"
13+
14+
// ServiceName is the name of the service as defined in the design. This is the
15+
// same value that is set in the endpoint request contexts under the ServiceKey
16+
// key.
17+
const ServiceName = "RawObjectPayloadTypeNameCollision"
18+
19+
// MethodNames lists the service method names as defined in the design. These
20+
// are the same values that are set in the endpoint request contexts under the
21+
// MethodKey key.
22+
var MethodNames = [1]string{"Foo"}
23+
24+
type FooPayload struct {
25+
X *string
26+
}
27+
28+
type FooPayload2 struct {
29+
Y *string
30+
}
31+
32+
// FooPayload3 is the payload type of the RawObjectPayloadTypeNameCollision
33+
// service Foo method.
34+
type FooPayload3 struct {
35+
A string
36+
}
37+
38+
// Error returns an error description.
39+
func (e *FooPayload) Error() string {
40+
return ""
41+
}
42+
43+
// ErrorName returns the error name.
44+
//
45+
// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105
46+
func (e *FooPayload) ErrorName() string {
47+
return e.GoaErrorName()
48+
}
49+
50+
// GoaErrorName returns the error name.
51+
func (e *FooPayload) GoaErrorName() string {
52+
return "reserve_foo_payload"
53+
}
54+
55+
// Error returns an error description.
56+
func (e *FooPayload2) Error() string {
57+
return ""
58+
}
59+
60+
// ErrorName returns the error name.
61+
//
62+
// Deprecated: Use GoaErrorName - https://github.com/goadesign/goa/issues/3105
63+
func (e *FooPayload2) ErrorName() string {
64+
return e.GoaErrorName()
65+
}
66+
67+
// GoaErrorName returns the error name.
68+
func (e *FooPayload2) GoaErrorName() string {
69+
return "reserve_foo_payload2"
70+
}

codegen/service/testdata/service_dsls.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,36 @@ var MixedAndMultipleAPIKeySecurityDSL = func() {
11001100
})
11011101
}
11021102

1103+
// RawObjectPayloadTypeNameCollisionDSL exercises the naming of synthetic user
1104+
// types created when wrapping raw object payloads.
1105+
//
1106+
// The service generator wraps raw object payloads in a synthetic user type
1107+
// named after the method (e.g. Foo -> FooPayload). That name must be computed
1108+
// using NameScope uniqueness rules even when a would-be suffix (FooPayload2) is
1109+
// already taken, otherwise codegen can emit invalid references.
1110+
var RawObjectPayloadTypeNameCollisionDSL = func() {
1111+
var FooPayload = Type("FooPayload", func() {
1112+
Attribute("x", String)
1113+
})
1114+
var FooPayload2 = Type("FooPayload2", func() {
1115+
Attribute("y", String)
1116+
})
1117+
1118+
Service("RawObjectPayloadTypeNameCollision", func() {
1119+
// Reserve FooPayload and FooPayload2 in the NameScope *before* raw-object
1120+
// payload wrapping occurs (wrapObject runs before method data is built).
1121+
Error("reserve_foo_payload", FooPayload)
1122+
Error("reserve_foo_payload2", FooPayload2)
1123+
1124+
Method("Foo", func() {
1125+
Payload(func() {
1126+
Attribute("a", String)
1127+
Required("a")
1128+
})
1129+
})
1130+
})
1131+
}
1132+
11031133
// UnionCustomKeysDSL tests union with custom type and value keys via Meta tags.
11041134
var UnionCustomKeysDSL = func() {
11051135
var CustomUnion = Type("CustomUnion", func() {

0 commit comments

Comments
 (0)