Skip to content

Commit e7697dc

Browse files
committed
Allow pointer-to-slice/map with explicit MinItems=0/MinProperties=0
This change fixes the linter incorrectly suggesting removal of pointers for slice and map fields that have explicit MinItems=0 or MinProperties=0 markers. Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
1 parent 82d79d9 commit e7697dc

File tree

5 files changed

+182
-17
lines changed

5 files changed

+182
-17
lines changed

pkg/analysis/utils/serialization/serialization_check.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -98,31 +98,31 @@ func (s *serializationCheck) Check(pass *analysis.Pass, field *ast.Field, marker
9898
switch s.pointerPreference {
9999
case PointersPreferenceAlways:
100100
// The field must always be a pointer, pointers require omitempty, so enforce that too.
101-
s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, "should be a pointer.")
101+
s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, "should be a pointer.")
102102
s.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags)
103103
case PointersPreferenceWhenRequired:
104-
s.handleFieldOmitZero(pass, field, fieldName, jsonTags, hasOmitZero, hasValidZeroValue, isPointer, isStruct)
104+
s.handleFieldOmitZero(pass, field, fieldName, jsonTags, underlying, hasOmitZero, hasValidZeroValue, isPointer, isStruct, markersAccess)
105105

106106
if s.omitEmptyPolicy != OmitEmptyPolicyIgnore || hasOmitEmpty {
107107
// If we require omitempty, or the field has omitempty, we can check the field properties based on it being an omitempty field.
108-
s.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct)
108+
s.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct, markersAccess)
109109
} else {
110110
// The field does not have omitempty, and does not require it.
111-
s.checkFieldPropertiesWithoutOmitEmpty(pass, field, fieldName, jsonTags, underlying, hasValidZeroValue, completeValidation, isPointer, isStruct)
111+
s.checkFieldPropertiesWithoutOmitEmpty(pass, field, fieldName, jsonTags, underlying, hasValidZeroValue, completeValidation, isPointer, isStruct, markersAccess)
112112
}
113113
default:
114114
panic(fmt.Sprintf("unknown pointer preference: %s", s.pointerPreference))
115115
}
116116
}
117117

118-
func (s *serializationCheck) handleFieldOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, hasOmitZero, hasValidZeroValue, isPointer, isStruct bool) {
118+
func (s *serializationCheck) handleFieldOmitZero(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitZero, hasValidZeroValue, isPointer, isStruct bool, markersAccess markershelper.Markers) {
119119
switch s.omitZeroPolicy {
120120
case OmitZeroPolicyForbid:
121121
// when the omitzero policy is set to forbid, we need to report removing omitzero if set on the struct fields.
122122
s.checkFieldPropertiesWithOmitZeroForbidPolicy(pass, field, fieldName, isStruct, hasOmitZero, jsonTags)
123123
case OmitZeroPolicyWarn, OmitZeroPolicySuggestFix:
124124
// If we require omitzero, or the field has omitzero, we can check the field properties based on it being an omitzero field.
125-
s.checkFieldPropertiesWithOmitZeroRequired(pass, field, fieldName, jsonTags, hasOmitZero, isPointer, isStruct, hasValidZeroValue)
125+
s.checkFieldPropertiesWithOmitZeroRequired(pass, field, fieldName, jsonTags, underlying, hasOmitZero, isPointer, isStruct, hasValidZeroValue, markersAccess)
126126
default:
127127
panic(fmt.Sprintf("unknown omit zero policy: %s", s.omitZeroPolicy))
128128
}
@@ -136,7 +136,7 @@ func (s *serializationCheck) handleFieldShouldHaveOmitEmpty(pass *analysis.Pass,
136136
reportShouldAddOmitEmpty(pass, field, s.omitEmptyPolicy, fieldName, "field %s should have the omitempty tag.", jsonTags)
137137
}
138138

139-
func (s *serializationCheck) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct bool) {
139+
func (s *serializationCheck) checkFieldPropertiesWithOmitEmptyRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitEmpty, hasValidZeroValue, completeValidation, isPointer, isStruct bool, markersAccess markershelper.Markers) {
140140
switch {
141141
case isStruct && !hasValidZeroValue && s.omitZeroPolicy != OmitZeroPolicyForbid:
142142
// The struct field need not be pointer if it does not have a valid zero value.
@@ -145,27 +145,27 @@ func (s *serializationCheck) checkFieldPropertiesWithOmitEmptyRequired(pass *ana
145145
zeroValue := utils.GetTypedZeroValue(pass, underlying)
146146
validationHint := utils.GetTypedValidationHint(pass, underlying)
147147

148-
s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, fmt.Sprintf("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.", zeroValue, validationHint))
148+
s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, fmt.Sprintf("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.", zeroValue, validationHint))
149149
case hasValidZeroValue, isStruct:
150150
// The field validation infers that the zero value is valid, the field needs to be a pointer.
151151
// Structs with omitempty should always be pointers, else they won't actually be omitted.
152152
zeroValue := utils.GetTypedZeroValue(pass, underlying)
153153

154-
s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, fmt.Sprintf("has a valid zero value (%s) and should be a pointer.", zeroValue))
154+
s.handleFieldShouldBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, fmt.Sprintf("has a valid zero value (%s) and should be a pointer.", zeroValue))
155155
case !hasValidZeroValue && completeValidation && !isStruct:
156156
// The validation is fully complete, and the zero value is not valid, so we don't need a pointer.
157-
s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s does not allow the zero value. The field does not need to be a pointer.")
157+
s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, "field %s does not allow the zero value. The field does not need to be a pointer.")
158158
}
159159

