Skip to content

Commit 1ac8dbc

Browse files
Added float32/64, moved docs, and followed other conventions as discussed in reviews
1 parent e159a00 commit 1ac8dbc

File tree

5 files changed

+296
-130
lines changed

5 files changed

+296
-130
lines changed

docs/linters.md

Lines changed: 18 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -479,65 +479,34 @@ linterConfig:
479479

480480
## NumericBounds
481481

482-
The `numericbounds` linter checks that numeric fields (`int32` and `int64`) have appropriate bounds validation markers.
482+
The `numericbounds` linter checks that numeric fields (`int32`, `int64`, `float32`, `float64`) have appropriate bounds validation markers.
483483

484484
According to Kubernetes API conventions, numeric fields should have bounds checking to prevent values that are too small, negative (when not intended), or too large.
485485

486486
This linter ensures that:
487-
- `int32` and `int64` fields have both `+kubebuilder:validation:Minimum` and `+kubebuilder:validation:Maximum` markers
488-
- `int64` fields with bounds outside the JavaScript safe integer range are flagged
487+
- Numeric fields have both `+kubebuilder:validation:Minimum` and `+kubebuilder:validation:Maximum` markers
488+
- K8s declarative validation markers (`+k8s:minimum` and `+k8s:maximum`) are also supported
489+
- Bounds values are within the type's valid range (e.g., int32 bounds must fit in int32)
490+
- `int64` fields with bounds outside the JavaScript safe integer range (±2^53-1) are flagged
489491

490-
### JavaScript Safe Integer Range
492+
**Note:** While `+k8s:minimum` is documented in the official Kubernetes declarative validation spec, `+k8s:maximum` is not yet officially documented but is supported by this linter for forward compatibility and consistency.
491493

492-
For `int64` fields, the linter checks if the bounds exceed the JavaScript safe integer range of `-(2^53)` to `(2^53)` (specifically, `-9007199254740991` to `9007199254740991`).
493-
494-
JavaScript represents all numbers as IEEE 754 double-precision floating-point values, which can only safely represent integers in this range. Values outside this range may lose precision when processed by JavaScript clients.
495-
496-
When an `int64` field has bounds that exceed this range, the linter will suggest using a string type instead to avoid precision loss.
497-
498-
### Examples
499-
500-
**Valid:** Numeric field with proper bounds markers
501-
```go
502-
type Example struct {
503-
// +kubebuilder:validation:Minimum=0
504-
// +kubebuilder:validation:Maximum=100
505-
Count int32
506-
}
507-
```
508-
509-
**Valid:** Int64 field with JavaScript-safe bounds
510-
```go
511-
type Example struct {
512-
// +kubebuilder:validation:Minimum=-9007199254740991
513-
// +kubebuilder:validation:Maximum=9007199254740991
514-
Timestamp int64
515-
}
516-
```
494+
### Configuration
517495

518-
**Invalid:** Missing bounds markers
519-
```go
520-
type Example struct {
521-
Count int32 // want: should have minimum and maximum bounds validation markers
522-
}
523-
```
496+
This linter is **not enabled by default** as it is primarily focused on CRD validation with kubebuilder markers. It can be enabled in your configuration.
524497

