Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions internal/schemas/generator/accessors.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@ func receiverTypeName(e ast.Expr) string {
return ""
}

// embeddedFieldName returns the name Go promotes into the struct's namespace
// for an anonymous field, mirroring the Go spec: it's the embedded type's own
// name, ignoring any pointer indirection or package qualification (e.g.
// embedding `*Bar` or `pkg.Bar` both promote the name `Bar`). Returns "" for
// type shapes generated models don't use (generics, etc.), which simply
// aren't tracked as potential collisions.
func embeddedFieldName(e ast.Expr) string {
if star, ok := e.(*ast.StarExpr); ok {
e = star.X
}
switch t := e.(type) {
case *ast.Ident:
return t.Name
case *ast.SelectorExpr:
return t.Sel.Name
default:
return ""
}
}

// isNilable reports whether a zero value of the given type is spelled `nil`,
// letting the generated getter return nil directly instead of declaring a zero
// variable. Generated models use pointers throughout, so this is the common
Expand All @@ -148,8 +168,30 @@ func isNilable(e ast.Expr) bool {

// writeStructAccessors appends a getter and setter for each named field of the
// given struct to b. Any accessor whose name already exists in skip is omitted
// to avoid colliding with methods oapi-codegen already generated.
// to avoid colliding with methods oapi-codegen already generated. An accessor
// is also omitted if its name collides with another field of the same struct
// (e.g. a field named PasswordData alongside a sibling field named
// GetPasswordData): Go forbids a method and a field sharing a name on the same
// type, and Terraform schemas occasionally produce exactly that pair.
func writeStructAccessors(b *strings.Builder, fset *token.FileSet, typeName string, st *ast.StructType, skip map[string]bool) {
fieldNames := map[string]bool{}
for _, field := range st.Fields.List {
if len(field.Names) == 0 {
// Anonymous/embedded field: Go promotes the embedded type's own
// name into the struct's namespace (e.g. embedding GetFoo gives
// the struct a field effectively named GetFoo), so it can still
// collide with a generated accessor even though it has no
// explicit field.Names entry of its own.
if n := embeddedFieldName(field.Type); n != "" {
fieldNames[n] = true
}
continue
}
for _, name := range field.Names {
fieldNames[name.Name] = true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

for _, field := range st.Fields.List {
// Skip embedded/anonymous fields; generated models don't use them.
if len(field.Names) == 0 {
Expand All @@ -176,7 +218,7 @@ func writeStructAccessors(b *strings.Builder, fset *token.FileSet, typeName stri

// Getter. It tolerates a nil receiver so that chained getters are
// safe on partially-populated resources.
if !skip["Get"+fieldName] {
if !skip["Get"+fieldName] && !fieldNames["Get"+fieldName] {
b.WriteString("\n// Get" + fieldName + " returns the " + fieldName + " field.\n")
b.WriteString("// It returns the zero value if the receiver is nil.\n")
b.WriteString("func (" + accessorReceiver + " *" + typeName + ") Get" + fieldName + "() " + fieldType + " {\n")
Expand All @@ -193,7 +235,7 @@ func writeStructAccessors(b *strings.Builder, fset *token.FileSet, typeName stri
}

// Setter.
if !skip["Set"+fieldName] {
if !skip["Set"+fieldName] && !fieldNames["Set"+fieldName] {
b.WriteString("\n// Set" + fieldName + " sets the " + fieldName + " field.\n")
b.WriteString("func (" + accessorReceiver + " *" + typeName + ") Set" + fieldName + "(v " + fieldType + ") {\n")
b.WriteString("\t" + accessorReceiver + "." + fieldName + " = v\n")
Expand Down
90 changes: 90 additions & 0 deletions internal/schemas/generator/accessors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,93 @@ func (o *Foo) GetAdditionalProperties() *map[string]string {
t.Errorf("expected SetAdditionalProperties to be generated, got %d", n)
}
}

// TestAddAccessorsSkipsFieldNameCollisions guards against a Terraform schema
// that legitimately has two fields where one's name matches the accessor the
// other would generate (e.g. a field named PasswordData alongside a sibling
// field named GetPasswordData). Go forbids a method and a field sharing a name
// on the same type, so emitting the colliding accessor would make the package
// fail to compile, as happened for provider-aws-ec2's InstanceStatusAtProvider.
// Table-driven so a regression in either the getter or the setter collision
// check — or in the embedded-field case, which go/ast reports via an empty
// field.Names rather than a plain name — is caught, and cmp.Diff over the full
// generated method set confirms nothing else was skipped along the way.
func TestAddAccessorsSkipsFieldNameCollisions(t *testing.T) {
cases := []struct {
name string
args string
want map[string]string
reason string
}{
{
name: "GetterCollidesWithSiblingField",
args: `package v1alpha1

type Foo struct {
PasswordData *string ` + "`json:\"passwordData,omitempty\"`" + `
GetPasswordData *bool ` + "`json:\"getPasswordData,omitempty\"`" + `
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
`,
want: map[string]string{
// No Foo.GetPasswordData: it would collide with the sibling
// field of that exact name.
"Foo.SetPasswordData": "*string",
"Foo.GetGetPasswordData": "*bool",
"Foo.SetGetPasswordData": "*bool",
},
reason: "a field named PasswordData must not get a GetPasswordData() method when a sibling field is itself named GetPasswordData",
},
{
name: "SetterCollidesWithSiblingField",
args: `package v1alpha1

type Foo struct {
Data *string ` + "`json:\"data,omitempty\"`" + `
SetData *bool ` + "`json:\"setData,omitempty\"`" + `
}
`,
want: map[string]string{
"Foo.GetData": "*string",
// No Foo.SetData: it would collide with the sibling field of
// that exact name.
"Foo.GetSetData": "*bool",
"Foo.SetSetData": "*bool",
},
reason: "a field named Data must not get a SetData() method when a sibling field is itself named SetData",
},
{
name: "GetterCollidesWithEmbeddedFieldName",
args: `package v1alpha1

type GetPasswordData struct {
Enabled *bool ` + "`json:\"enabled,omitempty\"`" + `
}

type Foo struct {
PasswordData *string ` + "`json:\"passwordData,omitempty\"`" + `
GetPasswordData
}
`,
want: map[string]string{
// No Foo.GetPasswordData: Go promotes the embedded
// GetPasswordData field under that same name.
"Foo.SetPasswordData": "*string",
"GetPasswordData.GetEnabled": "*bool",
"GetPasswordData.SetEnabled": "*bool",
},
reason: "an anonymous/embedded field promotes its type name into the struct's namespace just like a named field would, so it must be treated as a collision candidate too",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := addAccessors(tc.args)
if err != nil {
t.Fatalf("addAccessors returned error: %v", err)
}
if diff := cmp.Diff(tc.want, collectMethods(t, got)); diff != "" {
t.Errorf("%s\ngenerated accessors (-want +got):\n%s", tc.reason, diff)
}
})
}
}