160160
// In this case, we should always add the omitempty if it isn't present.
161161
s.handleFieldShouldHaveOmitEmpty(pass, field, fieldName, hasOmitEmpty, jsonTags)
162162
}
163163

164-
func (s *serializationCheck) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasValidZeroValue, completeValidation, isPointer, isStruct bool) {
164+
func (s *serializationCheck) checkFieldPropertiesWithoutOmitEmpty(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasValidZeroValue, completeValidation, isPointer, isStruct bool, markersAccess markershelper.Markers) {
165165
switch {
166166
case hasValidZeroValue:
167167
// The field is not omitempty, and the zero value is valid, the field does not need to be a pointer.
168-
s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s does not have omitempty and allows the zero value. The field does not need to be a pointer.")
168+
s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, "field %s does not have omitempty and allows the zero value. The field does not need to be a pointer.")
169169
case !hasValidZeroValue:
170170
// The zero value would not be accepted, so the field needs to have omitempty.
171171
// Force the omitempty policy to suggest a fix. We can only get to this function when the policy is configured to Ignore.
@@ -174,17 +174,17 @@ func (s *serializationCheck) checkFieldPropertiesWithoutOmitEmpty(pass *analysis
174174
// Once it has the omitempty tag, it will also need to be a pointer in some cases.
175175
// Now handle it as if it had the omitempty already.
176176
// We already handle the omitempty tag above, so force the `hasOmitEmpty` to true.
177-
s.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, true, hasValidZeroValue, completeValidation, isPointer, isStruct)
177+
s.checkFieldPropertiesWithOmitEmptyRequired(pass, field, fieldName, jsonTags, underlying, true, hasValidZeroValue, completeValidation, isPointer, isStruct, markersAccess)
178178
}
179179
}
180180