525-
**Invalid:** Only one bound specified
526-
```go
527-
type Example struct {
528-
// +kubebuilder:validation:Minimum=0
529-
Count int32 // want: has minimum but is missing maximum bounds validation marker
530-
}
498+
To enable in `.golangci.yml`:
499+
```yaml
500+
linters-settings:
501+
custom:
502+
kubeapilinter:
503+
settings:
504+
linters:
505+
enable:
506+
- numericbounds
531507
```
532508

533-
**Invalid:** Int64 with bounds exceeding JavaScript safe range
534-
```go
535-
type Example struct {
536-
// +kubebuilder:validation:Minimum=-10000000000000000
537-
// +kubebuilder:validation:Maximum=10000000000000000
538-
LargeNumber int64 // want: bounds exceed JavaScript safe integer range
539-
}
540-
```
509+
See the [package documentation](https://pkg.go.dev/sigs.k8s.io/kube-api-linter/pkg/analysis/numericbounds) for detailed examples and usage.
541510

542511
## NoBools
543512

pkg/analysis/numericbounds/analyzer.go

Lines changed: 114 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,24 @@ import (
3030
"sigs.k8s.io/kube-api-linter/pkg/markers"
3131
)
3232

33-
const name = "numericbounds"
33+
const (
34+
name = "numericbounds"
35+
)
36+
37+
// Type bounds for validation
38+
const (
39+
maxInt32 = 2147483647 // 2^31 - 1
40+
minInt32 = -2147483648 // -2^31
41+
maxFloat32 = 3.40282346638528859811704183484516925440e+38 // max float32
42+
minFloat32 = -3.40282346638528859811704183484516925440e+38 // min float32
43+
)
3444

3545
// JavaScript safe integer bounds (2^53 - 1 and -(2^53 - 1))
3646
const (
37-
maxSafeInt = 9007199254740991 // 2^53 - 1
38-
minSafeInt = -9007199254740991 // -(2^53 - 1)
47+
maxSafeInt32 = 2147483647 // 2^31 - 1 (fits in JS Number)
48+
minSafeInt32 = -2147483648 // -2^31 (fits in JS Number)
49+
maxSafeInt64 = 9007199254740991 // 2^53 - 1 (max safe integer in JS)
50+
minSafeInt64 = -9007199254740991 // -(2^53 - 1) (min safe integer in JS)
3951
)
4052

4153
var errMarkerMissingValue = errors.New("marker value not found")
@@ -44,7 +56,7 @@ var errMarkerMissingValue = errors.New("marker value not found")
4456
// It checks that numeric fields have appropriate bounds validation markers.
4557
var Analyzer = &analysis.Analyzer{
4658
Name: name,
47-
Doc: "Checks that numeric fields (int32, int64) have appropriate minimum and maximum bounds validation markers",
59+
Doc: "Checks that numeric fields (int32, int64, float32, float64) have appropriate minimum and maximum bounds validation markers",
4860
Run: run,
4961
Requires: []*analysis.Analyzer{inspector.Analyzer},
5062
}
@@ -71,25 +83,31 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelp
7183
// Unwrap pointers and slices to get the underlying type
7284
fieldType, isSlice := unwrapType(field.Type)
7385

74-
// Get the underlying numeric type identifier (int32 or int64)
86+
// Get the underlying numeric type identifier (int32, int64, float32, float64)
7587
ident := getNumericTypeIdent(pass, fieldType)
7688
if ident == nil {
7789
return
7890
}
7991

80-
// Only check int32 and int64 types
81-
if ident.Name != "int32" && ident.Name != "int64" {
92+
// Only check int32, int64, float32, and float64 types
93+
if ident.Name != "int32" && ident.Name != "int64" && ident.Name != "float32" && ident.Name != "float64" {
8294
return
8395
}
8496

97+
// Create type description that clarifies array element types
98+
typeDesc := ident.Name
99+
if isSlice {
100+
typeDesc = fmt.Sprintf("array element type of %s", ident.Name)
101+
}
102+
85103
fieldMarkers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field)
86104

87105
// Determine which markers to look for based on whether the field is a slice
88-
minMarker, maxMarker := getMarkerNames(isSlice)
106+
minMarkers, maxMarkers := getMarkerNames(isSlice)
89107

90108
// Get minimum and maximum marker values
91-
minimum, minErr := getMarkerNumericValue(fieldMarkers, minMarker)
92-
maximum, maxErr := getMarkerNumericValue(fieldMarkers, maxMarker)
109+
minimum, minErr := getMarkerNumericValue(fieldMarkers, minMarkers)
110+
maximum, maxErr := getMarkerNumericValue(fieldMarkers, maxMarkers)
93111

94112
// Check if markers are missing
95113
minMissing := errors.Is(minErr, errMarkerMissingValue)
@@ -98,34 +116,32 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelp
98116
// Report any invalid marker values (e.g., non-numeric values)
99117
if minErr != nil && !minMissing {
100118
pass.Reportf(field.Pos(), "field %s has an invalid minimum marker: %v", fieldName, minErr)
101-
return
102119
}
103120
if maxErr != nil && !maxMissing {
104121
pass.Reportf(field.Pos(), "field %s has an invalid maximum marker: %v", fieldName, maxErr)
105-
return
106122
}
107123

108-
// Report if both markers are missing
109-
if minMissing && maxMissing {
110-
pass.Reportf(field.Pos(), "field %s of type %s should have minimum and maximum bounds validation markers", fieldName, ident.Name)
111-
return
112-
}
113-
114-
// Report if only one marker is present
124+
// Report if markers are missing
115125
if minMissing {
116-
pass.Reportf(field.Pos(), "field %s of type %s has maximum but is missing minimum bounds validation marker", fieldName, ident.Name)
117-
return
126+
pass.Reportf(field.Pos(), "field %s %s is missing minimum bounds validation marker", fieldName, typeDesc)
118127
}
119128
if maxMissing {
120-
pass.Reportf(field.Pos(), "field %s of type %s has minimum but is missing maximum bounds validation marker", fieldName, ident.Name)
129+
pass.Reportf(field.Pos(), "field %s %s is missing maximum bounds validation marker", fieldName, typeDesc)
130+
}
131+
132+
// If any markers are missing or invalid, don't continue with bounds checks
133+
if minErr != nil || maxErr != nil {
121134
return
122135
}
123136

137+
// Validate bounds are within the type's range
138+
checkBoundsWithinTypeRange(pass, field, fieldName, typeDesc, minimum, maximum)
139+
124140
// For int64 fields, check if bounds are within JavaScript safe integer range
125-
checkJavaScriptSafeBounds(pass, field, fieldName, ident.Name, minimum, maximum)
141+
checkJavaScriptSafeBounds(pass, field, fieldName, typeDesc, minimum, maximum)
126142
}
127143

128-
// getNumericTypeIdent returns the identifier for int32 or int64 types.
144+
// getNumericTypeIdent returns the identifier for numeric types (int32, int64, float32, float64).
129145
// It handles type aliases by looking up the underlying type.
130146
// Note: This function expects pointers and slices to already be unwrapped.
131147
func getNumericTypeIdent(pass *analysis.Pass, expr ast.Expr) *ast.Ident {
@@ -134,12 +150,12 @@ func getNumericTypeIdent(pass *analysis.Pass, expr ast.Expr) *ast.Ident {
134150
return nil
135151
}
136152

137-
// Check if it's a basic int32 or int64 type
138-
if ident.Name == "int32" || ident.Name == "int64" {
153+
// Check if it's a basic numeric type we care about
154+
if ident.Name == "int32" || ident.Name == "int64" || ident.Name == "float32" || ident.Name == "float64" {
139155
return ident
140156
}
141157

142-
// Check if it's a type alias to int32 or int64
158+
// Check if it's a type alias to a numeric type
143159
if !utils.IsBasicType(pass, ident) {
144160
typeSpec, ok := utils.LookupTypeSpec(pass, ident)
145161
if ok {
@@ -152,6 +168,9 @@ func getNumericTypeIdent(pass *analysis.Pass, expr ast.Expr) *ast.Ident {
152168

153169
// unwrapType unwraps pointers and slices to get the underlying type.
154170
// Returns the unwrapped type and a boolean indicating if it's a slice.
171+
// When the field is a slice, we extract the element type since that's what
172+
// needs bounds validation (e.g., []int32 -> int32). The isSlice flag allows
173+
// the caller to report errors with "array element type" for clarity.
155174
func unwrapType(expr ast.Expr) (ast.Expr, bool) {
156175
isSlice := false
157176

@@ -160,12 +179,12 @@ func unwrapType(expr ast.Expr) (ast.Expr, bool) {
160179
expr = starExpr.X
161180
}
162181

163-
// Check if it's a slice and unwrap (e.g., []int32)
182+
// Check if it's a slice and unwrap to get element type (e.g., []int32 -> int32)
164183
if arrayType, ok := expr.(*ast.ArrayType); ok {
165184
isSlice = true
166185
expr = arrayType.Elt
167186

168-
// Handle pointer inside slice (e.g., []*int32)
187+
// Handle pointer inside slice (e.g., []*int32 -> int32)
169188
if starExpr, ok := expr.(*ast.StarExpr); ok {
170189
expr = starExpr.X
171190
}
@@ -176,44 +195,87 @@ func unwrapType(expr ast.Expr) (ast.Expr, bool) {
176195

177196
// getMarkerNames returns the appropriate minimum and maximum marker names
178197
// based on whether the field is a slice.
179-
func getMarkerNames(isSlice bool) (minMarker, maxMarker string) {
198+
// Returns both kubebuilder and k8s declarative validation markers.
199+
func getMarkerNames(isSlice bool) (minMarkers, maxMarkers []string) {
180200
if isSlice {
181-
return markers.KubebuilderItemsMinimumMarker, markers.KubebuilderItemsMaximumMarker
201+
return []string{markers.KubebuilderItemsMinimumMarker}, []string{markers.KubebuilderItemsMaximumMarker}
182202
}
183-
return markers.KubebuilderMinimumMarker, markers.KubebuilderMaximumMarker
203+
return []string{markers.KubebuilderMinimumMarker, markers.K8sMinimumMarker}, []string{markers.KubebuilderMaximumMarker, markers.K8sMaximumMarker}
184204
}
185205

186-
// getMarkerNumericValue extracts the numeric value from the first instance of the marker with the given name.
187-
func getMarkerNumericValue(markerSet markershelper.MarkerSet, markerName string) (float64, error) {
188-
markerList := markerSet.Get(markerName)
189-
if len(markerList) == 0 {
190-
return 0, errMarkerMissingValue
191-
}
206+
// getMarkerNumericValue extracts the numeric value from the first instance of any of the given marker names.
207+
// Checks multiple marker names to support both kubebuilder and k8s declarative validation markers.
208+
func getMarkerNumericValue(markerSet markershelper.MarkerSet, markerNames []string) (float64, error) {
209+
for _, markerName := range markerNames {
210+
markerList := markerSet.Get(markerName)
211+
if len(markerList) == 0 {
212+
continue
213+
}
192214

193-
marker := markerList[0]
194-
rawValue, ok := marker.Expressions[""]
195-
if !ok {
196-
return 0, errMarkerMissingValue
197-
}
215+
marker := markerList[0]
216+
rawValue, ok := marker.Expressions[""]
217+
if !ok {
218+
continue
219+
}
198220

199-
// Parse as float64 using strconv for better error handling
200-
value, err := strconv.ParseFloat(rawValue, 64)
201-
if err != nil {
202-
return 0, fmt.Errorf("error converting value to number: %w", err)
221+
// Parse as float64 using strconv for better error handling
222+
value, err := strconv.ParseFloat(rawValue, 64)
223+
if err != nil {
224+
return 0, fmt.Errorf("error converting value to number: %w", err)
225+
}
226+
227+
return value, nil
203228
}
204229

205-
return value, nil
230+
return 0, errMarkerMissingValue
231+
}
232+
233+
// checkBoundsWithinTypeRange validates that the bounds are within the valid range for the type.
234+
func checkBoundsWithinTypeRange(pass *analysis.Pass, field *ast.Field, fieldName, typeDesc string, minimum, maximum float64) {
235+
// Extract the actual type name from typeDesc (e.g., "array element type of int32" -> "int32")
236+
typeName := extractTypeName(typeDesc)
237+
238+
switch typeName {
239+
case "int32":
240+
if minimum < minInt32 || minimum > maxInt32 {
241+
pass.Reportf(field.Pos(), "field %s %s has minimum bound %v that is outside the valid int32 range [%d, %d]", fieldName, typeDesc, minimum, minInt32, maxInt32)
242+
}
243+
if maximum < minInt32 || maximum > maxInt32 {
244+
pass.Reportf(field.Pos(), "field %s %s has maximum bound %v that is outside the valid int32 range [%d, %d]", fieldName, typeDesc, maximum, minInt32, maxInt32)
245+
}
246+
case "float32":
247+
if minimum < minFloat32 || minimum > maxFloat32 {
248+
pass.Reportf(field.Pos(), "field %s %s has minimum bound %v that is outside the valid float32 range", fieldName, typeDesc, minimum)
249+
}
250+
if maximum < minFloat32 || maximum > maxFloat32 {
251+
pass.Reportf(field.Pos(), "field %s %s has maximum bound %v that is outside the valid float32 range", fieldName, typeDesc, maximum)
252+
}
253+
}
206254
}
207255

208256
// checkJavaScriptSafeBounds checks if int64 bounds are within JavaScript safe integer range.
209-
func checkJavaScriptSafeBounds(pass *analysis.Pass, field *ast.Field, fieldName, typeName string, minimum, maximum float64) {
257+
func checkJavaScriptSafeBounds(pass *analysis.Pass, field *ast.Field, fieldName, typeDesc string, minimum, maximum float64) {
258+
// Extract the actual type name from typeDesc
259+
typeName := extractTypeName(typeDesc)
260+
210261
if typeName != "int64" {
211262
return
212263
}
213264

214-
if minimum < minSafeInt || maximum > maxSafeInt {
265+
if minimum < minSafeInt64 || maximum > maxSafeInt64 {
215266
pass.Reportf(field.Pos(),
216-
"field %s of type int64 has bounds [%d, %d] that exceed safe integer range [%d, %d]. Consider using a string type to avoid precision loss in JavaScript clients",
217-
fieldName, int64(minimum), int64(maximum), minSafeInt, maxSafeInt)
267+
"field %s %s has bounds [%d, %d] that exceed safe integer range [%d, %d]. Consider using a string type to avoid precision loss in JavaScript clients",
268+
fieldName, typeDesc, int64(minimum), int64(maximum), minSafeInt64, maxSafeInt64)
269+
}
270+
}
271+
272+
// extractTypeName extracts the base type name from a type description.
273+
// E.g., "array element type of int32" -> "int32", "int64" -> "int64"
274+
func extractTypeName(typeDesc string) string {
275+
// Check if it's an array element type description
276+
const prefix = "array element type of "
277+
if len(typeDesc) > len(prefix) && typeDesc[:len(prefix)] == prefix {
278+
return typeDesc[len(prefix):]
218279
}
280+
return typeDesc
219281
}

0 commit comments

Comments
 (0)