Skip to content

Commit b56aab1

Browse files
committed
Address review comments
1 parent cc90adc commit b56aab1

5 files changed

Lines changed: 136 additions & 14 deletions

File tree

pkg/analysis/optionalfields/analyzer.go

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type analyzer struct {
5656
pointerPolicy OptionalFieldsPointerPolicy
5757
pointerPreference OptionalFieldsPointerPreference
5858
omitEmptyPolicy OptionalFieldsOmitEmptyPolicy
59+
omitZeroPolicy OptionalFieldsOmitZeroPolicy
5960
}
6061

6162
// newAnalyzer creates a new analyzer.
@@ -70,6 +71,7 @@ func newAnalyzer(cfg *OptionalFieldsConfig) *analysis.Analyzer {
7071
pointerPolicy: cfg.Pointers.Policy,
7172
pointerPreference: cfg.Pointers.Preference,
7273
omitEmptyPolicy: cfg.OmitEmpty.Policy,
74+
omitZeroPolicy: cfg.OmitZero.Policy,
7375
}
7476

7577
return &analysis.Analyzer{
@@ -132,6 +134,10 @@ func defaultConfig(cfg *OptionalFieldsConfig) {
132134
if cfg.OmitEmpty.Policy == "" {
133135
cfg.OmitEmpty.Policy = OptionalFieldsOmitEmptyPolicySuggestFix
134136
}
137+
138+
if cfg.OmitZero.Policy == "" {
139+
cfg.OmitZero.Policy = OptionalFieldsOmitZeroPolicySuggestFix
140+
}
135141
}
136142

137143
func (a *analyzer) checkFieldProperties(pass *analysis.Pass, field *ast.Field, fieldName string, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo) {
@@ -144,25 +150,35 @@ func (a *analyzer) checkFieldProperties(pass *analysis.Pass, field *ast.Field, f
144150
if a.pointerPreference == OptionalFieldsPointerPreferenceAlways {
145151
// The field must always be a pointer, pointers require omitempty, so enforce that too.
146152
a.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying)
147-
a.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, hasOmitZero, hasValidZeroValue, isStruct, jsonTags)
153+
a.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags)
148154

149155
return
150156
}
151157

152158
// The pointer preference is now when required.
159+
// Validate for omitzero policy.
160+
if a.omitZeroPolicy != OptionalFieldsOmitZeroPolicyIgnore || hasOmitZero {
161+
// If we require omitzero, or the field has omitzero, we can check the field properties based on it being an omitzero field.
162+
a.checkFieldPropertiesWithOmitZeroRequired(pass, field, fieldName, jsonTags, hasOmitZero, hasValidZeroValue, isPointer, isStruct)
163+
} else {
164+
// The field does not have omitzero, and does not require it.
165+
a.checkFieldPropertiesWithoutOmitZero(pass, field, fieldName, jsonTags, hasValidZeroValue, isPointer, isStruct)
166+
}
153167

168+
// The pointer preference is now when required.
169+
// Validate for omitempty policy.
154170
if a.omitEmptyPolicy != OptionalFieldsOmitEmptyPolicyIgnore || hasOmitEmpty {
155171
// If we require omitempty, or the field has omitempty, we can check the field properties based on it being an omitempty field.
156172
a.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, hasOmitEmpty, hasOmitZero, hasValidZeroValue, completeValidation, isPointer, isStruct)
157173
} else {
158174
// The field does not have omitempty, and does not require it.
159-
a.checkFieldPropertiesWithoutOmitEmpty(pass, field, fieldName, jsonTags, underlying, hasValidZeroValue, completeValidation, isPointer, isStruct, hasOmitZero)
175+
a.checkFieldPropertiesWithoutOmitEmpty(pass, field, fieldName, jsonTags, underlying, hasValidZeroValue, completeValidation, isPointer, isStruct)
160176
}
161177
}
162178

163179
func (a *analyzer) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitEmpty, hasOmitZero, hasValidZeroValue, completeValidation, isPointer, isStruct bool) {
164180
// In this case, we should always add the omitempty if it isn't present.
165-
a.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, hasOmitZero, hasValidZeroValue, isStruct, jsonTags)
181+
a.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags)
166182

167183
switch {
168184
case !hasValidZeroValue && isStruct && hasOmitZero:
@@ -181,7 +197,7 @@ func (a *analyzer) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass
181197
}
182198
}
183199

