diff --git a/docs/linters.md b/docs/linters.md index 594f8407..ef39c2aa 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -12,6 +12,7 @@ - [JSONTags](#jsontags) - Ensures proper JSON tag formatting - [MaxLength](#maxlength) - Checks for maximum length constraints on strings and arrays - [NamingConventions](#namingconventions) - Ensures field names adhere to user-defined naming conventions +- [NumericBounds](#numericbounds) - Validates numeric fields have appropriate bounds validation markers - [NoBools](#nobools) - Prevents usage of boolean types - [NoDurations](#nodurations) - Prevents usage of duration types - [NoFloats](#nofloats) - Prevents usage of floating-point types @@ -477,6 +478,24 @@ linterConfig: message: prefer 'colour' over 'color' when referring to colours in field names ``` +## NumericBounds + +The `numericbounds` linter checks that numeric fields (`int32`, `int64`, `float32`, `float64`) have appropriate bounds validation markers. + +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. + +This linter ensures that: +- Numeric fields have both `+k8s:minimum` and `+k8s:maximum` markers +- Kubebuilder validation markers (`+kubebuilder:validation:Minimum` and `+kubebuilder:validation:Maximum`) are also supported +- Bounds values are validated: + - int32: within int32 range (±2^31-1) + - int64: within JavaScript-safe range (±2^53-1) per K8s API conventions for JSON compatibility + - float32/float64: marker values are valid (within type ranges) + +**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. + +This linter is **not enabled by default** as it is primarily focused on CRD validation with kubebuilder markers. + ## NoBools The `nobools` linter checks that fields in the API types do not contain a `bool` type. diff --git a/go.mod b/go.mod index 633691d4..af908fdc 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.38.0 + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/tools v0.37.0 k8s.io/apimachinery v0.32.3 k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b diff --git a/pkg/analysis/numericbounds/analyzer.go b/pkg/analysis/numericbounds/analyzer.go new file mode 100644 index 00000000..d57a338a --- /dev/null +++ b/pkg/analysis/numericbounds/analyzer.go @@ -0,0 +1,212 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package numericbounds + +import ( + "errors" + "fmt" + "go/ast" + + "golang.org/x/exp/constraints" + "golang.org/x/tools/go/analysis" + kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector" + markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +const ( + name = "numericbounds" +) + +// Type bounds for validation. +const ( + maxInt32 = 2147483647 // 2^31 - 1 + minInt32 = -2147483648 // -2^31 + maxFloat32 = 3.40282346638528859811704183484516925440e+38 // max float32 + minFloat32 = -3.40282346638528859811704183484516925440e+38 // min float32 + maxFloat64 = 1.797693134862315708145274237317043567981e+308 // max float64 + minFloat64 = -1.797693134862315708145274237317043567981e+308 // min float64 +) + +// JavaScript safe integer bounds for int64 (±2^53-1). +// Per Kubernetes API conventions, int64 fields should use bounds within this range +// to ensure compatibility with JavaScript clients. +const ( + maxSafeInt64 = 9007199254740991 // 2^53 - 1 (max safe integer in JS) + minSafeInt64 = -9007199254740991 // -(2^53 - 1) (min safe integer in JS) +) + +var errMarkerMissingValue = errors.New("marker value not found") + +// Analyzer is the analyzer for the numericbounds package. +// It checks that numeric fields have appropriate bounds validation markers. +var Analyzer = &analysis.Analyzer{ + Name: name, + Doc: "Checks that numeric fields (int32, int64, float32, float64) have appropriate minimum and maximum bounds validation markers", + Run: run, + Requires: []*analysis.Analyzer{inspector.Analyzer}, +} + +func run(pass *analysis.Pass) (any, error) { + inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector) + if !ok { + return nil, kalerrors.ErrCouldNotGetInspector + } + + inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) { + // Create TypeChecker with closure capturing markersAccess and qualifiedFieldName + // Ignore TypeChecker's prefix since we use qualifiedFieldName from inspector + typeChecker := utils.NewTypeChecker(func(pass *analysis.Pass, ident *ast.Ident, node ast.Node, _ string) { + checkNumericType(pass, ident, node, markersAccess, qualifiedFieldName) + }) + + typeChecker.CheckNode(pass, field) + }) + + return nil, nil //nolint:nilnil +} + +//nolint:cyclop +func checkNumericType(pass *analysis.Pass, ident *ast.Ident, node ast.Node, markersAccess markershelper.Markers, qualifiedFieldName string) { + // Only check int32, int64, float32, and float64 types + if ident.Name != "int32" && ident.Name != "int64" && ident.Name != "float32" && ident.Name != "float64" { + return + } + + field, ok := node.(*ast.Field) + if !ok { + return + } + + fieldMarkers := utils.TypeAwareMarkerCollectionForField(pass, markersAccess, field) + + // Check if this is an array/slice field + isSlice := utils.IsArrayTypeOrAlias(pass, field) + + // Determine which markers to look for based on whether the field is a slice + minMarkers, maxMarkers := getMarkerNames(isSlice) + + // Get minimum and maximum marker values + minimum, minErr := getMarkerNumericValue(fieldMarkers, minMarkers) + maximum, maxErr := getMarkerNumericValue(fieldMarkers, maxMarkers) + + // Check if markers are missing + minMissing := errors.Is(minErr, errMarkerMissingValue) + maxMissing := errors.Is(maxErr, errMarkerMissingValue) + + // Report any invalid marker values (e.g., non-numeric values) + if minErr != nil && !minMissing { + pass.Reportf(field.Pos(), "%s has an invalid minimum marker: %v", qualifiedFieldName, minErr) + } + + if maxErr != nil && !maxMissing { + pass.Reportf(field.Pos(), "%s has an invalid maximum marker: %v", qualifiedFieldName, maxErr) + } + + // Report if markers are missing + if minMissing { + pass.Reportf(field.Pos(), "%s is missing minimum bound validation marker", qualifiedFieldName) + } + + if maxMissing { + pass.Reportf(field.Pos(), "%s is missing maximum bound validation marker", qualifiedFieldName) + } + + // If any markers are missing or invalid, don't continue with bounds checks + if minErr != nil || maxErr != nil { + return + } + + // Validate bounds are within the type's valid range + checkBoundsWithinTypeRange(pass, field, qualifiedFieldName, ident.Name, minimum, maximum) +} + +// getMarkerNames returns the appropriate minimum and maximum marker names +// based on whether the field is a slice. +// Returns both kubebuilder and k8s declarative validation markers. +func getMarkerNames(isSlice bool) (minMarkers, maxMarkers []string) { + if isSlice { + return []string{markers.KubebuilderItemsMinimumMarker}, []string{markers.KubebuilderItemsMaximumMarker} + } + + return []string{markers.KubebuilderMinimumMarker, markers.K8sMinimumMarker}, []string{markers.KubebuilderMaximumMarker, markers.K8sMaximumMarker} +} + +// getMarkerNumericValue extracts the numeric value from the first instance of any of the given marker names. +// Checks multiple marker names to support both kubebuilder and k8s declarative validation markers. +// Precedence: Markers checked in the order provided and first valid value found is returned. +// We require a valid numeric value (not just marker presence) for both minimum and maximum markers. +func getMarkerNumericValue(markerSet markershelper.MarkerSet, markerNames []string) (float64, error) { + for _, markerName := range markerNames { + markerList := markerSet.Get(markerName) + if len(markerList) == 0 { + continue + } + + // Use the exported utils.GetMarkerNumericValue function to parse the marker value + value, err := utils.GetMarkerNumericValue[float64](markerList[0]) + if err != nil { + if errors.Is(err, errMarkerMissingValue) { + continue + } + + return 0, fmt.Errorf("error getting marker value: %w", err) + } + + return value, nil + } + + return 0, errMarkerMissingValue +} + +// checkBoundsWithinTypeRange validates that the bounds are within the valid range for the type. +// For int64, enforces JavaScript-safe bounds as per Kubernetes API conventions to ensure +// compatibility with JavaScript clients. +func checkBoundsWithinTypeRange(pass *analysis.Pass, field *ast.Field, prefix, typeName string, minimum, maximum float64) { + switch typeName { + case "int32": + checkBoundInRange(pass, field, prefix, minimum, minInt32, maxInt32, "minimum", "int32") + checkBoundInRange(pass, field, prefix, maximum, minInt32, maxInt32, "maximum", "int32") + case "int64": + // K8s API conventions enforce JavaScript-safe bounds for int64 (±2^53-1) + checkBoundInRange(pass, field, prefix, minimum, int64(minSafeInt64), int64(maxSafeInt64), "minimum", "JavaScript-safe int64", + "Consider using a string type to avoid precision loss in JavaScript clients") + checkBoundInRange(pass, field, prefix, maximum, int64(minSafeInt64), int64(maxSafeInt64), "maximum", "JavaScript-safe int64", + "Consider using a string type to avoid precision loss in JavaScript clients") + case "float32": + checkBoundInRange(pass, field, prefix, minimum, minFloat32, maxFloat32, "minimum", "float32") + checkBoundInRange(pass, field, prefix, maximum, minFloat32, maxFloat32, "maximum", "float32") + case "float64": + checkBoundInRange(pass, field, prefix, minimum, minFloat64, maxFloat64, "minimum", "float64") + checkBoundInRange(pass, field, prefix, maximum, minFloat64, maxFloat64, "maximum", "float64") + } +} + +// checkBoundInRange checks if a bound value is within the valid range. +// Uses generics to work with both integer and float types. +func checkBoundInRange[T constraints.Integer | constraints.Float](pass *analysis.Pass, field *ast.Field, prefix string, value float64, minBound, maxBound T, boundType, typeName string, extraMsg ...string) { + if value < float64(minBound) || value > float64(maxBound) { + msg := fmt.Sprintf("%s has %s bound %%v that is outside the %s range [%%v, %%v]", prefix, boundType, typeName) + if len(extraMsg) > 0 { + msg += ". " + extraMsg[0] + } + + pass.Reportf(field.Pos(), msg, value, minBound, maxBound) + } +} diff --git a/pkg/analysis/numericbounds/analyzer_test.go b/pkg/analysis/numericbounds/analyzer_test.go new file mode 100644 index 00000000..b0c829db --- /dev/null +++ b/pkg/analysis/numericbounds/analyzer_test.go @@ -0,0 +1,28 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package numericbounds_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "sigs.k8s.io/kube-api-linter/pkg/analysis/numericbounds" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, numericbounds.Analyzer, "a") +} diff --git a/pkg/analysis/numericbounds/doc.go b/pkg/analysis/numericbounds/doc.go new file mode 100644 index 00000000..7cd08e9e --- /dev/null +++ b/pkg/analysis/numericbounds/doc.go @@ -0,0 +1,44 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +numericbounds is an analyzer that checks numeric fields have appropriate bounds validation markers. + +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. + +The analyzer checks that int32, int64, float32, and float64 fields have both minimum and +maximum bounds markers. It supports both kubebuilder markers (+kubebuilder:validation:Minimum/Maximum) +and k8s declarative validation markers (+k8s:minimum/maximum). + +For slices of numeric types, the analyzer checks the element type for items:Minimum and items:Maximum markers. + +Type aliases are resolved and checked. Pointer types are unwrapped and validated. + +Bounds values are validated to be within the type's range: + - int32: full int32 range (±2^31-1) + - int64: JavaScript-safe range (±2^53-1) per Kubernetes API conventions + - float32/float64: within their respective ranges + +For int64 fields, Kubernetes API conventions enforce JavaScript-safe bounds (±2^53-1) +to ensure compatibility with JavaScript clients and prevent precision loss. + +For arrays of numeric types, the minimum/maximum of each element can be set using ++kubebuilder:validation:items:Minimum and +kubebuilder:validation:items:Maximum markers. +Alternatively, if the array uses a numeric type alias, the markers can be placed on the +alias type definition itself. +*/ +package numericbounds diff --git a/pkg/analysis/numericbounds/initializer.go b/pkg/analysis/numericbounds/initializer.go new file mode 100644 index 00000000..02c2baf9 --- /dev/null +++ b/pkg/analysis/numericbounds/initializer.go @@ -0,0 +1,35 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package numericbounds + +import ( + "sigs.k8s.io/kube-api-linter/pkg/analysis/initializer" + "sigs.k8s.io/kube-api-linter/pkg/analysis/registry" +) + +func init() { + registry.DefaultRegistry().RegisterLinter(Initializer()) +} + +// Initializer returns the AnalyzerInitializer for this +// Analyzer so that it can be added to the registry. +func Initializer() initializer.AnalyzerInitializer { + return initializer.NewInitializer( + name, + Analyzer, + false, + ) +} diff --git a/pkg/analysis/numericbounds/testdata/src/a/a.go b/pkg/analysis/numericbounds/testdata/src/a/a.go new file mode 100644 index 00000000..8b25ccc0 --- /dev/null +++ b/pkg/analysis/numericbounds/testdata/src/a/a.go @@ -0,0 +1,238 @@ +package a + +// ValidInt32WithBound has proper bounds validation +type ValidInt32WithBound struct { + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + Count int32 +} + +// ValidInt64WithBound has proper bounds validation +type ValidInt64WithBound struct { + // +kubebuilder:validation:Minimum=-1000 + // +kubebuilder:validation:Maximum=1000 + Value int64 +} + +// ValidInt64WithJSSafeBound has bounds within JavaScript safe integer range +type ValidInt64WithJSSafeBound struct { + // +kubebuilder:validation:Minimum=-9007199254740991 + // +kubebuilder:validation:Maximum=9007199254740991 + SafeValue int64 +} + +// InvalidInt32NoBound should have bounds markers +type InvalidInt32NoBound struct { + NoBound int32 // want "InvalidInt32NoBound.NoBound is missing minimum bound validation marker" "InvalidInt32NoBound.NoBound is missing maximum bound validation marker" +} + +// InvalidInt64NoBound should have bounds markers +type InvalidInt64NoBound struct { + NoBound int64 // want "InvalidInt64NoBound.NoBound is missing minimum bound validation marker" "InvalidInt64NoBound.NoBound is missing maximum bound validation marker" +} + +// InvalidInt32OnlyMin should have maximum marker +type InvalidInt32OnlyMin struct { + // +kubebuilder:validation:Minimum=0 + OnlyMin int32 // want "InvalidInt32OnlyMin.OnlyMin is missing maximum bound validation marker" +} + +// InvalidInt32OnlyMax should have minimum marker +type InvalidInt32OnlyMax struct { + // +kubebuilder:validation:Maximum=100 + OnlyMax int32 // want "InvalidInt32OnlyMax.OnlyMax is missing minimum bound validation marker" +} + +// InvalidInt64OnlyMin should have maximum marker +type InvalidInt64OnlyMin struct { + // +kubebuilder:validation:Minimum=0 + OnlyMin int64 // want "InvalidInt64OnlyMin.OnlyMin is missing maximum bound validation marker" +} + +// InvalidInt64OnlyMax should have minimum marker +type InvalidInt64OnlyMax struct { + // +kubebuilder:validation:Maximum=100 + OnlyMax int64 // want "InvalidInt64OnlyMax.OnlyMax is missing minimum bound validation marker" +} + +// InvalidInt64ExceedsJSMaxBounds has maximum that exceeds JavaScript safe integer range +type InvalidInt64ExceedsJSMaxBounds struct { + // +kubebuilder:validation:Minimum=-1000 + // +kubebuilder:validation:Maximum=9007199254740992 + UnsafeMax int64 // want "InvalidInt64ExceedsJSMaxBounds.UnsafeMax has maximum bound 9\\.007199254740992e\\+15 that is outside the JavaScript-safe int64 range \\[-9007199254740991, 9007199254740991\\]\\. Consider using a string type to avoid precision loss in JavaScript clients" +} + +// InvalidInt64ExceedsJSMinBounds has minimum that exceeds JavaScript safe integer range +type InvalidInt64ExceedsJSMinBounds struct { + // +kubebuilder:validation:Minimum=-9007199254740992 + // +kubebuilder:validation:Maximum=1000 + UnsafeMin int64 // want "InvalidInt64ExceedsJSMinBounds.UnsafeMin has minimum bound -9\\.007199254740992e\\+15 that is outside the JavaScript-safe int64 range \\[-9007199254740991, 9007199254740991\\]\\. Consider using a string type to avoid precision loss in JavaScript clients" +} + +// InvalidInt64ExceedsJSBothBounds has both bounds exceeding JavaScript safe integer range +type InvalidInt64ExceedsJSBothBounds struct { + // +kubebuilder:validation:Minimum=-9007199254740992 + // +kubebuilder:validation:Maximum=9007199254740992 + UnsafeBoth int64 // want "InvalidInt64ExceedsJSBothBounds.UnsafeBoth has minimum bound -9\\.007199254740992e\\+15 that is outside the JavaScript-safe int64 range \\[-9007199254740991, 9007199254740991\\]\\. Consider using a string type to avoid precision loss in JavaScript clients" "InvalidInt64ExceedsJSBothBounds.UnsafeBoth has maximum bound 9\\.007199254740992e\\+15 that is outside the JavaScript-safe int64 range \\[-9007199254740991, 9007199254740991\\]\\. Consider using a string type to avoid precision loss in JavaScript clients" +} + +// IgnoredStringField should not be checked +type IgnoredStringField struct { + Name string +} + +// IgnoredBoolField should not be checked +type IgnoredBoolField struct { + Enabled bool +} + +// ValidFloat64WithBounds has proper bounds validation +type ValidFloat64WithBounds struct { + // +kubebuilder:validation:Minimum=-1000.5 + // +kubebuilder:validation:Maximum=1000.5 + Value float64 +} + +// ValidFloat32WithBounds has proper bounds validation +type ValidFloat32WithBounds struct { + // +kubebuilder:validation:Minimum=0.0 + // +kubebuilder:validation:Maximum=100.0 + Ratio float32 +} + +// InvalidFloat64NoBounds should have bounds markers +type InvalidFloat64NoBounds struct { + Value float64 // want "InvalidFloat64NoBounds.Value is missing minimum bound validation marker" "InvalidFloat64NoBounds.Value is missing maximum bound validation marker" +} + +// InvalidFloat32NoBounds should have bounds markers +type InvalidFloat32NoBounds struct { + Ratio float32 // want "InvalidFloat32NoBounds.Ratio is missing minimum bound validation marker" "InvalidFloat32NoBounds.Ratio is missing maximum bound validation marker" +} + +// MixedFields has both valid and invalid fields +type MixedFields struct { + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + ValidCount int32 + + InvalidCount int32 // want "MixedFields.InvalidCount is missing minimum bound validation marker" "MixedFields.InvalidCount is missing maximum bound validation marker" + + Name string + + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=1000 + ValidValue int64 + + InvalidValue int64 // want "MixedFields.InvalidValue is missing minimum bound validation marker" "MixedFields.InvalidValue is missing maximum bound validation marker" +} + +// PointerFields with pointers should also be checked +type PointerFields struct { + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + ValidPointerWithBounds *int32 + + InvalidPointer *int32 // want "PointerFields.InvalidPointer is missing minimum bound validation marker" "PointerFields.InvalidPointer is missing maximum bound validation marker" + + InvalidPointer64 *int64 // want "PointerFields.InvalidPointer64 is missing minimum bound validation marker" "PointerFields.InvalidPointer64 is missing maximum bound validation marker" +} + +// SliceFields with slices should check the element type +type SliceFields struct { + ValidSlice []string + + InvalidSlice []int32 // want "SliceFields.InvalidSlice is missing minimum bound validation marker" "SliceFields.InvalidSlice is missing maximum bound validation marker" + + InvalidSlice64 []int64 // want "SliceFields.InvalidSlice64 is missing minimum bound validation marker" "SliceFields.InvalidSlice64 is missing maximum bound validation marker" +} + +// TypeAliasFields with type aliases should be checked +type Int32Alias int32 +type Int64Alias int64 +type Float32Alias float32 +type Float64Alias float64 + +// Type aliases with bounds on the type itself +// +kubebuilder:validation:Minimum=0 +// +kubebuilder:validation:Maximum=255 +type BoundedInt32Alias int32 + +// +kubebuilder:validation:Minimum=-1000 +// +kubebuilder:validation:Maximum=1000 +type BoundedInt64Alias int64 + +// +kubebuilder:validation:Minimum=0.0 +// +kubebuilder:validation:Maximum=1.0 +type BoundedFloat32Alias float32 + +// +kubebuilder:validation:Minimum=-100.5 +// +kubebuilder:validation:Maximum=100.5 +type BoundedFloat64Alias float64 + +type TypeAliasFields struct { + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + ValidAlias Int32Alias + + InvalidAlias Int32Alias // want "TypeAliasFields.InvalidAlias is missing minimum bound validation marker" "TypeAliasFields.InvalidAlias is missing maximum bound validation marker" + + InvalidAlias64 Int64Alias // want "TypeAliasFields.InvalidAlias64 is missing minimum bound validation marker" "TypeAliasFields.InvalidAlias64 is missing maximum bound validation marker" + + InvalidAliasFloat32 Float32Alias // want "TypeAliasFields.InvalidAliasFloat32 is missing minimum bound validation marker" "TypeAliasFields.InvalidAliasFloat32 is missing maximum bound validation marker" + + InvalidAliasFloat64 Float64Alias // want "TypeAliasFields.InvalidAliasFloat64 is missing minimum bound validation marker" "TypeAliasFields.InvalidAliasFloat64 is missing maximum bound validation marker" + + // Valid: bounds are on the type alias itself + ValidBoundedAlias BoundedInt32Alias + + ValidBoundedAlias64 BoundedInt64Alias + + ValidBoundedFloat32 BoundedFloat32Alias + + ValidBoundedFloat64 BoundedFloat64Alias +} + +// PointerSliceFields with pointer slices should also be checked +type PointerSliceFields struct { + InvalidPointerSlice []*int32 // want "PointerSliceFields.InvalidPointerSlice is missing minimum bound validation marker" "PointerSliceFields.InvalidPointerSlice is missing maximum bound validation marker" +} + +// K8sDeclarativeValidation with k8s declarative validation markers +type K8sDeclarativeValidation struct { + // +k8s:minimum=0 + // +k8s:maximum=100 + ValidWithK8sMarkers int32 + + // +k8s:minimum=-1000 + // +k8s:maximum=1000 + ValidInt64WithK8s int64 +} + +// InvalidInt32BoundsOutOfRange has int32 bounds outside the valid int32 range +type InvalidInt32BoundsOutOfRange struct { + // +kubebuilder:validation:Minimum=-3000000000 + // +kubebuilder:validation:Maximum=3000000000 + OutOfRange int32 // want "InvalidInt32BoundsOutOfRange.OutOfRange has minimum bound -3e\\+09 that is outside the int32 range \\[-2147483648, 2147483647\\]" "InvalidInt32BoundsOutOfRange.OutOfRange has maximum bound 3e\\+09 that is outside the int32 range \\[-2147483648, 2147483647\\]" +} + +// MixedMarkersKubebuilderAndK8s can use either kubebuilder or k8s markers +type MixedMarkersKubebuilderAndK8s struct { + // +kubebuilder:validation:Minimum=0 + // +k8s:maximum=100 + MixedMarkers int32 +} + +// InvalidMarkerNonNumericMin has a non-numeric minimum value +type InvalidMarkerNonNumericMin struct { + // +kubebuilder:validation:Minimum=invalid + // +kubebuilder:validation:Maximum=100 + BadMin int32 // want "InvalidMarkerNonNumericMin.BadMin has an invalid minimum marker" +} + +// InvalidMarkerNonNumericMax has a non-numeric maximum value +type InvalidMarkerNonNumericMax struct { + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=notanumber + BadMax int64 // want "InvalidMarkerNonNumericMax.BadMax has an invalid maximum marker" +} diff --git a/pkg/analysis/utils/type_check.go b/pkg/analysis/utils/type_check.go index d6b8e1b4..47e372d4 100644 --- a/pkg/analysis/utils/type_check.go +++ b/pkg/analysis/utils/type_check.go @@ -31,14 +31,14 @@ type TypeChecker interface { } // NewTypeChecker returns a new TypeChecker with the provided checkFunc. -func NewTypeChecker(checkFunc func(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string)) TypeChecker { +func NewTypeChecker(checkFunc func(pass *analysis.Pass, ident *ast.Ident, node ast.Node, qualifiedFieldName string)) TypeChecker { return &typeChecker{ checkFunc: checkFunc, } } type typeChecker struct { - checkFunc func(pass *analysis.Pass, ident *ast.Ident, node ast.Node, prefix string) + checkFunc func(pass *analysis.Pass, ident *ast.Ident, node ast.Node, qualifiedFieldName string) } // CheckNode checks the provided node for built-in types. diff --git a/pkg/analysis/utils/utils.go b/pkg/analysis/utils/utils.go index 71a481f5..1b27879d 100644 --- a/pkg/analysis/utils/utils.go +++ b/pkg/analysis/utils/utils.go @@ -363,7 +363,7 @@ func isTypeBasic(t types.Type) bool { // It returns a nil value when the marker is not present, and an error // when the marker is present, but malformed. func GetMinProperties(markerSet markershelper.MarkerSet) (*int, error) { - minProperties, err := getMarkerNumericValueByName[int](markerSet, markers.KubebuilderMinPropertiesMarker) + minProperties, err := GetMarkerNumericValueByName[int](markerSet, markers.KubebuilderMinPropertiesMarker) if err != nil && !errors.Is(err, errMarkerMissingValue) { return nil, fmt.Errorf("invalid format for minimum properties marker: %w", err) } diff --git a/pkg/analysis/utils/zero_value.go b/pkg/analysis/utils/zero_value.go index 94701664..71144b0b 100644 --- a/pkg/analysis/utils/zero_value.go +++ b/pkg/analysis/utils/zero_value.go @@ -221,7 +221,7 @@ func isArrayZeroValueValid(pass *analysis.Pass, field *ast.Field, arrayType *ast fieldMarkers := TypeAwareMarkerCollectionForField(pass, markersAccess, field) // For arrays, we can use a zero value if the array is not required to have a minimum number of items. - minItems, err := getMarkerNumericValueByName[int](fieldMarkers, markers.KubebuilderMinItemsMarker) + minItems, err := GetMarkerNumericValueByName[int](fieldMarkers, markers.KubebuilderMinItemsMarker) if err != nil && !errors.Is(err, errMarkerMissingValue) { return false, false } @@ -257,13 +257,13 @@ type number interface { func isNumericZeroValueValid[N number](pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) (bool, bool) { fieldMarkers := TypeAwareMarkerCollectionForField(pass, markersAccess, field) - minimum, err := getMarkerNumericValueByName[N](fieldMarkers, markers.KubebuilderMinimumMarker) + minimum, err := GetMarkerNumericValueByName[N](fieldMarkers, markers.KubebuilderMinimumMarker) if err != nil && !errors.Is(err, errMarkerMissingValue) { pass.Reportf(field.Pos(), "field %s has an invalid minimum marker: %v", FieldName(field), err) return false, false } - maximum, err := getMarkerNumericValueByName[N](fieldMarkers, markers.KubebuilderMaximumMarker) + maximum, err := GetMarkerNumericValueByName[N](fieldMarkers, markers.KubebuilderMaximumMarker) if err != nil && !errors.Is(err, errMarkerMissingValue) { pass.Reportf(field.Pos(), "field %s has an invalid maximum marker: %v", FieldName(field), err) return false, false @@ -276,15 +276,16 @@ func isNumericZeroValueValid[N number](pass *analysis.Pass, field *ast.Field, ma return ptr.Deref(minimum, -1) <= 0 && ptr.Deref(maximum, 1) >= 0, hasCompleteRange || hasGreaterThanZeroMinimum || hasLessThanZeroMaximum } -// getMarkerNumericValueByName extracts the numeric value from the first instance of the marker with the given name. -// Works for markers like MaxLength, MinLength, etc. -func getMarkerNumericValueByName[N number](marker markershelper.MarkerSet, markerName string) (*N, error) { +// GetMarkerNumericValueByName extracts a numeric value from a marker with the given name. +// Returns a pointer to the value, or nil if the marker is not found. +// Works for markers like MaxLength, MinLength, Minimum, Maximum, etc. +func GetMarkerNumericValueByName[N number](marker markershelper.MarkerSet, markerName string) (*N, error) { markerList := marker.Get(markerName) if len(markerList) == 0 { return nil, errMarkerMissingValue } - markerValue, err := getMarkerNumericValue[N](markerList[0]) + markerValue, err := GetMarkerNumericValue[N](markerList[0]) if err != nil { return nil, fmt.Errorf("error getting marker value: %w", err) } @@ -292,9 +293,9 @@ func getMarkerNumericValueByName[N number](marker markershelper.MarkerSet, marke return &markerValue, nil } -// getMarkerNumericValue extracts a numeric value from the default value of a marker. -// Works for markers like MaxLength, MinLength, etc. -func getMarkerNumericValue[N number](marker markershelper.Marker) (N, error) { +// GetMarkerNumericValue extracts a numeric value from the default value of a marker. +// Works for markers like MaxLength, MinLength, Minimum, Maximum, etc. +func GetMarkerNumericValue[N number](marker markershelper.Marker) (N, error) { if marker.Payload.Value == "" { return N(0), errMarkerMissingValue } diff --git a/pkg/registration/doc.go b/pkg/registration/doc.go index 439a7513..916b54b6 100644 --- a/pkg/registration/doc.go +++ b/pkg/registration/doc.go @@ -42,6 +42,7 @@ import ( _ "sigs.k8s.io/kube-api-linter/pkg/analysis/nophase" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/noreferences" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/notimestamp" + _ "sigs.k8s.io/kube-api-linter/pkg/analysis/numericbounds" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/optionalfields" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/optionalorrequired" _ "sigs.k8s.io/kube-api-linter/pkg/analysis/preferredmarkers"