181-
func (s *serializationCheck) checkFieldPropertiesWithOmitZeroRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, hasOmitZero, isPointer, isStruct, hasValidZeroValue bool) {
181+
func (s *serializationCheck) checkFieldPropertiesWithOmitZeroRequired(pass *analysis.Pass, field *ast.Field, fieldName string, jsonTags extractjsontags.FieldTagInfo, underlying ast.Expr, hasOmitZero, isPointer, isStruct, hasValidZeroValue bool, markersAccess markershelper.Markers) {
182182
if !isStruct || hasValidZeroValue {
183183
return
184184
}
185185

186186
s.handleFieldShouldHaveOmitZero(pass, field, fieldName, hasOmitZero, jsonTags)
187-
s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, "field %s does not allow the zero value. The field does not need to be a pointer.")
187+
s.handleFieldShouldNotBePointer(pass, field, fieldName, isPointer, underlying, markersAccess, "field %s does not allow the zero value. The field does not need to be a pointer.")
188188
}
189189

190190
func (s *serializationCheck) checkFieldPropertiesWithOmitZeroForbidPolicy(pass *analysis.Pass, field *ast.Field, fieldName string, isStruct, hasOmitZero bool, jsonTags extractjsontags.FieldTagInfo) {
@@ -205,9 +205,15 @@ func (s *serializationCheck) handleFieldShouldHaveOmitZero(pass *analysis.Pass,
205205
reportShouldAddOmitZero(pass, field, s.omitZeroPolicy, fieldName, "field %s does not allow the zero value. It must have the omitzero tag.", jsonTags)
206206
}
207207

208-
func (s *serializationCheck) handleFieldShouldBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr, reason string) {
208+
func (s *serializationCheck) handleFieldShouldBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr, markersAccess markershelper.Markers, reason string) {
209209
if utils.IsPointerType(pass, underlying) {
210210
if isPointer {
211+
// Check if this is a pointer-to-slice/map with explicit MinItems=0 or MinProperties=0
212+
// In this case, the pointer is intentional to distinguish nil from empty
213+
if hasExplicitZeroMinValidation(pass, field, underlying, markersAccess) {
214+
return
215+
}
216+
211217
switch s.pointerPolicy {
212218
case PointersPolicySuggestFix:
213219
reportShouldRemovePointer(pass, field, PointersPolicySuggestFix, fieldName, "field %s underlying type does not need to be a pointer. The pointer should be removed.", fieldName)
@@ -231,10 +237,33 @@ func (s *serializationCheck) handleFieldShouldBePointer(pass *analysis.Pass, fie
231237
}
232238
}
233239

234-
func (s *serializationCheck) handleFieldShouldNotBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, message string) {
240+
func (s *serializationCheck) handleFieldShouldNotBePointer(pass *analysis.Pass, field *ast.Field, fieldName string, isPointer bool, underlying ast.Expr, markersAccess markershelper.Markers, message string) {
235241
if !isPointer {
236242
return
237243
}
238244

245+
// Check if this is a pointer-to-slice/map with explicit MinItems=0 or MinProperties=0
246+
// In this case, the pointer is intentional to distinguish nil from empty
247+
if hasExplicitZeroMinValidation(pass, field, underlying, markersAccess) {
248+
return
249+
}
250+
239251
reportShouldRemovePointer(pass, field, s.pointerPolicy, fieldName, message, fieldName)
240252
}
253+
254+
// hasExplicitZeroMinValidation checks if a field has an explicit MinItems=0 or MinProperties=0 marker.
255+
// This indicates the developer intentionally wants to distinguish between nil and empty for slices/maps.
256+
func hasExplicitZeroMinValidation(pass *analysis.Pass, field *ast.Field, underlying ast.Expr, markersAccess markershelper.Markers) bool {
257+
fieldMarkers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field)
258+
259+
switch underlying.(type) {
260+
case *ast.ArrayType:
261+
// Check for explicit MinItems=0
262+
return fieldMarkers.HasWithValue("kubebuilder:validation:MinItems=0")
263+
case *ast.MapType:
264+
// Check for explicit MinProperties=0
265+
return fieldMarkers.HasWithValue("kubebuilder:validation:MinProperties=0")
266+
}
267+
268+
return false
269+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package a
2+
3+
type TestPointerToSliceWithMinZeroAlways struct {
4+
// +kubebuilder:validation:MinItems=0
5+
PtrArrayWithZeroMinItems *[]string `json:"ptrArrayWithZeroMinItems,omitempty"`
6+
7+
// +kubebuilder:validation:MinItems=0
8+
PtrArrayWithZeroMinItemsNoOmitEmpty *[]string `json:"ptrArrayWithZeroMinItemsNoOmitEmpty"` // want "field PtrArrayWithZeroMinItemsNoOmitEmpty should have the omitempty tag."
9+
}
10+
11+
type TestPointerToMapWithMinZeroAlways struct {
12+
// +kubebuilder:validation:MinProperties=0
13+
MapPtrWithZeroMinProperties *map[string]string `json:"mapPtrWithZeroMinProperties,omitempty"`
14+
15+
// +kubebuilder:validation:MinProperties=0
16+
MapPtrWithZeroMinPropertiesNoOmitEmpty *map[string]string `json:"mapPtrWithZeroMinPropertiesNoOmitEmpty"` // want "field MapPtrWithZeroMinPropertiesNoOmitEmpty should have the omitempty tag."
17+
}
18+
19+
// Test that pointers ARE still flagged when MinItems/MinProperties is NOT zero
20+
type TestPointerToSliceWithNonZeroMinAlways struct {
21+
// +kubebuilder:validation:MinItems=1
22+
PtrArrayWithNonZeroMinItems *[]string `json:"ptrArrayWithNonZeroMinItems,omitempty"` // want "field PtrArrayWithNonZeroMinItems underlying type does not need to be a pointer. The pointer should be removed."
23+
24+
// No MinItems validation
25+
PtrArrayWithoutMinItems *[]string `json:"ptrArrayWithoutMinItems,omitempty"` // want "field PtrArrayWithoutMinItems underlying type does not need to be a pointer. The pointer should be removed."
26+
}
27+
28+
type TestPointerToMapWithNonZeroMinAlways struct {
29+
// +kubebuilder:validation:MinProperties=1
30+
MapPtrWithNonZeroMinProperties *map[string]string `json:"mapPtrWithNonZeroMinProperties,omitempty"` // want "field MapPtrWithNonZeroMinProperties underlying type does not need to be a pointer. The pointer should be removed."
31+
32+
// No MinProperties validation
33+
MapPtrWithoutMinProperties *map[string]string `json:"mapPtrWithoutMinProperties,omitempty"` // want "field MapPtrWithoutMinProperties underlying type does not need to be a pointer. The pointer should be removed."
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package a
2+
3+
type TestPointerToSliceWithMinZeroAlways struct {
4+
// +kubebuilder:validation:MinItems=0
5+
PtrArrayWithZeroMinItems *[]string `json:"ptrArrayWithZeroMinItems,omitempty"`
6+
7+
// +kubebuilder:validation:MinItems=0
8+
PtrArrayWithZeroMinItemsNoOmitEmpty *[]string `json:"ptrArrayWithZeroMinItemsNoOmitEmpty,omitempty"` // want "field PtrArrayWithZeroMinItemsNoOmitEmpty should have the omitempty tag."
9+
}
10+
11+
type TestPointerToMapWithMinZeroAlways struct {
12+
// +kubebuilder:validation:MinProperties=0
13+
MapPtrWithZeroMinProperties *map[string]string `json:"mapPtrWithZeroMinProperties,omitempty"`
14+
15+
// +kubebuilder:validation:MinProperties=0
16+
MapPtrWithZeroMinPropertiesNoOmitEmpty *map[string]string `json:"mapPtrWithZeroMinPropertiesNoOmitEmpty,omitempty"` // want "field MapPtrWithZeroMinPropertiesNoOmitEmpty should have the omitempty tag."
17+
}
18+
19+
// Test that pointers ARE still flagged when MinItems/MinProperties is NOT zero
20+
type TestPointerToSliceWithNonZeroMinAlways struct {
21+
// +kubebuilder:validation:MinItems=1
22+
PtrArrayWithNonZeroMinItems []string `json:"ptrArrayWithNonZeroMinItems,omitempty"` // want "field PtrArrayWithNonZeroMinItems underlying type does not need to be a pointer. The pointer should be removed."
23+
24+
// No MinItems validation
25+
PtrArrayWithoutMinItems []string `json:"ptrArrayWithoutMinItems,omitempty"` // want "field PtrArrayWithoutMinItems underlying type does not need to be a pointer. The pointer should be removed."
26+
}
27+
28+
type TestPointerToMapWithNonZeroMinAlways struct {
29+
// +kubebuilder:validation:MinProperties=1
30+
MapPtrWithNonZeroMinProperties map[string]string `json:"mapPtrWithNonZeroMinProperties,omitempty"` // want "field MapPtrWithNonZeroMinProperties underlying type does not need to be a pointer. The pointer should be removed."
31+
32+
// No MinProperties validation
33+
MapPtrWithoutMinProperties map[string]string `json:"mapPtrWithoutMinProperties,omitempty"` // want "field MapPtrWithoutMinProperties underlying type does not need to be a pointer. The pointer should be removed."
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package a
2+
3+
type TestPointerToSliceWithMinZero struct {
4+
// +kubebuilder:validation:MinItems=0
5+
PtrArrayWithZeroMinItems *[]string `json:"ptrArrayWithZeroMinItems,omitempty"`
6+
7+
// +kubebuilder:validation:MinItems=0
8+
PtrArrayWithZeroMinItemsNoOmitEmpty *[]string `json:"ptrArrayWithZeroMinItemsNoOmitEmpty"` // want "field PtrArrayWithZeroMinItemsNoOmitEmpty should have the omitempty tag."
9+
}
10+
11+
type TestPointerToMapWithMinZero struct {
12+
// +kubebuilder:validation:MinProperties=0
13+
MapPtrWithZeroMinProperties *map[string]string `json:"mapPtrWithZeroMinProperties,omitempty"`
14+
15+
// +kubebuilder:validation:MinProperties=0
16+
MapPtrWithZeroMinPropertiesNoOmitEmpty *map[string]string `json:"mapPtrWithZeroMinPropertiesNoOmitEmpty"` // want "field MapPtrWithZeroMinPropertiesNoOmitEmpty should have the omitempty tag."
17+
}
18+
19+
// Test that pointers ARE still flagged when MinItems/MinProperties is NOT zero
20+
type TestPointerToSliceWithNonZeroMin struct {
21+
// +kubebuilder:validation:MinItems=1
22+
PtrArrayWithNonZeroMinItems *[]string `json:"ptrArrayWithNonZeroMinItems,omitempty"` // want "field PtrArrayWithNonZeroMinItems does not allow the zero value. The field does not need to be a pointer."
23+
24+
// No MinItems validation
25+
PtrArrayWithoutMinItems *[]string `json:"ptrArrayWithoutMinItems,omitempty"` // want "field PtrArrayWithoutMinItems underlying type does not need to be a pointer. The pointer should be removed."
26+
}
27+
28+
type TestPointerToMapWithNonZeroMin struct {
29+
// +kubebuilder:validation:MinProperties=1
30+
MapPtrWithNonZeroMinProperties *map[string]string `json:"mapPtrWithNonZeroMinProperties,omitempty"` // want "field MapPtrWithNonZeroMinProperties does not allow the zero value. The field does not need to be a pointer."
31+
32+
// No MinProperties validation
33+
MapPtrWithoutMinProperties *map[string]string `json:"mapPtrWithoutMinProperties,omitempty"` // want "field MapPtrWithoutMinProperties underlying type does not need to be a pointer. The pointer should be removed."
34+
}

0 commit comments

Comments
 (0)