Skip to content

Commit 0660253

Browse files
authored
Merge pull request #141 from JoelSpeed/allow-embedded-map-key
Allow map keys from embedded types in ssatags
2 parents a575e84 + e2e9550 commit 0660253

File tree

3 files changed

+117
-6
lines changed

3 files changed

+117
-6
lines changed

pkg/analysis/ssatags/analyzer.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ func (a *analyzer) validateListMapKeys(pass *analysis.Pass, field *ast.Field, li
200200
return
201201
}
202202

203-
structFields := a.getStructFieldsFromField(pass, field)
203+
structFields := a.getStructFieldsFromField(pass, jsonTags, field)
204204
if structFields == nil {
205205
return
206206
}
@@ -222,7 +222,7 @@ func (a *analyzer) validateListMapKeys(pass *analysis.Pass, field *ast.Field, li
222222
}
223223
}
224224

225-
func (a *analyzer) getStructFieldsFromField(pass *analysis.Pass, field *ast.Field) *ast.FieldList {
225+
func (a *analyzer) getStructFieldsFromField(pass *analysis.Pass, jsonTags extractjsontags.StructFieldTags, field *ast.Field) *ast.FieldList {
226226
var elementType ast.Expr
227227

228228
// Get the element type from array or field type
@@ -232,10 +232,10 @@ func (a *analyzer) getStructFieldsFromField(pass *analysis.Pass, field *ast.Fiel
232232
elementType = field.Type
233233
}
234234

235-
return a.getStructFieldsFromExpr(pass, elementType)
235+
return a.getStructFieldsFromExpr(pass, jsonTags, elementType)
236236
}
237237

238-
func (a *analyzer) getStructFieldsFromExpr(pass *analysis.Pass, expr ast.Expr) *ast.FieldList {
238+
func (a *analyzer) getStructFieldsFromExpr(pass *analysis.Pass, jsonTags extractjsontags.StructFieldTags, expr ast.Expr) *ast.FieldList {
239239
switch elementType := expr.(type) {
240240
case *ast.Ident:
241241
typeSpec, ok := utils.LookupTypeSpec(pass, elementType)
@@ -248,9 +248,9 @@ func (a *analyzer) getStructFieldsFromExpr(pass *analysis.Pass, expr ast.Expr) *
248248
return nil
249249
}
250250

251-
return structType.Fields
251+
return flattenStructFields(pass, jsonTags, structType.Fields)
252252
case *ast.StarExpr:
253-
return a.getStructFieldsFromExpr(pass, elementType.X)
253+
return a.getStructFieldsFromExpr(pass, jsonTags, elementType.X)
254254
case *ast.SelectorExpr:
255255
return nil
256256
}
@@ -288,3 +288,44 @@ func defaultConfig(cfg *SSATagsConfig) {
288288
cfg.ListTypeSetUsage = SSATagsListTypeSetUsageWarn
289289
}
290290
}
291+
292+
// flattenStructFields flattens a struct's fields by looking for embedded structs and promoting their fields to the top level.
293+
// This allows us to correctly check listMapKey markers where the map key is a member of the embedded struct.
294+
func flattenStructFields(pass *analysis.Pass, jsonTags extractjsontags.StructFieldTags, fields *ast.FieldList) *ast.FieldList {
295+
if fields == nil {
296+
return nil
297+
}
298+
299+
flattenedFields := &ast.FieldList{}
300+
301+
for _, field := range fields.List {
302+
tagInfo := jsonTags.FieldTags(field)
303+
if len(field.Names) > 0 || tagInfo.Name != "" {
304+
// Field is not embedded, it has an explicit name.
305+
flattenedFields.List = append(flattenedFields.List, field)
306+
continue
307+
}
308+
309+
ident, ok := field.Type.(*ast.Ident)
310+
if !ok {
311+
flattenedFields.List = append(flattenedFields.List, field)
312+
continue
313+
}
314+
315+
typeSpec, ok := utils.LookupTypeSpec(pass, ident)
316+
if !ok {
317+
flattenedFields.List = append(flattenedFields.List, field)
318+
continue
319+
}
320+
321+
embeddedStruct, ok := typeSpec.Type.(*ast.StructType)
322+
if !ok {
323+
flattenedFields.List = append(flattenedFields.List, field)
324+
continue
325+
}
326+
327+
flattenedFields.List = append(flattenedFields.List, flattenStructFields(pass, jsonTags, embeddedStruct.Fields).List...)
328+
}
329+
330+
return flattenedFields
331+
}

pkg/analysis/ssatags/testdata/src/a/a.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ type SimpleObject struct {
1313
Name string `json:"name"`
1414
}
1515

16+
// ObjectWithEmbedded is an object with an embedded struct.
17+
// As there's no json tag, the embedded struct is inlined.
18+
type ObjectWithEmbedded struct {
19+
TestObject
20+
Embedded string `json:"embedded"`
21+
}
22+
23+
type ObjectWithInlineEmbedded struct {
24+
TestObject `json:",inline"`
25+
Embedded string `json:"embedded"`
26+
}
27+
28+
type ObjectWithNamedEmbedded struct {
29+
TestObject `json:"testObject"`
30+
Embedded string `json:"embedded"`
31+
}
32+
1633
type String string
1734
type Int int
1835
type Object TestObject
@@ -195,6 +212,24 @@ type SSATagsTestSpec struct {
195212
// +kubebuilder:pruning:PreserveUnknownFields
196213
// +kubebuilder:validation:Schemaless
197214
AllOf []JSONSchemaProps `json:"allOf,omitempty"`
215+
216+
// Object with embedded struct - list map key from embedded struct should be allowed
217+
// Without a json tag, the default is to inline the embedded struct.
218+
// +listType=map
219+
// +listMapKey=name
220+
ObjectWithEmbeddedStruct []ObjectWithEmbedded `json:"objectWithEmbeddedStruct,omitempty"`
221+
222+
// Object with inline embedded struct - list map key from embedded struct should be allowed
223+
// With a json tag, the embedded struct is not inlined.
224+
// +listType=map
225+
// +listMapKey=name
226+
ObjectWithInlineEmbeddedStruct []ObjectWithInlineEmbedded `json:"objectWithInlineEmbeddedStruct,omitempty"`
227+
228+
// Object with named embedded struct - list map key from embedded struct should be allowed
229+
// With a json tag, the embedded struct is not inlined.
230+
// +listType=map
231+
// +listMapKey=name
232+
ObjectWithNamedEmbeddedStruct []ObjectWithNamedEmbedded `json:"objectWithNamedEmbeddedStruct,omitempty"` // want "ObjectWithNamedEmbeddedStruct listMapKey \"name\" does not exist as a field in the struct"
198233
}
199234

200235
// JSONSchemaProps is a placeholder for the JSON schema properties.

pkg/analysis/ssatags/testdata/src/a/a.go.golden

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ type SimpleObject struct {
1313
Name string `json:"name"`
1414
}
1515

16+
// ObjectWithEmbedded is an object with an embedded struct.
17+
// As there's no json tag, the embedded struct is inlined.
18+
type ObjectWithEmbedded struct {
19+
TestObject
20+
Embedded string `json:"embedded"`
21+
}
22+
23+
type ObjectWithInlineEmbedded struct {
24+
TestObject `json:",inline"`
25+
Embedded string `json:"embedded"`
26+
}
27+
28+
type ObjectWithNamedEmbedded struct {
29+
TestObject `json:"testObject"`
30+
Embedded string `json:"embedded"`
31+
}
32+
1633
type String string
1734
type Int int
1835
type Object TestObject
@@ -194,6 +211,24 @@ type SSATagsTestSpec struct {
194211
// +kubebuilder:pruning:PreserveUnknownFields
195212
// +kubebuilder:validation:Schemaless
196213
AllOf []JSONSchemaProps `json:"allOf,omitempty"`
214+
215+
// Object with embedded struct - list map key from embedded struct should be allowed
216+
// Without a json tag, the default is to inline the embedded struct.
217+
// +listType=map
218+
// +listMapKey=name
219+
ObjectWithEmbeddedStruct []ObjectWithEmbedded `json:"objectWithEmbeddedStruct,omitempty"`
220+
221+
// Object with inline embedded struct - list map key from embedded struct should be allowed
222+
// With a json tag, the embedded struct is not inlined.
223+
// +listType=map
224+
// +listMapKey=name
225+
ObjectWithInlineEmbeddedStruct []ObjectWithInlineEmbedded `json:"objectWithInlineEmbeddedStruct,omitempty"`
226+
227+
// Object with named embedded struct - list map key from embedded struct should be allowed
228+
// With a json tag, the embedded struct is not inlined.
229+
// +listType=map
230+
// +listMapKey=name
231+
ObjectWithNamedEmbeddedStruct []ObjectWithNamedEmbedded `json:"objectWithNamedEmbeddedStruct,omitempty"` // want "ObjectWithNamedEmbeddedStruct listMapKey \"name\" does not exist as a field in the struct"
197232
}
198233

199234
// JSONSchemaProps is a placeholder for the JSON schema properties.

0 commit comments

Comments
 (0)