Skip to content

Commit bdb40c6

Browse files
authored
Merge pull request #131 from JoelSpeed/extract-serialization-checker
Extract serialization checker into a separate utility
2 parents 6469492 + 7b1c6f8 commit bdb40c6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+4703
-567
lines changed

pkg/analysis/optionalfields/analyzer.go

Lines changed: 20 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
2424
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
2525
markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
26-
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
26+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils/serialization"
2727
"sigs.k8s.io/kube-api-linter/pkg/markers"
2828
)
2929

@@ -46,10 +46,7 @@ func init() {
4646
}
4747

4848
type analyzer struct {
49-
pointerPolicy OptionalFieldsPointerPolicy
50-
pointerPreference OptionalFieldsPointerPreference
51-
omitEmptyPolicy OptionalFieldsOmitEmptyPolicy
52-
omitZeroPolicy OptionalFieldsOmitZeroPolicy
49+
serializationCheck serialization.SerializationCheck
5350
}
5451

5552
// newAnalyzer creates a new analyzer.
@@ -60,11 +57,21 @@ func newAnalyzer(cfg *OptionalFieldsConfig) *analysis.Analyzer {
6057

6158
defaultConfig(cfg)
6259

60+
serializationCheck := serialization.New(&serialization.Config{
61+
Pointers: serialization.PointersConfig{
62+
Policy: serialization.PointersPolicy(cfg.Pointers.Policy),
63+
Preference: serialization.PointersPreference(cfg.Pointers.Preference),
64+
},
65+
OmitEmpty: serialization.OmitEmptyConfig{
66+
Policy: serialization.OmitEmptyPolicy(cfg.OmitEmpty.Policy),
67+
},
68+
OmitZero: serialization.OmitZeroConfig{
69+
Policy: serialization.OmitZeroPolicy(cfg.OmitZero.Policy),
70+
},
71+
})
72+
6373
a := &analyzer{
64-
pointerPolicy: cfg.Pointers.Policy,
65-
pointerPreference: cfg.Pointers.Preference,
66-
omitEmptyPolicy: cfg.OmitEmpty.Policy,
67-
omitZeroPolicy: cfg.OmitZero.Policy,
74+
serializationCheck: serializationCheck,
6875
}
6976

7077
return &analysis.Analyzer{
@@ -99,9 +106,6 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce
99106
}
100107

101108
fieldMarkers := markersAccess.FieldMarkers(field)
102-
103-
fieldName := field.Names[0].Name
104-
105109
if !isFieldOptional(fieldMarkers) {
106110
// The field is not marked optional, so we don't need to check it.
107111
return
@@ -112,7 +116,7 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce
112116
return
113117
}
114118

115-
a.checkFieldProperties(pass, field, fieldName, markersAccess, jsonTags)
119+
a.serializationCheck.Check(pass, field, markersAccess, jsonTags)
116120
}
117121

118122
func defaultConfig(cfg *OptionalFieldsConfig) {
@@ -133,158 +137,7 @@ func defaultConfig(cfg *OptionalFieldsConfig) {
133137
}
134138
}
135139