184-
func (a *analyzer) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasValidZeroValue, completeValidation, isPointer, isStruct, hasOmitZero bool) {
200+
func (a *analyzer) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasValidZeroValue, completeValidation, isPointer, isStruct bool) {
185201
switch {
186202
case hasValidZeroValue:
187203
// The field is not omitempty, and the zero value is valid, the field does not need to be a pointer.
@@ -198,6 +214,39 @@ func (a *analyzer) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, fie
198214
}
199215
}
200216

217+
func (a *analyzer) checkFieldPropertiesWithOmitZeroRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, hasOmitZero, hasValidZeroValue, isPointer, isStruct bool) {
218+
// In this case, we should always add the omitzero for struct fields if it isn't present.
219+
a.handleFieldShouldHaveOmitZero(pass, field, fieldName, hasOmitZero, isStruct, jsonTags)
220+
221+
if !hasValidZeroValue && isStruct {
222+
// The struct field need not be pointer if it does not have a valid zero value.
223+
a.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s is optional and does not allow the zero value. The field does not need to be a pointer.")
224+
}
225+
}
226+
227+
func (a *analyzer) checkFieldPropertiesWithoutOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, hasValidZeroValue, isPointer, isStruct bool) {
228+
if !isStruct {
229+
// Handle omitzero only for struct fields.
230+
return
231+
}
232+
233+
switch {
234+
case hasValidZeroValue:
235+
// The field is not omitzero, and the zero value is valid, the field does not need to be a pointer.
236+
a.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s is optional, without omitzero and allows the zero value. The field does not need to be a pointer.")
237+
case !hasValidZeroValue && isStruct:
238+
// The zero value would not be accepted, so the struct field needs to have omitzero.
239+
// Force the omitzero policy to suggest a fix. We can only get to this function when the omitzero policy is configured to Ignore.
240+
// Since we absolutely have to add the omitzero tag, we can report it as a suggestion.
241+
reportShouldAddOmitZero(pass, field, OptionalFieldsOmitZeroPolicySuggestFix, fieldName, "field %s is struct and optional and does not allow the zero value. It must have the omitzero tag.", jsonTags)
242+
243+
// Once it has the omitzero tag, it will also need to be a pointer in some cases.
244+
// Now handle it as if it had the omitzero already.
245+
// We already handle the omitzero tag above, so force the `hasOmitZero` to true.
246+
a.checkFieldPropertiesWithOmitZeroRequired(pass, field, fieldName, jsonTags, true, hasValidZeroValue, isPointer, isStruct)
247+
}
248+
}
249+
201250
func (a *analyzer) handleFieldShouldBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr) {
202251
if isPointerType(pass, underlying) {
203252
if isPointer {
@@ -232,17 +281,20 @@ func (a *analyzer) handleFieldShouldNotBePointer(pass *analysis.Pass, field *ast
232281
reportShouldRemovePointer(pass, field, a.pointerPolicy, fieldName, message)
233282
}
234283

235-
func (a *analyzer) handleFieldShouldHaveOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, hasOmitEmpty, hasOmitZero, hasValidZeroValue, isStruct bool, jsonTags extractjsontags.FieldTagInfo) {
284+
func (a *analyzer) handleFieldShouldHaveOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, hasOmitEmpty bool, jsonTags extractjsontags.FieldTagInfo) {
236285
if hasOmitEmpty {
237286
return
238287
}
239288

240-
if !hasValidZeroValue && isStruct && hasOmitZero {
241-
// The struct field need not have omitempty tag if it does not have a valid zero value and has omitzero tag.
289+
reportShouldAddOmitEmpty(pass, field, a.omitEmptyPolicy, fieldName, "field %s is optional and should have the omitempty tag", jsonTags)
290+
}
291+
292+
func (a *analyzer) handleFieldShouldHaveOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, hasOmitZero, isStruct bool, jsonTags extractjsontags.FieldTagInfo) {
293+
if hasOmitZero || !isStruct {
242294
return
243295
}
244-
245-
reportShouldAddOmitEmpty(pass, field, a.omitEmptyPolicy, fieldName, "field %s is optional and should have the omitempty tag", jsonTags)
296+
// Currently, add omitzero tags to only struct fields.
297+
reportShouldAddOmitZero(pass, field, a.omitZeroPolicy, fieldName, "field %s is optional and should have the omitzero tag", jsonTags)
246298
}
247299

248300
func (a *analyzer) handleIncompleteFieldValidation(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr) {
@@ -312,9 +364,15 @@ func getStructZeroValue(pass *analysis.Pass, structType *ast.StructType) string
312364
for _, field := range structType.Fields.List {
313365
fieldTagInfo := jsonTagInfo.FieldTags(field)
314366

315-
if fieldTagInfo.OmitEmpty || fieldTagInfo.OmitZero {
367+
isPointer, _ := isStarExpr(field.Type)
368+
if !isPointer && fieldTagInfo.OmitZero {
369+
// for non-pointer field if it has omitzero, we can use a zero value.
370+
continue
371+
}
372+
373+
if fieldTagInfo.OmitEmpty {
316374
// If the field is omitted, we can use a zero value.
317-
// For structs, if they aren't a pointer another error will be raised.
375+
// For structs, if they aren't a pointer, another error will be raised.
318376
continue
319377
}
320378

pkg/analysis/optionalfields/config.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ type OptionalFieldsConfig struct {
2626
// This defines how the linter should handle optional fields, and whether they should have the omitempty tag or not.
2727
// By default, all fields will be expected to have the `omitempty` tag.
2828
OmitEmpty OptionalFieldsOmitEmpty `json:"omitempty"`
29+
30+
// omitzero is the policy for the `omitzero` tag within the json tag for fields.
31+
// This defines how the linter should handle optional fields, and whether they should have the omitzero tag or not.
32+
// By default, all the struct fields will be expected to have the `omitzero` tag.
33+
OmitZero OptionalFieldsOmitZero `json:"omitzero"`
2934
}
3035

3136
// OptionalFieldsPointers is the configuration for pointers in optional fields.
@@ -57,6 +62,17 @@ type OptionalFieldsOmitEmpty struct {
5762
Policy OptionalFieldsOmitEmptyPolicy `json:"policy"`
5863
}
5964

65+
// OptionalFieldsOmitZero is the configuration for the `omitzero` tag on optional fields.
66+
type OptionalFieldsOmitZero struct {
67+
// policy determines whether the linter should require omitzero for all optional `struct` fields.
68+
// Valid values are "SuggestFix" and "Ignore".
69+
// When set to "SuggestFix", the linter will suggest adding the `omitzero` tag when an optional field does not have it.
70+
// When set to "Warn", the linter will emit a warning if the field does not have the `omitzero` tag.
71+
// When set to "Ignore", and optional field missing the `omitzero` tag will be ignored.
72+
// Note, when set to "Ignore", and a field does not have the `omitzero` tag, this may affect whether the field should be a pointer or not.
73+
Policy OptionalFieldsOmitZeroPolicy `json:"policy"`
74+
}
75+
6076
// OptionalFieldsPointerPreference is the preference for pointers in optional fields.
6177
type OptionalFieldsPointerPreference string
6278

@@ -92,3 +108,17 @@ const (
92108
// OptionalFieldsOmitEmptyPolicyIgnore indicates that the linter will ignore any field missing the omitempty tag.
93109
OptionalFieldsOmitEmptyPolicyIgnore OptionalFieldsOmitEmptyPolicy = "Ignore"
94110
)
111+
112+
// OptionalFieldsOmitZeroPolicy is the policy for the omitzero tag on optional fields.
113+
type OptionalFieldsOmitZeroPolicy string
114+
115+
const (
116+
// OptionalFieldsOmitZeroPolicySuggestFix indicates that the linter will emit a warning if the field does not have omitzero, and suggest a fix.
117+
OptionalFieldsOmitZeroPolicySuggestFix OptionalFieldsOmitZeroPolicy = "SuggestFix"
118+
119+
// OptionalFieldsOmitZeroPolicyWarn indicates that the linter will emit a warning if the field does not have omitzero.
120+
OptionalFieldsOmitZeroPolicyWarn OptionalFieldsOmitZeroPolicy = "Warn"
121+
122+
// OptionalFieldsOmitZeroPolicyIgnore indicates that the linter will ignore any field missing the omitzero tag.
123+
OptionalFieldsOmitZeroPolicyIgnore OptionalFieldsOmitZeroPolicy = "Ignore"
124+
)

pkg/analysis/optionalfields/testdata/src/b/a.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ type A struct {
179179
// structWithOnlyOmitZeroTag is a struct field with a minimum number of properties and has only omitzero tag.
180180
// +kubebuilder:validation:MinProperties=1
181181
// +optional
182-
StructWithOnlyOmitZeroTag B `json:"structWithOnlyOmitZeroTag,omitzero"`
182+
StructWithOnlyOmitZeroTag B `json:"structWithOnlyOmitZeroTag,omitzero"` // want "field StructWithOnlyOmitZeroTag is optional and should have the omitempty tag"
183183

184184
// structWithMinPropertiesOnStruct is a struct field with a minimum number of properties on the struct.
185185
// +optional

pkg/analysis/optionalfields/testdata/src/b/a.go.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ type A struct {
179179
// structWithOnlyOmitZeroTag is a struct field with a minimum number of properties and has only omitzero tag.
180180
// +kubebuilder:validation:MinProperties=1
181181
// +optional
182-
StructWithOnlyOmitZeroTag B `json:"structWithOnlyOmitZeroTag,omitzero"`
182+
StructWithOnlyOmitZeroTag B `json:"structWithOnlyOmitZeroTag,omitempty,omitzero"` // want "field StructWithOnlyOmitZeroTag is optional and should have the omitempty tag"
183183

184184
// structWithMinPropertiesOnStruct is a struct field with a minimum number of properties on the struct.
185185
// +optional

pkg/analysis/optionalfields/util.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,34 @@ func reportShouldAddOmitEmpty(pass *analysis.Pass, field *ast.Field, omitEmptyPo
149149
}
150150
}
151151

152+
// reportShouldAddOmitZero adds an analysis diagnostic that explains that an omitzero tag should be added.
153+
func reportShouldAddOmitZero(pass *analysis.Pass, field *ast.Field, omitZeroPolicy OptionalFieldsOmitZeroPolicy, fieldName, messageFmt string, fieldTagInfo extractjsontags.FieldTagInfo) {
154+
switch omitZeroPolicy {
155+
case OptionalFieldsOmitZeroPolicySuggestFix:
156+
pass.Report(analysis.Diagnostic{
157+
Pos: field.Pos(),
158+
Message: fmt.Sprintf(messageFmt, fieldName),
159+
SuggestedFixes: []analysis.SuggestedFix{
160+
{
161+
Message: fmt.Sprintf("should add 'omitzero' to the field tag for field %s", fieldName),
162+
TextEdits: []analysis.TextEdit{
163+
{
164+
Pos: fieldTagInfo.Pos + token.Pos(len(fieldTagInfo.Name)),
165+
NewText: []byte(",omitzero"),
166+
},
167+
},
168+
},
169+
},
170+
})
171+
case OptionalFieldsOmitZeroPolicyWarn:
172+
pass.Reportf(field.Pos(), messageFmt, fieldName)
173+
case OptionalFieldsOmitZeroPolicyIgnore:
174+
// Do nothing, as the policy is to ignore the missing omitzero tag.
175+
default:
176+
panic(fmt.Sprintf("unknown omit zero policy: %s", omitZeroPolicy))
177+
}
178+
}
179+
152180
// isZeroValueValid determines whether the zero value of the field is valid per the validation markers.
153181
// For example, if the string has a minimum length greater than 0, the zero value is not valid.
154182
// Or if the minimum value of an integer field is greater than 0, the zero value is not valid.
@@ -223,7 +251,13 @@ func areStructFieldZeroValuesValid(pass *analysis.Pass, structType *ast.StructTy
223251
for _, field := range structType.Fields.List {
224252
fieldTagInfo := jsonTagInfo.FieldTags(field)
225253

226-
if fieldTagInfo.OmitEmpty || fieldTagInfo.OmitZero {
254+
isPointer, _ := isStarExpr(field.Type)
255+
if !isPointer && fieldTagInfo.OmitZero {
256+
// for non-pointer field if it has omitzero, we can use a zero value.
257+
continue
258+
}
259+
260+
if fieldTagInfo.OmitEmpty {
227261
// If the field is omitted, we can use a zero value.
228262
// For structs, if they aren't a pointer another error will be raised.
229263
continue

0 commit comments

Comments
 (0)