Skip to content

Commit ab7a290

Browse files
authored
Merge pull request #115 from Karthik-K-N/optionalfields
Support omitzero tags in optionalfields linter
2 parents bf4e28a + c515e96 commit ab7a290

File tree

18 files changed

+1072
-28
lines changed

18 files changed

+1072
-28
lines changed

docs/linters.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,13 @@ The `nophase` linter checks that the fields in the API types don't contain a 'Ph
180180

181181
## OptionalFields
182182

183-
The `optionalfields` linter checks that all fields marked as optional adhere to being pointers and having the `omitempty` value in their `json` tag where appropriate.
183+
The `optionalfields` linter checks that all fields marked as optional adhere to being pointers and having either the `omitempty` or `omitzero` value in their `json` tag where appropriate.
184+
Currently `omitzero` is handled only for fields with struct type.
184185

185186
If you prefer to avoid pointers where possible, the linter can be configured with the `WhenRequired` preference to determine, based on the serialization and valid values for the field, whether the field should be a pointer or not.
186-
For example, an optional string with a non-zero minimum length does not need to be a pointer, as the zero value is not valid, and it is safe for the Go marshaller to omit the empty value.
187+
For example
188+
- an optional string with a non-zero minimum length does not need to be a pointer, as the zero value is not valid, and it is safe for the Go marshaller to omit the empty value.
189+
- an optional struct having omitzero json tag with a non-zero minimum properties does not need to be a pointer, as the zero value is not valid, and it is safe for the Go marshaller to omit the empty value.
187190

