Skip to content

Commit 8912482

Browse files
added the numeric bounds linter
1 parent 5fca1e8 commit 8912482

File tree

7 files changed

+552
-0
lines changed

7 files changed

+552
-0
lines changed

docs/linters.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [JSONTags](#jsontags) - Ensures proper JSON tag formatting
1212
- [MaxLength](#maxlength) - Checks for maximum length constraints on strings and arrays
1313
- [NamingConventions](#namingconventions) - Ensures field names adhere to user-defined naming conventions
14+
- [NumericBounds](#numericbounds) - Validates numeric fields have appropriate bounds validation markers
1415
- [NoBools](#nobools) - Prevents usage of boolean types
1516
- [NoDurations](#nodurations) - Prevents usage of duration types
1617
- [NoFloats](#nofloats) - Prevents usage of floating-point types
@@ -437,6 +438,68 @@ linterConfig:
437438
message: prefer 'colour' over 'color' when referring to colours in field names
438439
```
439440

441+
## NumericBounds
442+
443+
The `numericbounds` linter checks that numeric fields (`int32` and `int64`) have appropriate bounds validation markers.
444+
445+
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.
446+
447+
This linter ensures that:
448+
- `int32` and `int64` fields have both `+kubebuilder:validation:Minimum` and `+kubebuilder:validation:Maximum` markers
449+
- `int64` fields with bounds outside the JavaScript safe integer range are flagged
450+
451+
### JavaScript Safe Integer Range
452+
453+
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`).
454+
455+
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.
456+
457+
When an `int64` field has bounds that exceed this range, the linter will suggest using a string type instead to avoid precision loss.
458+
459+
### Examples
460+
461+
**Valid:** Numeric field with proper bounds markers
462+
```go
463+
type Example struct {
464+
// +kubebuilder:validation:Minimum=0
465+
// +kubebuilder:validation:Maximum=100
466+
Count int32
467+
}
468+
```
469+
470+
**Valid:** Int64 field with JavaScript-safe bounds
471+
```go
472+
type Example struct {
473+
// +kubebuilder:validation:Minimum=-9007199254740991
474+
// +kubebuilder:validation:Maximum=9007199254740991
475+
Timestamp int64
476+
}
477+
```
478+
479+
**Invalid:** Missing bounds markers
480+
```go
481+
type Example struct {
482+
Count int32 // want: should have minimum and maximum bounds validation markers
483+
}
484+
```
485+
486+
**Invalid:** Only one bound specified
487+
```go
488+
type Example struct {
489+
// +kubebuilder:validation:Minimum=0
490+
Count int32 // want: has minimum but is missing maximum bounds validation marker
491+
}
492+
```
493+
494+
**Invalid:** Int64 with bounds exceeding JavaScript safe range
495+
```go
496+
type Example struct {
497+
// +kubebuilder:validation:Minimum=-10000000000000000
498+
// +kubebuilder:validation:Maximum=10000000000000000
499+
LargeNumber int64 // want: bounds exceed JavaScript safe integer range
500+
}
501+
```
502+
440503
## NoBools
441504

442505
The `nobools` linter checks that fields in the API types do not contain a `bool` type.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package numericbounds
17+
18+
import (
19+
"errors"
20+
"fmt"
21+
"go/ast"
22+
"strconv"
23+
24+
"golang.org/x/tools/go/analysis"
25+
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
26+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
27+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
28+
markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
29+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
30+
"sigs.k8s.io/kube-api-linter/pkg/markers"
31+
)
32+
33+
const name = "numericbounds"
34+
35+
// JavaScript safe integer bounds (2^53 - 1 and -(2^53 - 1))
36+
const (
37+
maxSafeInt = 9007199254740991 // 2^53 - 1
38+
minSafeInt = -9007199254740991 // -(2^53 - 1)
39+
)
40+
41+
var errMarkerMissingValue = errors.New("marker value not found")
42+
43+
// Analyzer is the analyzer for the numericbounds package.
44+
// It checks that numeric fields have appropriate bounds validation markers.
45+
var Analyzer = &analysis.Analyzer{
46+
Name: name,
47+
Doc: "Checks that numeric fields (int32, int64) have appropriate minimum and maximum bounds validation markers",
48+
Run: run,
49+
Requires: []*analysis.Analyzer{inspector.Analyzer},
50+
}
51+
52+
func run(pass *analysis.Pass) (any, error) {
53+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
54+
if !ok {
55+
return nil, kalerrors.ErrCouldNotGetInspector
56+
}
57+
58+
inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) {
59+
checkField(pass, field, markersAccess)
60+
})
61+
62+
return nil, nil //nolint:nilnil
63+
}
64+
65+
func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) {
66+
if field == nil || len(field.Names) == 0 {
67+
return
68+
}
69+
70+
// Unwrap pointers and slices to get the underlying type
71+
fieldType, isSlice := unwrapType(field.Type)
72+
73+
// Get the underlying numeric type identifier (int32 or int64)
74+
ident := getNumericTypeIdent(pass, fieldType)
75+
if ident == nil {
76+
return
77+
}
78+
79+
// Only check int32 and int64 types
80+
if ident.Name != "int32" && ident.Name != "int64" {
81+
return
82+
}
83+
84+
fieldName := utils.FieldName(field)
85+
fieldMarkers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field)
86+
87+
// Determine which markers to look for based on whether the field is a slice
88+
minMarker, maxMarker := getMarkerNames(isSlice)
89+
90+
// Get minimum and maximum marker values
91+
minimum, minErr := getMarkerNumericValue(fieldMarkers, minMarker)
92+
maximum, maxErr := getMarkerNumericValue(fieldMarkers, maxMarker)
93+
94+
// Check if markers are missing
95+
minMissing := errors.Is(minErr, errMarkerMissingValue)
96+
maxMissing := errors.Is(maxErr, errMarkerMissingValue)
97+
98+
// Report any invalid marker values (e.g., non-numeric values)
99+
if minErr != nil && !minMissing {
100+
pass.Reportf(field.Pos(), "field %s has an invalid minimum marker: %v", fieldName, minErr)
101+
return
102+
}
103+
if maxErr != nil && !maxMissing {
104+
pass.Reportf(field.Pos(), "field %s has an invalid maximum marker: %v", fieldName, maxErr)
105+
return
106+
}
107+
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
115+
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
118+
}
119+
if maxMissing {
120+
pass.Reportf(field.Pos(), "field %s of type %s has minimum but is missing maximum bounds validation marker", fieldName, ident.Name)
121+
return
122+
}
123+
124+
// For int64 fields, check if bounds are within JavaScript safe integer range
125+
checkJavaScriptSafeBounds(pass, field, fieldName, ident.Name, minimum, maximum)
126+
}
127+
128+
// unwrapType unwraps pointers and slices to get the underlying type.
129+
// Returns the unwrapped type and a boolean indicating if it's a slice.
130+
func unwrapType(expr ast.Expr) (ast.Expr, bool) {
131+
isSlice := false
132+
133+
// Unwrap pointer if present (e.g., *int32)
134+
if starExpr, ok := expr.(*ast.StarExpr); ok {
135+
expr = starExpr.X
136+
}
137+
138+
// Check if it's a slice and unwrap (e.g., []int32)
139+
if arrayType, ok := expr.(*ast.ArrayType); ok {
140+
isSlice = true
141+
expr = arrayType.Elt
142+
143+
// Handle pointer inside slice (e.g., []*int32)
144+
if starExpr, ok := expr.(*ast.StarExpr); ok {
145+
expr = starExpr.X
146+
}
147+
}
148+
149+
return expr, isSlice
150+
}
151+
152+
// getMarkerNames returns the appropriate minimum and maximum marker names
153+
// based on whether the field is a slice.
154+
func getMarkerNames(isSlice bool) (minMarker, maxMarker string) {
155+
if isSlice {
156+
return markers.KubebuilderItemsMinimumMarker, markers.KubebuilderItemsMaximumMarker
157+
}
158+
return markers.KubebuilderMinimumMarker, markers.KubebuilderMaximumMarker
159+
}
160+
161+
// checkJavaScriptSafeBounds checks if int64 bounds are within JavaScript safe integer range.
162+
func checkJavaScriptSafeBounds(pass *analysis.Pass, field *ast.Field, fieldName, typeName string, minimum, maximum float64) {
163+
if typeName != "int64" {
164+
return
165+
}
166+
167+
if minimum < minSafeInt || maximum > maxSafeInt {
168+
pass.Reportf(field.Pos(),
169+
"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",
170+
fieldName, int64(minimum), int64(maximum), minSafeInt, maxSafeInt)
171+
}
172+
}
173+
174+
// getMarkerNumericValue extracts the numeric value from the first instance of the marker with the given name.
175+
func getMarkerNumericValue(markerSet markershelper.MarkerSet, markerName string) (float64, error) {
176+
markerList := markerSet.Get(markerName)
177+
if len(markerList) == 0 {
178+
return 0, errMarkerMissingValue
179+
}
180+
181+
marker := markerList[0]
182+
rawValue, ok := marker.Expressions[""]
183+
if !ok {
184+
return 0, errMarkerMissingValue
185+
}
186+
187+
// Parse as float64 using strconv for better error handling
188+
value, err := strconv.ParseFloat(rawValue, 64)
189+
if err != nil {
190+
return 0, fmt.Errorf("error converting value to number: %w", err)
191+
}
192+
193+
return value, nil
194+
}
195+
196+
// getNumericTypeIdent returns the identifier for int32 or int64 types.
197+
// It handles type aliases by looking up the underlying type.
198+
// Note: This function expects pointers and slices to already be unwrapped.
199+
func getNumericTypeIdent(pass *analysis.Pass, expr ast.Expr) *ast.Ident {
200+
ident, ok := expr.(*ast.Ident)
201+
if !ok {
202+
return nil
203+
}
204+
205+
// Check if it's a basic int32 or int64 type
206+
if ident.Name == "int32" || ident.Name == "int64" {
207+
return ident
208+
}
209+
210+
// Check if it's a type alias to int32 or int64
211+
if !utils.IsBasicType(pass, ident) {
212+
typeSpec, ok := utils.LookupTypeSpec(pass, ident)
213+
if ok {
214+
return getNumericTypeIdent(pass, typeSpec.Type)
215+
}
216+
}
217+
218+
return nil
219+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package numericbounds_test
17+
18+
import (
19+
"testing"
20+
21+
"golang.org/x/tools/go/analysis/analysistest"
22+
"sigs.k8s.io/kube-api-linter/pkg/analysis/numericbounds"
23+
)
24+
25+
func Test(t *testing.T) {
26+
testdata := analysistest.TestData()
27+
analysistest.Run(t, testdata, numericbounds.Analyzer, "a")
28+
}

pkg/analysis/numericbounds/doc.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/*
18+
numericbounds is an analyzer that checks for proper bounds validation on numeric fields.
19+
20+
According to Kubernetes API conventions, numeric fields should have appropriate bounds
21+
checking to prevent values that are too small, negative (when not intended), or too large.
22+
23+
This analyzer ensures that:
24+
- int32 and int64 fields have both minimum and maximum bounds markers
25+
- For slices of numeric types, the analyzer checks for items:Minimum and items:Maximum markers
26+
- Type aliases to int32 or int64 are also checked
27+
- Pointer types (e.g., *int32, []*int64) are unwrapped and validated
28+
- int64 fields with values outside the JavaScript safe integer range (-(2^53-1) to (2^53-1))
29+
are flagged, as they may cause precision loss in JavaScript clients
30+
31+
The analyzer checks for the presence of +kubebuilder:validation:Minimum and
32+
+kubebuilder:validation:Maximum markers on numeric fields, or the items: variants for slices.
33+
34+
For int64 fields, if the bounds exceed the JavaScript safe integer range of
35+
[-9007199254740991, 9007199254740991], the analyzer suggests using a string type instead
36+
to avoid precision loss in JavaScript environments.
37+
38+
Examples of valid and invalid code:
39+
40+
Valid:
41+
42+
type Example struct {
43+
// +kubebuilder:validation:Minimum=0
44+
// +kubebuilder:validation:Maximum=100
45+
Count int32
46+
}
47+
48+
Invalid:
49+
50+
type Example struct {
51+
Count int32 // Missing minimum and maximum markers
52+
}
53+
*/
54+
package numericbounds

0 commit comments

Comments
 (0)