136-
func (a *analyzer) checkFieldProperties(pass *analysis.Pass, field *ast.Field, fieldName string, markersAccess markershelper.Markers, jsonTags extractjsontags.FieldTagInfo) {
137-
hasValidZeroValue, completeValidation := utils.IsZeroValueValid(pass, field, field.Type, markersAccess, a.omitZeroPolicy != OptionalFieldsOmitZeroPolicyForbid)
138-
hasOmitEmpty := jsonTags.OmitEmpty
139-
hasOmitZero := jsonTags.OmitZero
140-
isPointer, underlying := isStarExpr(field.Type)
141-
isStruct := utils.IsStructType(pass, field.Type)
142-
143-
if a.pointerPreference == OptionalFieldsPointerPreferenceAlways {
144-
// The field must always be a pointer, pointers require omitempty, so enforce that too.
145-
a.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying)
146-
a.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags)
147-
148-
return
149-
}
150-
151-
// 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-
}
160-
161-
// The pointer preference is now when required.
162-
// Validate for omitempty policy.
163-
if a.omitEmptyPolicy != OptionalFieldsOmitEmptyPolicyIgnore || hasOmitEmpty {
164-
// If we require omitempty, or the field has omitempty, we can check the field properties based on it being an omitempty field.
165-
a.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct)
166-
} else {
167-
// The field does not have omitempty, and does not require it.
168-
a.checkFieldPropertiesWithoutOmitEmpty(pass, field, fieldName, jsonTags, underlying, hasValidZeroValue, completeValidation, isPointer, isStruct)
169-
}
170-
}
171-
172-
func (a *analyzer) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct bool) {
173-
// In this case, we should always add the omitempty if it isn't present.
174-
a.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags)
175-
176-
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
180-
case hasValidZeroValue && !completeValidation:
181-
a.handleIncompleteFieldValidation(pass, field, fieldName, isPointer, underlying)
182-
fallthrough // Since it's a valid zero value, we should still enforce the pointer.
183-
case hasValidZeroValue, isStruct:
184-
// The field validation infers that the zero value is valid, the field needs to be a pointer.
185-
// Optional structs with omitempty should always be pointers, else they won't actually be omitted.
186-
a.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying)
187-
case !hasValidZeroValue && completeValidation && !isStruct:
188-
// The validation is fully complete, and the zero value is not valid, so we don't need a pointer.
189-
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.")
190-
}
191-
}
192-
193-
func (a *analyzer) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasValidZeroValue, completeValidation, isPointer, isStruct bool) {
194-
switch {
195-
case hasValidZeroValue:
196-
// The field is not omitempty, and the zero value is valid, the field does not need to be a pointer.
197-
a.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s is optional, without omitempty and allows the zero value. The field does not need to be a pointer.")
198-
case !hasValidZeroValue:
199-
// The zero value would not be accepted, so the field needs to have omitempty.
200-
// Force the omitempty policy to suggest a fix. We can only get to this function when the policy is configured to Ignore.
201-
// Since we absolutely have to add the omitempty tag, we can report it as a suggestion.
202-
reportShouldAddOmitEmpty(pass, field, OptionalFieldsOmitEmptyPolicySuggestFix, fieldName, "field %s is optional and does not allow the zero value. It must have the omitempty tag.", jsonTags)
203-
// Once it has the omitempty tag, it will also need to be a pointer in some cases.
204-
// Now handle it as if it had the omitempty already.
205-
// We already handle the omitempty tag above, so force the `hasOmitEmpty` to true.
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
213-
}
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)
226-
}
227-
228-
func (a *analyzer) handleFieldShouldBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr) {
229-
if isPointerType(pass, underlying) {
230-
if isPointer {
231-
switch a.pointerPolicy {
232-
case OptionalFieldsPointerPolicySuggestFix:
233-
reportShouldRemovePointer(pass, field, OptionalFieldsPointerPolicySuggestFix, fieldName, "field %s is optional but the underlying type does not need to be a pointer. The pointer should be removed.")
234-
case OptionalFieldsPointerPolicyWarn:
235-
pass.Reportf(field.Pos(), "field %s is optional but the underlying type does not need to be a pointer. The pointer should be removed.", fieldName)
236-
}
237-
}
238-
239-
return
240-
}
241-
242-
if isPointer {
243-
return
244-
}
245-
246-
switch a.pointerPolicy {
247-
case OptionalFieldsPointerPolicySuggestFix:
248-
reportShouldAddPointer(pass, field, OptionalFieldsPointerPolicySuggestFix, fieldName, "field %s is optional and should be a pointer")
249-
case OptionalFieldsPointerPolicyWarn:
250-
pass.Reportf(field.Pos(), "field %s is optional and should be a pointer", fieldName)
251-
}
252-
}
253-
254-
func (a *analyzer) handleFieldShouldNotBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, message string) {
255-
if !isPointer {
256-
return
257-
}
258-
259-
reportShouldRemovePointer(pass, field, a.pointerPolicy, fieldName, message)
260-
}
261-
262-
func (a *analyzer) handleFieldShouldHaveOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, hasOmitEmpty bool, jsonTags extractjsontags.FieldTagInfo) {
263-
if hasOmitEmpty {
264-
return
265-
}
266-
267-
reportShouldAddOmitEmpty(pass, field, a.omitEmptyPolicy, fieldName, "field %s is optional and should have the omitempty tag", jsonTags)
268-
}
269-
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-
278-
func (a *analyzer) handleIncompleteFieldValidation(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr) {
279-
if isPointer || isPointerType(pass, underlying) {
280-
// Don't warn them if the field is already a pointer.
281-
// If they change the validation then they'll fall into the correct logic for categorizing the field.
282-
// When the field is a pointer type (e.g. map, array), we don't need to warn them either as they should not make these fields pointers.
283-
return
284-
}
285-
286-
zeroValue := utils.GetTypedZeroValue(pass, underlying)
287-
validationHint := utils.GetTypedValidationHint(pass, underlying)
288-
289-
pass.Reportf(field.Pos(), "field %s is optional and has a valid zero value (%s), but the validation is not complete (e.g. %s). The field should be a pointer to allow the zero value to be set. If the zero value is not a valid use case, complete the validation and remove the pointer.", fieldName, zeroValue, validationHint)
140+
// isFieldOptional checks if a field has an optional marker.
141+
func isFieldOptional(fieldMarkers markershelper.MarkerSet) bool {
142+
return fieldMarkers.Has(markers.OptionalMarker) || fieldMarkers.Has(markers.KubebuilderOptionalMarker)
290143
}

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,36 @@ type A struct {
1919

2020
// string is a string field.
2121
// +optional
22-
String string `json:"string,omitempty"` // want "field String is optional and should be a pointer"
22+
String string `json:"string,omitempty"` // want "field String should be a pointer."
2323

2424
// NonOmittedString is a string field without omitempty
2525
// +optional
26-
NonOmittedString string `json:"nonOmittedString"` // want "field NonOmittedString is optional and should be a pointer" "field NonOmittedString is optional and should have the omitempty tag"
26+
NonOmittedString string `json:"nonOmittedString"` // want "field NonOmittedString should be a pointer." "field NonOmittedString should have the omitempty tag."
2727

2828
// int is an int field.
2929
// +optional
30-
Int int `json:"int,omitempty"` // want "field Int is optional and should be a pointer"
30+
Int int `json:"int,omitempty"` // want "field Int should be a pointer."
3131

3232
// nonOmittedInt is an int field without omitempty
3333
// +optional
34-
NonOmittedInt int `json:"nonOmittedInt"` // want "field NonOmittedInt is optional and should be a pointer" "field NonOmittedInt is optional and should have the omitempty tag"
34+
NonOmittedInt int `json:"nonOmittedInt"` // want "field NonOmittedInt should be a pointer." "field NonOmittedInt should have the omitempty tag."
3535

3636
// struct is a struct field.
3737
// +optional
38-
Struct B `json:"struct,omitempty"` // want "field Struct is optional and should be a pointer"
38+
Struct B `json:"struct,omitempty"` // want "field Struct should be a pointer."
3939

4040
// nonOmittedStruct is a struct field without omitempty.
4141
// +optional
42-
NonOmittedStruct B `json:"nonOmittedStruct"` // want "field NonOmittedStruct is optional and should be a pointer" "field NonOmittedStruct is optional and should have the omitempty tag"
42+
NonOmittedStruct B `json:"nonOmittedStruct"` // want "field NonOmittedStruct should be a pointer." "field NonOmittedStruct should have the omitempty tag."
4343

4444
// structWithMinProperties is a struct field with a minimum number of properties.
4545
// +kubebuilder:validation:MinProperties=1
4646
// +optional
47-
StructWithMinProperties B `json:"structWithMinProperties,omitempty"` // want "field StructWithMinProperties is optional and should be a pointer"
47+
StructWithMinProperties B `json:"structWithMinProperties,omitempty"` // want "field StructWithMinProperties should be a pointer."
4848

4949
// structWithMinPropertiesOnStruct is a struct field with a minimum number of properties on the struct.
5050
// +optional
51-
StructWithMinPropertiesOnStruct D `json:"structWithMinPropertiesOnStruct,omitempty"` // want "field StructWithMinPropertiesOnStruct is optional and should be a pointer"
51+
StructWithMinPropertiesOnStruct D `json:"structWithMinPropertiesOnStruct,omitempty"` // want "field StructWithMinPropertiesOnStruct should be a pointer."
5252

5353
// slice is a slice field.
5454
// +optional
@@ -60,15 +60,15 @@ type A struct {
6060

6161
// PointerSlice is a pointer slice field.
6262
// +optional
63-
PointerSlice *[]string `json:"pointerSlice,omitempty"` // want "field PointerSlice is optional but the underlying type does not need to be a pointer. The pointer should be removed."
63+
PointerSlice *[]string `json:"pointerSlice,omitempty"` // want "field PointerSlice underlying type does not need to be a pointer. The pointer should be removed."
6464

6565
// PointerMap is a pointer map field.
6666
// +optional
67-
PointerMap *map[string]string `json:"pointerMap,omitempty"` // want "field PointerMap is optional but the underlying type does not need to be a pointer. The pointer should be removed."
67+
PointerMap *map[string]string `json:"pointerMap,omitempty"` // want "field PointerMap underlying type does not need to be a pointer. The pointer should be removed."
6868

6969
// PointerPointerString is a double pointer string field.
7070
// +optional
71-
DoublePointerString **string `json:"doublePointerString,omitempty"` // want "field DoublePointerString is optional but the underlying type does not need to be a pointer. The pointer should be removed."
71+
DoublePointerString **string `json:"doublePointerString,omitempty"` // want "field DoublePointerString underlying type does not need to be a pointer. The pointer should be removed."
7272

7373
// PointerStringAlias is a pointer string alias field.
7474
// +optional
@@ -93,16 +93,16 @@ type A struct {
9393

9494
// PointerSliceAlias is a pointer slice alias field.
9595
// +optional
96-
PointerSliceAlias *SliceAlias `json:"pointerSliceAlias,omitempty"` // want "field PointerSliceAlias is optional but the underlying type does not need to be a pointer. The pointer should be removed."
96+
PointerSliceAlias *SliceAlias `json:"pointerSliceAlias,omitempty"` // want "field PointerSliceAlias underlying type does not need to be a pointer. The pointer should be removed."
9797

9898
// PointerMapAlias is a pointer map alias field.
9999
// +optional
100-
PointerMapAlias *MapAlias `json:"pointerMapAlias,omitempty"` // want "field PointerMapAlias is optional but the underlying type does not need to be a pointer. The pointer should be removed."
100+
PointerMapAlias *MapAlias `json:"pointerMapAlias,omitempty"` // want "field PointerMapAlias underlying type does not need to be a pointer. The pointer should be removed."
101101

102102
// StringAliasWithEnum is a string alias field with enum validation.
103103
// With the "Always" pointer preference, optional fields should be pointers regardless of zero value validity.
104104
// +optional
105-
StringAliasWithEnum StringAliasWithEnum `json:"stringAliasWithEnum,omitempty"` // want "field StringAliasWithEnum is optional and should be a pointer"
105+
StringAliasWithEnum StringAliasWithEnum `json:"stringAliasWithEnum,omitempty"` // want "field StringAliasWithEnum should be a pointer."
106106

107107
// StringAliasWithEnumPointer is a pointer string alias field with enum validation.
108108
// This is correctly a pointer since the zero value is not valid.
@@ -111,7 +111,7 @@ type A struct {
111111

112112
// StringAliasWithEnumNoOmitEmpty is a string alias field with enum validation and no omitempty.
113113
// +optional
114-
StringAliasWithEnumNoOmitEmpty *StringAliasWithEnum `json:"stringAliasWithEnumNoOmitEmpty"` // want "field StringAliasWithEnumNoOmitEmpty is optional and should have the omitempty tag"
114+
StringAliasWithEnumNoOmitEmpty *StringAliasWithEnum `json:"stringAliasWithEnumNoOmitEmpty"` // want "field StringAliasWithEnumNoOmitEmpty should have the omitempty tag."
115115
}
116116

117117
type B struct {

0 commit comments

Comments
 (0)