188191
In certain use cases, it can be desirable to not omit optional fields from the serialized form of the object.
189192
In this case, the `omitempty` policy can be set to `Ignore`, and the linter will ensure that the zero value of the object is an acceptable value for the field.
@@ -198,19 +201,23 @@ lintersConfig:
198201
policy: SuggestFix | Warn # The policy for pointers in optional fields. Defaults to `SuggestFix`.
199202
omitempty:
200203
policy: SuggestFix | Warn | Ignore # The policy for omitempty in optional fields. Defaults to `SuggestFix`.
204+
omitzero:
205+
policy: SuggestFix | Warn | Forbid # The policy for omitzero in optional fields. Defaults to `SuggestFix`.
201206
```
202207
203208
### Fixes
204209
205-
The `optionalfields` linter can automatically fix fields that are marked as optional, that are either not pointers or do not have the `omitempty` value in their `json` tag.
206-
It will suggest to add the pointer to the field, and update the `json` tag to include the `omitempty` value.
210+
The `optionalfields` linter can automatically fix fields that are marked as optional, that are either not pointers or do not have the `omitempty` or `omitzero` value in their `json` tag.
211+
It will suggest to add the pointer to the field, and update the `json` tag to include the `omitempty` value or, for struct fields specifically, it will suggest to remove the pointer to the field, and update the `json` tag to include the `omitzero` value.
207212

208213
If you prefer not to suggest fixes for pointers in optional fields, you can change the `pointers.policy` to `Warn`.
209214

210215
If you prefer not to suggest fixes for `omitempty` in optional fields, you can change the `omitempty.policy` to `Warn` or `Ignore`.
216+
If you prefer not to suggest fixes for `omitzero` in optional fields, you can change the `omitzero.policy` to `Warn` and also not to consider `omitzero` policy at all, it can be set to `Forbid`.
211217

212218
When the `pointers.preference` is set to `WhenRequired`, the linter will suggest to add the pointer to the field only when the field zero value is a valid value for the field.
213219
When the field zero value is not a valid value for the field, the linter will suggest to remove the pointer from the field.
220+
When the field zero value is not a valid value for the field of type struct, the linter will suggest to add `omitzero` json tag and to remove the pointer from the field.
214221

215222
When the `pointers.preference` is set to `Always`, the linter will always suggest to add the pointer to the field, regardless of the validity of the zero value of the field.
216223

pkg/analysis/helpers/extractjsontags/analyzer.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@ import (
2525
"golang.org/x/tools/go/analysis"
2626
"golang.org/x/tools/go/analysis/passes/inspect"
2727
"golang.org/x/tools/go/ast/inspector"
28-
28+
"k8s.io/utils/set"
2929
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
3030
)
3131

32+
const (
33+
omitEmpty = "omitempty"
34+
omitZero = "omitzero"
35+
)
36+
3237
// StructFieldTags is used to find information about
3338
// json tags on fields within struct.
3439
type StructFieldTags interface {
@@ -137,10 +142,12 @@ func extractTagInfo(tag *ast.BasicLit) FieldTagInfo {
137142
}
138143

139144
tagName := tagValues[0]
145+
tagSet := set.New[string](tagValues...)
140146

141147
return FieldTagInfo{
142148
Name: tagName,
143-
OmitEmpty: len(tagValues) == 2 && tagValues[1] == "omitempty",
149+
OmitEmpty: tagSet.Has(omitEmpty),
150+
OmitZero: tagSet.Has(omitZero),
144151
RawValue: tagValue,
145152
Pos: pos,
146153
End: end,
@@ -159,6 +166,9 @@ type FieldTagInfo struct {
159166
// OmitEmpty is true if the field has the omitempty option in the json tag.
160167
OmitEmpty bool
161168

169+
// OmitZero is true if the field has the omitzero option in the json tag.
170+
OmitZero bool
171+
162172
// Inline is true if the field has the inline option in the json tag.
163173
Inline bool
164174

pkg/analysis/optionalfields/analyzer.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type analyzer struct {
4949
pointerPolicy OptionalFieldsPointerPolicy
5050
pointerPreference OptionalFieldsPointerPreference
5151
omitEmptyPolicy OptionalFieldsOmitEmptyPolicy
52+
omitZeroPolicy OptionalFieldsOmitZeroPolicy
5253
}
5354

5455
// newAnalyzer creates a new analyzer.
@@ -63,6 +64,7 @@ func newAnalyzer(cfg *OptionalFieldsConfig) *analysis.Analyzer {
6364
pointerPolicy: cfg.Pointers.Policy,
6465
pointerPreference: cfg.Pointers.Preference,
6566
omitEmptyPolicy: cfg.OmitEmpty.Policy,
67+
omitZeroPolicy: cfg.OmitZero.Policy,
6668
}
6769

6870
return &analysis.Analyzer{
@@ -125,11 +127,16 @@ func defaultConfig(cfg *OptionalFieldsConfig) {
125127
if cfg.OmitEmpty.Policy == "" {
126128
cfg.OmitEmpty.Policy = OptionalFieldsOmitEmptyPolicySuggestFix
127129
}
130+
131+
if cfg.OmitZero.Policy == "" {
132+
cfg.OmitZero.Policy = OptionalFieldsOmitZeroPolicySuggestFix
133+
}
128134
}
129135

130136
func (a *analyzer) checkFieldProperties(pass *analysis.Pass, field *ast.Field, fieldName string, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo) {
131-
hasValidZeroValue, completeValidation := utils.IsZeroValueValid(pass, field, field.Type, markersAccess)
137+
hasValidZeroValue, completeValidation := utils.IsZeroValueValid(pass, field, field.Type, markersAccess, a.omitZeroPolicy != OptionalFieldsOmitZeroPolicyForbid)
132138
hasOmitEmpty := jsonTags.OmitEmpty
139+
hasOmitZero := jsonTags.OmitZero
133140
isPointer, underlying := isStarExpr(field.Type)
134141
isStruct := utils.IsStructType(pass, field.Type)
135142

@@ -142,7 +149,17 @@ func (a *analyzer) checkFieldProperties(pass *analysis.Pass, field *ast.Field, f
142149
}
143150

144151
// The pointer preference is now when required.
152+
// Validate for omitzero policy.
153+
if a.omitZeroPolicy != OptionalFieldsOmitZeroPolicyForbid {
154+
// If we require omitzero, we can check the field properties based on it being an omitzero field.
155+
a.checkFieldPropertiesWithOmitZeroRequired(pass, field, fieldName, jsonTags, hasOmitZero, isPointer, isStruct, hasValidZeroValue)
156+
} else {
157+
// when the omitzero policy is set to forbid, we need to report removing omitzero if set on the struct fields.
158+
a.checkFieldPropertiesWithOmitZeroForbidPolicy(pass, field, fieldName, isStruct, hasOmitZero, jsonTags)
159+
}
145160

161+
// The pointer preference is now when required.
162+
// Validate for omitempty policy.
146163
if a.omitEmptyPolicy != OptionalFieldsOmitEmptyPolicyIgnore || hasOmitEmpty {
147164
// If we require omitempty, or the field has omitempty, we can check the field properties based on it being an omitempty field.
148165
a.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct)
@@ -157,6 +174,9 @@ func (a *analyzer) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass
157174
a.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags)
158175

159176
switch {
177+
case isStruct && !hasValidZeroValue && a.omitZeroPolicy != OptionalFieldsOmitZeroPolicyForbid:
178+
// The struct field need not be pointer if it does not have a valid zero value.
179+
return
160180
case hasValidZeroValue && !completeValidation:
161181
a.handleIncompleteFieldValidation(pass, field, fieldName, isPointer, underlying)
162182
fallthrough // Since it's a valid zero value, we should still enforce the pointer.
@@ -183,8 +203,26 @@ func (a *analyzer) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, fie
183203
// Once it has the omitempty tag, it will also need to be a pointer in some cases.
184204
// Now handle it as if it had the omitempty already.
185205
// We already handle the omitempty tag above, so force the `hasOmitEmpty` to true.
186-
a.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, true, hasValidZeroValue, completeValidation, isPointer, isStruct)
206+
a.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, false, hasValidZeroValue, completeValidation, isPointer, isStruct)
207+
}
208+
}
209+
210+
func (a *analyzer) checkFieldPropertiesWithOmitZeroRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, hasOmitZero, isPointer, isStruct, hasValidZeroValue bool) {
211+
if !isStruct || hasValidZeroValue {
212+
return
187213
}
214+
215+
a.handleFieldShouldHaveOmitZero(pass, field, fieldName, hasOmitZero, jsonTags)
216+
a.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s is optional and does not have a valid zero value. The field does not need to be a pointer.")
217+
}
218+
219+
func (a *analyzer) checkFieldPropertiesWithOmitZeroForbidPolicy(pass *analysis.Pass, field *ast.Field, fieldName string, isStruct, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) {
220+
if !isStruct || !hasOmitZero {
221+
// Handle omitzero only for struct field having omitZero tag.
222+
return
223+
}
224+
225+
reportShouldRemoveOmitZero(pass, field, fieldName, jsonTags)
188226
}
189227

190228
func (a *analyzer) handleFieldShouldBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr) {
@@ -229,6 +267,14 @@ func (a *analyzer) handleFieldShouldHaveOmitEmpty(pass *analysis.Pass, field *as
229267
reportShouldAddOmitEmpty(pass, field, a.omitEmptyPolicy, fieldName, "field %s is optional and should have the omitempty tag", jsonTags)
230268
}
231269

270+
func (a *analyzer) handleFieldShouldHaveOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) {
271+
if hasOmitZero {
272+
return
273+
}
274+
// Currently, add omitzero tags to only struct fields.
275+
reportShouldAddOmitZero(pass, field, a.omitZeroPolicy, fieldName, "field %s is optional and does not allow the zero value. It must have the omitzero tag.", jsonTags)
276+
}
277+
232278
func (a *analyzer) handleIncompleteFieldValidation(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr) {
233279
if isPointer || isPointerType(pass, underlying) {
234280
// Don't warn them if the field is already a pointer.

pkg/analysis/optionalfields/analyzer_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ func TestWhenRequiredPreferenceConfiguration(t *testing.T) {
4040
Pointers: optionalfields.OptionalFieldsPointers{
4141
Preference: optionalfields.OptionalFieldsPointerPreferenceWhenRequired,
4242
},
43+
OmitZero: optionalfields.OptionalFieldsOmitZero{
44+
Policy: optionalfields.OptionalFieldsOmitZeroPolicyForbid,
45+
},
4346
})
4447
if err != nil {
4548
t.Fatal(err)
@@ -58,10 +61,35 @@ func TestWhenRequiredWithOmitEmptyIgnorePreferenceConfiguration(t *testing.T) {
5861
OmitEmpty: optionalfields.OptionalFieldsOmitEmpty{
5962
Policy: optionalfields.OptionalFieldsOmitEmptyPolicyIgnore,
6063
},
64+
OmitZero: optionalfields.OptionalFieldsOmitZero{
65+
Policy: optionalfields.OptionalFieldsOmitZeroPolicyForbid,
66+
},
6167
})
6268
if err != nil {
6369
t.Fatal(err)
6470
}
6571

6672
analysistest.RunWithSuggestedFixes(t, testdata, a, "c")
6773
}
74+
75+
func TestWithSuggestFixForOmitZero(t *testing.T) {
76+
testdata := analysistest.TestData()
77+
78+
a, err := optionalfields.Initializer().Init(&optionalfields.OptionalFieldsConfig{
79+
Pointers: optionalfields.OptionalFieldsPointers{
80+
Preference: optionalfields.OptionalFieldsPointerPreferenceWhenRequired,
81+
Policy: optionalfields.OptionalFieldsPointerPolicySuggestFix,
82+
},
83+
OmitEmpty: optionalfields.OptionalFieldsOmitEmpty{
84+
Policy: optionalfields.OptionalFieldsOmitEmptyPolicySuggestFix,
85+
},
86+
OmitZero: optionalfields.OptionalFieldsOmitZero{
87+
Policy: optionalfields.OptionalFieldsOmitZeroPolicySuggestFix,
88+
},
89+
})
90+
if err != nil {
91+
t.Fatal(err)
92+
}
93+
94+
analysistest.RunWithSuggestedFixes(t, testdata, a, "d")
95+
}

pkg/analysis/optionalfields/config.go

Lines changed: 33 additions & 1 deletion
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 when their zero value is not an acceptable user choice.
33+
OmitZero OptionalFieldsOmitZero `json:"omitzero"`
2934
}
3035

3136
// OptionalFieldsPointers is the configuration for pointers in optional fields.
@@ -49,14 +54,27 @@ type OptionalFieldsPointers struct {
4954
// OptionalFieldsOmitEmpty is the configuration for the `omitempty` tag on optional fields.
5055
type OptionalFieldsOmitEmpty struct {
5156
// policy determines whether the linter should require omitempty for all optional fields.
52-
// Valid values are "SuggestFix" and "Ignore".
57+
// Valid values are "SuggestFix", "Warn" and "Ignore".
5358
// When set to "SuggestFix", the linter will suggest adding the `omitempty` tag when an optional field does not have it.
5459
// When set to "Warn", the linter will emit a warning if the field does not have the `omitempty` tag.
5560
// When set to "Ignore", and optional field missing the `omitempty` tag will be ignored.
5661
// Note, when set to "Ignore", and a field does not have the `omitempty` tag, this may affect whether the field should be a pointer or not.
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", "Warn" and "Forbid".
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 "Forbid", 'omitzero' tags wont be considered.
72+
// Note, when set to "Forbid", and a field have the `omitzero` tag, the linter will suggest to remove the `omitzero` tag.
73+
// Note, `omitzero` tag is supported in go version starting from go 1.24.
74+
// Note, Configure omitzero policy to 'Forbid', if using with go version less than go 1.24.
75+
Policy OptionalFieldsOmitZeroPolicy `json:"policy"`
76+
}
77+
6078
// OptionalFieldsPointerPreference is the preference for pointers in optional fields.
6179
type OptionalFieldsPointerPreference string
6280

@@ -92,3 +110,17 @@ const (
92110
// OptionalFieldsOmitEmptyPolicyIgnore indicates that the linter will ignore any field missing the omitempty tag.
93111
OptionalFieldsOmitEmptyPolicyIgnore OptionalFieldsOmitEmptyPolicy = "Ignore"
94112
)
113+
114+
// OptionalFieldsOmitZeroPolicy is the policy for the omitzero tag on optional fields.
115+
type OptionalFieldsOmitZeroPolicy string
116+
117+
const (
118+
// OptionalFieldsOmitZeroPolicySuggestFix indicates that the linter will emit a warning if the field does not have omitzero, and suggest a fix.
119+
OptionalFieldsOmitZeroPolicySuggestFix OptionalFieldsOmitZeroPolicy = "SuggestFix"
120+
121+
// OptionalFieldsOmitZeroPolicyWarn indicates that the linter will emit a warning if the field does not have omitzero.
122+
OptionalFieldsOmitZeroPolicyWarn OptionalFieldsOmitZeroPolicy = "Warn"
123+
124+
// OptionalFieldsOmitZeroPolicyForbid indicates that the linter will forbid using omitzero tag.
125+
OptionalFieldsOmitZeroPolicyForbid OptionalFieldsOmitZeroPolicy = "Forbid"
126+
)

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,22 @@ type A struct {
288288
// StringAliasWithEnumEmptyValue is a string alias field with enum validation and empty value.
289289
// +optional
290290
StringAliasWithEnumEmptyValue *StringAliasWithEnumEmptyValue `json:"stringAliasWithEnumEmptyValue,omitempty"`
291+
292+
// structWithValidOmitZero is a struct field with a minimum number of properties on the struct so not a valid zero value.
293+
// +optional
294+
StructWithValidOmitZero *D `json:"structWithValidOmitZero,omitempty,omitzero"` // want "field StructWithValidOmitZero has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed."
295+
296+
// structWithOnlyOmitZero is a struct field with a minimum number of properties on the struct so not a valid zero value.
297+
// +optional
298+
StructWithOnlyOmitZero *D `json:"structWithOnlyOmitZero,omitzero"` // want "field StructWithOnlyOmitZero has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed." "field StructWithOnlyOmitZero is optional and should have the omitempty tag"
299+
300+
// structWithValidOmitZeroWithoutPointer is a struct field with a minimum number of properties on the struct so not a valid zero value.
301+
// +optional
302+
StructWithValidOmitZeroWithoutPointer D `json:"structWithValidOmitZeroWithoutPointer,omitempty,omitzero"` // want "field StructWithValidOmitZeroWithoutPointer has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed." "field StructWithValidOmitZeroWithoutPointer is optional and should be a pointer"
303+
304+
// structWithOnlyOmitZeroWithoutPointer is a struct field with a minimum number of properties on the struct so not a valid zero value.
305+
// +optional
306+
StructWithOnlyOmitZeroWithoutPointer D `json:"structWithOnlyOmitZeroWithoutPointer,omitzero"` // want "field StructWithOnlyOmitZeroWithoutPointer has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed." "field StructWithOnlyOmitZeroWithoutPointer is optional and should have the omitempty tag" "field StructWithOnlyOmitZeroWithoutPointer is optional and should be a pointer"
291307
}
292308

293309
type B struct {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,22 @@ type A struct {
288288
// StringAliasWithEnumEmptyValue is a string alias field with enum validation and empty value.
289289
// +optional
290290
StringAliasWithEnumEmptyValue *StringAliasWithEnumEmptyValue `json:"stringAliasWithEnumEmptyValue,omitempty"`
291+
292+
// structWithValidOmitZero is a struct field with a minimum number of properties on the struct so not a valid zero value.
293+
// +optional
294+
StructWithValidOmitZero *D `json:"structWithValidOmitZero,omitempty"` // want "field StructWithValidOmitZero has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed."
295+
296+
// structWithOnlyOmitZero is a struct field with a minimum number of properties on the struct so not a valid zero value.
297+
// +optional
298+
StructWithOnlyOmitZero *D `json:"structWithOnlyOmitZero,omitempty"` // want "field StructWithOnlyOmitZero has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed." "field StructWithOnlyOmitZero is optional and should have the omitempty tag"
299+
300+
// structWithValidOmitZeroWithoutPointer is a struct field with a minimum number of properties on the struct so not a valid zero value.
301+
// +optional
302+
StructWithValidOmitZeroWithoutPointer *D `json:"structWithValidOmitZeroWithoutPointer,omitempty"` // want "field StructWithValidOmitZeroWithoutPointer has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed." "field StructWithValidOmitZeroWithoutPointer is optional and should be a pointer"
303+
304+
// structWithOnlyOmitZeroWithoutPointer is a struct field with a minimum number of properties on the struct so not a valid zero value.
305+
// +optional
306+
StructWithOnlyOmitZeroWithoutPointer *D `json:"structWithOnlyOmitZeroWithoutPointer,omitempty"` // want "field StructWithOnlyOmitZeroWithoutPointer has the omitzero tag, but by policy is not allowed. The omitzero tag should be removed." "field StructWithOnlyOmitZeroWithoutPointer is optional and should have the omitempty tag" "field StructWithOnlyOmitZeroWithoutPointer is optional and should be a pointer"
291307
}
292308

293309
type B struct {

0 commit comments

Comments
 (0)