From 40572483d242dfc29c7f7ee5b2296ff8d65c0ffc Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Mon, 6 Oct 2025 01:49:51 +0900 Subject: [PATCH 01/18] initial commit for markerscope linter Signed-off-by: nayuta-ai --- docs/linters.md | 39 ++ pkg/analysis/markerscope/analyzer.go | 438 ++++++++++++++++++ pkg/analysis/markerscope/analyzer_test.go | 40 ++ pkg/analysis/markerscope/config.go | 137 ++++++ pkg/analysis/markerscope/doc.go | 26 ++ pkg/analysis/markerscope/initializer.go | 77 +++ pkg/analysis/markerscope/testdata/src/a/a.go | 88 ++++ .../markerscope/testdata/src/a/a.go.golden | 81 ++++ pkg/analysis/markerscope/testdata/src/b/b.go | 41 ++ .../markerscope/testdata/src/b/b.go.golden | 41 ++ .../markerscope/testdata/src/test/doc.go | 22 + .../markerscope/testdata/src/test/go.mod | 22 + .../markerscope/testdata/src/test/go.sum | 82 ++++ .../markerscope/testdata/src/test/types.go | 65 +++ 14 files changed, 1199 insertions(+) create mode 100644 pkg/analysis/markerscope/analyzer.go create mode 100644 pkg/analysis/markerscope/analyzer_test.go create mode 100644 pkg/analysis/markerscope/config.go create mode 100644 pkg/analysis/markerscope/doc.go create mode 100644 pkg/analysis/markerscope/initializer.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/a.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/a.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/b/b.go create mode 100644 pkg/analysis/markerscope/testdata/src/b/b.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/test/doc.go create mode 100644 pkg/analysis/markerscope/testdata/src/test/go.mod create mode 100644 pkg/analysis/markerscope/testdata/src/test/go.sum create mode 100644 pkg/analysis/markerscope/testdata/src/test/types.go diff --git a/docs/linters.md b/docs/linters.md index 594f8407..3b857cc3 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -10,6 +10,7 @@ - [ForbiddenMarkers](#forbiddenmarkers) - Checks that no forbidden markers are present on types/fields. - [Integers](#integers) - Validates usage of supported integer types - [JSONTags](#jsontags) - Ensures proper JSON tag formatting +- [MarkerScope](#markerscope) - Validates that markers are applied in the correct scope - [MaxLength](#maxlength) - Checks for maximum length constraints on strings and arrays - [NamingConventions](#namingconventions) - Ensures field names adhere to user-defined naming conventions - [NoBools](#nobools) - Prevents usage of boolean types @@ -383,6 +384,44 @@ lintersConfig: jsonTagRegex: "^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$" # Provide a custom regex, which the json tag must match. ``` +## MarkerScope + +The `markerscope` linter validates that markers are applied in the correct scope. It ensures that markers are placed on appropriate Go language constructs (types, fields) according to their intended usage. + +The linter defines different scope types for markers: + +- **Field-only markers**: Can only be applied to struct fields (e.g., `required`, `kubebuilder:validation:Required`) +- **Type-only markers**: Can only be applied to type definitions +- **Type or Map/Slice fields**: Can be applied to type definitions, map fields, or slice fields (e.g., `kubebuilder:validation:MinProperties`) +- **Field or Type markers**: Can be applied to either fields or type definitions + +### Default Scope Rules + +By default, the linter enforces these scope rules: + +- `required` and `kubebuilder:validation:Required`: Field-only +- `kubebuilder:validation:MinProperties`: Type definitions, map fields, or slice fields only + +### Configuration + +```yaml +lintersConfig: + markerscope: + policy: SuggestFix | Warn # The policy for marker scope violations. Defaults to `SuggestFix`. +``` + +### Fixes + +The `markerscope` linter can automatically fix scope violations when `policy` is set to `SuggestFix`: + +1. **Remove incorrect markers**: Suggests removing markers that are in the wrong scope +2. **Move markers to correct locations**: + - Move field-only markers from types to appropriate fields + - Move type-only markers from fields to their corresponding type definitions +3. **Preserve marker values**: When moving markers like `kubebuilder:validation:MinProperties=1` + +**Note**: This linter is not enabled by default and must be explicitly enabled in the configuration. + ## MaxLength The `maxlength` linter checks that string and array fields in the API are bounded by a maximum length. diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go new file mode 100644 index 00000000..795f4b18 --- /dev/null +++ b/pkg/analysis/markerscope/analyzer.go @@ -0,0 +1,438 @@ +/* +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 markerscope + +import ( + "fmt" + "go/ast" + "go/token" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + + kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" + markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" +) + +const ( + name = "markerscope" +) + +func init() { + // Register all markers we want to validate scope for + defaults := DefaultMarkerScopes() + markers := make([]string, 0, len(defaults)) + for marker := range defaults { + markers = append(markers, marker) + } + markershelper.DefaultRegistry().Register(markers...) +} + +type analyzer struct { + markerScopes map[string]MarkerScope + policy MarkerScopePolicy +} + +// newAnalyzer creates a new analyzer. +func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { + if cfg == nil { + cfg = &MarkerScopeConfig{} + } + + a := &analyzer{ + markerScopes: DefaultMarkerScopes(), + policy: cfg.Policy, + } + + // Set default policy if not specified + if a.policy == "" { + a.policy = MarkerScopePolicySuggestFix + } + + return &analysis.Analyzer{ + Name: name, + Doc: "Validates that markers are applied in the correct scope.", + Run: a.run, + Requires: []*analysis.Analyzer{inspect.Analyzer, markershelper.Analyzer}, + RunDespiteErrors: true, + } +} + +func (a *analyzer) run(pass *analysis.Pass) (any, error) { + inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + if !ok { + return nil, kalerrors.ErrCouldNotGetInspector + } + + markersAccess, ok := pass.ResultOf[markershelper.Analyzer].(markershelper.Markers) + if !ok { + return nil, kalerrors.ErrCouldNotGetMarkers + } + + // Check field markers and type markers + nodeFilter := []ast.Node{ + (*ast.Field)(nil), + (*ast.GenDecl)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + switch node := n.(type) { + case *ast.Field: + a.checkFieldMarkers(pass, node, markersAccess) + case *ast.GenDecl: + a.checkTypeMarkers(pass, node, markersAccess) + } + }) + + return nil, nil +} + +// reportAndRemoveMarker reports an error and suggests removing the marker +func (a *analyzer) reportAndRemoveMarker(pass *analysis.Pass, marker markershelper.Marker, allowedScope string) { + suggestedFixes := []analysis.SuggestedFix{} + + // Only add suggested fixes if policy allows it + if a.policy == MarkerScopePolicySuggestFix { + suggestedFixes = []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("Remove marker %q", marker.Identifier), + TextEdits: []analysis.TextEdit{ + a.removeMarker(marker), + }, + }, + } + } + + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q can only be applied to %s", marker.Identifier, allowedScope), + SuggestedFixes: suggestedFixes, + }) +} + +// reportAndMoveMarkerToType reports an error and suggests moving the marker to the type definition +func (a *analyzer) reportAndMoveMarkerToType(pass *analysis.Pass, marker markershelper.Marker, typeSpec *ast.TypeSpec) { + suggestedFixes := []analysis.SuggestedFix{} + + // Only add suggested fixes if policy allows it + if a.policy == MarkerScopePolicySuggestFix { + insertPos := a.getTypeInsertPos(pass, typeSpec) + if insertPos != token.NoPos { + suggestedFixes = []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("Move marker %q to type %s", marker.Identifier, typeSpec.Name.Name), + TextEdits: []analysis.TextEdit{ + // Remove from field + a.removeMarker(marker), + // Add to struct type + { + Pos: insertPos, + End: insertPos, + NewText: []byte(fmt.Sprintf("// +%s\n", cleanMarkerString(marker))), + }, + }, + }, + { + Message: fmt.Sprintf("Remove marker %q", marker.Identifier), + TextEdits: []analysis.TextEdit{ + a.removeMarker(marker), + }, + }, + } + } + } + + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q can only be applied to type definitions or object fields (struct/map)", marker.Identifier), + SuggestedFixes: suggestedFixes, + }) +} + +// reportAndMoveMarkerToField reports an error and suggests moving the marker to a field +func (a *analyzer) reportAndMoveMarkerToField(pass *analysis.Pass, marker markershelper.Marker, targetField *ast.Field) { + suggestedFixes := []analysis.SuggestedFix{} + + // Only add suggested fixes if policy allows it + if a.policy == MarkerScopePolicySuggestFix { + insertPos := a.getFieldInsertPos(targetField) + fieldName := utils.FieldName(targetField) + if fieldName == "" { + fieldName = "field" + } + + suggestedFixes = []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("Move marker %q to field %s", marker.Identifier, fieldName), + TextEdits: []analysis.TextEdit{ + // Remove from type + a.removeMarker(marker), + // Add to field + { + Pos: insertPos, + End: insertPos, + NewText: []byte(fmt.Sprintf("// +%s\n\t", cleanMarkerString(marker))), + }, + }, + }, + { + Message: fmt.Sprintf("Remove marker %q", marker.Identifier), + TextEdits: []analysis.TextEdit{ + a.removeMarker(marker), + }, + }, + } + } + + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q can only be applied to fields", marker.Identifier), + SuggestedFixes: suggestedFixes, + }) +} + +// checkFieldMarkers checks markers on fields for violations +func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) { + fieldMarkers := markersAccess.FieldMarkers(field) + + for _, marker := range fieldMarkers.UnsortedList() { + scope, ok := a.markerScopes[marker.Identifier] + if !ok { + // No scope defined for this marker, skip validation + continue + } + + switch scope { + case ScopeType: + a.reportAndRemoveMarker(pass, marker, "type definitions") + case ScopeTypeOrObjectField: + // Check if field type is an object type (struct or map) + if !isMapType(field.Type) && !utils.IsStructType(pass, field.Type) { + // Check if it's a struct type and we can move the marker there + typeSpec := a.getStructTypeSpec(pass, field.Type) + + if typeSpec != nil { + // We can move the marker to the struct definition + a.reportAndMoveMarkerToType(pass, marker, typeSpec) + } else { + // Can't find struct type, just offer removal + a.reportAndRemoveMarker(pass, marker, "type definitions or object fields (struct/map)") + } + } + } + } +} + +// checkTypeMarkers checks markers on types for violations +func (a *analyzer) checkTypeMarkers(pass *analysis.Pass, genDecl *ast.GenDecl, markersAccess markershelper.Markers) { + if len(genDecl.Specs) == 0 { + return + } + + for i := range genDecl.Specs { + typeSpec, ok := genDecl.Specs[i].(*ast.TypeSpec) + if !ok { + continue + } + + typeMarkers := markersAccess.TypeMarkers(typeSpec) + + for _, marker := range typeMarkers.UnsortedList() { + scope, ok := a.markerScopes[marker.Identifier] + if !ok { + // No scope defined for this marker, skip validation + continue + } + + if scope == ScopeField { + // For field-only markers on types, try to move to a field using this type + targetField := a.findFieldUsingType(pass, typeSpec.Name.Name) + if targetField != nil { + a.reportAndMoveMarkerToField(pass, marker, targetField) + } else { + // No suitable field found, just suggest removal + a.reportAndRemoveMarker(pass, marker, "fields") + } + } + } + } +} + +// isMapType checks if the given expression is a map type +func isMapType(expr ast.Expr) bool { + _, ok := expr.(*ast.MapType) + return ok +} + +// getStructTypeSpec finds the type spec for a given field type if it's a struct +func (a *analyzer) getStructTypeSpec(pass *analysis.Pass, expr ast.Expr) *ast.TypeSpec { + // Check if it's a struct type first + if !utils.IsStructType(pass, expr) { + return nil + } + + // Get the identifier from the expression + var ident *ast.Ident + switch t := expr.(type) { + case *ast.Ident: + ident = t + case *ast.StarExpr: + // Handle pointer types + if identExpr, ok := t.X.(*ast.Ident); ok { + ident = identExpr + } + default: + return nil + } + + if ident == nil { + return nil + } + + // Use utils.LookupTypeSpec to find the type declaration + typeSpec, ok := utils.LookupTypeSpec(pass, ident) + if !ok { + return nil + } + + return typeSpec +} + +// findFieldUsingType finds a field that uses the given type name +func (a *analyzer) findFieldUsingType(pass *analysis.Pass, typeName string) *ast.Field { + inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + if !ok { + return nil + } + + var result *ast.Field + + // Find field nodes that use this type + nodeFilter := []ast.Node{ + (*ast.Field)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + field, ok := n.(*ast.Field) + if !ok || result != nil { + return + } + + // Check if this field uses the type we're looking for + fieldTypeName := getFieldTypeName(field.Type) + if fieldTypeName == typeName { + result = field + } + }) + + return result +} + +// getTypeInsertPos calculates the insertion position for a type declaration +func (a *analyzer) getTypeInsertPos(pass *analysis.Pass, typeSpec *ast.TypeSpec) token.Pos { + inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + if !ok { + return token.NoPos + } + + var genDecl *ast.GenDecl + nodeFilter := []ast.Node{ + (*ast.GenDecl)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + if genDecl != nil { + return + } + gd, ok := n.(*ast.GenDecl) + if !ok || gd.Tok != token.TYPE { + return + } + + for _, spec := range gd.Specs { + if spec == typeSpec { + genDecl = gd + return + } + } + }) + + if genDecl == nil { + return token.NoPos + } + + // Calculate insertion position (before the type declaration) + if genDecl.Doc != nil && len(genDecl.Doc.List) > 0 { + // Insert after existing comments + lastComment := genDecl.Doc.List[len(genDecl.Doc.List)-1] + return lastComment.End() + 1 // After last comment + newline + } + // Insert before the type keyword + return genDecl.Pos() +} + +// getFieldInsertPos calculates the insertion position for a field +func (a *analyzer) getFieldInsertPos(field *ast.Field) token.Pos { + // Calculate insertion position (before the field) + if field.Doc != nil && len(field.Doc.List) > 0 { + // Insert after existing field comments + lastComment := field.Doc.List[len(field.Doc.List)-1] + return lastComment.End() + 1 // After last comment + newline + } + // Insert before the field + return field.Pos() +} + +// getFieldTypeName extracts the type name from a field type expression +func getFieldTypeName(expr ast.Expr) string { + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.StarExpr: + // Handle pointer types + if ident, ok := t.X.(*ast.Ident); ok { + return ident.Name + } + } + return "" +} + +// cleanMarkerString returns the marker string without any // want comments +func cleanMarkerString(marker markershelper.Marker) string { + markerStr := marker.String() + // Remove any // want comment suffix + if idx := strings.Index(markerStr, " //"); idx != -1 { + markerStr = markerStr[:idx] + } + return markerStr +} + +// removeMarker creates a TextEdit that removes a marker +func (a *analyzer) removeMarker(marker markershelper.Marker) analysis.TextEdit { + // For now, just remove the marker text itself + // The go/analysis framework will handle line cleanup automatically in many cases + return analysis.TextEdit{ + Pos: marker.Pos, + End: marker.End + 1, + NewText: []byte(""), + } +} diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go new file mode 100644 index 00000000..b331e328 --- /dev/null +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -0,0 +1,40 @@ +/* +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 markerscope + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzerWarnOnly(t *testing.T) { + testdata := analysistest.TestData() + cfg := &MarkerScopeConfig{ + Policy: MarkerScopePolicyWarn, + } + analyzer := newAnalyzer(cfg) + analysistest.Run(t, testdata, analyzer, "b") +} + +func TestAnalyzerSuggestFix(t *testing.T) { + testdata := analysistest.TestData() + cfg := &MarkerScopeConfig{ + Policy: MarkerScopePolicySuggestFix, + } + analyzer := newAnalyzer(cfg) + analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "b") +} diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go new file mode 100644 index 00000000..84889694 --- /dev/null +++ b/pkg/analysis/markerscope/config.go @@ -0,0 +1,137 @@ +/* +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 markerscope + +import "sigs.k8s.io/kube-api-linter/pkg/markers" + +// MarkerScope defines where a marker is allowed to be placed. +type MarkerScope string + +const ( + // ScopeField indicates the marker can only be placed on fields. + ScopeField MarkerScope = "field" + + // ScopeType indicates the marker can only be placed on type definitions. + ScopeType MarkerScope = "type" + + // ScopeFieldOrType indicates the marker can be placed on either fields or types. + ScopeFieldOrType MarkerScope = "field_or_type" + + // ScopeTypeOrObjectField indicates the marker can be placed on type definitions or object fields (struct/map). + ScopeTypeOrObjectField MarkerScope = "type_or_object_field" +) + +// MarkerScopePolicy defines how the linter should handle violations. +type MarkerScopePolicy string + +const ( + // MarkerScopePolicyWarn only reports warnings without suggesting fixes. + MarkerScopePolicyWarn MarkerScopePolicy = "warn" + + // MarkerScopePolicySuggestFix reports warnings and suggests automatic fixes. + MarkerScopePolicySuggestFix MarkerScopePolicy = "suggest_fix" +) + +// MarkerScopeConfig contains configuration for marker scope validation. +type MarkerScopeConfig struct { + // Markers maps marker names to their allowed scopes. + // If a marker is not in this map, no scope validation is performed. + Markers map[string]MarkerScope `json:"markers,omitempty"` + + // Policy determines whether to suggest fixes or just warn. + Policy MarkerScopePolicy `json:"policy,omitempty"` +} + +// DefaultMarkerScopes returns the default marker scope configurations. +// ref: https://github.com/kubernetes-sigs/controller-tools/blob/main/pkg/crd/markers/validation.go +func DefaultMarkerScopes() map[string]MarkerScope { + return map[string]MarkerScope{ + // Field-only markers (based on controller-tools validation.go) + markers.OptionalMarker: ScopeField, + markers.RequiredMarker: ScopeField, + markers.K8sOptionalMarker: ScopeField, + markers.K8sRequiredMarker: ScopeField, + markers.KubebuilderOptionalMarker: ScopeField, + markers.KubebuilderRequiredMarker: ScopeField, + markers.NullableMarker: ScopeField, + markers.DefaultMarker: ScopeField, + markers.KubebuilderDefaultMarker: ScopeField, + markers.KubebuilderExampleMarker: ScopeField, + "kubebuilder:validation:EmbeddedResource": ScopeField, + markers.KubebuilderSchemaLessMarker: ScopeField, + + // Type-only markers (object-level validation and CRD generation) + "kubebuilder:validation:items:ExactlyOneOf": ScopeType, + "kubebuilder:validation:items:AtMostOneOf": ScopeType, + "kubebuilder:validation:items:AtLeastOneOf": ScopeType, + markers.KubebuilderRootMarker: ScopeType, + markers.KubebuilderStatusSubresourceMarker: ScopeType, + + // field-and-type markers + "kubebuilder:pruning:PreserveUnknownFields": ScopeFieldOrType, + "kubebuilder:title": ScopeFieldOrType, + + // numeric markers + markers.KubebuilderMinimumMarker: ScopeField, + markers.KubebuilderMaximumMarker: ScopeField, + markers.KubebuilderExclusiveMaximumMarker: ScopeField, + markers.KubebuilderExclusiveMinimumMarker: ScopeField, + markers.KubebuilderMultipleOfMarker: ScopeField, + + // object markers + markers.KubebuilderMinPropertiesMarker: ScopeTypeOrObjectField, + markers.KubebuilderMaxPropertiesMarker: ScopeTypeOrObjectField, + + // string markers + markers.KubebuilderPatternMarker: ScopeField, + markers.KubebuilderMinLengthMarker: ScopeField, + markers.KubebuilderMaxLengthMarker: ScopeField, + + // array markers + markers.KubebuilderMinItemsMarker: ScopeField, + markers.KubebuilderMaxItemsMarker: ScopeField, + markers.KubebuilderUniqueItemsMarker: ScopeField, + + // general markers + markers.KubebuilderEnumMarker: ScopeField, + markers.KubebuilderFormatMarker: ScopeField, + markers.KubebuilderTypeMarker: ScopeField, + markers.KubebuilderXValidationMarker: ScopeField, + + // Array/slice field markers (Server-Side Apply related) + markers.KubebuilderListTypeMarker: ScopeFieldOrType, + markers.KubebuilderListMapKeyMarker: ScopeFieldOrType, + + // Array items markers (field-only, apply to array elements) + markers.KubebuilderItemsMaxItemsMarker: ScopeField, + markers.KubebuilderItemsMaximumMarker: ScopeField, + markers.KubebuilderItemsMinItemsMarker: ScopeField, + markers.KubebuilderItemsMinLengthMarker: ScopeField, + markers.KubebuilderItemsMinimumMarker: ScopeField, + markers.KubebuilderItemsMaxLengthMarker: ScopeField, + markers.KubebuilderItemsEnumMarker: ScopeField, + markers.KubebuilderItemsFormatMarker: ScopeField, + markers.KubebuilderItemsExclusiveMaximumMarker: ScopeField, + markers.KubebuilderItemsExclusiveMinimumMarker: ScopeField, + markers.KubebuilderItemsMultipleOfMarker: ScopeField, + markers.KubebuilderItemsPatternMarker: ScopeField, + markers.KubebuilderItemsTypeMarker: ScopeField, + markers.KubebuilderItemsUniqueItemsMarker: ScopeField, + markers.KubebuilderItemsXValidationMarker: ScopeField, + markers.KubebuilderItemsMinPropertiesMarker: ScopeTypeOrObjectField, + markers.KubebuilderItemsMaxPropertiesMarker: ScopeTypeOrObjectField, + } +} diff --git a/pkg/analysis/markerscope/doc.go b/pkg/analysis/markerscope/doc.go new file mode 100644 index 00000000..3bf61204 --- /dev/null +++ b/pkg/analysis/markerscope/doc.go @@ -0,0 +1,26 @@ +/* +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 markerscope provides a linter that validates markers are applied in the correct scope. +// +// Some markers are only valid when applied to specific Go constructs: +// - Field-only markers: optional, required, nullable +// - Type/Struct-only markers: MinProperties, MaxProperties, kubebuilder:object:root, kubebuilder:subresource:status +// - Field or Type markers: default, MinLength, MaxLength, etc. +// +// This linter ensures markers are applied in their appropriate contexts to prevent +// configuration errors and improve API consistency. +package markerscope \ No newline at end of file diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go new file mode 100644 index 00000000..d4f9dec8 --- /dev/null +++ b/pkg/analysis/markerscope/initializer.go @@ -0,0 +1,77 @@ +/* +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 markerscope + +import ( + "fmt" + + "golang.org/x/tools/go/analysis" + "k8s.io/apimachinery/pkg/util/validation/field" + "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.NewConfigurableInitializer( + name, + initAnalyzer, + false, // Not enabled by default + validateConfig, + ) +} + +func initAnalyzer(cfg *MarkerScopeConfig) (*analysis.Analyzer, error) { + return newAnalyzer(cfg), nil +} + +// validateConfig validates the configuration in the MarkerScopeConfig struct. +func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList { + if cfg == nil { + return field.ErrorList{} + } + + fieldErrors := field.ErrorList{} + + // Validate policy + if cfg.Policy != "" && cfg.Policy != MarkerScopePolicyWarn && cfg.Policy != MarkerScopePolicySuggestFix { + fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("policy"), cfg.Policy, + fmt.Sprintf("invalid policy, must be one of: %q, %q", MarkerScopePolicyWarn, MarkerScopePolicySuggestFix))) + } + + // Validate marker scopes + for marker, scope := range cfg.Markers { + if err := validateScope(scope); err != nil { + fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("markers", marker), scope, err.Error())) + } + } + + return fieldErrors +} + +func validateScope(scope MarkerScope) error { + switch scope { + case ScopeField, ScopeType, ScopeFieldOrType, ScopeTypeOrObjectField: + return nil + default: + return fmt.Errorf("invalid scope %q, must be one of: %q, %q, %q, %q", scope, ScopeField, ScopeType, ScopeFieldOrType, ScopeTypeOrObjectField) + } +} diff --git a/pkg/analysis/markerscope/testdata/src/a/a.go b/pkg/analysis/markerscope/testdata/src/a/a.go new file mode 100644 index 00000000..2ef7cd18 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/a.go @@ -0,0 +1,88 @@ +/* +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 a + +// +kubebuilder:validation:MinProperties=1 +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type ValidTypeMarkers struct { + Name string `json:"name"` +} + +// +optional // want `marker "optional" can only be applied to fields, not type definitions` +// +required // want `marker "required" can only be applied to fields, not type definitions` +// +kubebuilder:validation:Optional // want `marker "kubebuilder:validation:Optional" can only be applied to fields, not type definitions` +type InvalidTypeMarkers struct { + Name string `json:"name"` +} + +type FieldMarkerTest struct { + // +optional + // +kubebuilder:validation:Optional + ValidOptionalField string `json:"validOptionalField,omitempty"` + + // +required + // +kubebuilder:validation:Required + ValidRequiredField string `json:"validRequiredField"` + + // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties" can only be applied to type definitions, not fields` + InvalidMinPropertiesField map[string]string `json:"invalidMinPropertiesField"` + + // +kubebuilder:object:root=true // want `marker "kubebuilder:object:root" can only be applied to type definitions, not fields` + InvalidRootField string `json:"invalidRootField"` + + // +kubebuilder:subresource:status // want `marker "kubebuilder:subresource:status" can only be applied to type definitions, not fields` + InvalidStatusField string `json:"invalidStatusField"` + + // +kubebuilder:validation:MaxProperties=10 // want `marker "kubebuilder:validation:MaxProperties" can only be applied to type definitions, not fields` + InvalidMaxPropertiesField map[string]string `json:"invalidMaxPropertiesField"` +} + +// Test markers that can be on both fields and types +// +kubebuilder:default="default-value" +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=100 +type ValidBothMarkers struct { + // +kubebuilder:default="field-default" + // +kubebuilder:validation:MinLength=5 + // +kubebuilder:validation:MaxLength=50 + Name string `json:"name"` +} + +// Test array field markers +type ArrayFieldTest struct { + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=10 + // +kubebuilder:validation:UniqueItems=true + // +listType=map + // +listMapKey=name + ValidArrayField []Item `json:"validArrayField"` +} + +type Item struct { + Name string `json:"name"` +} + +// Test custom enum and format markers +// +kubebuilder:validation:Enum=TypeA;TypeB;TypeC +// +kubebuilder:validation:Format=email +type EnumType string + +type EnumFieldTest struct { + // +kubebuilder:validation:Enum=FieldA;FieldB;FieldC + // +kubebuilder:validation:Format=ipv4 + EnumField string `json:"enumField"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/a.go.golden b/pkg/analysis/markerscope/testdata/src/a/a.go.golden new file mode 100644 index 00000000..25ca6580 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/a.go.golden @@ -0,0 +1,81 @@ +/* +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 a + +// +kubebuilder:validation:MinProperties=1 +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type ValidTypeMarkers struct { + Name string `json:"name"` +} + +type InvalidTypeMarkers struct { + Name string `json:"name"` +} + +type FieldMarkerTest struct { + // +optional + // +kubebuilder:validation:Optional + ValidOptionalField string `json:"validOptionalField,omitempty"` + + // +required + // +kubebuilder:validation:Required + ValidRequiredField string `json:"validRequiredField"` + + InvalidMinPropertiesField map[string]string `json:"invalidMinPropertiesField"` + + InvalidRootField string `json:"invalidRootField"` + + InvalidStatusField string `json:"invalidStatusField"` + + InvalidMaxPropertiesField map[string]string `json:"invalidMaxPropertiesField"` +} + +// Test markers that can be on both fields and types +// +kubebuilder:default="default-value" +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=100 +type ValidBothMarkers struct { + // +kubebuilder:default="field-default" + // +kubebuilder:validation:MinLength=5 + // +kubebuilder:validation:MaxLength=50 + Name string `json:"name"` +} + +// Test array field markers +type ArrayFieldTest struct { + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=10 + // +kubebuilder:validation:UniqueItems=true + // +listType=map + // +listMapKey=name + ValidArrayField []Item `json:"validArrayField"` +} + +type Item struct { + Name string `json:"name"` +} + +// Test custom enum and format markers +// +kubebuilder:validation:Enum=TypeA;TypeB;TypeC +// +kubebuilder:validation:Format=email +type EnumType string + +type EnumFieldTest struct { + // +kubebuilder:validation:Enum=FieldA;FieldB;FieldC + // +kubebuilder:validation:Format=ipv4 + EnumField string `json:"enumField"` +} \ No newline at end of file diff --git a/pkg/analysis/markerscope/testdata/src/b/b.go b/pkg/analysis/markerscope/testdata/src/b/b.go new file mode 100644 index 00000000..51cdb34e --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/b/b.go @@ -0,0 +1,41 @@ +/* +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 b + +// +kubebuilder:validation:MinProperties=1 +type ValidTypeMarkers struct { + // +optional + Name string `json:"name"` +} + +// +required // want `marker "required" can only be applied to fields` +type InvalidTypeMarkers struct { + // +kubebuilder:validation:Required + Name string `json:"name"` +} + +type FieldMarkerTest struct { + // +required + // +kubebuilder:validation:MinProperties=1 + ValidMinPropertiesField map[string]string `json:"validMinPropertiesField"` + + // +required + // +kubebuilder:validation:MinProperties=1 + ValidMinPropertiesField2 []string `json:"validMinPropertiesField2"` + + // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties" can only be applied to type definitions, map fields, or slice fields` + InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` +} diff --git a/pkg/analysis/markerscope/testdata/src/b/b.go.golden b/pkg/analysis/markerscope/testdata/src/b/b.go.golden new file mode 100644 index 00000000..28e0d424 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/b/b.go.golden @@ -0,0 +1,41 @@ +/* +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 b + +// +kubebuilder:validation:MinProperties=1 +type ValidTypeMarkers struct { + // +optional + Name string `json:"name"` +} + +// +kubebuilder:validation:MinProperties=1 +type InvalidTypeMarkers struct { + // +kubebuilder:validation:Required + Name string `json:"name"` +} + +type FieldMarkerTest struct { + // +required + // +kubebuilder:validation:MinProperties=1 + ValidMinPropertiesField map[string]string `json:"validMinPropertiesField"` + + // +required + // +kubebuilder:validation:MinProperties=1 + ValidMinPropertiesField2 []string `json:"validMinPropertiesField2"` + + // +required + InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` +} \ No newline at end of file diff --git a/pkg/analysis/markerscope/testdata/src/test/doc.go b/pkg/analysis/markerscope/testdata/src/test/doc.go new file mode 100644 index 00000000..fd1f5244 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/test/doc.go @@ -0,0 +1,22 @@ +/* +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. +*/ + +// +kubebuilder:object:generate=true +// +kubebuilder:ac:generate=true +// +groupName=test.example.com +// +versionName=v1 + +package test \ No newline at end of file diff --git a/pkg/analysis/markerscope/testdata/src/test/go.mod b/pkg/analysis/markerscope/testdata/src/test/go.mod new file mode 100644 index 00000000..087d0a8f --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/test/go.mod @@ -0,0 +1,22 @@ +module example.com/test + +go 1.21 + +require k8s.io/apimachinery v0.29.0 + +require ( + github.com/go-logr/logr v1.3.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect +) diff --git a/pkg/analysis/markerscope/testdata/src/test/go.sum b/pkg/analysis/markerscope/testdata/src/test/go.sum new file mode 100644 index 00000000..7c9d2800 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/test/go.sum @@ -0,0 +1,82 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/analysis/markerscope/testdata/src/test/types.go b/pkg/analysis/markerscope/testdata/src/test/types.go new file mode 100644 index 00000000..5929e3a8 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/test/types.go @@ -0,0 +1,65 @@ +/* +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 test + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:object:root=true + +// TestResource is the Schema for the testresources API +type TestResource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TestResourceSpec `json:"spec,omitempty"` + Status TestResourceStatus `json:"status,omitempty"` +} + +// TestResourceSpec defines the desired state of TestResource +type TestResourceSpec struct { + // Valid: MinProperties on map field + // +kubebuilder:validation:MinProperties=1 + ConfigMap map[string]string `json:"configMap,omitempty"` + + // Valid: MinProperties on struct field + Settings []SettingsStruct `json:"settings,omitempty"` + + // Invalid: MinProperties on slice field (should cause error) + Items []string `json:"items,omitempty"` + + // Invalid: MinProperties on string field (should cause error) + Name string `json:"name,omitempty"` +} + +// +kubebuilder:validation:MinProperties=2 +type SettingsStruct struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// TestResourceStatus defines the observed state of TestResource +type TestResourceStatus struct { + Ready bool `json:"ready,omitempty"` +} + +// +kubebuilder:object:root=true + +// TestResourceList contains a list of TestResource +type TestResourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestResource `json:"items"` +} From 9e2a4982564b40085abf7785fbd99635723a1d59 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Mon, 13 Oct 2025 18:18:02 +0900 Subject: [PATCH 02/18] Add ScopeViolation Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 333 ++----------- pkg/analysis/markerscope/analyzer_test.go | 11 +- pkg/analysis/markerscope/config.go | 455 +++++++++++++++--- pkg/analysis/markerscope/initializer.go | 58 ++- pkg/analysis/markerscope/testdata/src/a/a.go | 258 ++++++++-- .../markerscope/testdata/src/a/a.go.golden | 81 ---- pkg/markers/markers.go | 24 + 7 files changed, 704 insertions(+), 516 deletions(-) delete mode 100644 pkg/analysis/markerscope/testdata/src/a/a.go.golden diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 795f4b18..8d9151d7 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -18,7 +18,6 @@ package markerscope import ( "fmt" "go/ast" - "go/token" "strings" "golang.org/x/tools/go/analysis" @@ -27,7 +26,6 @@ import ( kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" - "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" ) const ( @@ -36,7 +34,7 @@ const ( func init() { // Register all markers we want to validate scope for - defaults := DefaultMarkerScopes() + defaults := DefaultMarkerRules() markers := make([]string, 0, len(defaults)) for marker := range defaults { markers = append(markers, marker) @@ -45,8 +43,8 @@ func init() { } type analyzer struct { - markerScopes map[string]MarkerScope - policy MarkerScopePolicy + markerRules map[string]MarkerScopeRule + policy MarkerScopePolicy } // newAnalyzer creates a new analyzer. @@ -56,13 +54,20 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { } a := &analyzer{ - markerScopes: DefaultMarkerScopes(), - policy: cfg.Policy, + markerRules: DefaultMarkerRules(), + policy: cfg.Policy, + } + + // Override with custom rules if provided + if cfg.MarkerRules != nil { + for marker, rule := range cfg.MarkerRules { + a.markerRules[marker] = rule + } } // Set default policy if not specified if a.policy == "" { - a.policy = MarkerScopePolicySuggestFix + a.policy = MarkerScopePolicyWarn } return &analysis.Analyzer{ @@ -103,110 +108,25 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { return nil, nil } -// reportAndRemoveMarker reports an error and suggests removing the marker -func (a *analyzer) reportAndRemoveMarker(pass *analysis.Pass, marker markershelper.Marker, allowedScope string) { - suggestedFixes := []analysis.SuggestedFix{} - - // Only add suggested fixes if policy allows it - if a.policy == MarkerScopePolicySuggestFix { - suggestedFixes = []analysis.SuggestedFix{ - { - Message: fmt.Sprintf("Remove marker %q", marker.Identifier), - TextEdits: []analysis.TextEdit{ - a.removeMarker(marker), - }, - }, - } +// reportScopeViolation reports a scope violation error +func (a *analyzer) reportScopeViolation(pass *analysis.Pass, marker markershelper.Marker, rule MarkerScopeRule) { + var allowedScopes []string + if rule.Scope&FieldScope != 0 { + allowedScopes = append(allowedScopes, "fields") } - - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q can only be applied to %s", marker.Identifier, allowedScope), - SuggestedFixes: suggestedFixes, - }) -} - -// reportAndMoveMarkerToType reports an error and suggests moving the marker to the type definition -func (a *analyzer) reportAndMoveMarkerToType(pass *analysis.Pass, marker markershelper.Marker, typeSpec *ast.TypeSpec) { - suggestedFixes := []analysis.SuggestedFix{} - - // Only add suggested fixes if policy allows it - if a.policy == MarkerScopePolicySuggestFix { - insertPos := a.getTypeInsertPos(pass, typeSpec) - if insertPos != token.NoPos { - suggestedFixes = []analysis.SuggestedFix{ - { - Message: fmt.Sprintf("Move marker %q to type %s", marker.Identifier, typeSpec.Name.Name), - TextEdits: []analysis.TextEdit{ - // Remove from field - a.removeMarker(marker), - // Add to struct type - { - Pos: insertPos, - End: insertPos, - NewText: []byte(fmt.Sprintf("// +%s\n", cleanMarkerString(marker))), - }, - }, - }, - { - Message: fmt.Sprintf("Remove marker %q", marker.Identifier), - TextEdits: []analysis.TextEdit{ - a.removeMarker(marker), - }, - }, - } - } + if rule.Scope&TypeScope != 0 { + allowedScopes = append(allowedScopes, "types") } - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q can only be applied to type definitions or object fields (struct/map)", marker.Identifier), - SuggestedFixes: suggestedFixes, - }) -} - -// reportAndMoveMarkerToField reports an error and suggests moving the marker to a field -func (a *analyzer) reportAndMoveMarkerToField(pass *analysis.Pass, marker markershelper.Marker, targetField *ast.Field) { - suggestedFixes := []analysis.SuggestedFix{} - - // Only add suggested fixes if policy allows it - if a.policy == MarkerScopePolicySuggestFix { - insertPos := a.getFieldInsertPos(targetField) - fieldName := utils.FieldName(targetField) - if fieldName == "" { - fieldName = "field" - } - - suggestedFixes = []analysis.SuggestedFix{ - { - Message: fmt.Sprintf("Move marker %q to field %s", marker.Identifier, fieldName), - TextEdits: []analysis.TextEdit{ - // Remove from type - a.removeMarker(marker), - // Add to field - { - Pos: insertPos, - End: insertPos, - NewText: []byte(fmt.Sprintf("// +%s\n\t", cleanMarkerString(marker))), - }, - }, - }, - { - Message: fmt.Sprintf("Remove marker %q", marker.Identifier), - TextEdits: []analysis.TextEdit{ - a.removeMarker(marker), - }, - }, - } + scopeMsg := strings.Join(allowedScopes, " or ") + if len(allowedScopes) == 0 { + scopeMsg = "unknown scope" } pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q can only be applied to fields", marker.Identifier), - SuggestedFixes: suggestedFixes, + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q can only be applied to %s", marker.Identifier, scopeMsg), }) } @@ -215,30 +135,17 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark fieldMarkers := markersAccess.FieldMarkers(field) for _, marker := range fieldMarkers.UnsortedList() { - scope, ok := a.markerScopes[marker.Identifier] + rule, ok := a.markerRules[marker.Identifier] if !ok { - // No scope defined for this marker, skip validation + // No rule defined for this marker, skip validation continue } - switch scope { - case ScopeType: - a.reportAndRemoveMarker(pass, marker, "type definitions") - case ScopeTypeOrObjectField: - // Check if field type is an object type (struct or map) - if !isMapType(field.Type) && !utils.IsStructType(pass, field.Type) { - // Check if it's a struct type and we can move the marker there - typeSpec := a.getStructTypeSpec(pass, field.Type) - - if typeSpec != nil { - // We can move the marker to the struct definition - a.reportAndMoveMarkerToType(pass, marker, typeSpec) - } else { - // Can't find struct type, just offer removal - a.reportAndRemoveMarker(pass, marker, "type definitions or object fields (struct/map)") - } - } + // Check if FieldScope is allowed + if !rule.Scope.Allows(FieldScope) { + a.reportScopeViolation(pass, marker, rule) } + // TODO: Add type constraint validation here } } @@ -257,182 +164,18 @@ func (a *analyzer) checkTypeMarkers(pass *analysis.Pass, genDecl *ast.GenDecl, m typeMarkers := markersAccess.TypeMarkers(typeSpec) for _, marker := range typeMarkers.UnsortedList() { - scope, ok := a.markerScopes[marker.Identifier] + rule, ok := a.markerRules[marker.Identifier] if !ok { - // No scope defined for this marker, skip validation + // No rule defined for this marker, skip validation continue } - if scope == ScopeField { - // For field-only markers on types, try to move to a field using this type - targetField := a.findFieldUsingType(pass, typeSpec.Name.Name) - if targetField != nil { - a.reportAndMoveMarkerToField(pass, marker, targetField) - } else { - // No suitable field found, just suggest removal - a.reportAndRemoveMarker(pass, marker, "fields") - } + // Check if TypeScope is allowed + if !rule.Scope.Allows(TypeScope) { + a.reportScopeViolation(pass, marker, rule) } + // TODO: Add type constraint validation here } } } -// isMapType checks if the given expression is a map type -func isMapType(expr ast.Expr) bool { - _, ok := expr.(*ast.MapType) - return ok -} - -// getStructTypeSpec finds the type spec for a given field type if it's a struct -func (a *analyzer) getStructTypeSpec(pass *analysis.Pass, expr ast.Expr) *ast.TypeSpec { - // Check if it's a struct type first - if !utils.IsStructType(pass, expr) { - return nil - } - - // Get the identifier from the expression - var ident *ast.Ident - switch t := expr.(type) { - case *ast.Ident: - ident = t - case *ast.StarExpr: - // Handle pointer types - if identExpr, ok := t.X.(*ast.Ident); ok { - ident = identExpr - } - default: - return nil - } - - if ident == nil { - return nil - } - - // Use utils.LookupTypeSpec to find the type declaration - typeSpec, ok := utils.LookupTypeSpec(pass, ident) - if !ok { - return nil - } - - return typeSpec -} - -// findFieldUsingType finds a field that uses the given type name -func (a *analyzer) findFieldUsingType(pass *analysis.Pass, typeName string) *ast.Field { - inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - if !ok { - return nil - } - - var result *ast.Field - - // Find field nodes that use this type - nodeFilter := []ast.Node{ - (*ast.Field)(nil), - } - - inspect.Preorder(nodeFilter, func(n ast.Node) { - field, ok := n.(*ast.Field) - if !ok || result != nil { - return - } - - // Check if this field uses the type we're looking for - fieldTypeName := getFieldTypeName(field.Type) - if fieldTypeName == typeName { - result = field - } - }) - - return result -} - -// getTypeInsertPos calculates the insertion position for a type declaration -func (a *analyzer) getTypeInsertPos(pass *analysis.Pass, typeSpec *ast.TypeSpec) token.Pos { - inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - if !ok { - return token.NoPos - } - - var genDecl *ast.GenDecl - nodeFilter := []ast.Node{ - (*ast.GenDecl)(nil), - } - - inspect.Preorder(nodeFilter, func(n ast.Node) { - if genDecl != nil { - return - } - gd, ok := n.(*ast.GenDecl) - if !ok || gd.Tok != token.TYPE { - return - } - - for _, spec := range gd.Specs { - if spec == typeSpec { - genDecl = gd - return - } - } - }) - - if genDecl == nil { - return token.NoPos - } - - // Calculate insertion position (before the type declaration) - if genDecl.Doc != nil && len(genDecl.Doc.List) > 0 { - // Insert after existing comments - lastComment := genDecl.Doc.List[len(genDecl.Doc.List)-1] - return lastComment.End() + 1 // After last comment + newline - } - // Insert before the type keyword - return genDecl.Pos() -} - -// getFieldInsertPos calculates the insertion position for a field -func (a *analyzer) getFieldInsertPos(field *ast.Field) token.Pos { - // Calculate insertion position (before the field) - if field.Doc != nil && len(field.Doc.List) > 0 { - // Insert after existing field comments - lastComment := field.Doc.List[len(field.Doc.List)-1] - return lastComment.End() + 1 // After last comment + newline - } - // Insert before the field - return field.Pos() -} - -// getFieldTypeName extracts the type name from a field type expression -func getFieldTypeName(expr ast.Expr) string { - switch t := expr.(type) { - case *ast.Ident: - return t.Name - case *ast.StarExpr: - // Handle pointer types - if ident, ok := t.X.(*ast.Ident); ok { - return ident.Name - } - } - return "" -} - -// cleanMarkerString returns the marker string without any // want comments -func cleanMarkerString(marker markershelper.Marker) string { - markerStr := marker.String() - // Remove any // want comment suffix - if idx := strings.Index(markerStr, " //"); idx != -1 { - markerStr = markerStr[:idx] - } - return markerStr -} - -// removeMarker creates a TextEdit that removes a marker -func (a *analyzer) removeMarker(marker markershelper.Marker) analysis.TextEdit { - // For now, just remove the marker text itself - // The go/analysis framework will handle line cleanup automatically in many cases - return analysis.TextEdit{ - Pos: marker.Pos, - End: marker.End + 1, - NewText: []byte(""), - } -} diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index b331e328..c164be52 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -27,14 +27,5 @@ func TestAnalyzerWarnOnly(t *testing.T) { Policy: MarkerScopePolicyWarn, } analyzer := newAnalyzer(cfg) - analysistest.Run(t, testdata, analyzer, "b") -} - -func TestAnalyzerSuggestFix(t *testing.T) { - testdata := analysistest.TestData() - cfg := &MarkerScopeConfig{ - Policy: MarkerScopePolicySuggestFix, - } - analyzer := newAnalyzer(cfg) - analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "b") + analysistest.Run(t, testdata, analyzer, "a") } diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 84889694..692caa3d 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -15,9 +15,88 @@ limitations under the License. */ package markerscope -import "sigs.k8s.io/kube-api-linter/pkg/markers" +import ( + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +// ScopeConstraint defines where a marker is allowed to be placed using bit flags. +type ScopeConstraint uint8 + +const ( + // FieldScope indicates the marker can be placed on fields. + FieldScope ScopeConstraint = 1 << iota + // TypeScope indicates the marker can be placed on type definitions. + TypeScope + + // AnyScope indicates the marker can be placed on either fields or types. + AnyScope = FieldScope | TypeScope +) + +// String returns a human-readable representation of the scope constraint. +func (s ScopeConstraint) String() string { + switch s { + case FieldScope: + return "field" + case TypeScope: + return "type" + case AnyScope: + return "any" + default: + return "unknown" + } +} + +// Allows checks if the given scope is allowed by this constraint. +func (s ScopeConstraint) Allows(scope ScopeConstraint) bool { + return s&scope != 0 +} + +// SchemaType represents OpenAPI schema types that markers can target. +type SchemaType string + +const ( + // SchemaTypeInteger represents integer types (int, int32, int64, uint, etc.) + SchemaTypeInteger SchemaType = "integer" + // SchemaTypeNumber represents floating-point types (float32, float64) + SchemaTypeNumber SchemaType = "number" + // SchemaTypeString represents string types + SchemaTypeString SchemaType = "string" + // SchemaTypeBoolean represents boolean types + SchemaTypeBoolean SchemaType = "boolean" + // SchemaTypeArray represents array/slice types + SchemaTypeArray SchemaType = "array" + // SchemaTypeObject represents struct/map types + SchemaTypeObject SchemaType = "object" +) -// MarkerScope defines where a marker is allowed to be placed. +// TypeConstraint defines what types a marker can be applied to. +// NOTE: This constraint is only used when the marker is placed on a field (not TypeScope). +// Type-level markers (TypeScope) do not use type constraints. +type TypeConstraint struct { + // AllowedSchemaTypes specifies the allowed OpenAPI schema types. + // If nil or empty, any type is allowed. + // Maps to JSONSchemaProps.Type (integer, number, string, boolean, array, object) + AllowedSchemaTypes []SchemaType + + // ElementConstraint specifies constraints on slice/array element types. + // Only applies when AllowSlice or AllowArray is true. + ElementConstraint *TypeConstraint +} + +// MarkerScopeRule defines comprehensive scope validation rules for a marker. +type MarkerScopeRule struct { + // Scope specifies where the marker can be placed (field vs type). + Scope ScopeConstraint + + // TypeConstraint specifies what types the marker can be applied to. + // NOTE: This is used for both field and type scopes, but typically only enforced + // when Scope includes FieldScope. For TypeScope-only markers, this is usually nil. + // If nil, no type constraint is enforced (any type is allowed). + TypeConstraint *TypeConstraint +} + +// MarkerScope defines where a marker is allowed to be placed (legacy). +// Deprecated: Use MarkerScopeRule with ScopeConstraint instead. type MarkerScope string const ( @@ -47,91 +126,309 @@ const ( // MarkerScopeConfig contains configuration for marker scope validation. type MarkerScopeConfig struct { - // Markers maps marker names to their allowed scopes. + // MarkerRules maps marker names to their scope rules with scope and type constraints. // If a marker is not in this map, no scope validation is performed. - Markers map[string]MarkerScope `json:"markers,omitempty"` + MarkerRules map[string]MarkerScopeRule `json:"markerRules,omitempty"` // Policy determines whether to suggest fixes or just warn. Policy MarkerScopePolicy `json:"policy,omitempty"` } -// DefaultMarkerScopes returns the default marker scope configurations. -// ref: https://github.com/kubernetes-sigs/controller-tools/blob/main/pkg/crd/markers/validation.go -func DefaultMarkerScopes() map[string]MarkerScope { - return map[string]MarkerScope{ +// DefaultMarkerRules returns the default marker scope rules with type constraints. +// ref: https://github.com/kubernetes-sigs/controller-tools/blob/v0.19.0/pkg/crd/markers/ +func DefaultMarkerRules() map[string]MarkerScopeRule { + return map[string]MarkerScopeRule{ // Field-only markers (based on controller-tools validation.go) - markers.OptionalMarker: ScopeField, - markers.RequiredMarker: ScopeField, - markers.K8sOptionalMarker: ScopeField, - markers.K8sRequiredMarker: ScopeField, - markers.KubebuilderOptionalMarker: ScopeField, - markers.KubebuilderRequiredMarker: ScopeField, - markers.NullableMarker: ScopeField, - markers.DefaultMarker: ScopeField, - markers.KubebuilderDefaultMarker: ScopeField, - markers.KubebuilderExampleMarker: ScopeField, - "kubebuilder:validation:EmbeddedResource": ScopeField, - markers.KubebuilderSchemaLessMarker: ScopeField, + markers.OptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.RequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.K8sOptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.K8sRequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.NullableMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.DefaultMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.KubebuilderDefaultMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.KubebuilderExampleMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.KubebuilderEmbeddedResourceMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.KubebuilderSchemaLessMarker: {Scope: FieldScope, TypeConstraint: nil}, // Type-only markers (object-level validation and CRD generation) - "kubebuilder:validation:items:ExactlyOneOf": ScopeType, - "kubebuilder:validation:items:AtMostOneOf": ScopeType, - "kubebuilder:validation:items:AtLeastOneOf": ScopeType, - markers.KubebuilderRootMarker: ScopeType, - markers.KubebuilderStatusSubresourceMarker: ScopeType, - - // field-and-type markers - "kubebuilder:pruning:PreserveUnknownFields": ScopeFieldOrType, - "kubebuilder:title": ScopeFieldOrType, - - // numeric markers - markers.KubebuilderMinimumMarker: ScopeField, - markers.KubebuilderMaximumMarker: ScopeField, - markers.KubebuilderExclusiveMaximumMarker: ScopeField, - markers.KubebuilderExclusiveMinimumMarker: ScopeField, - markers.KubebuilderMultipleOfMarker: ScopeField, - - // object markers - markers.KubebuilderMinPropertiesMarker: ScopeTypeOrObjectField, - markers.KubebuilderMaxPropertiesMarker: ScopeTypeOrObjectField, - - // string markers - markers.KubebuilderPatternMarker: ScopeField, - markers.KubebuilderMinLengthMarker: ScopeField, - markers.KubebuilderMaxLengthMarker: ScopeField, - - // array markers - markers.KubebuilderMinItemsMarker: ScopeField, - markers.KubebuilderMaxItemsMarker: ScopeField, - markers.KubebuilderUniqueItemsMarker: ScopeField, - - // general markers - markers.KubebuilderEnumMarker: ScopeField, - markers.KubebuilderFormatMarker: ScopeField, - markers.KubebuilderTypeMarker: ScopeField, - markers.KubebuilderXValidationMarker: ScopeField, - - // Array/slice field markers (Server-Side Apply related) - markers.KubebuilderListTypeMarker: ScopeFieldOrType, - markers.KubebuilderListMapKeyMarker: ScopeFieldOrType, - - // Array items markers (field-only, apply to array elements) - markers.KubebuilderItemsMaxItemsMarker: ScopeField, - markers.KubebuilderItemsMaximumMarker: ScopeField, - markers.KubebuilderItemsMinItemsMarker: ScopeField, - markers.KubebuilderItemsMinLengthMarker: ScopeField, - markers.KubebuilderItemsMinimumMarker: ScopeField, - markers.KubebuilderItemsMaxLengthMarker: ScopeField, - markers.KubebuilderItemsEnumMarker: ScopeField, - markers.KubebuilderItemsFormatMarker: ScopeField, - markers.KubebuilderItemsExclusiveMaximumMarker: ScopeField, - markers.KubebuilderItemsExclusiveMinimumMarker: ScopeField, - markers.KubebuilderItemsMultipleOfMarker: ScopeField, - markers.KubebuilderItemsPatternMarker: ScopeField, - markers.KubebuilderItemsTypeMarker: ScopeField, - markers.KubebuilderItemsUniqueItemsMarker: ScopeField, - markers.KubebuilderItemsXValidationMarker: ScopeField, - markers.KubebuilderItemsMinPropertiesMarker: ScopeTypeOrObjectField, - markers.KubebuilderItemsMaxPropertiesMarker: ScopeTypeOrObjectField, + markers.KubebuilderValidationItemsExactlyOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, + markers.KubebuilderValidationItemsAtMostOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, + markers.KubebuilderValidationItemsAtLeastOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, + + // field-or-type markers + markers.KubebuilderPruningPreserveUnknownFieldsMarker: {Scope: AnyScope, TypeConstraint: nil}, + markers.KubebuilderTitleMarker: {Scope: AnyScope, TypeConstraint: nil}, + + // numeric markers (field or type, integer or number types) + markers.KubebuilderMinimumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + }, + }, + markers.KubebuilderMaximumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + }, + }, + markers.KubebuilderExclusiveMaximumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean}, + }, + }, + markers.KubebuilderExclusiveMinimumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean}, + }, + }, + markers.KubebuilderMultipleOfMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + }, + }, + + // object markers (field or type, object types) + markers.KubebuilderMinPropertiesMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + markers.KubebuilderMaxPropertiesMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + + // string markers (field or type, string types) + markers.KubebuilderPatternMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + markers.KubebuilderMinLengthMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + markers.KubebuilderMaxLengthMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + + // array markers (field or type, array types) + markers.KubebuilderMinItemsMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + markers.KubebuilderMaxItemsMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + markers.KubebuilderUniqueItemsMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + + // general markers (field or type, any type) + markers.KubebuilderEnumMarker: {Scope: AnyScope, TypeConstraint: nil}, + markers.KubebuilderFormatMarker: {Scope: AnyScope, TypeConstraint: nil}, + markers.KubebuilderTypeMarker: {Scope: AnyScope, TypeConstraint: nil}, + markers.KubebuilderXValidationMarker: {Scope: AnyScope, TypeConstraint: nil}, + + // Server-Side Apply topology markers + markers.KubebuilderListTypeMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + markers.KubebuilderListMapKeyMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + markers.KubebuilderMapTypeMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + markers.KubebuilderStructTypeMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + + // Array items markers (field or type, apply to array elements) + // These validate the ELEMENTS of arrays, not the arrays themselves + markers.KubebuilderItemsMaxItemsMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + }, + markers.KubebuilderItemsMaximumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + }, + }, + }, + markers.KubebuilderItemsMinItemsMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + }, + markers.KubebuilderItemsMinLengthMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + }, + markers.KubebuilderItemsMinimumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + }, + }, + }, + markers.KubebuilderItemsMaxLengthMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + }, + markers.KubebuilderItemsEnumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + // Enum can apply to any element type + ElementConstraint: nil, + }, + }, + markers.KubebuilderItemsFormatMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + // Format can apply to various types + ElementConstraint: nil, + }, + }, + markers.KubebuilderItemsExclusiveMaximumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean}, + }, + }, + }, + markers.KubebuilderItemsExclusiveMinimumMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean}, + }, + }, + }, + markers.KubebuilderItemsMultipleOfMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + }, + }, + }, + markers.KubebuilderItemsPatternMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + }, + markers.KubebuilderItemsTypeMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + // Type marker can override any element type + ElementConstraint: nil, + }, + }, + markers.KubebuilderItemsUniqueItemsMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + }, + markers.KubebuilderItemsXValidationMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + // CEL validation can apply to any element type + ElementConstraint: nil, + }, + }, + markers.KubebuilderItemsMinPropertiesMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + }, + markers.KubebuilderItemsMaxPropertiesMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + }, + // TODO crd.go + // TODO package.go } } diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index d4f9dec8..1d69fbbb 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -57,21 +57,61 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList fmt.Sprintf("invalid policy, must be one of: %q, %q", MarkerScopePolicyWarn, MarkerScopePolicySuggestFix))) } - // Validate marker scopes - for marker, scope := range cfg.Markers { - if err := validateScope(scope); err != nil { - fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("markers", marker), scope, err.Error())) + // Validate marker rules + for marker, rule := range cfg.MarkerRules { + if err := validateMarkerRule(rule); err != nil { + fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("markerRules", marker), rule, err.Error())) } } return fieldErrors } -func validateScope(scope MarkerScope) error { - switch scope { - case ScopeField, ScopeType, ScopeFieldOrType, ScopeTypeOrObjectField: - return nil +func validateMarkerRule(rule MarkerScopeRule) error { + // Validate scope constraint + if rule.Scope == 0 { + return fmt.Errorf("scope must be non-zero") + } + + // Validate that scope is a valid combination of FieldScope and/or TypeScope + validScopes := FieldScope | TypeScope + if rule.Scope&^validScopes != 0 { + return fmt.Errorf("invalid scope bits") + } + + // Validate type constraint if present + if rule.TypeConstraint != nil { + if err := validateTypeConstraint(rule.TypeConstraint); err != nil { + return fmt.Errorf("invalid type constraint: %w", err) + } + } + + return nil +} + +func validateTypeConstraint(tc *TypeConstraint) error { + // Validate schema types if specified + for _, st := range tc.AllowedSchemaTypes { + if !isValidSchemaType(st) { + return fmt.Errorf("invalid schema type: %q", st) + } + } + + // Validate element constraint recursively + if tc.ElementConstraint != nil { + if err := validateTypeConstraint(tc.ElementConstraint); err != nil { + return fmt.Errorf("invalid element constraint: %w", err) + } + } + + return nil +} + +func isValidSchemaType(st SchemaType) bool { + switch st { + case SchemaTypeInteger, SchemaTypeNumber, SchemaTypeString, SchemaTypeBoolean, SchemaTypeArray, SchemaTypeObject: + return true default: - return fmt.Errorf("invalid scope %q, must be one of: %q, %q, %q, %q", scope, ScopeField, ScopeType, ScopeFieldOrType, ScopeTypeOrObjectField) + return false } } diff --git a/pkg/analysis/markerscope/testdata/src/a/a.go b/pkg/analysis/markerscope/testdata/src/a/a.go index 2ef7cd18..00fb9915 100644 --- a/pkg/analysis/markerscope/testdata/src/a/a.go +++ b/pkg/analysis/markerscope/testdata/src/a/a.go @@ -15,74 +15,248 @@ limitations under the License. */ package a -// +kubebuilder:validation:MinProperties=1 -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -type ValidTypeMarkers struct { - Name string `json:"name"` -} +// ============================================================================ +// Field-only markers (FieldScope) +// These should ERROR when placed on types +// ============================================================================ -// +optional // want `marker "optional" can only be applied to fields, not type definitions` -// +required // want `marker "required" can only be applied to fields, not type definitions` -// +kubebuilder:validation:Optional // want `marker "kubebuilder:validation:Optional" can only be applied to fields, not type definitions` -type InvalidTypeMarkers struct { +// +optional // want `marker "optional" can only be applied to fields` +// +required // want `marker "required" can only be applied to fields` +// +nullable // want `marker "nullable" can only be applied to fields` +type InvalidFieldOnlyOnType struct { Name string `json:"name"` } -type FieldMarkerTest struct { +type FieldOnlyMarkersTest struct { + // Valid field-only markers // +optional - // +kubebuilder:validation:Optional - ValidOptionalField string `json:"validOptionalField,omitempty"` - // +required - // +kubebuilder:validation:Required - ValidRequiredField string `json:"validRequiredField"` + // +k8s:optional + // +k8s:required + // +nullable + // +kubebuilder:default="default" + // +kubebuilder:validation:Example="example" + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:validation:Schemaless + ValidFieldOnlyMarkers string `json:"validFieldOnlyMarkers"` +} + +// ============================================================================ +// Type-only markers (TypeScope) +// These should ERROR when placed on fields +// ============================================================================ + +type TypeOnlyMarkersTest struct { + // +kubebuilder:validation:items:ExactlyOneOf={field1,field2} // want `marker "kubebuilder:validation:items:ExactlyOneOf" can only be applied to types` + // +kubebuilder:validation:items:AtMostOneOf={field1,field2} // want `marker "kubebuilder:validation:items:AtMostOneOf" can only be applied to types` + // +kubebuilder:validation:items:AtLeastOneOf={field1,field2} // want `marker "kubebuilder:validation:items:AtLeastOneOf" can only be applied to types` + InvalidTypeOnlyOnField string `json:"invalidTypeOnlyOnField"` +} + +// +kubebuilder:validation:items:ExactlyOneOf={Field1,Field2} +// +kubebuilder:validation:items:AtMostOneOf={Field1,Field2} +// +kubebuilder:validation:items:AtLeastOneOf={Field1,Field2} +type ValidTypeOnlyMarkers struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// ============================================================================ +// AnyScope markers - can be on both fields and types +// ============================================================================ + +// +kubebuilder:pruning:PreserveUnknownFields +// +kubebuilder:title="My Title" +type AnyScopeOnType struct { + Name string `json:"name"` +} + +type AnyScopeOnFieldTest struct { + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:title="Field Title" + ValidAnyScopeField map[string]string `json:"validAnyScopeField"` +} - // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties" can only be applied to type definitions, not fields` - InvalidMinPropertiesField map[string]string `json:"invalidMinPropertiesField"` +// ============================================================================ +// Numeric markers (AnyScope, integer/number types) +// ============================================================================ - // +kubebuilder:object:root=true // want `marker "kubebuilder:object:root" can only be applied to type definitions, not fields` - InvalidRootField string `json:"invalidRootField"` +// +kubebuilder:validation:Minimum=0 +// +kubebuilder:validation:Maximum=100 +// +kubebuilder:validation:ExclusiveMinimum=false +// +kubebuilder:validation:ExclusiveMaximum=false +// +kubebuilder:validation:MultipleOf=5 +type NumericType int32 - // +kubebuilder:subresource:status // want `marker "kubebuilder:subresource:status" can only be applied to type definitions, not fields` - InvalidStatusField string `json:"invalidStatusField"` +type NumericMarkersFieldTest struct { + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + // +kubebuilder:validation:ExclusiveMinimum=false + // +kubebuilder:validation:ExclusiveMaximum=false + // +kubebuilder:validation:MultipleOf=5 + ValidNumericField int32 `json:"validNumericField"` - // +kubebuilder:validation:MaxProperties=10 // want `marker "kubebuilder:validation:MaxProperties" can only be applied to type definitions, not fields` - InvalidMaxPropertiesField map[string]string `json:"invalidMaxPropertiesField"` + // +kubebuilder:validation:Minimum=0.0 + // +kubebuilder:validation:Maximum=1.0 + ValidFloatField float64 `json:"validFloatField"` } -// Test markers that can be on both fields and types -// +kubebuilder:default="default-value" +// ============================================================================ +// String markers (AnyScope, string types) +// ============================================================================ + +// +kubebuilder:validation:Pattern="^[a-z]+$" // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=100 -type ValidBothMarkers struct { - // +kubebuilder:default="field-default" - // +kubebuilder:validation:MinLength=5 - // +kubebuilder:validation:MaxLength=50 - Name string `json:"name"` +type StringType string + +type StringMarkersFieldTest struct { + // +kubebuilder:validation:Pattern="^[a-z]+$" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=100 + ValidStringField string `json:"validStringField"` } -// Test array field markers -type ArrayFieldTest struct { +// ============================================================================ +// Array markers (AnyScope, array types) +// ============================================================================ + +// +kubebuilder:validation:MinItems=1 +// +kubebuilder:validation:MaxItems=10 +// +kubebuilder:validation:UniqueItems=true +type StringArray []string + +type ArrayMarkersFieldTest struct { // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 // +kubebuilder:validation:UniqueItems=true + ValidArrayField []string `json:"validArrayField"` +} + +// ============================================================================ +// Object markers (AnyScope, object types) +// ============================================================================ + +// +kubebuilder:validation:MinProperties=1 +// +kubebuilder:validation:MaxProperties=10 +type ObjectType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +type ObjectMarkersFieldTest struct { + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=10 + ValidObjectField map[string]string `json:"validObjectField"` +} + +// ============================================================================ +// General markers (AnyScope, any type) +// ============================================================================ + +// +kubebuilder:validation:Enum=A;B;C +// +kubebuilder:validation:Format=email +// +kubebuilder:validation:Type=string +// +kubebuilder:validation:XValidation:rule="self.size() > 0" +type GeneralType string + +type GeneralMarkersFieldTest struct { + // +kubebuilder:validation:Enum=A;B;C + // +kubebuilder:validation:Format=email + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:XValidation:rule="self.size() > 0" + ValidGeneralField string `json:"validGeneralField"` +} + +// ============================================================================ +// Server-Side Apply topology markers (AnyScope) +// ============================================================================ + +// +listType=map +type ItemList []Item + +// +mapType=granular +type ConfigMap map[string]string + +// +structType=atomic +type AtomicStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +type TopologyMarkersFieldTest struct { // +listType=map // +listMapKey=name - ValidArrayField []Item `json:"validArrayField"` + ValidListMarkers []Item `json:"validListMarkers"` + + // +mapType=granular + ValidMapType map[string]string `json:"validMapType"` + + // +structType=atomic + ValidStruct EmbeddedStruct `json:"validStruct"` +} + +type EmbeddedStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` } type Item struct { Name string `json:"name"` } -// Test custom enum and format markers -// +kubebuilder:validation:Enum=TypeA;TypeB;TypeC -// +kubebuilder:validation:Format=email -type EnumType string +// ============================================================================ +// Array items markers (AnyScope, array with element constraints) +// ============================================================================ + +// +kubebuilder:validation:items:Maximum=100 +// +kubebuilder:validation:items:Minimum=0 +// +kubebuilder:validation:items:MultipleOf=5 +type NumericArrayType []int32 + +// +kubebuilder:validation:items:Pattern="^[a-z]+$" +// +kubebuilder:validation:items:MinLength=1 +// +kubebuilder:validation:items:MaxLength=50 +type StringArrayType []string + +// +kubebuilder:validation:items:MinItems=1 +// +kubebuilder:validation:items:MaxItems=5 +type NestedArrayType [][]string + +// +kubebuilder:validation:items:MinProperties=1 +// +kubebuilder:validation:items:MaxProperties=5 +type ObjectArrayType []map[string]string + +type ArrayItemsMarkersFieldTest struct { + // Numeric element constraints + // +kubebuilder:validation:items:Maximum=100 + // +kubebuilder:validation:items:Minimum=0 + // +kubebuilder:validation:items:MultipleOf=5 + // +kubebuilder:validation:items:ExclusiveMaximum=false + // +kubebuilder:validation:items:ExclusiveMinimum=false + ValidNumericArrayItems []int32 `json:"validNumericArrayItems"` + + // String element constraints + // +kubebuilder:validation:items:Pattern="^[a-z]+$" + // +kubebuilder:validation:items:MinLength=1 + // +kubebuilder:validation:items:MaxLength=50 + ValidStringArrayItems []string `json:"validStringArrayItems"` + + // Nested array constraints + // +kubebuilder:validation:items:MinItems=1 + // +kubebuilder:validation:items:MaxItems=5 + // +kubebuilder:validation:items:UniqueItems=true + ValidNestedArrayItems [][]string `json:"validNestedArrayItems"` + + // Object element constraints + // +kubebuilder:validation:items:MinProperties=1 + // +kubebuilder:validation:items:MaxProperties=5 + ValidObjectArrayItems []map[string]string `json:"validObjectArrayItems"` -type EnumFieldTest struct { - // +kubebuilder:validation:Enum=FieldA;FieldB;FieldC - // +kubebuilder:validation:Format=ipv4 - EnumField string `json:"enumField"` + // General items markers + // +kubebuilder:validation:items:Enum=A;B;C + // +kubebuilder:validation:items:Format=uuid + // +kubebuilder:validation:items:Type=string + // +kubebuilder:validation:items:XValidation:rule="self != ''" + ValidGeneralArrayItems []string `json:"validGeneralArrayItems"` } diff --git a/pkg/analysis/markerscope/testdata/src/a/a.go.golden b/pkg/analysis/markerscope/testdata/src/a/a.go.golden deleted file mode 100644 index 25ca6580..00000000 --- a/pkg/analysis/markerscope/testdata/src/a/a.go.golden +++ /dev/null @@ -1,81 +0,0 @@ -/* -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 a - -// +kubebuilder:validation:MinProperties=1 -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -type ValidTypeMarkers struct { - Name string `json:"name"` -} - -type InvalidTypeMarkers struct { - Name string `json:"name"` -} - -type FieldMarkerTest struct { - // +optional - // +kubebuilder:validation:Optional - ValidOptionalField string `json:"validOptionalField,omitempty"` - - // +required - // +kubebuilder:validation:Required - ValidRequiredField string `json:"validRequiredField"` - - InvalidMinPropertiesField map[string]string `json:"invalidMinPropertiesField"` - - InvalidRootField string `json:"invalidRootField"` - - InvalidStatusField string `json:"invalidStatusField"` - - InvalidMaxPropertiesField map[string]string `json:"invalidMaxPropertiesField"` -} - -// Test markers that can be on both fields and types -// +kubebuilder:default="default-value" -// +kubebuilder:validation:MinLength=1 -// +kubebuilder:validation:MaxLength=100 -type ValidBothMarkers struct { - // +kubebuilder:default="field-default" - // +kubebuilder:validation:MinLength=5 - // +kubebuilder:validation:MaxLength=50 - Name string `json:"name"` -} - -// Test array field markers -type ArrayFieldTest struct { - // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:MaxItems=10 - // +kubebuilder:validation:UniqueItems=true - // +listType=map - // +listMapKey=name - ValidArrayField []Item `json:"validArrayField"` -} - -type Item struct { - Name string `json:"name"` -} - -// Test custom enum and format markers -// +kubebuilder:validation:Enum=TypeA;TypeB;TypeC -// +kubebuilder:validation:Format=email -type EnumType string - -type EnumFieldTest struct { - // +kubebuilder:validation:Enum=FieldA;FieldB;FieldC - // +kubebuilder:validation:Format=ipv4 - EnumField string `json:"enumField"` -} \ No newline at end of file diff --git a/pkg/markers/markers.go b/pkg/markers/markers.go index d1a5c0ac..99924e80 100644 --- a/pkg/markers/markers.go +++ b/pkg/markers/markers.go @@ -156,8 +156,32 @@ const ( // KubebuilderListMapKeyMarker is the marker used to specify the key field for map-type lists. KubebuilderListMapKeyMarker = "listMapKey" + // KubebuilderMapTypeMarker is the marker used to specify the atomicity level of a map. + KubebuilderMapTypeMarker = "mapType" + + // KubebuilderStructTypeMarker is the marker used to specify the atomicity level of a struct. + KubebuilderStructTypeMarker = "structType" + // KubebuilderSchemaLessMarker is the marker that indicates that a struct is schemaless. KubebuilderSchemaLessMarker = "kubebuilder:validation:Schemaless" + + // KubebuilderEmbeddedResourceMarker is the marker that indicates that a field is an embedded resource. + KubebuilderEmbeddedResourceMarker = "kubebuilder:validation:EmbeddedResource" + + // KubebuilderValidationItemsExactlyOneOfMarker is the marker for type-level field constraint. + KubebuilderValidationItemsExactlyOneOfMarker = "kubebuilder:validation:items:ExactlyOneOf" + + // KubebuilderValidationItemsAtMostOneOfMarker is the marker for type-level field constraint. + KubebuilderValidationItemsAtMostOneOfMarker = "kubebuilder:validation:items:AtMostOneOf" + + // KubebuilderValidationItemsAtLeastOneOfMarker is the marker for type-level field constraint. + KubebuilderValidationItemsAtLeastOneOfMarker = "kubebuilder:validation:items:AtLeastOneOf" + + // KubebuilderPruningPreserveUnknownFieldsMarker is the marker for preserving unknown fields during pruning. + KubebuilderPruningPreserveUnknownFieldsMarker = "kubebuilder:pruning:PreserveUnknownFields" + + // KubebuilderTitleMarker is the marker for specifying a title. + KubebuilderTitleMarker = "kubebuilder:title" ) const ( From 1ed3bafc2394d28c0c0814baf6fc00808ad51cd7 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Mon, 13 Oct 2025 18:45:31 +0900 Subject: [PATCH 03/18] Add TypeViolation Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 145 ++++++++++++++++++- pkg/analysis/markerscope/config.go | 26 +--- pkg/analysis/markerscope/testdata/src/a/a.go | 62 +++++++- 3 files changed, 204 insertions(+), 29 deletions(-) diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 8d9151d7..9cf26e7f 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -18,6 +18,7 @@ package markerscope import ( "fmt" "go/ast" + "go/types" "strings" "golang.org/x/tools/go/analysis" @@ -144,8 +145,19 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark // Check if FieldScope is allowed if !rule.Scope.Allows(FieldScope) { a.reportScopeViolation(pass, marker, rule) + continue + } + + // Check type constraints if present + if rule.TypeConstraint != nil { + if err := a.validateFieldTypeConstraint(pass, field, rule.TypeConstraint); err != nil { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), + }) + } } - // TODO: Add type constraint validation here } } @@ -173,9 +185,138 @@ func (a *analyzer) checkTypeMarkers(pass *analysis.Pass, genDecl *ast.GenDecl, m // Check if TypeScope is allowed if !rule.Scope.Allows(TypeScope) { a.reportScopeViolation(pass, marker, rule) + continue + } + + // Check type constraints if present + if rule.TypeConstraint != nil { + if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint); err != nil { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), + }) + } } - // TODO: Add type constraint validation here } } } +// validateFieldTypeConstraint validates that a field's type matches the type constraint +func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.Field, tc *TypeConstraint) error { + // Get the type of the field + tv, ok := pass.TypesInfo.Types[field.Type] + if !ok { + return nil // Skip if we can't determine the type + } + + return validateTypeAgainstConstraint(tv.Type, tc) +} + +// validateTypeSpecTypeConstraint validates that a type spec's type matches the type constraint +func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec *ast.TypeSpec, tc *TypeConstraint) error { + // Get the type of the type spec + obj := pass.TypesInfo.Defs[typeSpec.Name] + if obj == nil { + return nil // Skip if we can't determine the type + } + + typeName, ok := obj.(*types.TypeName) + if !ok { + return nil + } + + return validateTypeAgainstConstraint(typeName.Type(), tc) +} + +// validateTypeAgainstConstraint validates that a Go type satisfies the type constraint +func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { + if tc == nil { + return nil + } + + // Get the schema type from the Go type + schemaType := getSchemaType(t) + + // Check if the schema type is allowed + if len(tc.AllowedSchemaTypes) > 0 { + allowed := false + for _, allowedType := range tc.AllowedSchemaTypes { + if schemaType == allowedType { + allowed = true + break + } + } + if !allowed { + return fmt.Errorf("type %s is not allowed (expected one of: %v)", schemaType, tc.AllowedSchemaTypes) + } + } + + // Validate element constraint for arrays/slices + if tc.ElementConstraint != nil && schemaType == SchemaTypeArray { + elemType := getElementType(t) + if elemType != nil { + if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint); err != nil { + return fmt.Errorf("array element: %w", err) + } + } + } + + return nil +} + +// getSchemaType converts a Go type to an OpenAPI schema type +func getSchemaType(t types.Type) SchemaType { + // Unwrap pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Unwrap named types to get underlying type + if named, ok := t.(*types.Named); ok { + t = named.Underlying() + } + + switch ut := t.Underlying().(type) { + case *types.Basic: + switch ut.Kind() { + case types.Bool: + return SchemaTypeBoolean + case types.Int, types.Int8, types.Int16, types.Int32, types.Int64, + types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64: + return SchemaTypeInteger + case types.Float32, types.Float64: + return SchemaTypeNumber + case types.String: + return SchemaTypeString + } + case *types.Slice, *types.Array: + return SchemaTypeArray + case *types.Map, *types.Struct: + return SchemaTypeObject + } + + return "" +} + +// getElementType returns the element type of an array or slice +func getElementType(t types.Type) types.Type { + // Unwrap pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Unwrap named types to get underlying type + if named, ok := t.(*types.Named); ok { + t = named.Underlying() + } + + switch ut := t.(type) { + case *types.Slice: + return ut.Elem() + case *types.Array: + return ut.Elem() + } + + return nil +} diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 692caa3d..22473cdc 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -95,24 +95,6 @@ type MarkerScopeRule struct { TypeConstraint *TypeConstraint } -// MarkerScope defines where a marker is allowed to be placed (legacy). -// Deprecated: Use MarkerScopeRule with ScopeConstraint instead. -type MarkerScope string - -const ( - // ScopeField indicates the marker can only be placed on fields. - ScopeField MarkerScope = "field" - - // ScopeType indicates the marker can only be placed on type definitions. - ScopeType MarkerScope = "type" - - // ScopeFieldOrType indicates the marker can be placed on either fields or types. - ScopeFieldOrType MarkerScope = "field_or_type" - - // ScopeTypeOrObjectField indicates the marker can be placed on type definitions or object fields (struct/map). - ScopeTypeOrObjectField MarkerScope = "type_or_object_field" -) - // MarkerScopePolicy defines how the linter should handle violations. type MarkerScopePolicy string @@ -175,13 +157,13 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { markers.KubebuilderExclusiveMaximumMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, markers.KubebuilderExclusiveMinimumMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, markers.KubebuilderMultipleOfMarker: { @@ -354,7 +336,7 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, }, @@ -363,7 +345,7 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, }, diff --git a/pkg/analysis/markerscope/testdata/src/a/a.go b/pkg/analysis/markerscope/testdata/src/a/a.go index 00fb9915..150cb5ee 100644 --- a/pkg/analysis/markerscope/testdata/src/a/a.go +++ b/pkg/analysis/markerscope/testdata/src/a/a.go @@ -89,6 +89,7 @@ type AnyScopeOnFieldTest struct { type NumericType int32 type NumericMarkersFieldTest struct { + // Valid: numeric markers on numeric types // +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:Maximum=100 // +kubebuilder:validation:ExclusiveMinimum=false @@ -99,6 +100,14 @@ type NumericMarkersFieldTest struct { // +kubebuilder:validation:Minimum=0.0 // +kubebuilder:validation:Maximum=1.0 ValidFloatField float64 `json:"validFloatField"` + + // Invalid: numeric marker on string field + // +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)` + InvalidMinimumOnString string `json:"invalidMinimumOnString"` + + // Invalid: numeric marker on bool field + // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer number\]\)` + InvalidMaximumOnBool bool `json:"invalidMaximumOnBool"` } // ============================================================================ @@ -111,10 +120,19 @@ type NumericMarkersFieldTest struct { type StringType string type StringMarkersFieldTest struct { + // Valid: string markers on string field // +kubebuilder:validation:Pattern="^[a-z]+$" // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=100 ValidStringField string `json:"validStringField"` + + // Invalid: string marker on int field + // +kubebuilder:validation:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:Pattern": type integer is not allowed \(expected one of: \[string\]\)` + InvalidPatternOnInt int32 `json:"invalidPatternOnInt"` + + // Invalid: string marker on array field + // +kubebuilder:validation:MinLength=5 // want `marker "kubebuilder:validation:MinLength": type array is not allowed \(expected one of: \[string\]\)` + InvalidMinLengthOnArray []string `json:"invalidMinLengthOnArray"` } // ============================================================================ @@ -127,10 +145,19 @@ type StringMarkersFieldTest struct { type StringArray []string type ArrayMarkersFieldTest struct { + // Valid: array markers on array field // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 // +kubebuilder:validation:UniqueItems=true ValidArrayField []string `json:"validArrayField"` + + // Invalid: array marker on string field + // +kubebuilder:validation:MinItems=1 // want `marker "kubebuilder:validation:MinItems": type string is not allowed \(expected one of: \[array\]\)` + InvalidMinItemsOnString string `json:"invalidMinItemsOnString"` + + // Invalid: array marker on object field + // +kubebuilder:validation:MaxItems=10 // want `marker "kubebuilder:validation:MaxItems": type object is not allowed \(expected one of: \[array\]\)` + InvalidMaxItemsOnObject map[string]string `json:"invalidMaxItemsOnObject"` } // ============================================================================ @@ -145,9 +172,18 @@ type ObjectType struct { } type ObjectMarkersFieldTest struct { + // Valid: object markers on map field // +kubebuilder:validation:MinProperties=1 // +kubebuilder:validation:MaxProperties=10 ValidObjectField map[string]string `json:"validObjectField"` + + // Invalid: object marker on string field + // +kubebuilder:validation:MinProperties=2 // want `marker "kubebuilder:validation:MinProperties": type string is not allowed \(expected one of: \[object\]\)` + InvalidMinPropertiesOnString string `json:"invalidMinPropertiesOnString"` + + // Invalid: object marker on array field + // +kubebuilder:validation:MaxProperties=5 // want `marker "kubebuilder:validation:MaxProperties": type array is not allowed \(expected one of: \[object\]\)` + InvalidMaxPropertiesOnArray []string `json:"invalidMaxPropertiesOnArray"` } // ============================================================================ @@ -228,7 +264,7 @@ type NestedArrayType [][]string type ObjectArrayType []map[string]string type ArrayItemsMarkersFieldTest struct { - // Numeric element constraints + // Valid: Numeric element constraints // +kubebuilder:validation:items:Maximum=100 // +kubebuilder:validation:items:Minimum=0 // +kubebuilder:validation:items:MultipleOf=5 @@ -236,27 +272,43 @@ type ArrayItemsMarkersFieldTest struct { // +kubebuilder:validation:items:ExclusiveMinimum=false ValidNumericArrayItems []int32 `json:"validNumericArrayItems"` - // String element constraints + // Valid: String element constraints // +kubebuilder:validation:items:Pattern="^[a-z]+$" // +kubebuilder:validation:items:MinLength=1 // +kubebuilder:validation:items:MaxLength=50 ValidStringArrayItems []string `json:"validStringArrayItems"` - // Nested array constraints + // Valid: Nested array constraints // +kubebuilder:validation:items:MinItems=1 // +kubebuilder:validation:items:MaxItems=5 // +kubebuilder:validation:items:UniqueItems=true ValidNestedArrayItems [][]string `json:"validNestedArrayItems"` - // Object element constraints + // Valid: Object element constraints // +kubebuilder:validation:items:MinProperties=1 // +kubebuilder:validation:items:MaxProperties=5 ValidObjectArrayItems []map[string]string `json:"validObjectArrayItems"` - // General items markers + // Valid: General items markers // +kubebuilder:validation:items:Enum=A;B;C // +kubebuilder:validation:items:Format=uuid // +kubebuilder:validation:items:Type=string // +kubebuilder:validation:items:XValidation:rule="self != ''" ValidGeneralArrayItems []string `json:"validGeneralArrayItems"` + + // Invalid: items:Maximum on string array (element type mismatch) + // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)` + InvalidItemsMaximumOnStringArray []string `json:"invalidItemsMaximumOnStringArray"` + + // Invalid: items:Pattern on int array (element type mismatch) + // +kubebuilder:validation:items:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:items:Pattern": array element: type integer is not allowed \(expected one of: \[string\]\)` + InvalidItemsPatternOnIntArray []int32 `json:"invalidItemsPatternOnIntArray"` + + // Invalid: items:MinProperties on string array (element type mismatch) + // +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": array element: type string is not allowed \(expected one of: \[object\]\)` + InvalidItemsMinPropertiesOnStringArray []string `json:"invalidItemsMinPropertiesOnStringArray"` + + // Invalid: items marker on non-array field + // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": type string is not allowed \(expected one of: \[array\]\)` + InvalidItemsMarkerOnNonArray string `json:"invalidItemsMarkerOnNonArray"` } From ae5012e45ea81bad3dcad0e3d53c0d714586b4ac Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Mon, 13 Oct 2025 21:41:10 +0900 Subject: [PATCH 04/18] add custom marker rule setting Signed-off-by: nayuta-ai --- docs/linters.md | 110 ++++++++++++++++++++++----- pkg/analysis/markerscope/analyzer.go | 28 +++++-- pkg/analysis/markerscope/config.go | 19 ++++- 3 files changed, 130 insertions(+), 27 deletions(-) diff --git a/docs/linters.md b/docs/linters.md index 3b857cc3..1b6d2357 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -386,39 +386,113 @@ lintersConfig: ## MarkerScope -The `markerscope` linter validates that markers are applied in the correct scope. It ensures that markers are placed on appropriate Go language constructs (types, fields) according to their intended usage. +The `markerscope` linter validates that markers are applied in the correct scope and to the correct types. It ensures that markers are placed on appropriate Go language constructs (types, fields) and applied to compatible data types according to their intended usage. + +The linter performs two levels of validation: + +1. **Scope validation**: Ensures markers are placed on the correct location (field vs type) +2. **Type constraint validation**: Ensures markers are applied to compatible data types (e.g., numeric markers on numeric types only) + +### Scope Types The linter defines different scope types for markers: -- **Field-only markers**: Can only be applied to struct fields (e.g., `required`, `kubebuilder:validation:Required`) -- **Type-only markers**: Can only be applied to type definitions -- **Type or Map/Slice fields**: Can be applied to type definitions, map fields, or slice fields (e.g., `kubebuilder:validation:MinProperties`) -- **Field or Type markers**: Can be applied to either fields or type definitions +- **FieldScope**: Can only be applied to struct fields (e.g., `optional`, `required`, `nullable`) +- **TypeScope**: Can only be applied to type definitions (e.g., `kubebuilder:validation:items:ExactlyOneOf`) +- **AnyScope**: Can be applied to either fields or type definitions (e.g., `kubebuilder:validation:Minimum`, `kubebuilder:validation:Pattern`) + +### Type Constraints + +The linter validates that markers are applied to compatible OpenAPI schema types: + +- **Numeric markers** (`Minimum`, `Maximum`, `MultipleOf`): Only for `integer` or `number` types +- **String markers** (`Pattern`, `MinLength`, `MaxLength`): Only for `string` types +- **Array markers** (`MinItems`, `MaxItems`, `UniqueItems`): Only for `array` types +- **Object markers** (`MinProperties`, `MaxProperties`): Only for `object` types (struct/map) +- **Array items markers** (`items:Minimum`, `items:Pattern`, etc.): Apply constraints to array element types + +OpenAPI schema types map to Go types as follows: +- `integer`: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 +- `number`: float32, float64 +- `string`: string +- `boolean`: bool +- `array`: []T, [N]T (slices and arrays) +- `object`: struct, map[K]V -### Default Scope Rules +### Default Marker Rules -By default, the linter enforces these scope rules: +The linter includes built-in rules for all standard kubebuilder markers and k8s declarative validation markers. Examples: -- `required` and `kubebuilder:validation:Required`: Field-only -- `kubebuilder:validation:MinProperties`: Type definitions, map fields, or slice fields only +**Field-only markers:** +- `optional`, `required`, `nullable` +- `kubebuilder:default`, `kubebuilder:validation:Example` + +**Type-only markers:** +- `kubebuilder:validation:items:ExactlyOneOf` +- `kubebuilder:validation:items:AtMostOneOf` +- `kubebuilder:validation:items:AtLeastOneOf` + +**AnyScope markers with type constraints:** +- `kubebuilder:validation:Minimum` (integer/number types only) +- `kubebuilder:validation:Pattern` (string types only) +- `kubebuilder:validation:MinItems` (array types only) +- `kubebuilder:validation:MinProperties` (object types only) + +**AnyScope markers without type constraints:** +- `kubebuilder:validation:Enum`, `kubebuilder:validation:Format` +- `kubebuilder:pruning:PreserveUnknownFields`, `kubebuilder:title` ### Configuration +You can customize marker rules or add support for custom markers: + ```yaml lintersConfig: markerscope: - policy: SuggestFix | Warn # The policy for marker scope violations. Defaults to `SuggestFix`. -``` + policy: Warn | SuggestFix # The policy for marker scope violations. Defaults to `Warn`. + markerRules: + # Override default rule for a built-in marker + "optional": + scope: field # or: type, any + + # Add a custom marker with scope constraint only + "mycompany:validation:CustomMarker": + scope: any + + # Add a custom marker with scope and type constraints + "mycompany:validation:NumericLimit": + scope: any + typeConstraint: + allowedSchemaTypes: + - integer + - number + + # Add a custom array items marker with element type constraint + "mycompany:validation:items:StringFormat": + scope: any + typeConstraint: + allowedSchemaTypes: + - array + elementConstraint: + allowedSchemaTypes: + - string +``` + +**Scope values:** +- `field`: FieldScope - marker can only be on fields +- `type`: TypeScope - marker can only be on types +- `any`: AnyScope - marker can be on fields or types + +**Type constraint fields:** +- `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `number`, `string`, `boolean`, `array`, `object`) +- `elementConstraint`: Nested constraint for array element types (only valid when `allowedSchemaTypes` includes `array`) + +If a marker is not in `markerRules` and not in the default rules, no validation is performed for that marker. +If a marker is in both `markerRules` and the default rules, your configuration takes precedence. ### Fixes -The `markerscope` linter can automatically fix scope violations when `policy` is set to `SuggestFix`: - -1. **Remove incorrect markers**: Suggests removing markers that are in the wrong scope -2. **Move markers to correct locations**: - - Move field-only markers from types to appropriate fields - - Move type-only markers from fields to their corresponding type definitions -3. **Preserve marker values**: When moving markers like `kubebuilder:validation:MinProperties=1` +The `markerscope` linter does not currently provide automatic fixes. It reports violations as warnings or errors based on the configured policy. **Note**: This linter is not enabled by default and must be explicitly enabled in the configuration. diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 9cf26e7f..8b823d9b 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -33,6 +33,7 @@ const ( name = "markerscope" ) +// TODO: SuggestFix func init() { // Register all markers we want to validate scope for defaults := DefaultMarkerRules() @@ -55,17 +56,10 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { } a := &analyzer{ - markerRules: DefaultMarkerRules(), + markerRules: mergeMarkerRules(DefaultMarkerRules(), cfg.MarkerRules), policy: cfg.Policy, } - // Override with custom rules if provided - if cfg.MarkerRules != nil { - for marker, rule := range cfg.MarkerRules { - a.markerRules[marker] = rule - } - } - // Set default policy if not specified if a.policy == "" { a.policy = MarkerScopePolicyWarn @@ -80,6 +74,24 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { } } +// mergeMarkerRules merges custom marker rules with default marker rules. +// Custom rules take precedence over default rules for the same marker. +func mergeMarkerRules(defaults, custom map[string]MarkerScopeRule) map[string]MarkerScopeRule { + merged := make(map[string]MarkerScopeRule, len(defaults)+len(custom)) + + // Copy all default rules + for marker, rule := range defaults { + merged[marker] = rule + } + + // Override with custom rules + for marker, rule := range custom { + merged[marker] = rule + } + + return merged +} + func (a *analyzer) run(pass *analysis.Pass) (any, error) { inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) if !ok { diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 22473cdc..73b9ea3b 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -109,7 +109,19 @@ const ( // MarkerScopeConfig contains configuration for marker scope validation. type MarkerScopeConfig struct { // MarkerRules maps marker names to their scope rules with scope and type constraints. - // If a marker is not in this map, no scope validation is performed. + // This map can be used to: + // - Override default rules for built-in markers (from DefaultMarkerRules) + // - Add rules for custom markers not included in DefaultMarkerRules + // + // If a marker is not in this map AND not in DefaultMarkerRules(), no scope validation is performed. + // If a marker is in both this map and DefaultMarkerRules(), this map takes precedence. + // + // Example: Adding a custom marker + // markerRules: + // "mycompany:validation:CustomMarker": + // scope: any + // typeConstraint: + // allowedSchemaTypes: ["string"] MarkerRules map[string]MarkerScopeRule `json:"markerRules,omitempty"` // Policy determines whether to suggest fixes or just warn. @@ -117,6 +129,11 @@ type MarkerScopeConfig struct { } // DefaultMarkerRules returns the default marker scope rules with type constraints. +// These rules are based on kubebuilder markers and k8s declarative validation markers. +// +// Users can override these rules or add custom markers by providing a MarkerScopeConfig +// with MarkerRules that will be merged with (and take precedence over) these defaults. +// // ref: https://github.com/kubernetes-sigs/controller-tools/blob/v0.19.0/pkg/crd/markers/ func DefaultMarkerRules() map[string]MarkerScopeRule { return map[string]MarkerScopeRule{ From 853918db112dc04b893705392bd300d90ca2dff1 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Sun, 19 Oct 2025 19:49:26 +0900 Subject: [PATCH 05/18] Add custom marker rules and validation logic. Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 317 +++++++++++++----- pkg/analysis/markerscope/analyzer_test.go | 52 +++ pkg/analysis/markerscope/config.go | 226 ++++++++++--- pkg/analysis/markerscope/doc.go | 2 +- pkg/analysis/markerscope/errors.go | 25 ++ pkg/analysis/markerscope/initializer.go | 10 +- pkg/analysis/markerscope/initializer_test.go | 247 ++++++++++++++ .../markerscope/markerscope_suite_test.go | 29 ++ pkg/analysis/markerscope/testdata/src/b/b.go | 66 +++- pkg/analysis/markerscope/testdata/src/c/c.go | 41 +++ .../src/{b/b.go.golden => c/c.go.golden} | 10 +- pkg/analysis/utils/utils.go | 41 +++ 12 files changed, 917 insertions(+), 149 deletions(-) create mode 100644 pkg/analysis/markerscope/errors.go create mode 100644 pkg/analysis/markerscope/initializer_test.go create mode 100644 pkg/analysis/markerscope/markerscope_suite_test.go create mode 100644 pkg/analysis/markerscope/testdata/src/c/c.go rename pkg/analysis/markerscope/testdata/src/{b/b.go.golden => c/c.go.golden} (90%) diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 8b823d9b..9f5852ab 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -19,6 +19,8 @@ import ( "fmt" "go/ast" "go/types" + "maps" + "slices" "strings" "golang.org/x/tools/go/analysis" @@ -27,20 +29,23 @@ import ( kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers" + "sigs.k8s.io/kube-api-linter/pkg/analysis/utils" ) const ( name = "markerscope" ) -// TODO: SuggestFix +// TODO: SuggestFix. func init() { // Register all markers we want to validate scope for defaults := DefaultMarkerRules() markers := make([]string, 0, len(defaults)) + for marker := range defaults { markers = append(markers, marker) } + markershelper.DefaultRegistry().Register(markers...) } @@ -60,6 +65,13 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { policy: cfg.Policy, } + // Register all markers (both default and custom) with the markers helper + // This must be done before the analyzer runs because the markers helper + // analyzer needs to know about these markers + for marker := range a.markerRules { + markershelper.DefaultRegistry().Register(marker) + } + // Set default policy if not specified if a.policy == "" { a.policy = MarkerScopePolicyWarn @@ -80,14 +92,10 @@ func mergeMarkerRules(defaults, custom map[string]MarkerScopeRule) map[string]Ma merged := make(map[string]MarkerScopeRule, len(defaults)+len(custom)) // Copy all default rules - for marker, rule := range defaults { - merged[marker] = rule - } + maps.Copy(merged, defaults) // Override with custom rules - for marker, rule := range custom { - merged[marker] = rule - } + maps.Copy(merged, custom) return merged } @@ -118,32 +126,10 @@ func (a *analyzer) run(pass *analysis.Pass) (any, error) { } }) - return nil, nil -} - -// reportScopeViolation reports a scope violation error -func (a *analyzer) reportScopeViolation(pass *analysis.Pass, marker markershelper.Marker, rule MarkerScopeRule) { - var allowedScopes []string - if rule.Scope&FieldScope != 0 { - allowedScopes = append(allowedScopes, "fields") - } - if rule.Scope&TypeScope != 0 { - allowedScopes = append(allowedScopes, "types") - } - - scopeMsg := strings.Join(allowedScopes, " or ") - if len(allowedScopes) == 0 { - scopeMsg = "unknown scope" - } - - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q can only be applied to %s", marker.Identifier, scopeMsg), - }) + return nil, nil //nolint:nilnil } -// checkFieldMarkers checks markers on fields for violations +// checkFieldMarkers checks markers on fields for violations. func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) { fieldMarkers := markersAccess.FieldMarkers(field) @@ -156,24 +142,54 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark // Check if FieldScope is allowed if !rule.Scope.Allows(FieldScope) { - a.reportScopeViolation(pass, marker, rule) + var message string + + var fixes []analysis.SuggestedFix + + if rule.Scope == TypeScope { + message = fmt.Sprintf("marker %q can only be applied to types", marker.Identifier) + + if a.policy == MarkerScopePolicySuggestFix { + fixes = a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule) + } + } else { + // This shouldn't happen in practice, but handle it gracefully + message = fmt.Sprintf("marker %q cannot be applied to fields", marker.Identifier) + } + + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: message, + SuggestedFixes: fixes, + }) + continue } // Check type constraints if present if rule.TypeConstraint != nil { if err := a.validateFieldTypeConstraint(pass, field, rule.TypeConstraint); err != nil { - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), - }) + if a.policy == MarkerScopePolicySuggestFix { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), + SuggestedFixes: a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule), + }) + } else { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), + }) + } } } } } -// checkTypeMarkers checks markers on types for violations +// checkTypeMarkers checks markers on types for violations. func (a *analyzer) checkTypeMarkers(pass *analysis.Pass, genDecl *ast.GenDecl, markersAccess markershelper.Markers) { if len(genDecl.Specs) == 0 { return @@ -185,36 +201,78 @@ func (a *analyzer) checkTypeMarkers(pass *analysis.Pass, genDecl *ast.GenDecl, m continue } - typeMarkers := markersAccess.TypeMarkers(typeSpec) + a.checkSingleTypeMarkers(pass, typeSpec, markersAccess) + } +} - for _, marker := range typeMarkers.UnsortedList() { - rule, ok := a.markerRules[marker.Identifier] - if !ok { - // No rule defined for this marker, skip validation - continue - } +// checkSingleTypeMarkers checks markers on a single type for violations. +func (a *analyzer) checkSingleTypeMarkers(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markershelper.Markers) { + typeMarkers := markersAccess.TypeMarkers(typeSpec) - // Check if TypeScope is allowed - if !rule.Scope.Allows(TypeScope) { - a.reportScopeViolation(pass, marker, rule) - continue - } + for _, marker := range typeMarkers.UnsortedList() { + rule, ok := a.markerRules[marker.Identifier] + if !ok { + // No rule defined for this marker, skip validation + continue + } - // Check type constraints if present - if rule.TypeConstraint != nil { - if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint); err != nil { - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), - }) - } - } + // Check if TypeScope is allowed + if !rule.Scope.Allows(TypeScope) { + a.reportTypeScopeViolation(pass, typeSpec, marker, rule) + continue + } + + // Check type constraints if present + if rule.TypeConstraint != nil { + a.checkTypeConstraintViolation(pass, typeSpec, marker, rule) } } } -// validateFieldTypeConstraint validates that a field's type matches the type constraint +// reportTypeScopeViolation reports a scope violation for a type marker. +func (a *analyzer) reportTypeScopeViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) { + var message string + + var fixes []analysis.SuggestedFix + + if rule.Scope == FieldScope { + message = fmt.Sprintf("marker %q can only be applied to fields", marker.Identifier) + + if a.policy == MarkerScopePolicySuggestFix { + fixes = a.suggestMoveToField(pass, typeSpec, marker, rule) + } + } else { + message = fmt.Sprintf("marker %q cannot be applied to types", marker.Identifier) + } + + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: message, + SuggestedFixes: fixes, + }) +} + +// checkTypeConstraintViolation checks and reports type constraint violations. +func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) { + if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint); err != nil { + var fixes []analysis.SuggestedFix + + if a.policy == MarkerScopePolicySuggestFix { + fixes = a.suggestMoveToField(pass, typeSpec, marker, rule) + } + + message := fmt.Sprintf("marker %q: %s", marker.Identifier, err) + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: message, + SuggestedFixes: fixes, + }) + } +} + +// validateFieldTypeConstraint validates that a field's type matches the type constraint. func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.Field, tc *TypeConstraint) error { // Get the type of the field tv, ok := pass.TypesInfo.Types[field.Type] @@ -222,10 +280,14 @@ func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.F return nil // Skip if we can't determine the type } - return validateTypeAgainstConstraint(tv.Type, tc) + if err := validateTypeAgainstConstraint(tv.Type, tc); err != nil { + return err + } + + return nil } -// validateTypeSpecTypeConstraint validates that a type spec's type matches the type constraint +// validateTypeSpecTypeConstraint validates that a type spec's type matches the type constraint. func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec *ast.TypeSpec, tc *TypeConstraint) error { // Get the type of the type spec obj := pass.TypesInfo.Defs[typeSpec.Name] @@ -241,7 +303,7 @@ func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec return validateTypeAgainstConstraint(typeName.Type(), tc) } -// validateTypeAgainstConstraint validates that a Go type satisfies the type constraint +// validateTypeAgainstConstraint validates that a Go type satisfies the type constraint. func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { if tc == nil { return nil @@ -252,15 +314,8 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { // Check if the schema type is allowed if len(tc.AllowedSchemaTypes) > 0 { - allowed := false - for _, allowedType := range tc.AllowedSchemaTypes { - if schemaType == allowedType { - allowed = true - break - } - } - if !allowed { - return fmt.Errorf("type %s is not allowed (expected one of: %v)", schemaType, tc.AllowedSchemaTypes) + if !slices.Contains(tc.AllowedSchemaTypes, schemaType) { + return fmt.Errorf("%w: type %s (expected one of: %v)", errTypeNotAllowed, schemaType, tc.AllowedSchemaTypes) } } @@ -277,7 +332,9 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { return nil } -// getSchemaType converts a Go type to an OpenAPI schema type +// getSchemaType converts a Go type to an OpenAPI schema type. +// +//nolint:cyclop // This function has many cases for different Go types func getSchemaType(t types.Type) SchemaType { // Unwrap pointer types if ptr, ok := t.(*types.Pointer); ok { @@ -301,6 +358,11 @@ func getSchemaType(t types.Type) SchemaType { return SchemaTypeNumber case types.String: return SchemaTypeString + case types.Invalid, types.Uintptr, types.Complex64, types.Complex128, + types.UnsafePointer, types.UntypedBool, types.UntypedInt, types.UntypedRune, + types.UntypedFloat, types.UntypedComplex, types.UntypedString, types.UntypedNil: + // These types are not supported in OpenAPI schemas + return "" } case *types.Slice, *types.Array: return SchemaTypeArray @@ -311,7 +373,7 @@ func getSchemaType(t types.Type) SchemaType { return "" } -// getElementType returns the element type of an array or slice +// getElementType returns the element type of an array or slice. func getElementType(t types.Type) types.Type { // Unwrap pointer types if ptr, ok := t.(*types.Pointer); ok { @@ -332,3 +394,108 @@ func getElementType(t types.Type) types.Type { return nil } + +// extractIdent extracts an *ast.Ident from an ast.Expr, unwrapping pointers and arrays. +func extractIdent(expr ast.Expr) *ast.Ident { + switch e := expr.(type) { + case *ast.Ident: + return e + case *ast.StarExpr: + return extractIdent(e.X) + case *ast.ArrayType: + return extractIdent(e.Elt) + default: + return nil + } +} + +func (a *analyzer) suggestMoveToField(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) []analysis.SuggestedFix { + // Only suggest moving to field if FieldScope is allowed + if !rule.Scope.Allows(FieldScope) { + return nil + } + + fieldTypeSpecs := utils.LookupFieldsUsingType(pass, typeSpec) + fmt.Println("fieldTypeSpecs", fieldTypeSpecs) + + var edits []analysis.TextEdit + + // Remove marker from current field (including the newline) + edits = append(edits, analysis.TextEdit{ + Pos: marker.Pos, + End: marker.End + 1, + }) + + for _, fieldTypeSpec := range fieldTypeSpecs { + // Add marker to the line before the type definition + markerText := a.extractMarkerText(marker) + + file := pass.Fset.File(fieldTypeSpec.Pos()) + if file != nil { + lineStart := file.LineStart(file.Line(fieldTypeSpec.Pos())) + edits = append(edits, analysis.TextEdit{ + Pos: lineStart, + End: lineStart, + NewText: []byte(markerText), + }) + } + } + + return []analysis.SuggestedFix{ + { + Message: "Move marker to field definition", + TextEdits: edits, + }, + } +} + +// suggestMoveToFieldsIfCompatible generates suggested fixes to move a marker from type to compatible fields. +func (a *analyzer) suggestMoveToFieldsIfCompatible(pass *analysis.Pass, field *ast.Field, marker markershelper.Marker, rule MarkerScopeRule) []analysis.SuggestedFix { + // Only suggest moving to type if TypeScope is allowed + if !rule.Scope.Allows(TypeScope) { + return nil + } + + // Extract identifier from field type + ident := extractIdent(field.Type) + if ident == nil { + return nil + } + + fieldTypeSpec, ok := utils.LookupTypeSpec(pass, ident) + if !ok { + return nil + } + + var edits []analysis.TextEdit + + // Remove marker from current field (including the newline) + edits = append(edits, analysis.TextEdit{ + Pos: marker.Pos, + End: marker.End + 1, + }) + + // Add marker to the line before the type definition + markerText := a.extractMarkerText(marker) + + file := pass.Fset.File(fieldTypeSpec.Pos()) + if file != nil { + lineStart := file.LineStart(file.Line(fieldTypeSpec.Pos())) + edits = append(edits, analysis.TextEdit{ + Pos: lineStart, + End: lineStart, + NewText: []byte(markerText), + }) + } + + return []analysis.SuggestedFix{ + { + Message: "Move marker to type definition", + TextEdits: edits, + }, + } +} + +func (a *analyzer) extractMarkerText(marker markershelper.Marker) string { + return strings.Split(marker.RawComment, " //")[0] + "\n" +} diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index c164be52..26852525 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -29,3 +29,55 @@ func TestAnalyzerWarnOnly(t *testing.T) { analyzer := newAnalyzer(cfg) analysistest.Run(t, testdata, analyzer, "a") } + +func TestAnalyzerWithCustomMarkers(t *testing.T) { + testdata := analysistest.TestData() + cfg := &MarkerScopeConfig{ + Policy: MarkerScopePolicyWarn, + MarkerRules: map[string]MarkerScopeRule{ + // Custom field-only marker + "custom:field-only": { + Scope: FieldScope, + }, + // Custom type-only marker + "custom:type-only": { + Scope: TypeScope, + }, + // Custom marker with string type constraint + "custom:string-only": { + Scope: FieldScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + // Custom marker with integer type constraint + "custom:integer-only": { + Scope: FieldScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + // Custom marker with array of strings constraint + "custom:string-array": { + Scope: FieldScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + }, + }, + } + analyzer := newAnalyzer(cfg) + analysistest.Run(t, testdata, analyzer, "b") +} + +func TestAnalyzerWithSuggestFix(t *testing.T) { + testdata := analysistest.TestData() + cfg := &MarkerScopeConfig{ + Policy: MarkerScopePolicySuggestFix, + } + analyzer := newAnalyzer(cfg) + analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "c") +} diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 73b9ea3b..250aef1d 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -16,6 +16,8 @@ limitations under the License. package markerscope import ( + "maps" + "sigs.k8s.io/kube-api-linter/pkg/markers" ) @@ -57,15 +59,15 @@ type SchemaType string const ( // SchemaTypeInteger represents integer types (int, int32, int64, uint, etc.) SchemaTypeInteger SchemaType = "integer" - // SchemaTypeNumber represents floating-point types (float32, float64) + // SchemaTypeNumber represents floating-point types (float32, float64). SchemaTypeNumber SchemaType = "number" - // SchemaTypeString represents string types + // SchemaTypeString represents string types. SchemaTypeString SchemaType = "string" - // SchemaTypeBoolean represents boolean types + // SchemaTypeBoolean represents boolean types. SchemaTypeBoolean SchemaType = "boolean" - // SchemaTypeArray represents array/slice types + // SchemaTypeArray represents array/slice types. SchemaTypeArray SchemaType = "array" - // SchemaTypeObject represents struct/map types + // SchemaTypeObject represents struct/map types. SchemaTypeObject SchemaType = "object" ) @@ -136,7 +138,28 @@ type MarkerScopeConfig struct { // // ref: https://github.com/kubernetes-sigs/controller-tools/blob/v0.19.0/pkg/crd/markers/ func DefaultMarkerRules() map[string]MarkerScopeRule { - return map[string]MarkerScopeRule{ + rules := make(map[string]MarkerScopeRule) + + addFieldOnlyMarkers(rules) + addTypeOnlyMarkers(rules) + addFieldOrTypeMarkers(rules) + addNumericMarkers(rules) + addObjectMarkers(rules) + addStringMarkers(rules) + addArrayMarkers(rules) + addGeneralMarkers(rules) + addSSATopologyMarkers(rules) + addArrayItemsMarkers(rules) + + // TODO crd.go + // TODO package.go + + return rules +} + +// addFieldOnlyMarkers adds field-only markers based on controller-tools validation.go. +func addFieldOnlyMarkers(rules map[string]MarkerScopeRule) { + fieldOnlyMarkers := map[string]MarkerScopeRule{ // Field-only markers (based on controller-tools validation.go) markers.OptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, markers.RequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, @@ -148,16 +171,37 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { markers.KubebuilderExampleMarker: {Scope: FieldScope, TypeConstraint: nil}, markers.KubebuilderEmbeddedResourceMarker: {Scope: FieldScope, TypeConstraint: nil}, markers.KubebuilderSchemaLessMarker: {Scope: FieldScope, TypeConstraint: nil}, + } + + maps.Copy(rules, fieldOnlyMarkers) +} +// addTypeOnlyMarkers adds type-only markers for object-level validation and CRD generation. +func addTypeOnlyMarkers(rules map[string]MarkerScopeRule) { + typeOnlyMarkers := map[string]MarkerScopeRule{ // Type-only markers (object-level validation and CRD generation) markers.KubebuilderValidationItemsExactlyOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, markers.KubebuilderValidationItemsAtMostOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, markers.KubebuilderValidationItemsAtLeastOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, + } + + maps.Copy(rules, typeOnlyMarkers) +} +// addFieldOrTypeMarkers adds markers that can be applied to both fields and types. +func addFieldOrTypeMarkers(rules map[string]MarkerScopeRule) { + fieldOrTypeMarkers := map[string]MarkerScopeRule{ // field-or-type markers markers.KubebuilderPruningPreserveUnknownFieldsMarker: {Scope: AnyScope, TypeConstraint: nil}, markers.KubebuilderTitleMarker: {Scope: AnyScope, TypeConstraint: nil}, + } + maps.Copy(rules, fieldOrTypeMarkers) +} + +// addNumericMarkers adds numeric validation markers for integer and number types. +func addNumericMarkers(rules map[string]MarkerScopeRule) { + numericMarkers := map[string]MarkerScopeRule{ // numeric markers (field or type, integer or number types) markers.KubebuilderMinimumMarker: { Scope: AnyScope, @@ -189,7 +233,14 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, + } + maps.Copy(rules, numericMarkers) +} + +// addObjectMarkers adds object validation markers for struct and map types. +func addObjectMarkers(rules map[string]MarkerScopeRule) { + objectMarkers := map[string]MarkerScopeRule{ // object markers (field or type, object types) markers.KubebuilderMinPropertiesMarker: { Scope: AnyScope, @@ -203,7 +254,14 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, }, + } + + maps.Copy(rules, objectMarkers) +} +// addStringMarkers adds string validation markers. +func addStringMarkers(rules map[string]MarkerScopeRule) { + stringMarkers := map[string]MarkerScopeRule{ // string markers (field or type, string types) markers.KubebuilderPatternMarker: { Scope: AnyScope, @@ -223,7 +281,14 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, + } + + maps.Copy(rules, stringMarkers) +} +// addArrayMarkers adds array validation markers. +func addArrayMarkers(rules map[string]MarkerScopeRule) { + arrayMarkers := map[string]MarkerScopeRule{ // array markers (field or type, array types) markers.KubebuilderMinItemsMarker: { Scope: AnyScope, @@ -243,13 +308,27 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, + } + maps.Copy(rules, arrayMarkers) +} + +// addGeneralMarkers adds general markers that can apply to any type. +func addGeneralMarkers(rules map[string]MarkerScopeRule) { + generalMarkers := map[string]MarkerScopeRule{ // general markers (field or type, any type) markers.KubebuilderEnumMarker: {Scope: AnyScope, TypeConstraint: nil}, markers.KubebuilderFormatMarker: {Scope: AnyScope, TypeConstraint: nil}, markers.KubebuilderTypeMarker: {Scope: AnyScope, TypeConstraint: nil}, markers.KubebuilderXValidationMarker: {Scope: AnyScope, TypeConstraint: nil}, + } + + maps.Copy(rules, generalMarkers) +} +// addSSATopologyMarkers adds Server-Side Apply topology markers. +func addSSATopologyMarkers(rules map[string]MarkerScopeRule) { + ssaMarkers := map[string]MarkerScopeRule{ // Server-Side Apply topology markers markers.KubebuilderListTypeMarker: { Scope: AnyScope, @@ -275,19 +354,34 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, }, + } - // Array items markers (field or type, apply to array elements) - // These validate the ELEMENTS of arrays, not the arrays themselves - markers.KubebuilderItemsMaxItemsMarker: { + maps.Copy(rules, ssaMarkers) +} + +// addArrayItemsMarkers adds array items markers that validate array elements. +// These validate the ELEMENTS of arrays, not the arrays themselves. +func addArrayItemsMarkers(rules map[string]MarkerScopeRule) { + addArrayItemsNumericMarkers(rules) + addArrayItemsStringMarkers(rules) + addArrayItemsArrayMarkers(rules) + addArrayItemsObjectMarkers(rules) + addArrayItemsGeneralMarkers(rules) +} + +// addArrayItemsNumericMarkers adds items markers for numeric array elements. +func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { + itemsNumericMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsMaximumMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, }, - markers.KubebuilderItemsMaximumMarker: { + markers.KubebuilderItemsMinimumMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -296,25 +390,25 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { }, }, }, - markers.KubebuilderItemsMinItemsMarker: { + markers.KubebuilderItemsExclusiveMaximumMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, }, - markers.KubebuilderItemsMinLengthMarker: { + markers.KubebuilderItemsExclusiveMinimumMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, }, - markers.KubebuilderItemsMinimumMarker: { + markers.KubebuilderItemsMultipleOfMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -323,7 +417,15 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { }, }, }, - markers.KubebuilderItemsMaxLengthMarker: { + } + + maps.Copy(rules, itemsNumericMarkers) +} + +// addArrayItemsStringMarkers adds items markers for string array elements. +func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { + itemsStringMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsMinLengthMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -332,102 +434,126 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { }, }, }, - markers.KubebuilderItemsEnumMarker: { + markers.KubebuilderItemsMaxLengthMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - // Enum can apply to any element type - ElementConstraint: nil, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, }, }, - markers.KubebuilderItemsFormatMarker: { + markers.KubebuilderItemsPatternMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - // Format can apply to various types - ElementConstraint: nil, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, }, }, - markers.KubebuilderItemsExclusiveMaximumMarker: { + } + + maps.Copy(rules, itemsStringMarkers) +} + +// addArrayItemsArrayMarkers adds items markers for array-of-arrays. +func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { + itemsArrayMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsMaxItemsMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, }, - markers.KubebuilderItemsExclusiveMinimumMarker: { + markers.KubebuilderItemsMinItemsMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, }, - markers.KubebuilderItemsMultipleOfMarker: { + markers.KubebuilderItemsUniqueItemsMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, }, - markers.KubebuilderItemsPatternMarker: { + } + + maps.Copy(rules, itemsArrayMarkers) +} + +// addArrayItemsObjectMarkers adds items markers for arrays of objects. +func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { + itemsObjectMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsMinPropertiesMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, }, }, - markers.KubebuilderItemsTypeMarker: { + markers.KubebuilderItemsMaxPropertiesMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - // Type marker can override any element type - ElementConstraint: nil, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, }, }, - markers.KubebuilderItemsUniqueItemsMarker: { + } + + maps.Copy(rules, itemsObjectMarkers) +} + +// addArrayItemsGeneralMarkers adds general items markers that apply to any element type. +func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { + itemsGeneralMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsEnumMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, + // Enum can apply to any element type + ElementConstraint: nil, }, }, - markers.KubebuilderItemsXValidationMarker: { + markers.KubebuilderItemsFormatMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - // CEL validation can apply to any element type + // Format can apply to various types ElementConstraint: nil, }, }, - markers.KubebuilderItemsMinPropertiesMarker: { + markers.KubebuilderItemsTypeMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, - }, + // Type marker can override any element type + ElementConstraint: nil, }, }, - markers.KubebuilderItemsMaxPropertiesMarker: { + markers.KubebuilderItemsXValidationMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, - }, + // CEL validation can apply to any element type + ElementConstraint: nil, }, }, - // TODO crd.go - // TODO package.go } + + maps.Copy(rules, itemsGeneralMarkers) } diff --git a/pkg/analysis/markerscope/doc.go b/pkg/analysis/markerscope/doc.go index 3bf61204..9f6c953b 100644 --- a/pkg/analysis/markerscope/doc.go +++ b/pkg/analysis/markerscope/doc.go @@ -23,4 +23,4 @@ limitations under the License. // // This linter ensures markers are applied in their appropriate contexts to prevent // configuration errors and improve API consistency. -package markerscope \ No newline at end of file +package markerscope diff --git a/pkg/analysis/markerscope/errors.go b/pkg/analysis/markerscope/errors.go new file mode 100644 index 00000000..10a7de0c --- /dev/null +++ b/pkg/analysis/markerscope/errors.go @@ -0,0 +1,25 @@ +/* +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 markerscope + +import "errors" + +var ( + errScopeNonZero = errors.New("scope must be non-zero") + errInvalidScopeBits = errors.New("invalid scope bits") + errInvalidSchemaType = errors.New("invalid schema type") + errTypeNotAllowed = errors.New("type not allowed") +) diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index 1d69fbbb..d889ac8e 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -70,13 +70,13 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList func validateMarkerRule(rule MarkerScopeRule) error { // Validate scope constraint if rule.Scope == 0 { - return fmt.Errorf("scope must be non-zero") + return errScopeNonZero } // Validate that scope is a valid combination of FieldScope and/or TypeScope validScopes := FieldScope | TypeScope if rule.Scope&^validScopes != 0 { - return fmt.Errorf("invalid scope bits") + return errInvalidScopeBits } // Validate type constraint if present @@ -90,10 +90,14 @@ func validateMarkerRule(rule MarkerScopeRule) error { } func validateTypeConstraint(tc *TypeConstraint) error { + if tc == nil { + return nil + } + // Validate schema types if specified for _, st := range tc.AllowedSchemaTypes { if !isValidSchemaType(st) { - return fmt.Errorf("invalid schema type: %q", st) + return fmt.Errorf("%w: %q", errInvalidSchemaType, st) } } diff --git a/pkg/analysis/markerscope/initializer_test.go b/pkg/analysis/markerscope/initializer_test.go new file mode 100644 index 00000000..165028c8 --- /dev/null +++ b/pkg/analysis/markerscope/initializer_test.go @@ -0,0 +1,247 @@ +/* +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 markerscope_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/kube-api-linter/pkg/analysis/initializer" + "sigs.k8s.io/kube-api-linter/pkg/analysis/markerscope" +) + +var _ = Describe("markerscope initializer", func() { + Context("config validation", func() { + type testCase struct { + config markerscope.MarkerScopeConfig + expectedErr string + } + + DescribeTable("should validate the provided config", + func(in testCase) { + ci, ok := markerscope.Initializer().(initializer.ConfigurableAnalyzerInitializer) + Expect(ok).To(BeTrue()) + + errs := ci.ValidateConfig(&in.config, field.NewPath("markerscope")) + if len(in.expectedErr) > 0 { + Expect(errs.ToAggregate()).To(HaveOccurred()) + Expect(errs.ToAggregate().Error()).To(ContainSubstring(in.expectedErr)) + } else { + Expect(errs).To(HaveLen(0), "No errors were expected") + } + }, + + Entry("With nil config", testCase{ + config: markerscope.MarkerScopeConfig{}, + expectedErr: "", + }), + + Entry("With empty config", testCase{ + config: markerscope.MarkerScopeConfig{}, + expectedErr: "", + }), + + Entry("With valid warn policy", testCase{ + config: markerscope.MarkerScopeConfig{ + Policy: markerscope.MarkerScopePolicyWarn, + }, + expectedErr: "", + }), + + Entry("With valid suggest_fix policy", testCase{ + config: markerscope.MarkerScopeConfig{ + Policy: markerscope.MarkerScopePolicySuggestFix, + }, + expectedErr: "", + }), + + Entry("With invalid policy", testCase{ + config: markerscope.MarkerScopeConfig{ + Policy: "invalid-policy", + }, + expectedErr: `markerscope.policy: Invalid value: "invalid-policy": invalid policy, must be one of: "warn", "suggest_fix"`, + }), + + Entry("With valid marker rules", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:marker": { + Scope: markerscope.FieldScope, + }, + }, + }, + expectedErr: "", + }), + + Entry("With marker rule having zero scope", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:marker": { + Scope: 0, + }, + }, + }, + expectedErr: `markerscope.markerRules.custom:marker: Invalid value: markerscope.MarkerScopeRule{Scope:0x0, TypeConstraint:(*markerscope.TypeConstraint)(nil)}: scope must be non-zero`, + }), + + Entry("With marker rule having invalid scope bits", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:marker": { + Scope: 8, // Invalid bit + }, + }, + }, + expectedErr: `markerscope.markerRules.custom:marker: Invalid value: markerscope.MarkerScopeRule{Scope:0x8, TypeConstraint:(*markerscope.TypeConstraint)(nil)}: invalid scope bits`, + }), + + Entry("With marker rule having invalid schema type", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:marker": { + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{"invalid-type"}, + }, + }, + }, + }, + expectedErr: `invalid type constraint: invalid schema type: "invalid-type"`, + }), + + Entry("With valid type constraint with string type", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:string-marker": { + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeString}, + }, + }, + }, + }, + expectedErr: "", + }), + + Entry("With valid type constraint with integer type", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:integer-marker": { + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeInteger}, + }, + }, + }, + }, + expectedErr: "", + }), + + Entry("With valid type constraint with multiple types", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:numeric-marker": { + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{ + markerscope.SchemaTypeInteger, + markerscope.SchemaTypeNumber, + }, + }, + }, + }, + }, + expectedErr: "", + }), + + Entry("With valid type constraint with element constraint", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:string-array": { + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeArray}, + ElementConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeString}, + }, + }, + }, + }, + }, + expectedErr: "", + }), + + Entry("With invalid element constraint", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:invalid-array": { + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeArray}, + ElementConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{"invalid-type"}, + }, + }, + }, + }, + }, + expectedErr: `invalid type constraint: invalid element constraint: invalid schema type: "invalid-type"`, + }), + + Entry("With both field and type scope", testCase{ + config: markerscope.MarkerScopeConfig{ + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:flexible-marker": { + Scope: markerscope.FieldScope | markerscope.TypeScope, + }, + }, + }, + expectedErr: "", + }), + ) + }) + + Context("analyzer initialization", func() { + It("should initialize analyzer with nil config", func() { + // Note: Init expects a MarkerScopeConfig, passing nil will error + // Use empty config instead + analyzer, err := markerscope.Initializer().Init(&markerscope.MarkerScopeConfig{}) + Expect(err).ToNot(HaveOccurred()) + Expect(analyzer).ToNot(BeNil()) + }) + + It("should initialize analyzer with empty config", func() { + analyzer, err := markerscope.Initializer().Init(&markerscope.MarkerScopeConfig{}) + Expect(err).ToNot(HaveOccurred()) + Expect(analyzer).ToNot(BeNil()) + }) + + It("should initialize analyzer with custom markers", func() { + cfg := &markerscope.MarkerScopeConfig{ + Policy: markerscope.MarkerScopePolicyWarn, + MarkerRules: map[string]markerscope.MarkerScopeRule{ + "custom:marker": { + Scope: markerscope.FieldScope, + }, + }, + } + analyzer, err := markerscope.Initializer().Init(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(analyzer).ToNot(BeNil()) + }) + }) +}) diff --git a/pkg/analysis/markerscope/markerscope_suite_test.go b/pkg/analysis/markerscope/markerscope_suite_test.go new file mode 100644 index 00000000..ab1bab1c --- /dev/null +++ b/pkg/analysis/markerscope/markerscope_suite_test.go @@ -0,0 +1,29 @@ +/* +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 markerscope_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMarkerScope(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "markerscope") +} diff --git a/pkg/analysis/markerscope/testdata/src/b/b.go b/pkg/analysis/markerscope/testdata/src/b/b.go index 51cdb34e..d52cfa2f 100644 --- a/pkg/analysis/markerscope/testdata/src/b/b.go +++ b/pkg/analysis/markerscope/testdata/src/b/b.go @@ -15,27 +15,63 @@ limitations under the License. */ package b -// +kubebuilder:validation:MinProperties=1 -type ValidTypeMarkers struct { - // +optional +// ============================================================================ +// Custom marker tests +// These test custom marker configurations +// ============================================================================ + +// Custom marker that should only apply to fields +type CustomFieldMarkerTest struct { + // +custom:field-only + ValidFieldMarker string `json:"validFieldMarker"` +} + +// +custom:field-only // want `marker "custom:field-only" can only be applied to fields` +type InvalidCustomFieldMarkerOnType struct { Name string `json:"name"` } -// +required // want `marker "required" can only be applied to fields` -type InvalidTypeMarkers struct { - // +kubebuilder:validation:Required +// Custom marker that should only apply to types +// +custom:type-only +type ValidCustomTypeMarker struct { Name string `json:"name"` } -type FieldMarkerTest struct { - // +required - // +kubebuilder:validation:MinProperties=1 - ValidMinPropertiesField map[string]string `json:"validMinPropertiesField"` +type CustomTypeMarkerTest struct { + // +custom:type-only // want `marker "custom:type-only" can only be applied to types` + InvalidTypeMarker string `json:"invalidTypeMarker"` +} + +// Custom marker with type constraints +type CustomTypeConstraintTest struct { + // Valid: string type field with string-only custom marker + // +custom:string-only + ValidStringField string `json:"validStringField"` + + // Invalid: integer type field with string-only custom marker + // +custom:string-only // want `marker "custom:string-only": type integer is not allowed \(expected one of: \[string\]\)` + InvalidIntegerField int `json:"invalidIntegerField"` + + // Valid: integer type field with integer-only custom marker + // +custom:integer-only + ValidIntegerField int `json:"validIntegerField"` + + // Invalid: string type field with integer-only custom marker + // +custom:integer-only // want `marker "custom:integer-only": type string is not allowed \(expected one of: \[integer\]\)` + InvalidStringField string `json:"invalidStringField"` +} + +// Custom marker with array element type constraints +type CustomArrayConstraintTest struct { + // Valid: array of strings with string element constraint + // +custom:string-array + ValidStringArray []string `json:"validStringArray"` - // +required - // +kubebuilder:validation:MinProperties=1 - ValidMinPropertiesField2 []string `json:"validMinPropertiesField2"` + // Invalid: array of integers with string element constraint + // +custom:string-array // want `marker "custom:string-array": array element: type integer is not allowed \(expected one of: \[string\]\)` + InvalidIntegerArray []int `json:"invalidIntegerArray"` - // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties" can only be applied to type definitions, map fields, or slice fields` - InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` + // Invalid: not an array type with array constraint + // +custom:string-array // want `marker "custom:string-array": type string is not allowed \(expected one of: \[array\]\)` + InvalidNonArray string `json:"invalidNonArray"` } diff --git a/pkg/analysis/markerscope/testdata/src/c/c.go b/pkg/analysis/markerscope/testdata/src/c/c.go new file mode 100644 index 00000000..84abd2b0 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/c/c.go @@ -0,0 +1,41 @@ +/* +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 c + +// +kubebuilder:validation:MinProperties=1 +type ValidTypeMarkers struct { + // +optional + Name string `json:"name"` +} + +// +kubebuilder:MinProperties=1 +// +required // want `marker "required" can only be applied to fields` +type InvalidTypeMarkers struct { + // +kubebuilder:validation:Required + Name string `json:"name"` +} + +type FieldMarkerTest struct { + // +required + // +kubebuilder:validation:MinProperties=1 + ValidMinPropertiesField map[string]string `json:"validMinPropertiesField"` + + // +required + ValidMinPropertiesField2 []string `json:"validMinPropertiesField2"` + + // +kubebuilder:validation:items:ExactlyOneOf={field1} // want `marker "kubebuilder:validation:items:ExactlyOneOf" can only be applied to types` + InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` +} diff --git a/pkg/analysis/markerscope/testdata/src/b/b.go.golden b/pkg/analysis/markerscope/testdata/src/c/c.go.golden similarity index 90% rename from pkg/analysis/markerscope/testdata/src/b/b.go.golden rename to pkg/analysis/markerscope/testdata/src/c/c.go.golden index 28e0d424..5e623995 100644 --- a/pkg/analysis/markerscope/testdata/src/b/b.go.golden +++ b/pkg/analysis/markerscope/testdata/src/c/c.go.golden @@ -13,7 +13,7 @@ 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 b +package c // +kubebuilder:validation:MinProperties=1 type ValidTypeMarkers struct { @@ -21,7 +21,8 @@ type ValidTypeMarkers struct { Name string `json:"name"` } -// +kubebuilder:validation:MinProperties=1 +// +kubebuilder:MinProperties=1 +// +kubebuilder:validation:items:ExactlyOneOf={field1} type InvalidTypeMarkers struct { // +kubebuilder:validation:Required Name string `json:"name"` @@ -33,9 +34,8 @@ type FieldMarkerTest struct { ValidMinPropertiesField map[string]string `json:"validMinPropertiesField"` // +required - // +kubebuilder:validation:MinProperties=1 ValidMinPropertiesField2 []string `json:"validMinPropertiesField2"` - // +required + // +required InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` -} \ No newline at end of file +} diff --git a/pkg/analysis/utils/utils.go b/pkg/analysis/utils/utils.go index 71a481f5..074af20b 100644 --- a/pkg/analysis/utils/utils.go +++ b/pkg/analysis/utils/utils.go @@ -450,3 +450,44 @@ func getFieldTypeName(field *ast.Field) string { return "" } + +// LookupFieldsUsingType returns all fields in the package that use the given type. +func LookupFieldsUsingType(pass *analysis.Pass, typeSpec *ast.TypeSpec) []*ast.Field { + var fields []*ast.Field + + // Get the type name + typeName := typeSpec.Name.Name + + // Iterate through all files in the package + for _, file := range pass.Files { + ast.Inspect(file, func(n ast.Node) bool { + field, ok := n.(*ast.Field) + if !ok { + return true + } + + // Check if the field's type matches the type we're looking for + if matchesType(field.Type, typeName) { + fields = append(fields, field) + } + + return true + }) + } + + return fields +} + +// matchesType checks if an expression matches the given type name. +func matchesType(expr ast.Expr, typeName string) bool { + switch e := expr.(type) { + case *ast.Ident: + return e.Name == typeName + case *ast.StarExpr: + return matchesType(e.X, typeName) + case *ast.ArrayType: + return matchesType(e.Elt, typeName) + default: + return false + } +} From b65c4487da056c3aa126042f5fc22c9afed0ef7a Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Mon, 20 Oct 2025 03:56:15 +0900 Subject: [PATCH 06/18] Enhance marker scope validation by introducing allowDangerousTypes flag and updating type constraints. Refactor schema type definitions and improve error handling for invalid markers. Add new test cases for various marker scenarios. Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 145 +++----- pkg/analysis/markerscope/config.go | 144 +++++--- pkg/analysis/markerscope/errors.go | 8 +- pkg/analysis/markerscope/initializer_test.go | 4 +- pkg/analysis/markerscope/schema.go | 125 +++++++ pkg/analysis/markerscope/testdata/src/a/a.go | 314 ------------------ .../markerscope/testdata/src/a/array.go | 97 ++++++ .../markerscope/testdata/src/a/field_only.go | 89 +++++ .../markerscope/testdata/src/a/general.go | 98 ++++++ .../markerscope/testdata/src/a/items.go | 147 ++++++++ .../markerscope/testdata/src/a/numeric.go | 128 +++++++ .../markerscope/testdata/src/a/object.go | 86 +++++ .../markerscope/testdata/src/a/scope.go | 68 ++++ .../markerscope/testdata/src/a/string.go | 101 ++++++ .../markerscope/testdata/src/a/topology.go | 128 +++++++ .../markerscope/testdata/src/a/type_only.go | 82 +++++ pkg/analysis/markerscope/testdata/src/c/c.go | 12 + .../markerscope/testdata/src/c/c.go.golden | 12 + .../markerscope/testdata/src/test/doc.go | 22 -- .../markerscope/testdata/src/test/go.mod | 22 -- .../markerscope/testdata/src/test/go.sum | 82 ----- .../markerscope/testdata/src/test/types.go | 65 ---- 22 files changed, 1315 insertions(+), 664 deletions(-) create mode 100644 pkg/analysis/markerscope/schema.go delete mode 100644 pkg/analysis/markerscope/testdata/src/a/a.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/array.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/field_only.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/general.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/items.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/numeric.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/object.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/scope.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/string.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/topology.go create mode 100644 pkg/analysis/markerscope/testdata/src/a/type_only.go delete mode 100644 pkg/analysis/markerscope/testdata/src/test/doc.go delete mode 100644 pkg/analysis/markerscope/testdata/src/test/go.mod delete mode 100644 pkg/analysis/markerscope/testdata/src/test/go.sum delete mode 100644 pkg/analysis/markerscope/testdata/src/test/types.go diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 9f5852ab..6b4c462a 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -50,8 +50,9 @@ func init() { } type analyzer struct { - markerRules map[string]MarkerScopeRule - policy MarkerScopePolicy + markerRules map[string]MarkerScopeRule + policy MarkerScopePolicy + allowDangerousTypes bool } // newAnalyzer creates a new analyzer. @@ -61,8 +62,9 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { } a := &analyzer{ - markerRules: mergeMarkerRules(DefaultMarkerRules(), cfg.MarkerRules), - policy: cfg.Policy, + markerRules: mergeMarkerRules(DefaultMarkerRules(), cfg.MarkerRules), + policy: cfg.Policy, + allowDangerousTypes: cfg.AllowDangerousTypes, } // Register all markers (both default and custom) with the markers helper @@ -168,22 +170,20 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark } // Check type constraints if present - if rule.TypeConstraint != nil { - if err := a.validateFieldTypeConstraint(pass, field, rule.TypeConstraint); err != nil { - if a.policy == MarkerScopePolicySuggestFix { - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), - SuggestedFixes: a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule), - }) - } else { - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), - }) - } + if err := a.validateFieldTypeConstraint(pass, field, rule, a.allowDangerousTypes); err != nil { + if a.policy == MarkerScopePolicySuggestFix { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), + SuggestedFixes: a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule), + }) + } else { + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), + }) } } } @@ -223,9 +223,7 @@ func (a *analyzer) checkSingleTypeMarkers(pass *analysis.Pass, typeSpec *ast.Typ } // Check type constraints if present - if rule.TypeConstraint != nil { - a.checkTypeConstraintViolation(pass, typeSpec, marker, rule) - } + a.checkTypeConstraintViolation(pass, typeSpec, marker, rule, a.allowDangerousTypes) } } @@ -254,8 +252,8 @@ func (a *analyzer) reportTypeScopeViolation(pass *analysis.Pass, typeSpec *ast.T } // checkTypeConstraintViolation checks and reports type constraint violations. -func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) { - if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint); err != nil { +func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule, allowDangerousTypes bool) { + if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint, allowDangerousTypes); err != nil { var fixes []analysis.SuggestedFix if a.policy == MarkerScopePolicySuggestFix { @@ -273,22 +271,29 @@ func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *a } // validateFieldTypeConstraint validates that a field's type matches the type constraint. -func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.Field, tc *TypeConstraint) error { +func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.Field, rule MarkerScopeRule, allowDangerousTypes bool) error { // Get the type of the field tv, ok := pass.TypesInfo.Types[field.Type] if !ok { return nil // Skip if we can't determine the type } - if err := validateTypeAgainstConstraint(tv.Type, tc); err != nil { + if err := validateTypeAgainstConstraint(tv.Type, rule.TypeConstraint, allowDangerousTypes); err != nil { return err } + if rule.StrictTypeConstraint && rule.Scope == AnyScope { + namedType, ok := tv.Type.(*types.Named) + if ok { + return fmt.Errorf("%w of %s instead of the field", errMarkerShouldBeOnTypeDefinition, namedType.Obj().Name()) + } + } + return nil } // validateTypeSpecTypeConstraint validates that a type spec's type matches the type constraint. -func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec *ast.TypeSpec, tc *TypeConstraint) error { +func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec *ast.TypeSpec, tc *TypeConstraint, allowDangerousTypes bool) error { // Get the type of the type spec obj := pass.TypesInfo.Defs[typeSpec.Name] if obj == nil { @@ -300,22 +305,29 @@ func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec return nil } - return validateTypeAgainstConstraint(typeName.Type(), tc) + return validateTypeAgainstConstraint(typeName.Type(), tc, allowDangerousTypes) } // validateTypeAgainstConstraint validates that a Go type satisfies the type constraint. -func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { +func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint, allowDangerousTypes bool) error { + // Get the schema type from the Go type + schemaType := getSchemaType(t) + + // Check if dangerous types are disallowed + if !allowDangerousTypes && schemaType == SchemaTypeNumber { + // Get the underlying type for better error messages + underlyingType := getUnderlyingType(t) + return fmt.Errorf("type %s is dangerous and not allowed (set allowDangerousTypes to true to permit)", underlyingType.String()) + } + if tc == nil { return nil } - // Get the schema type from the Go type - schemaType := getSchemaType(t) - // Check if the schema type is allowed if len(tc.AllowedSchemaTypes) > 0 { if !slices.Contains(tc.AllowedSchemaTypes, schemaType) { - return fmt.Errorf("%w: type %s (expected one of: %v)", errTypeNotAllowed, schemaType, tc.AllowedSchemaTypes) + return fmt.Errorf("type %s is not allowed (expected one of: %v)", schemaType, tc.AllowedSchemaTypes) } } @@ -323,7 +335,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { if tc.ElementConstraint != nil && schemaType == SchemaTypeArray { elemType := getElementType(t) if elemType != nil { - if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint); err != nil { + if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint, allowDangerousTypes); err != nil { return fmt.Errorf("array element: %w", err) } } @@ -332,69 +344,6 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { return nil } -// getSchemaType converts a Go type to an OpenAPI schema type. -// -//nolint:cyclop // This function has many cases for different Go types -func getSchemaType(t types.Type) SchemaType { - // Unwrap pointer types - if ptr, ok := t.(*types.Pointer); ok { - t = ptr.Elem() - } - - // Unwrap named types to get underlying type - if named, ok := t.(*types.Named); ok { - t = named.Underlying() - } - - switch ut := t.Underlying().(type) { - case *types.Basic: - switch ut.Kind() { - case types.Bool: - return SchemaTypeBoolean - case types.Int, types.Int8, types.Int16, types.Int32, types.Int64, - types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64: - return SchemaTypeInteger - case types.Float32, types.Float64: - return SchemaTypeNumber - case types.String: - return SchemaTypeString - case types.Invalid, types.Uintptr, types.Complex64, types.Complex128, - types.UnsafePointer, types.UntypedBool, types.UntypedInt, types.UntypedRune, - types.UntypedFloat, types.UntypedComplex, types.UntypedString, types.UntypedNil: - // These types are not supported in OpenAPI schemas - return "" - } - case *types.Slice, *types.Array: - return SchemaTypeArray - case *types.Map, *types.Struct: - return SchemaTypeObject - } - - return "" -} - -// getElementType returns the element type of an array or slice. -func getElementType(t types.Type) types.Type { - // Unwrap pointer types - if ptr, ok := t.(*types.Pointer); ok { - t = ptr.Elem() - } - - // Unwrap named types to get underlying type - if named, ok := t.(*types.Named); ok { - t = named.Underlying() - } - - switch ut := t.(type) { - case *types.Slice: - return ut.Elem() - case *types.Array: - return ut.Elem() - } - - return nil -} - // extractIdent extracts an *ast.Ident from an ast.Expr, unwrapping pointers and arrays. func extractIdent(expr ast.Expr) *ast.Ident { switch e := expr.(type) { diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 250aef1d..b7b116ea 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -53,24 +53,6 @@ func (s ScopeConstraint) Allows(scope ScopeConstraint) bool { return s&scope != 0 } -// SchemaType represents OpenAPI schema types that markers can target. -type SchemaType string - -const ( - // SchemaTypeInteger represents integer types (int, int32, int64, uint, etc.) - SchemaTypeInteger SchemaType = "integer" - // SchemaTypeNumber represents floating-point types (float32, float64). - SchemaTypeNumber SchemaType = "number" - // SchemaTypeString represents string types. - SchemaTypeString SchemaType = "string" - // SchemaTypeBoolean represents boolean types. - SchemaTypeBoolean SchemaType = "boolean" - // SchemaTypeArray represents array/slice types. - SchemaTypeArray SchemaType = "array" - // SchemaTypeObject represents struct/map types. - SchemaTypeObject SchemaType = "object" -) - // TypeConstraint defines what types a marker can be applied to. // NOTE: This constraint is only used when the marker is placed on a field (not TypeScope). // Type-level markers (TypeScope) do not use type constraints. @@ -90,6 +72,11 @@ type MarkerScopeRule struct { // Scope specifies where the marker can be placed (field vs type). Scope ScopeConstraint + // StrictTypeConstraint specifies if the type constraint is strict. + // If true, the type constraint is strict and only the allowed schema types are allowed. + // If false, the type constraint is not strict and any type is allowed. + StrictTypeConstraint bool + // TypeConstraint specifies what types the marker can be applied to. // NOTE: This is used for both field and type scopes, but typically only enforced // when Scope includes FieldScope. For TypeScope-only markers, this is usually nil. @@ -126,6 +113,11 @@ type MarkerScopeConfig struct { // allowedSchemaTypes: ["string"] MarkerRules map[string]MarkerScopeRule `json:"markerRules,omitempty"` + // AllowDangerousTypes specifies if dangerous types are allowed. + // If true, dangerous types are allowed. + // If false, dangerous types are not allowed. + AllowDangerousTypes bool `json:"allowDangerousTypes,omitempty"` + // Policy determines whether to suggest fixes or just warn. Policy MarkerScopePolicy `json:"policy,omitempty"` } @@ -204,31 +196,36 @@ func addNumericMarkers(rules map[string]MarkerScopeRule) { numericMarkers := map[string]MarkerScopeRule{ // numeric markers (field or type, integer or number types) markers.KubebuilderMinimumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, markers.KubebuilderMaximumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, markers.KubebuilderExclusiveMaximumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, markers.KubebuilderExclusiveMinimumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, }, markers.KubebuilderMultipleOfMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, }, @@ -243,13 +240,15 @@ func addObjectMarkers(rules map[string]MarkerScopeRule) { objectMarkers := map[string]MarkerScopeRule{ // object markers (field or type, object types) markers.KubebuilderMinPropertiesMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, }, markers.KubebuilderMaxPropertiesMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, @@ -264,19 +263,22 @@ func addStringMarkers(rules map[string]MarkerScopeRule) { stringMarkers := map[string]MarkerScopeRule{ // string markers (field or type, string types) markers.KubebuilderPatternMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, markers.KubebuilderMinLengthMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, markers.KubebuilderMaxLengthMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, @@ -291,19 +293,22 @@ func addArrayMarkers(rules map[string]MarkerScopeRule) { arrayMarkers := map[string]MarkerScopeRule{ // array markers (field or type, array types) markers.KubebuilderMinItemsMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderMaxItemsMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderUniqueItemsMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, @@ -317,10 +322,19 @@ func addArrayMarkers(rules map[string]MarkerScopeRule) { func addGeneralMarkers(rules map[string]MarkerScopeRule) { generalMarkers := map[string]MarkerScopeRule{ // general markers (field or type, any type) - markers.KubebuilderEnumMarker: {Scope: AnyScope, TypeConstraint: nil}, - markers.KubebuilderFormatMarker: {Scope: AnyScope, TypeConstraint: nil}, - markers.KubebuilderTypeMarker: {Scope: AnyScope, TypeConstraint: nil}, - markers.KubebuilderXValidationMarker: {Scope: AnyScope, TypeConstraint: nil}, + markers.KubebuilderEnumMarker: { + Scope: AnyScope, + }, + markers.KubebuilderFormatMarker: { + Scope: AnyScope, + }, + markers.KubebuilderTypeMarker: { + Scope: AnyScope, + }, + markers.KubebuilderXValidationMarker: { + Scope: AnyScope, + StrictTypeConstraint: true, + }, } maps.Copy(rules, generalMarkers) @@ -331,19 +345,22 @@ func addSSATopologyMarkers(rules map[string]MarkerScopeRule) { ssaMarkers := map[string]MarkerScopeRule{ // Server-Side Apply topology markers markers.KubebuilderListTypeMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderListMapKeyMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderMapTypeMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, @@ -373,7 +390,8 @@ func addArrayItemsMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { itemsNumericMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMaximumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -382,7 +400,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMinimumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -391,7 +410,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsExclusiveMaximumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -400,7 +420,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsExclusiveMinimumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -409,7 +430,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMultipleOfMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -426,7 +448,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { itemsStringMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMinLengthMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -435,7 +458,8 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMaxLengthMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -444,7 +468,8 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsPatternMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -461,7 +486,8 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { itemsArrayMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMaxItemsMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -470,7 +496,8 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMinItemsMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -479,7 +506,8 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsUniqueItemsMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -496,7 +524,8 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { itemsObjectMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMinPropertiesMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -505,7 +534,8 @@ func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMaxPropertiesMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -522,7 +552,8 @@ func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { itemsGeneralMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsEnumMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, // Enum can apply to any element type @@ -530,7 +561,8 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsFormatMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, // Format can apply to various types @@ -538,7 +570,8 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsTypeMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, // Type marker can override any element type @@ -546,7 +579,8 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsXValidationMarker: { - Scope: AnyScope, + Scope: AnyScope, + StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, // CEL validation can apply to any element type diff --git a/pkg/analysis/markerscope/errors.go b/pkg/analysis/markerscope/errors.go index 10a7de0c..6415ba74 100644 --- a/pkg/analysis/markerscope/errors.go +++ b/pkg/analysis/markerscope/errors.go @@ -18,8 +18,8 @@ package markerscope import "errors" var ( - errScopeNonZero = errors.New("scope must be non-zero") - errInvalidScopeBits = errors.New("invalid scope bits") - errInvalidSchemaType = errors.New("invalid schema type") - errTypeNotAllowed = errors.New("type not allowed") + errScopeNonZero = errors.New("scope must be non-zero") + errInvalidScopeBits = errors.New("invalid scope bits") + errInvalidSchemaType = errors.New("invalid schema type") + errMarkerShouldBeOnTypeDefinition = errors.New("marker should be declared on the type definition") ) diff --git a/pkg/analysis/markerscope/initializer_test.go b/pkg/analysis/markerscope/initializer_test.go index 165028c8..3fa4203e 100644 --- a/pkg/analysis/markerscope/initializer_test.go +++ b/pkg/analysis/markerscope/initializer_test.go @@ -95,7 +95,7 @@ var _ = Describe("markerscope initializer", func() { }, }, }, - expectedErr: `markerscope.markerRules.custom:marker: Invalid value: markerscope.MarkerScopeRule{Scope:0x0, TypeConstraint:(*markerscope.TypeConstraint)(nil)}: scope must be non-zero`, + expectedErr: `markerscope.markerRules.custom:marker: Invalid value: markerscope.MarkerScopeRule{Scope:0x0, StrictTypeConstraint:false, TypeConstraint:(*markerscope.TypeConstraint)(nil)}: scope must be non-zero`, }), Entry("With marker rule having invalid scope bits", testCase{ @@ -106,7 +106,7 @@ var _ = Describe("markerscope initializer", func() { }, }, }, - expectedErr: `markerscope.markerRules.custom:marker: Invalid value: markerscope.MarkerScopeRule{Scope:0x8, TypeConstraint:(*markerscope.TypeConstraint)(nil)}: invalid scope bits`, + expectedErr: `markerscope.markerRules.custom:marker: Invalid value: markerscope.MarkerScopeRule{Scope:0x8, StrictTypeConstraint:false, TypeConstraint:(*markerscope.TypeConstraint)(nil)}: invalid scope bits`, }), Entry("With marker rule having invalid schema type", testCase{ diff --git a/pkg/analysis/markerscope/schema.go b/pkg/analysis/markerscope/schema.go new file mode 100644 index 00000000..8e1329b1 --- /dev/null +++ b/pkg/analysis/markerscope/schema.go @@ -0,0 +1,125 @@ +/* +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 markerscope + +import "go/types" + +// SchemaType represents OpenAPI schema types that markers can target. +type SchemaType string + +const ( + // SchemaTypeInteger represents integer types (int, int32, int64, uint, etc.) + SchemaTypeInteger SchemaType = "integer" + // SchemaTypeNumber represents floating-point types (float32, float64). + SchemaTypeNumber SchemaType = "number" + // SchemaTypeString represents string types. + SchemaTypeString SchemaType = "string" + // SchemaTypeBoolean represents boolean types. + SchemaTypeBoolean SchemaType = "boolean" + // SchemaTypeArray represents array/slice types. + SchemaTypeArray SchemaType = "array" + // SchemaTypeObject represents struct/map types. + SchemaTypeObject SchemaType = "object" +) + +// getSchemaType converts a Go type to its corresponding OpenAPI schema type. +func getSchemaType(t types.Type) SchemaType { + t = unwrapType(t) + + switch ut := t.Underlying().(type) { + case *types.Basic: + return getBasicTypeSchema(ut) + case *types.Slice, *types.Array: + return SchemaTypeArray + case *types.Map, *types.Struct: + return SchemaTypeObject + } + + return "" +} + +// unwrapType unwraps pointer and named types to get the underlying type. +func unwrapType(t types.Type) types.Type { + // Unwrap pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Unwrap named types to get underlying type + if named, ok := t.(*types.Named); ok { + t = named.Underlying() + } + + return t +} + +// getBasicTypeSchema returns the schema type for a basic Go type. +func getBasicTypeSchema(bt *types.Basic) SchemaType { + switch bt.Kind() { + case types.Bool: + return SchemaTypeBoolean + case types.Int, types.Int8, types.Int16, types.Int32, types.Int64, + types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64: + return SchemaTypeInteger + case types.Float32, types.Float64: + return SchemaTypeNumber + case types.String: + return SchemaTypeString + case types.Invalid, types.Uintptr, types.Complex64, types.Complex128, + types.UnsafePointer, types.UntypedBool, types.UntypedInt, types.UntypedRune, + types.UntypedFloat, types.UntypedComplex, types.UntypedString, types.UntypedNil: + // These types are not supported in OpenAPI schemas + return "" + default: + return "" + } +} + +// getElementType returns the element type of an array or slice. +func getElementType(t types.Type) types.Type { + // Unwrap pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Unwrap named types + if named, ok := t.(*types.Named); ok { + t = named.Underlying() + } + + switch ut := t.Underlying().(type) { + case *types.Slice: + return ut.Elem() + case *types.Array: + return ut.Elem() + } + + return nil +} + +// getUnderlyingType recursively unwraps type to find the underlying type. +func getUnderlyingType(expr types.Type) types.Type { + switch t := expr.(type) { + case *types.Pointer: + return getUnderlyingType(t.Elem()) + case *types.Named: + return getUnderlyingType(t.Underlying()) + case *types.Alias: + return getUnderlyingType(t.Underlying()) + default: + return expr + } +} diff --git a/pkg/analysis/markerscope/testdata/src/a/a.go b/pkg/analysis/markerscope/testdata/src/a/a.go deleted file mode 100644 index 150cb5ee..00000000 --- a/pkg/analysis/markerscope/testdata/src/a/a.go +++ /dev/null @@ -1,314 +0,0 @@ -/* -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 a - -// ============================================================================ -// Field-only markers (FieldScope) -// These should ERROR when placed on types -// ============================================================================ - -// +optional // want `marker "optional" can only be applied to fields` -// +required // want `marker "required" can only be applied to fields` -// +nullable // want `marker "nullable" can only be applied to fields` -type InvalidFieldOnlyOnType struct { - Name string `json:"name"` -} - -type FieldOnlyMarkersTest struct { - // Valid field-only markers - // +optional - // +required - // +k8s:optional - // +k8s:required - // +nullable - // +kubebuilder:default="default" - // +kubebuilder:validation:Example="example" - // +kubebuilder:validation:EmbeddedResource - // +kubebuilder:validation:Schemaless - ValidFieldOnlyMarkers string `json:"validFieldOnlyMarkers"` -} - -// ============================================================================ -// Type-only markers (TypeScope) -// These should ERROR when placed on fields -// ============================================================================ - -type TypeOnlyMarkersTest struct { - // +kubebuilder:validation:items:ExactlyOneOf={field1,field2} // want `marker "kubebuilder:validation:items:ExactlyOneOf" can only be applied to types` - // +kubebuilder:validation:items:AtMostOneOf={field1,field2} // want `marker "kubebuilder:validation:items:AtMostOneOf" can only be applied to types` - // +kubebuilder:validation:items:AtLeastOneOf={field1,field2} // want `marker "kubebuilder:validation:items:AtLeastOneOf" can only be applied to types` - InvalidTypeOnlyOnField string `json:"invalidTypeOnlyOnField"` -} - -// +kubebuilder:validation:items:ExactlyOneOf={Field1,Field2} -// +kubebuilder:validation:items:AtMostOneOf={Field1,Field2} -// +kubebuilder:validation:items:AtLeastOneOf={Field1,Field2} -type ValidTypeOnlyMarkers struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` -} - -// ============================================================================ -// AnyScope markers - can be on both fields and types -// ============================================================================ - -// +kubebuilder:pruning:PreserveUnknownFields -// +kubebuilder:title="My Title" -type AnyScopeOnType struct { - Name string `json:"name"` -} - -type AnyScopeOnFieldTest struct { - // +kubebuilder:pruning:PreserveUnknownFields - // +kubebuilder:title="Field Title" - ValidAnyScopeField map[string]string `json:"validAnyScopeField"` -} - -// ============================================================================ -// Numeric markers (AnyScope, integer/number types) -// ============================================================================ - -// +kubebuilder:validation:Minimum=0 -// +kubebuilder:validation:Maximum=100 -// +kubebuilder:validation:ExclusiveMinimum=false -// +kubebuilder:validation:ExclusiveMaximum=false -// +kubebuilder:validation:MultipleOf=5 -type NumericType int32 - -type NumericMarkersFieldTest struct { - // Valid: numeric markers on numeric types - // +kubebuilder:validation:Minimum=0 - // +kubebuilder:validation:Maximum=100 - // +kubebuilder:validation:ExclusiveMinimum=false - // +kubebuilder:validation:ExclusiveMaximum=false - // +kubebuilder:validation:MultipleOf=5 - ValidNumericField int32 `json:"validNumericField"` - - // +kubebuilder:validation:Minimum=0.0 - // +kubebuilder:validation:Maximum=1.0 - ValidFloatField float64 `json:"validFloatField"` - - // Invalid: numeric marker on string field - // +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)` - InvalidMinimumOnString string `json:"invalidMinimumOnString"` - - // Invalid: numeric marker on bool field - // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer number\]\)` - InvalidMaximumOnBool bool `json:"invalidMaximumOnBool"` -} - -// ============================================================================ -// String markers (AnyScope, string types) -// ============================================================================ - -// +kubebuilder:validation:Pattern="^[a-z]+$" -// +kubebuilder:validation:MinLength=1 -// +kubebuilder:validation:MaxLength=100 -type StringType string - -type StringMarkersFieldTest struct { - // Valid: string markers on string field - // +kubebuilder:validation:Pattern="^[a-z]+$" - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=100 - ValidStringField string `json:"validStringField"` - - // Invalid: string marker on int field - // +kubebuilder:validation:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:Pattern": type integer is not allowed \(expected one of: \[string\]\)` - InvalidPatternOnInt int32 `json:"invalidPatternOnInt"` - - // Invalid: string marker on array field - // +kubebuilder:validation:MinLength=5 // want `marker "kubebuilder:validation:MinLength": type array is not allowed \(expected one of: \[string\]\)` - InvalidMinLengthOnArray []string `json:"invalidMinLengthOnArray"` -} - -// ============================================================================ -// Array markers (AnyScope, array types) -// ============================================================================ - -// +kubebuilder:validation:MinItems=1 -// +kubebuilder:validation:MaxItems=10 -// +kubebuilder:validation:UniqueItems=true -type StringArray []string - -type ArrayMarkersFieldTest struct { - // Valid: array markers on array field - // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:MaxItems=10 - // +kubebuilder:validation:UniqueItems=true - ValidArrayField []string `json:"validArrayField"` - - // Invalid: array marker on string field - // +kubebuilder:validation:MinItems=1 // want `marker "kubebuilder:validation:MinItems": type string is not allowed \(expected one of: \[array\]\)` - InvalidMinItemsOnString string `json:"invalidMinItemsOnString"` - - // Invalid: array marker on object field - // +kubebuilder:validation:MaxItems=10 // want `marker "kubebuilder:validation:MaxItems": type object is not allowed \(expected one of: \[array\]\)` - InvalidMaxItemsOnObject map[string]string `json:"invalidMaxItemsOnObject"` -} - -// ============================================================================ -// Object markers (AnyScope, object types) -// ============================================================================ - -// +kubebuilder:validation:MinProperties=1 -// +kubebuilder:validation:MaxProperties=10 -type ObjectType struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` -} - -type ObjectMarkersFieldTest struct { - // Valid: object markers on map field - // +kubebuilder:validation:MinProperties=1 - // +kubebuilder:validation:MaxProperties=10 - ValidObjectField map[string]string `json:"validObjectField"` - - // Invalid: object marker on string field - // +kubebuilder:validation:MinProperties=2 // want `marker "kubebuilder:validation:MinProperties": type string is not allowed \(expected one of: \[object\]\)` - InvalidMinPropertiesOnString string `json:"invalidMinPropertiesOnString"` - - // Invalid: object marker on array field - // +kubebuilder:validation:MaxProperties=5 // want `marker "kubebuilder:validation:MaxProperties": type array is not allowed \(expected one of: \[object\]\)` - InvalidMaxPropertiesOnArray []string `json:"invalidMaxPropertiesOnArray"` -} - -// ============================================================================ -// General markers (AnyScope, any type) -// ============================================================================ - -// +kubebuilder:validation:Enum=A;B;C -// +kubebuilder:validation:Format=email -// +kubebuilder:validation:Type=string -// +kubebuilder:validation:XValidation:rule="self.size() > 0" -type GeneralType string - -type GeneralMarkersFieldTest struct { - // +kubebuilder:validation:Enum=A;B;C - // +kubebuilder:validation:Format=email - // +kubebuilder:validation:Type=string - // +kubebuilder:validation:XValidation:rule="self.size() > 0" - ValidGeneralField string `json:"validGeneralField"` -} - -// ============================================================================ -// Server-Side Apply topology markers (AnyScope) -// ============================================================================ - -// +listType=map -type ItemList []Item - -// +mapType=granular -type ConfigMap map[string]string - -// +structType=atomic -type AtomicStruct struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` -} - -type TopologyMarkersFieldTest struct { - // +listType=map - // +listMapKey=name - ValidListMarkers []Item `json:"validListMarkers"` - - // +mapType=granular - ValidMapType map[string]string `json:"validMapType"` - - // +structType=atomic - ValidStruct EmbeddedStruct `json:"validStruct"` -} - -type EmbeddedStruct struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` -} - -type Item struct { - Name string `json:"name"` -} - -// ============================================================================ -// Array items markers (AnyScope, array with element constraints) -// ============================================================================ - -// +kubebuilder:validation:items:Maximum=100 -// +kubebuilder:validation:items:Minimum=0 -// +kubebuilder:validation:items:MultipleOf=5 -type NumericArrayType []int32 - -// +kubebuilder:validation:items:Pattern="^[a-z]+$" -// +kubebuilder:validation:items:MinLength=1 -// +kubebuilder:validation:items:MaxLength=50 -type StringArrayType []string - -// +kubebuilder:validation:items:MinItems=1 -// +kubebuilder:validation:items:MaxItems=5 -type NestedArrayType [][]string - -// +kubebuilder:validation:items:MinProperties=1 -// +kubebuilder:validation:items:MaxProperties=5 -type ObjectArrayType []map[string]string - -type ArrayItemsMarkersFieldTest struct { - // Valid: Numeric element constraints - // +kubebuilder:validation:items:Maximum=100 - // +kubebuilder:validation:items:Minimum=0 - // +kubebuilder:validation:items:MultipleOf=5 - // +kubebuilder:validation:items:ExclusiveMaximum=false - // +kubebuilder:validation:items:ExclusiveMinimum=false - ValidNumericArrayItems []int32 `json:"validNumericArrayItems"` - - // Valid: String element constraints - // +kubebuilder:validation:items:Pattern="^[a-z]+$" - // +kubebuilder:validation:items:MinLength=1 - // +kubebuilder:validation:items:MaxLength=50 - ValidStringArrayItems []string `json:"validStringArrayItems"` - - // Valid: Nested array constraints - // +kubebuilder:validation:items:MinItems=1 - // +kubebuilder:validation:items:MaxItems=5 - // +kubebuilder:validation:items:UniqueItems=true - ValidNestedArrayItems [][]string `json:"validNestedArrayItems"` - - // Valid: Object element constraints - // +kubebuilder:validation:items:MinProperties=1 - // +kubebuilder:validation:items:MaxProperties=5 - ValidObjectArrayItems []map[string]string `json:"validObjectArrayItems"` - - // Valid: General items markers - // +kubebuilder:validation:items:Enum=A;B;C - // +kubebuilder:validation:items:Format=uuid - // +kubebuilder:validation:items:Type=string - // +kubebuilder:validation:items:XValidation:rule="self != ''" - ValidGeneralArrayItems []string `json:"validGeneralArrayItems"` - - // Invalid: items:Maximum on string array (element type mismatch) - // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)` - InvalidItemsMaximumOnStringArray []string `json:"invalidItemsMaximumOnStringArray"` - - // Invalid: items:Pattern on int array (element type mismatch) - // +kubebuilder:validation:items:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:items:Pattern": array element: type integer is not allowed \(expected one of: \[string\]\)` - InvalidItemsPatternOnIntArray []int32 `json:"invalidItemsPatternOnIntArray"` - - // Invalid: items:MinProperties on string array (element type mismatch) - // +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": array element: type string is not allowed \(expected one of: \[object\]\)` - InvalidItemsMinPropertiesOnStringArray []string `json:"invalidItemsMinPropertiesOnStringArray"` - - // Invalid: items marker on non-array field - // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": type string is not allowed \(expected one of: \[array\]\)` - InvalidItemsMarkerOnNonArray string `json:"invalidItemsMarkerOnNonArray"` -} diff --git a/pkg/analysis/markerscope/testdata/src/a/array.go b/pkg/analysis/markerscope/testdata/src/a/array.go new file mode 100644 index 00000000..beaf7b8b --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/array.go @@ -0,0 +1,97 @@ +/* +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 a + +// +kubebuilder:validation:MinItems=1 +// +kubebuilder:validation:MaxItems=10 +// +kubebuilder:validation:UniqueItems=true +type StringArray []string + +// +kubebuilder:validation:MinItems=0 +type IntegerArray []int32 + +// +kubebuilder:validation:MaxItems=100 +type BooleanArray []bool + +// Type definitions with invalid markers +// +kubebuilder:validation:MinItems=1 // want `marker "kubebuilder:validation:MinItems": type string is not allowed \(expected one of: \[array\]\)` +type InvalidArrayMarkerOnStringType string + +// +kubebuilder:validation:MaxItems=10 // want `marker "kubebuilder:validation:MaxItems": type object is not allowed \(expected one of: \[array\]\)` +type InvalidArrayMarkerOnMapType map[string]string + +type ArrayMarkersFieldTest struct { + // Valid: MinItems marker on array field + // +kubebuilder:validation:MinItems=1 + ValidMinItems []string `json:"validMinItems"` + + // Valid: MaxItems marker on array field + // +kubebuilder:validation:MaxItems=10 + ValidMaxItems []string `json:"validMaxItems"` + + // Valid: UniqueItems marker on array field + // +kubebuilder:validation:UniqueItems=true + ValidUniqueItems []string `json:"validUniqueItems"` + + // Valid: All array markers combined on array field + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=10 + // +kubebuilder:validation:UniqueItems=true + ValidAllArrayMarkers []string `json:"validAllArrayMarkers"` + + // Invalid: MinItems marker on string field + // +kubebuilder:validation:MinItems=1 // want `marker "kubebuilder:validation:MinItems": type string is not allowed \(expected one of: \[array\]\)` + InvalidMinItemsOnString string `json:"invalidMinItemsOnString"` + + // Invalid: MaxItems marker on map field + // +kubebuilder:validation:MaxItems=10 // want `marker "kubebuilder:validation:MaxItems": type object is not allowed \(expected one of: \[array\]\)` + InvalidMaxItemsOnMap map[string]string `json:"invalidMaxItemsOnMap"` + + // Invalid: UniqueItems marker on integer field + // +kubebuilder:validation:UniqueItems=true // want `marker "kubebuilder:validation:UniqueItems": type integer is not allowed \(expected one of: \[array\]\)` + InvalidUniqueItemsOnInteger int32 `json:"invalidUniqueItemsOnInteger"` + + // Invalid: MinItems marker on boolean field + // +kubebuilder:validation:MinItems=1 // want `marker "kubebuilder:validation:MinItems": type boolean is not allowed \(expected one of: \[array\]\)` + InvalidMinItemsOnBoolean bool `json:"invalidMinItemsOnBoolean"` + + // Invalid: MaxItems marker on struct field + // +kubebuilder:validation:MaxItems=10 // want `marker "kubebuilder:validation:MaxItems": type object is not allowed \(expected one of: \[array\]\)` + InvalidMaxItemsOnStruct ArrayItem `json:"invalidMaxItemsOnStruct"` + + // Valid: Using named array type with markers + ValidNamedArrayType StringArray `json:"validNamedArrayType"` + + // Valid: Using named array type (IntegerArray) + ValidIntegerArrayType IntegerArray `json:"validIntegerArrayType"` + + // Valid: Using named array type (BooleanArray) + ValidBooleanArrayType BooleanArray `json:"validBooleanArrayType"` + + // Invalid: Field marker on named array type (should use type definition) + // +kubebuilder:validation:MinItems=5 // want `marker "kubebuilder:validation:MinItems": marker should be declared on the type definition of StringArray instead of the field` + InvalidFieldMarkerOnNamedArray StringArray `json:"invalidFieldMarkerOnNamedArray"` + + // Invalid: Using invalid named type with array marker + InvalidNamedStringType InvalidArrayMarkerOnStringType `json:"invalidNamedStringType"` + + // Invalid: Using invalid named type with array marker + InvalidNamedMapType InvalidArrayMarkerOnMapType `json:"invalidNamedMapType"` +} + +type ArrayItem struct { + Name string `json:"name"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/field_only.go b/pkg/analysis/markerscope/testdata/src/a/field_only.go new file mode 100644 index 00000000..e525ca98 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/field_only.go @@ -0,0 +1,89 @@ +/* +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 a + +// Invalid: Field-only markers on type +// +optional // want `marker "optional" can only be applied to fields` +// +required // want `marker "required" can only be applied to fields` +// +nullable // want `marker "nullable" can only be applied to fields` +type InvalidFieldOnlyOnType struct { + Name string `json:"name"` +} + +// Invalid: kubebuilder:default on type +// +kubebuilder:default="default" // want `marker "kubebuilder:default" can only be applied to fields` +type InvalidDefaultOnType string + +// Invalid: kubebuilder:example on type +// +kubebuilder:example="example" // want `marker "kubebuilder:example" can only be applied to fields` +type InvalidExampleOnType string + +// Invalid: kubebuilder:validation:EmbeddedResource on type +// +kubebuilder:validation:EmbeddedResource // want `marker "kubebuilder:validation:EmbeddedResource" can only be applied to fields` +type InvalidEmbeddedResourceOnType struct { + Data map[string]interface{} `json:"data"` +} + +// Invalid: kubebuilder:validation:Schemaless on type +// +kubebuilder:validation:Schemaless // want `marker "kubebuilder:validation:Schemaless" can only be applied to fields` +type InvalidSchemalessOnType struct { + Data map[string]interface{} `json:"data"` +} + +type FieldOnlyMarkersTest struct { + // Valid: optional marker + // +optional + ValidOptional string `json:"validOptional,omitempty"` + + // Valid: required marker + // +required + ValidRequired string `json:"validRequired"` + + // Valid: k8s:optional marker + // +k8s:optional + ValidK8sOptional string `json:"validK8sOptional,omitempty"` + + // Valid: k8s:required marker + // +k8s:required + ValidK8sRequired string `json:"validK8sRequired"` + + // Valid: nullable marker + // +nullable + ValidNullable *string `json:"validNullable"` + + // Valid: kubebuilder:default marker + // +kubebuilder:default="default" + ValidDefault string `json:"validDefault"` + + // Valid: kubebuilder:example marker + // +kubebuilder:example="example" + ValidExample string `json:"validExample"` + + // Valid: kubebuilder:validation:EmbeddedResource marker + // +kubebuilder:validation:EmbeddedResource + ValidEmbeddedResource map[string]interface{} `json:"validEmbeddedResource"` + + // Valid: kubebuilder:validation:Schemaless marker + // +kubebuilder:validation:Schemaless + ValidSchemaless map[string]interface{} `json:"validSchemaless"` + + // Valid: All field-only markers combined + // +optional + // +nullable + // +kubebuilder:default="combined" + // +kubebuilder:example="combined-example" + ValidAllFieldOnlyMarkers *string `json:"validAllFieldOnlyMarkers,omitempty"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/general.go b/pkg/analysis/markerscope/testdata/src/a/general.go new file mode 100644 index 00000000..08015d45 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/general.go @@ -0,0 +1,98 @@ +/* +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 a + +type GeneralType string + +// Valid: Enum on type +// +kubebuilder:validation:Enum=A;B;C +type EnumType string + +// Valid: Format on type +// +kubebuilder:validation:Format=email +type FormatType string + +// Valid: Type on type +// +kubebuilder:validation:Type=string +type TypeType string + +// Valid: XValidation on type +// +kubebuilder:validation:XValidation:rule="self.size() > 0" +type XValidationType string + +type GeneralMarkersFieldTest struct { + // Valid: Enum marker on string field + // +kubebuilder:validation:Enum=A;B;C + ValidEnum string `json:"validEnum"` + + // Valid: Format marker on string field + // +kubebuilder:validation:Format=email + ValidFormat string `json:"validFormat"` + + // Valid: Type marker on string field + // +kubebuilder:validation:Type=string + ValidType string `json:"validType"` + + // Valid: XValidation marker on string field + // +kubebuilder:validation:XValidation:rule="self.size() > 0" + ValidXValidation string `json:"validXValidation"` + + // Valid: All general markers on string field + // +kubebuilder:validation:Enum=A;B;C + // +kubebuilder:validation:Format=email + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:XValidation:rule="self.size() > 0" + ValidAllGeneralMarkers string `json:"validAllGeneralMarkers"` + + // Valid: Using named type with general markers + ValidGeneralTyped GeneralType `json:"validGeneralTyped"` + + // Valid: Using EnumType + ValidEnumTyped EnumType `json:"validEnumTyped"` + + // Valid: Using FormatType + ValidFormatTyped FormatType `json:"validFormatTyped"` + + // Valid: Using TypeType + ValidTypeTyped TypeType `json:"validTypeTyped"` + + // Valid: Using XValidationType + ValidXValidationTyped XValidationType `json:"validXValidationTyped"` + + // Valid: General markers can be applied to any type + // +kubebuilder:validation:Enum=1;2;3 + ValidEnumOnInt int32 `json:"validEnumOnInt"` + + // Valid: Format can be applied to any type + // +kubebuilder:validation:Format=int32 + ValidFormatOnInt int32 `json:"validFormatOnInt"` + + // Invalid: Enum marker on named type + // +kubebuilder:validation:Enum=A;B;C + InvalidEnumOnGeneralType GeneralType `json:"invalidEnumOnGeneralType"` + + // Invalid: Format marker on named type + // +kubebuilder:validation:Format=email + InvalidFormatOnGeneralType GeneralType `json:"invalidFormatOnGeneralType"` + + // Invalid: Type marker on named type + // +kubebuilder:validation:Type=string + InvalidTypeOnGeneralType GeneralType `json:"invalidTypeOnGeneralType"` + + // Invalid: XValidation marker on named type + // +kubebuilder:validation:XValidation:rule="self.size() > 0" // want `marker "kubebuilder:validation:XValidation": marker should be declared on the type definition of GeneralType instead of the field` + InvalidXValidationOnGeneralType GeneralType `json:"invalidXValidationOnGeneralType"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/items.go b/pkg/analysis/markerscope/testdata/src/a/items.go new file mode 100644 index 00000000..b4c40ab9 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/items.go @@ -0,0 +1,147 @@ +/* +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 a + +// +kubebuilder:validation:items:Maximum=100 +// +kubebuilder:validation:items:Minimum=0 +// +kubebuilder:validation:items:MultipleOf=5 +type NumericArrayType []int32 + +// +kubebuilder:validation:items:Pattern="^[a-z]+$" +// +kubebuilder:validation:items:MinLength=1 +// +kubebuilder:validation:items:MaxLength=50 +type StringArrayType []string + +// +kubebuilder:validation:items:MinItems=1 +// +kubebuilder:validation:items:MaxItems=5 +type NestedArrayType [][]string + +// +kubebuilder:validation:items:MinProperties=1 +// +kubebuilder:validation:items:MaxProperties=5 +type ObjectArrayType []map[string]string + +// +kubebuilder:validation:items:Enum=A;B;C +// +kubebuilder:validation:items:Format=uuid +// +kubebuilder:validation:items:Type=string +// +kubebuilder:validation:items:XValidation:rule="self != ”" +type GeneralArrayType []string + +// Invalid: items:Maximum on string array type +// +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)` +type InvalidItemsMaximumOnStringArrayType []string + +// Invalid: items:Pattern on int array type +// +kubebuilder:validation:items:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:items:Pattern": array element: type integer is not allowed \(expected one of: \[string\]\)` +type InvalidItemsPatternOnIntArrayType []int32 + +// Invalid: items:MinProperties on string array type +// +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": array element: type string is not allowed \(expected one of: \[object\]\)` +type InvalidItemsMinPropertiesOnStringArrayType []string + +type ArrayItemsMarkersFieldTest struct { + // Valid: Numeric element constraints + // +kubebuilder:validation:items:Maximum=100 + // +kubebuilder:validation:items:Minimum=0 + // +kubebuilder:validation:items:MultipleOf=5 + // +kubebuilder:validation:items:ExclusiveMaximum=false + // +kubebuilder:validation:items:ExclusiveMinimum=false + ValidNumericArrayItems []int32 `json:"validNumericArrayItems"` + + // Valid: String element constraints + // +kubebuilder:validation:items:Pattern="^[a-z]+$" + // +kubebuilder:validation:items:MinLength=1 + // +kubebuilder:validation:items:MaxLength=50 + ValidStringArrayItems []string `json:"validStringArrayItems"` + + // Valid: Nested array constraints + // +kubebuilder:validation:items:MinItems=1 + // +kubebuilder:validation:items:MaxItems=5 + // +kubebuilder:validation:items:UniqueItems=true + ValidNestedArrayItems [][]string `json:"validNestedArrayItems"` + + // Valid: Object element constraints + // +kubebuilder:validation:items:MinProperties=1 + // +kubebuilder:validation:items:MaxProperties=5 + ValidObjectArrayItems []map[string]string `json:"validObjectArrayItems"` + + // Valid: General items markers + // +kubebuilder:validation:items:Enum=A;B;C + // +kubebuilder:validation:items:Format=uuid + // +kubebuilder:validation:items:Type=string + // +kubebuilder:validation:items:XValidation:rule="self != ''" + ValidGeneralArrayItems []string `json:"validGeneralArrayItems"` + + // Valid: Using named type with items markers + ValidNumericArrayTyped NumericArrayType `json:"validNumericArrayTyped"` + + // Valid: Using named type with string items + ValidStringArrayTyped StringArrayType `json:"validStringArrayTyped"` + + // Valid: Using named type with nested array items + ValidNestedArrayTyped NestedArrayType `json:"validNestedArrayTyped"` + + // Valid: Using named type with object items + ValidObjectArrayTyped ObjectArrayType `json:"validObjectArrayTyped"` + + // Invalid: items:Maximum marker on named type (should be on type definition) + // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": marker should be declared on the type definition of NumericArrayType instead of the field` + InvalidItemsMaximumOnNumericArrayType NumericArrayType `json:"invalidItemsMaximumOnNumericArrayType"` + + // Invalid: items:Pattern marker on named type (should be on type definition) + // +kubebuilder:validation:items:Pattern="^[a-z]+$" // want `marker "kubebuilder:validation:items:Pattern": marker should be declared on the type definition of StringArrayType instead of the field` + InvalidItemsPatternOnStringArrayType StringArrayType `json:"invalidItemsPatternOnStringArrayType"` + + // Invalid: items:MinItems marker on named type (should be on type definition) + // +kubebuilder:validation:items:MinItems=1 // want `marker "kubebuilder:validation:items:MinItems": marker should be declared on the type definition of NestedArrayType instead of the field` + InvalidItemsMinItemsOnNestedArrayType NestedArrayType `json:"invalidItemsMinItemsOnNestedArrayType"` + + // Invalid: items:MinProperties marker on named type (should be on type definition) + // +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": marker should be declared on the type definition of ObjectArrayType instead of the field` + InvalidItemsMinPropertiesOnObjectArrayType ObjectArrayType `json:"invalidItemsMinPropertiesOnObjectArrayType"` + + // Invalid: items:Enum marker on named type (should be on type definition) + // +kubebuilder:validation:items:Enum=A;B;C // want `marker "kubebuilder:validation:items:Enum": marker should be declared on the type definition of GeneralArrayType instead of the field` + InvalidItemsEnumOnGeneralArrayType GeneralArrayType `json:"invalidItemsEnumOnGeneralArrayType"` + + // Invalid: items:Format marker on named type (should be on type definition) + // +kubebuilder:validation:items:Format=uuid // want `marker "kubebuilder:validation:items:Format": marker should be declared on the type definition of GeneralArrayType instead of the field` + InvalidItemsFormatOnGeneralArrayType GeneralArrayType `json:"invalidItemsFormatOnGeneralArrayType"` + + // Invalid: items:Maximum on string array (element type mismatch) + // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)` + InvalidItemsMaximumOnStringArray []string `json:"invalidItemsMaximumOnStringArray"` + + // Invalid: items:Pattern on int array (element type mismatch) + // +kubebuilder:validation:items:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:items:Pattern": array element: type integer is not allowed \(expected one of: \[string\]\)` + InvalidItemsPatternOnIntArray []int32 `json:"invalidItemsPatternOnIntArray"` + + // Invalid: items:MinProperties on string array (element type mismatch) + // +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": array element: type string is not allowed \(expected one of: \[object\]\)` + InvalidItemsMinPropertiesOnStringArray []string `json:"invalidItemsMinPropertiesOnStringArray"` + + // Invalid: items marker on non-array field + // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": type string is not allowed \(expected one of: \[array\]\)` + InvalidItemsMarkerOnNonArray string `json:"invalidItemsMarkerOnNonArray"` + + // Invalid: Using invalid named type + InvalidItemsMaximumOnStringArrayTyped InvalidItemsMaximumOnStringArrayType `json:"invalidItemsMaximumOnStringArrayTyped"` + + // Invalid: Using invalid named type + InvalidItemsPatternOnIntArrayTyped InvalidItemsPatternOnIntArrayType `json:"invalidItemsPatternOnIntArrayTyped"` + + // Invalid: Using invalid named type + InvalidItemsMinPropertiesOnStringArrayTyped InvalidItemsMinPropertiesOnStringArrayType `json:"invalidItemsMinPropertiesOnStringArrayTyped"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/numeric.go b/pkg/analysis/markerscope/testdata/src/a/numeric.go new file mode 100644 index 00000000..4b2d2da9 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/numeric.go @@ -0,0 +1,128 @@ +/* +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 a + +// Valid: All numeric markers on type +type NumericType int32 + +// Valid: Minimum on type +// +kubebuilder:validation:Minimum=10 +type MinimumType int32 + +// Valid: Maximum on type +// +kubebuilder:validation:Maximum=50 +type MaximumType int64 + +// Valid: MultipleOf on type +// +kubebuilder:validation:MultipleOf=3 +type MultipleOfType int32 + +// Valid: Float type with numeric markers +// +kubebuilder:validation:Minimum=0.0 // want `marker "kubebuilder:validation:Minimum": type float64 is dangerous and not allowed \(set allowDangerousTypes to true to permit\)` +// +kubebuilder:validation:Maximum=1.0 // want `marker "kubebuilder:validation:Maximum": type float64 is dangerous and not allowed \(set allowDangerousTypes to true to permit\)` +type FloatType float64 + +// Invalid: Minimum marker on string type +// +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)` +type InvalidMinimumOnStringType string + +// Invalid: Maximum marker on boolean type +// +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer number\]\)` +type InvalidMaximumOnBoolType bool + +type NumericMarkersFieldTest struct { + // Valid: Minimum marker on integer field + // +kubebuilder:validation:Minimum=0 + ValidMinimum int32 `json:"validMinimum"` + + // Valid: Maximum marker on integer field + // +kubebuilder:validation:Maximum=100 + ValidMaximum int32 `json:"validMaximum"` + + // Valid: ExclusiveMinimum marker + // +kubebuilder:validation:ExclusiveMinimum=true + // +kubebuilder:validation:Minimum=0 + ValidExclusiveMinimum int32 `json:"validExclusiveMinimum"` + + // Valid: ExclusiveMaximum marker + // +kubebuilder:validation:ExclusiveMaximum=true + // +kubebuilder:validation:Maximum=100 + ValidExclusiveMaximum int32 `json:"validExclusiveMaximum"` + + // Valid: MultipleOf marker + // +kubebuilder:validation:MultipleOf=5 + ValidMultipleOf int32 `json:"validMultipleOf"` + + // Valid: All numeric markers on integer field + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + // +kubebuilder:validation:ExclusiveMinimum=false + // +kubebuilder:validation:ExclusiveMaximum=false + // +kubebuilder:validation:MultipleOf=5 + ValidAllNumericMarkers int32 `json:"validAllNumericMarkers"` + + // Valid: Using MinimumType + ValidMinimumTyped MinimumType `json:"validMinimumTyped"` + + // Valid: Using MaximumType + ValidMaximumTyped MaximumType `json:"validMaximumTyped"` + + // Valid: Using MultipleOfType + ValidMultipleOfTyped MultipleOfType `json:"validMultipleOfTyped"` + + // Valid: Using FloatType + ValidFloatTyped FloatType `json:"validFloatTyped"` + + // Invalid: Maximum marker on named type + // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": marker should be declared on the type definition of NumericType instead of the field` + InvalidMaximumOnNumericType NumericType `json:"invalidMaximumOnNumericType"` + + // Invalid: Minimum marker on named type + // +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": marker should be declared on the type definition of NumericType instead of the field` + InvalidMinimumOnNumericType NumericType `json:"invalidMinimumOnNumericType"` + + // Invalid: ExclusiveMaximum marker on named type + // +kubebuilder:validation:ExclusiveMaximum=true // want `marker "kubebuilder:validation:ExclusiveMaximum": marker should be declared on the type definition of NumericType instead of the field` + InvalidExclusiveMaximumOnNumericType NumericType `json:"invalidExclusiveMaximumOnNumericType"` + + // Invalid: ExclusiveMinimum marker on named type + // +kubebuilder:validation:ExclusiveMinimum=true // want `marker "kubebuilder:validation:ExclusiveMinimum": marker should be declared on the type definition of NumericType instead of the field` + InvalidExclusiveMinimumOnNumericType NumericType `json:"invalidExclusiveMinimumOnNumericType"` + + // Invalid: MultipleOf marker on named type + // +kubebuilder:validation:MultipleOf=5 // want `marker "kubebuilder:validation:MultipleOf": marker should be declared on the type definition of NumericType instead of the field` + InvalidMultipleOfOnNumericType NumericType `json:"invalidMultipleOfOnNumericType"` + + // Invalid: Minimum marker on string field + // +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)` + InvalidMinimumOnString string `json:"invalidMinimumOnString"` + + // Invalid: Maximum marker on boolean field + // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer number\]\)` + InvalidMaximumOnBool bool `json:"invalidMaximumOnBool"` + + // Invalid: MultipleOf marker on array field + // +kubebuilder:validation:MultipleOf=5 // want `marker "kubebuilder:validation:MultipleOf": type array is not allowed \(expected one of: \[integer number\]\)` + InvalidMultipleOfOnArray []int32 `json:"invalidMultipleOfOnArray"` + + // Invalid: Using invalid named type + // +kubebuilder:validation:Minimum=50 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)` + // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type string is not allowed \(expected one of: \[integer number\]\)` + InvalidMinimumOnStringTyped InvalidMinimumOnStringType `json:"invalidMinimumOnStringTyped"` + + // Invalid: Using invalid named type + InvalidMaximumOnBoolTyped InvalidMaximumOnBoolType `json:"invalidMaximumOnBoolTyped"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/object.go b/pkg/analysis/markerscope/testdata/src/a/object.go new file mode 100644 index 00000000..0f98eca8 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/object.go @@ -0,0 +1,86 @@ +/* +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 a + +type ObjectType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Valid: MinProperties on type +// +kubebuilder:validation:MinProperties=1 +type MinPropertiesType struct { + Field string `json:"field"` +} + +// Valid: MaxProperties on type +// +kubebuilder:validation:MaxProperties=10 +type MaxPropertiesType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Invalid: MinProperties marker on string type +// +kubebuilder:validation:MinProperties=2 // want `marker "kubebuilder:validation:MinProperties": type string is not allowed \(expected one of: \[object\]\)` +type InvalidMinPropertiesOnStringType string + +// Invalid: MaxProperties marker on array type +// +kubebuilder:validation:MaxProperties=5 // want `marker "kubebuilder:validation:MaxProperties": type array is not allowed \(expected one of: \[object\]\)` +type InvalidMaxPropertiesOnArrayType []string + +type ObjectMarkersFieldTest struct { + // Valid: MinProperties marker on map field + // +kubebuilder:validation:MinProperties=1 + ValidMinProperties map[string]string `json:"validMinProperties"` + + // Valid: MaxProperties marker on map field + // +kubebuilder:validation:MaxProperties=10 + ValidMaxProperties map[string]string `json:"validMaxProperties"` + + // Valid: All map markers on map field + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=10 + ValidAllObjectMarkers map[string]string `json:"validAllObjectMarkers"` + + // Valid: Using MinPropertiesType + ValidMinPropertiesTyped MinPropertiesType `json:"validMinPropertiesTyped"` + + // Valid: Using MaxPropertiesType + ValidMaxPropertiesTyped MaxPropertiesType `json:"validMaxPropertiesTyped"` + + // Invalid: MinProperties marker on named type + // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties": marker should be declared on the type definition of MinPropertiesType instead of the field` + InvalidMinPropertiesOnMinPropertiesType MinPropertiesType `json:"invalidMinPropertiesOnMinPropertiesType"` + + // Invalid: MaxProperties marker on named type + // +kubebuilder:validation:MaxProperties=10 // want `marker "kubebuilder:validation:MaxProperties": marker should be declared on the type definition of MaxPropertiesType instead of the field` + InvalidMaxPropertiesOnMaxPropertiesType MaxPropertiesType `json:"invalidMaxPropertiesOnMaxPropertiesType"` + + // Invalid: MinProperties marker on string field + // +kubebuilder:validation:MinProperties=2 // want `marker "kubebuilder:validation:MinProperties": type string is not allowed \(expected one of: \[object\]\)` + InvalidMinPropertiesOnString string `json:"invalidMinPropertiesOnString"` + + // Invalid: MaxProperties marker on array field + // +kubebuilder:validation:MaxProperties=5 // want `marker "kubebuilder:validation:MaxProperties": type array is not allowed \(expected one of: \[object\]\)` + InvalidMaxPropertiesOnArray []string `json:"invalidMaxPropertiesOnArray"` + + // Invalid: Using invalid named type + // +kubebuilder:validation:MinProperties=2 // want `marker "kubebuilder:validation:MinProperties": type string is not allowed \(expected one of: \[object\]\)` + InvalidMinPropertiesOnStringTyped InvalidMinPropertiesOnStringType `json:"invalidMinPropertiesOnStringTyped"` + + // Invalid: Using invalid named type + InvalidMaxPropertiesOnArrayTyped InvalidMaxPropertiesOnArrayType `json:"invalidMaxPropertiesOnArrayTyped"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/scope.go b/pkg/analysis/markerscope/testdata/src/a/scope.go new file mode 100644 index 00000000..fa5b4da7 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/scope.go @@ -0,0 +1,68 @@ +/* +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 a + +// Valid: PreserveUnknownFields on type +// +kubebuilder:pruning:PreserveUnknownFields +type ValidPreserveUnknownFieldsType struct { + Name string `json:"name"` +} + +// Valid: title on type +// +kubebuilder:title="My Title" +type ValidTitleType struct { + Name string `json:"name"` +} + +// Valid: All AnyScope markers on type +// +kubebuilder:pruning:PreserveUnknownFields +// +kubebuilder:title="Combined Title" +type ValidAllAnyScopeType struct { + Name string `json:"name"` +} + +type NoMarkerAllAnyScopeType struct { + Name string `json:"name"` +} + +type AnyScopeOnFieldTest struct { + // Valid: PreserveUnknownFields on field + // +kubebuilder:pruning:PreserveUnknownFields + ValidPreserveUnknownFields map[string]string `json:"validPreserveUnknownFields"` + + // Valid: title on field + // +kubebuilder:title="Field Title" + ValidTitle map[string]string `json:"validTitle"` + + // Valid: All AnyScope markers on field + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:title="Combined Field Title" + ValidAllAnyScopeMarkers map[string]interface{} `json:"validAllAnyScopeMarkers"` + + // Valid: Using type with AnyScope markers + ValidPreserveUnknownFieldsTyped ValidPreserveUnknownFieldsType `json:"validPreserveUnknownFieldsTyped"` + + // Valid: Using type with title marker + ValidTitleTyped ValidTitleType `json:"validTitleTyped"` + + // Valid: Using type with all AnyScope markers + ValidAllAnyScopeTyped ValidAllAnyScopeType `json:"validAllAnyScopeTyped"` + + // Valid: Using type with no AnyScope markers + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:title="Field Combined Title" + NoMarkerAllAnyScopeType NoMarkerAllAnyScopeType `json:"noMarkerAllAnyScopeType"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/string.go b/pkg/analysis/markerscope/testdata/src/a/string.go new file mode 100644 index 00000000..53046bf5 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/string.go @@ -0,0 +1,101 @@ +/* +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 a + +type StringType string + +// Valid: Pattern on type +// +kubebuilder:validation:Pattern="^[a-z]+$" +type PatternType string + +// Valid: MinLength on type +// +kubebuilder:validation:MinLength=1 +type MinLengthType string + +// Valid: MaxLength on type +// +kubebuilder:validation:MaxLength=100 +type MaxLengthType string + +// Invalid: Pattern marker on integer type +// +kubebuilder:validation:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:Pattern": type integer is not allowed \(expected one of: \[string\]\)` +type InvalidPatternOnIntType int32 + +// Invalid: MinLength marker on boolean type +// +kubebuilder:validation:MinLength=5 // want `marker "kubebuilder:validation:MinLength": type boolean is not allowed \(expected one of: \[string\]\)` +type InvalidMinLengthOnBoolType bool + +// Invalid: MaxLength marker on array type +// +kubebuilder:validation:MaxLength=10 // want `marker "kubebuilder:validation:MaxLength": type array is not allowed \(expected one of: \[string\]\)` +type InvalidMaxLengthOnArrayType []string + +type StringMarkersFieldTest struct { + // Valid: Pattern marker on string field + // +kubebuilder:validation:Pattern="^[a-z]+$" + ValidPattern string `json:"validPattern"` + + // Valid: MinLength marker on string field + // +kubebuilder:validation:MinLength=1 + ValidMinLength string `json:"validMinLength"` + + // Valid: MaxLength marker on string field + // +kubebuilder:validation:MaxLength=100 + ValidMaxLength string `json:"validMaxLength"` + + // Valid: All string markers on string field + // +kubebuilder:validation:Pattern="^[a-z]+$" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=100 + ValidAllStringMarkers string `json:"validAllStringMarkers"` + + // Valid: Using PatternType + ValidPatternTyped PatternType `json:"validPatternTyped"` + + // Valid: Using MinLengthType + ValidMinLengthTyped MinLengthType `json:"validMinLengthTyped"` + + // Valid: Using MaxLengthType + ValidMaxLengthTyped MaxLengthType `json:"validMaxLengthTyped"` + + // Invalid: Pattern marker on integer field + // +kubebuilder:validation:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:Pattern": type integer is not allowed \(expected one of: \[string\]\)` + InvalidPatternOnInt int32 `json:"invalidPatternOnInt"` + + // Invalid: MinLength marker on boolean field + // +kubebuilder:validation:MinLength=5 // want `marker "kubebuilder:validation:MinLength": type boolean is not allowed \(expected one of: \[string\]\)` + InvalidMinLengthOnBool bool `json:"invalidMinLengthOnBool"` + + // Invalid: MaxLength marker on array field + // +kubebuilder:validation:MaxLength=10 // want `marker "kubebuilder:validation:MaxLength": type array is not allowed \(expected one of: \[string\]\)` + InvalidMaxLengthOnArray []string `json:"invalidMaxLengthOnArray"` + + // Invalid: MinLength marker on named type + // +kubebuilder:validation:MinLength=1 // want `marker "kubebuilder:validation:MinLength": marker should be declared on the type definition of StringType instead of the field` + InvalidMinLengthOnMinLengthType StringType `json:"invalidMinLengthOnMinLengthType"` + + // Invalid: MaxLength marker on named type + // +kubebuilder:validation:MaxLength=100 // want `marker "kubebuilder:validation:MaxLength": marker should be declared on the type definition of StringType instead of the field` + InvalidMaxLengthOnMaxLengthType StringType `json:"invalidMaxLengthOnMaxLengthType"` + + // Invalid: Pattern marker on named type + // +kubebuilder:validation:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:Pattern": marker should be declared on the type definition of StringType instead of the field` + InvalidPatternOnIntTyped StringType `json:"invalidPatternOnIntTyped"` + + // Invalid: Using invalid named type + InvalidMinLengthOnBoolTyped InvalidMinLengthOnBoolType `json:"invalidMinLengthOnBoolTyped"` + + // Invalid: Using invalid named type + InvalidMaxLengthOnArrayTyped InvalidMaxLengthOnArrayType `json:"invalidMaxLengthOnArrayTyped"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/topology.go b/pkg/analysis/markerscope/testdata/src/a/topology.go new file mode 100644 index 00000000..f405ad32 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/topology.go @@ -0,0 +1,128 @@ +/* +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 a + +// Valid: listType on array type +// +listType=map +type ItemList []Item + +// Valid: listMapKey on array type +// +listType=map +// +listMapKey=name +type ItemListWithKey []Item + +// Valid: mapType on map type +// +mapType=granular +type ConfigMap map[string]string + +// Valid: structType on struct type +// +structType=atomic +type AtomicStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Invalid: listType on non-array type +// +listType=map // want `marker "listType": type object is not allowed \(expected one of: \[array\]\)` +type InvalidListTypeOnStruct struct { + Field string `json:"field"` +} + +// Invalid: mapType on non-map type +// +mapType=granular // want `marker "mapType": type array is not allowed \(expected one of: \[object\]\)` +type InvalidMapTypeOnArray []string + +// Invalid: structType on non-struct type +// +structType=atomic // want `marker "structType": type string is not allowed \(expected one of: \[object\]\)` +type InvalidStructTypeOnString string + +type TopologyMarkersFieldTest struct { + // Valid: listType marker on array field + // +listType=map + ValidListType []Item `json:"validListType"` + + // Valid: listMapKey marker on array field + // +listType=map + // +listMapKey=name + ValidListMapKey []Item `json:"validListMapKey"` + + // Valid: mapType marker on map field + // +mapType=granular + ValidMapType map[string]string `json:"validMapType"` + + // Valid: structType marker on struct field + // +structType=atomic + ValidStructType EmbeddedStruct `json:"validStructType"` + + // Valid: Using named type with listType + ValidItemListTyped ItemList `json:"validItemListTyped"` + + // Valid: Using named type with listMapKey + ValidItemListWithKeyTyped ItemListWithKey `json:"validItemListWithKeyTyped"` + + // Valid: Using named type with mapType + ValidConfigMapTyped ConfigMap `json:"validConfigMapTyped"` + + // Valid: Using named type with structType + ValidAtomicStructTyped AtomicStruct `json:"validAtomicStructTyped"` + + // Invalid: listType marker on named type + // +listType=map // want `marker "listType": marker should be declared on the type definition of ItemList instead of the field` + InvalidListTypeOnItemList ItemList `json:"invalidListTypeOnItemList"` + + // Invalid: listMapKey marker on named type + // +listMapKey=name // want `marker "listMapKey": marker should be declared on the type definition of ItemList instead of the field` + InvalidListMapKeyOnItemList ItemList `json:"invalidListMapKeyOnItemList"` + + // Invalid: mapType marker on named type + // +mapType=granular // want `marker "mapType": marker should be declared on the type definition of ConfigMap instead of the field` + InvalidMapTypeOnConfigMap ConfigMap `json:"invalidMapTypeOnConfigMap"` + + // Invalid: structType marker on named type + // +structType=atomic + InvalidStructTypeOnAtomicStruct AtomicStruct `json:"invalidStructTypeOnAtomicStruct"` + + // Invalid: listType marker on string field + // +listType=map // want `marker "listType": type string is not allowed \(expected one of: \[array\]\)` + InvalidListTypeOnString string `json:"invalidListTypeOnString"` + + // Invalid: mapType marker on array field + // +mapType=granular // want `marker "mapType": type array is not allowed \(expected one of: \[object\]\)` + InvalidMapTypeOnArray []string `json:"invalidMapTypeOnArray"` + + // Invalid: structType marker on integer field + // +structType=atomic // want `marker "structType": type integer is not allowed \(expected one of: \[object\]\)` + InvalidStructTypeOnInt int32 `json:"invalidStructTypeOnInt"` + + // Invalid: Using invalid named type + // +listType=map // want `marker "listType": type object is not allowed \(expected one of: \[array\]\)` + InvalidListTypeOnStructTyped InvalidListTypeOnStruct `json:"invalidListTypeOnStructTyped"` + + // Invalid: Using invalid named type + InvalidMapTypeOnArrayTyped InvalidMapTypeOnArray `json:"invalidMapTypeOnArrayTyped"` + + // Invalid: Using invalid named type + InvalidStructTypeOnStringTyped InvalidStructTypeOnString `json:"invalidStructTypeOnStringTyped"` +} + +type EmbeddedStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +type Item struct { + Name string `json:"name"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/type_only.go b/pkg/analysis/markerscope/testdata/src/a/type_only.go new file mode 100644 index 00000000..35247bf8 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/type_only.go @@ -0,0 +1,82 @@ +/* +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 a + +// Valid: ExactlyOneOf on type +// +kubebuilder:validation:items:ExactlyOneOf={Field1,Field2} +type ValidExactlyOneOfType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Valid: AtMostOneOf on type +// +kubebuilder:validation:items:AtMostOneOf={Field1,Field2} +type ValidAtMostOneOfType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Valid: AtLeastOneOf on type +// +kubebuilder:validation:items:AtLeastOneOf={Field1,Field2} +type ValidAtLeastOneOfType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Valid: All type-only markers combined +// +kubebuilder:validation:items:ExactlyOneOf={Field1,Field2} +// +kubebuilder:validation:items:AtMostOneOf={Field3,Field4} +// +kubebuilder:validation:items:AtLeastOneOf={Field5,Field6} +type ValidAllTypeOnlyMarkers struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + Field4 string `json:"field4"` + Field5 string `json:"field5"` + Field6 string `json:"field6"` +} + +type TypeOnlyMarkersTest struct { + // Invalid: ExactlyOneOf on field + // +kubebuilder:validation:items:ExactlyOneOf={field1,field2} // want `marker "kubebuilder:validation:items:ExactlyOneOf" can only be applied to types` + InvalidExactlyOneOfOnField string `json:"invalidExactlyOneOfOnField"` + + // Invalid: AtMostOneOf on field + // +kubebuilder:validation:items:AtMostOneOf={field1,field2} // want `marker "kubebuilder:validation:items:AtMostOneOf" can only be applied to types` + InvalidAtMostOneOfOnField string `json:"invalidAtMostOneOfOnField"` + + // Invalid: AtLeastOneOf on field + // +kubebuilder:validation:items:AtLeastOneOf={field1,field2} // want `marker "kubebuilder:validation:items:AtLeastOneOf" can only be applied to types` + InvalidAtLeastOneOfOnField string `json:"invalidAtLeastOneOfOnField"` + + // Invalid: All type-only markers on field + // +kubebuilder:validation:items:ExactlyOneOf={field1,field2} // want `marker "kubebuilder:validation:items:ExactlyOneOf" can only be applied to types` + // +kubebuilder:validation:items:AtMostOneOf={field1,field2} // want `marker "kubebuilder:validation:items:AtMostOneOf" can only be applied to types` + // +kubebuilder:validation:items:AtLeastOneOf={field1,field2} // want `marker "kubebuilder:validation:items:AtLeastOneOf" can only be applied to types` + InvalidAllTypeOnlyOnField string `json:"invalidAllTypeOnlyOnField"` + + // Valid: Using type with type-only markers + ValidExactlyOneOf ValidExactlyOneOfType `json:"validExactlyOneOf"` + + // Valid: Using type with type-only markers + ValidAtMostOneOf ValidAtMostOneOfType `json:"validAtMostOneOf"` + + // Valid: Using type with type-only markers + ValidAtLeastOneOf ValidAtLeastOneOfType `json:"validAtLeastOneOf"` + + // Valid: Using type with all type-only markers + ValidAllTypeOnly ValidAllTypeOnlyMarkers `json:"validAllTypeOnly"` +} diff --git a/pkg/analysis/markerscope/testdata/src/c/c.go b/pkg/analysis/markerscope/testdata/src/c/c.go index 84abd2b0..c0b2e7e2 100644 --- a/pkg/analysis/markerscope/testdata/src/c/c.go +++ b/pkg/analysis/markerscope/testdata/src/c/c.go @@ -39,3 +39,15 @@ type FieldMarkerTest struct { // +kubebuilder:validation:items:ExactlyOneOf={field1} // want `marker "kubebuilder:validation:items:ExactlyOneOf" can only be applied to types` InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` } + +type MapType map[string]string + +type StringType string + +type MapTypeFieldTest struct { + // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties": marker should be declared on the type definition of MapType instead of the field` + ValidMinPropertiesField MapType `json:"validMinPropertiesField"` + + // +kubebuilder:validation:MaxLength=10 // want `marker "kubebuilder:validation:MaxLength": marker should be declared on the type definition of StringType instead of the field` + ValidMaxLengthField StringType `json:"validMaxLengthField"` +} diff --git a/pkg/analysis/markerscope/testdata/src/c/c.go.golden b/pkg/analysis/markerscope/testdata/src/c/c.go.golden index 5e623995..695a981b 100644 --- a/pkg/analysis/markerscope/testdata/src/c/c.go.golden +++ b/pkg/analysis/markerscope/testdata/src/c/c.go.golden @@ -39,3 +39,15 @@ type FieldMarkerTest struct { // +required InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` } + +// +kubebuilder:validation:MinProperties=1 +type MapType map[string]string + +// +kubebuilder:validation:MaxLength=10 +type StringType string + +type MapTypeFieldTest struct { + ValidMinPropertiesField MapType `json:"validMinPropertiesField"` + + ValidMaxLengthField StringType `json:"validMaxLengthField"` +} diff --git a/pkg/analysis/markerscope/testdata/src/test/doc.go b/pkg/analysis/markerscope/testdata/src/test/doc.go deleted file mode 100644 index fd1f5244..00000000 --- a/pkg/analysis/markerscope/testdata/src/test/doc.go +++ /dev/null @@ -1,22 +0,0 @@ -/* -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. -*/ - -// +kubebuilder:object:generate=true -// +kubebuilder:ac:generate=true -// +groupName=test.example.com -// +versionName=v1 - -package test \ No newline at end of file diff --git a/pkg/analysis/markerscope/testdata/src/test/go.mod b/pkg/analysis/markerscope/testdata/src/test/go.mod deleted file mode 100644 index 087d0a8f..00000000 --- a/pkg/analysis/markerscope/testdata/src/test/go.mod +++ /dev/null @@ -1,22 +0,0 @@ -module example.com/test - -go 1.21 - -require k8s.io/apimachinery v0.29.0 - -require ( - github.com/go-logr/logr v1.3.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/text v0.13.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/klog/v2 v2.110.1 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect -) diff --git a/pkg/analysis/markerscope/testdata/src/test/go.sum b/pkg/analysis/markerscope/testdata/src/test/go.sum deleted file mode 100644 index 7c9d2800..00000000 --- a/pkg/analysis/markerscope/testdata/src/test/go.sum +++ /dev/null @@ -1,82 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= -k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= -k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= -k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/analysis/markerscope/testdata/src/test/types.go b/pkg/analysis/markerscope/testdata/src/test/types.go deleted file mode 100644 index 5929e3a8..00000000 --- a/pkg/analysis/markerscope/testdata/src/test/types.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -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 test - -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - -// +kubebuilder:object:root=true - -// TestResource is the Schema for the testresources API -type TestResource struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec TestResourceSpec `json:"spec,omitempty"` - Status TestResourceStatus `json:"status,omitempty"` -} - -// TestResourceSpec defines the desired state of TestResource -type TestResourceSpec struct { - // Valid: MinProperties on map field - // +kubebuilder:validation:MinProperties=1 - ConfigMap map[string]string `json:"configMap,omitempty"` - - // Valid: MinProperties on struct field - Settings []SettingsStruct `json:"settings,omitempty"` - - // Invalid: MinProperties on slice field (should cause error) - Items []string `json:"items,omitempty"` - - // Invalid: MinProperties on string field (should cause error) - Name string `json:"name,omitempty"` -} - -// +kubebuilder:validation:MinProperties=2 -type SettingsStruct struct { - Key string `json:"key"` - Value string `json:"value"` -} - -// TestResourceStatus defines the observed state of TestResource -type TestResourceStatus struct { - Ready bool `json:"ready,omitempty"` -} - -// +kubebuilder:object:root=true - -// TestResourceList contains a list of TestResource -type TestResourceList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []TestResource `json:"items"` -} From 330da8d8c1d0ec508f7f8340437e33571ab0eafc Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Mon, 20 Oct 2025 11:26:46 +0900 Subject: [PATCH 07/18] Refactor error handling in marker scope validation by introducing custom error types for invalid schema types, type constraints, and element constraints. Update validation functions to return these new error types for improved clarity and maintainability. Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 9 ++-- pkg/analysis/markerscope/errors.go | 66 +++++++++++++++++++++++-- pkg/analysis/markerscope/initializer.go | 6 ++- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 6b4c462a..a5ac179e 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -36,7 +36,6 @@ const ( name = "markerscope" ) -// TODO: SuggestFix. func init() { // Register all markers we want to validate scope for defaults := DefaultMarkerRules() @@ -285,7 +284,7 @@ func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.F if rule.StrictTypeConstraint && rule.Scope == AnyScope { namedType, ok := tv.Type.(*types.Named) if ok { - return fmt.Errorf("%w of %s instead of the field", errMarkerShouldBeOnTypeDefinition, namedType.Obj().Name()) + return &MarkerShouldBeOnTypeDefinitionError{TypeName: namedType.Obj().Name()} } } @@ -317,7 +316,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint, allowDanger if !allowDangerousTypes && schemaType == SchemaTypeNumber { // Get the underlying type for better error messages underlyingType := getUnderlyingType(t) - return fmt.Errorf("type %s is dangerous and not allowed (set allowDangerousTypes to true to permit)", underlyingType.String()) + return &DengerousTypeError{Type: underlyingType.String()} } if tc == nil { @@ -327,7 +326,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint, allowDanger // Check if the schema type is allowed if len(tc.AllowedSchemaTypes) > 0 { if !slices.Contains(tc.AllowedSchemaTypes, schemaType) { - return fmt.Errorf("type %s is not allowed (expected one of: %v)", schemaType, tc.AllowedSchemaTypes) + return &TypeNotAllowedError{Type: schemaType, AllowedTypes: tc.AllowedSchemaTypes} } } @@ -336,7 +335,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint, allowDanger elemType := getElementType(t) if elemType != nil { if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint, allowDangerousTypes); err != nil { - return fmt.Errorf("array element: %w", err) + return &InvalidElementConstraintError{Err: err} } } } diff --git a/pkg/analysis/markerscope/errors.go b/pkg/analysis/markerscope/errors.go index 6415ba74..9f637329 100644 --- a/pkg/analysis/markerscope/errors.go +++ b/pkg/analysis/markerscope/errors.go @@ -15,11 +15,67 @@ limitations under the License. */ package markerscope -import "errors" +import ( + "errors" + "fmt" +) var ( - errScopeNonZero = errors.New("scope must be non-zero") - errInvalidScopeBits = errors.New("invalid scope bits") - errInvalidSchemaType = errors.New("invalid schema type") - errMarkerShouldBeOnTypeDefinition = errors.New("marker should be declared on the type definition") + errScopeNonZero = errors.New("scope must be non-zero") + errInvalidScopeBits = errors.New("invalid scope bits") ) + +// InvalidSchemaTypeError represents an error when a schema type is invalid. +type InvalidSchemaTypeError struct { + SchemaType string +} + +func (e *InvalidSchemaTypeError) Error() string { + return fmt.Sprintf("invalid schema type: %q", e.SchemaType) +} + +// InvalidTypeConstraintError represents an error when a type constraint is invalid. +type InvalidTypeConstraintError struct { + Err error +} + +func (e *InvalidTypeConstraintError) Error() string { + return fmt.Sprintf("invalid type constraint: %v", e.Err) +} + +// InvalidElementConstraintError represents an error when an element constraint is invalid. +type InvalidElementConstraintError struct { + Err error +} + +func (e *InvalidElementConstraintError) Error() string { + return fmt.Sprintf("array element: %v", e.Err) +} + +// MarkerShouldBeOnTypeDefinitionError represents an error when a marker should be declared on the type definition. +type MarkerShouldBeOnTypeDefinitionError struct { + TypeName string +} + +func (e *MarkerShouldBeOnTypeDefinitionError) Error() string { + return fmt.Sprintf("marker should be declared on the type definition of %s instead of the field", e.TypeName) +} + +// DengerousTypeError represents an error when a dangerous type is used. +type DengerousTypeError struct { + Type string +} + +func (e *DengerousTypeError) Error() string { + return fmt.Sprintf("type %s is dangerous and not allowed (set allowDangerousTypes to true to permit)", e.Type) +} + +// TypeNotAllowedError represents an error when a type is not allowed. +type TypeNotAllowedError struct { + Type SchemaType + AllowedTypes []SchemaType +} + +func (e *TypeNotAllowedError) Error() string { + return fmt.Sprintf("type %s is not allowed (expected one of: %v)", e.Type, e.AllowedTypes) +} diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index d889ac8e..cd0f9e7a 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -82,7 +82,9 @@ func validateMarkerRule(rule MarkerScopeRule) error { // Validate type constraint if present if rule.TypeConstraint != nil { if err := validateTypeConstraint(rule.TypeConstraint); err != nil { - return fmt.Errorf("invalid type constraint: %w", err) + return &InvalidTypeConstraintError{ + Err: err, + } } } @@ -97,7 +99,7 @@ func validateTypeConstraint(tc *TypeConstraint) error { // Validate schema types if specified for _, st := range tc.AllowedSchemaTypes { if !isValidSchemaType(st) { - return fmt.Errorf("%w: %q", errInvalidSchemaType, st) + return &InvalidSchemaTypeError{SchemaType: string(st)} } } From 222cd0dcac97fbed849e8517a99ab6971e460b74 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Sun, 26 Oct 2025 16:31:33 +0900 Subject: [PATCH 08/18] refactor(markerscope): fix linters and add test golden files Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 128 ++++++++++----- pkg/analysis/markerscope/analyzer_test.go | 18 +-- .../markerscope/testdata/src/a/array.go | 6 +- .../testdata/src/a/array.go.golden | 92 +++++++++++ .../testdata/src/a/field_only.go.golden | 82 ++++++++++ .../testdata/src/a/general.go.golden | 98 ++++++++++++ .../markerscope/testdata/src/a/items.go | 31 ++-- .../testdata/src/a/items.go.golden | 147 ++++++++++++++++++ .../testdata/src/a/numeric.go.golden | 119 ++++++++++++++ .../markerscope/testdata/src/a/object.go | 18 ++- .../testdata/src/a/object.go.golden | 91 +++++++++++ .../testdata/src/a/scope.go.golden | 68 ++++++++ .../testdata/src/a/string.go.golden | 95 +++++++++++ .../markerscope/testdata/src/a/topology.go | 25 +-- .../testdata/src/a/topology.go.golden | 128 +++++++++++++++ .../testdata/src/a/type_only.go.golden | 76 +++++++++ pkg/analysis/markerscope/testdata/src/c/c.go | 53 ------- .../markerscope/testdata/src/c/c.go.golden | 53 ------- 18 files changed, 1143 insertions(+), 185 deletions(-) create mode 100644 pkg/analysis/markerscope/testdata/src/a/array.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/field_only.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/general.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/items.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/numeric.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/object.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/scope.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/string.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/topology.go.golden create mode 100644 pkg/analysis/markerscope/testdata/src/a/type_only.go.golden delete mode 100644 pkg/analysis/markerscope/testdata/src/c/c.go delete mode 100644 pkg/analysis/markerscope/testdata/src/c/c.go.golden diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index a5ac179e..41c99005 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -16,12 +16,12 @@ limitations under the License. package markerscope import ( + "errors" "fmt" "go/ast" "go/types" "maps" "slices" - "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -143,48 +143,12 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark // Check if FieldScope is allowed if !rule.Scope.Allows(FieldScope) { - var message string - - var fixes []analysis.SuggestedFix - - if rule.Scope == TypeScope { - message = fmt.Sprintf("marker %q can only be applied to types", marker.Identifier) - - if a.policy == MarkerScopePolicySuggestFix { - fixes = a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule) - } - } else { - // This shouldn't happen in practice, but handle it gracefully - message = fmt.Sprintf("marker %q cannot be applied to fields", marker.Identifier) - } - - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: message, - SuggestedFixes: fixes, - }) - + a.reportFieldScopeViolation(pass, field, marker, rule) continue } // Check type constraints if present - if err := a.validateFieldTypeConstraint(pass, field, rule, a.allowDangerousTypes); err != nil { - if a.policy == MarkerScopePolicySuggestFix { - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), - SuggestedFixes: a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule), - }) - } else { - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), - }) - } - } + a.checkFieldTypeConstraintViolation(pass, field, marker, rule) } } @@ -226,6 +190,67 @@ func (a *analyzer) checkSingleTypeMarkers(pass *analysis.Pass, typeSpec *ast.Typ } } +// reportFieldScopeViolation reports a scope violation for a field marker. +func (a *analyzer) reportFieldScopeViolation(pass *analysis.Pass, field *ast.Field, marker markershelper.Marker, rule MarkerScopeRule) { + var message string + + var fixes []analysis.SuggestedFix + + if rule.Scope == TypeScope { + message = fmt.Sprintf("marker %q can only be applied to types", marker.Identifier) + + if a.policy == MarkerScopePolicySuggestFix { + fixes = a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule) + } + } else { + // This shouldn't happen in practice, but handle it gracefully + message = fmt.Sprintf("marker %q cannot be applied to fields", marker.Identifier) + } + + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: message, + SuggestedFixes: fixes, + }) +} + +// checkFieldTypeConstraintViolation checks and reports type constraint violations for field markers. +func (a *analyzer) checkFieldTypeConstraintViolation(pass *analysis.Pass, field *ast.Field, marker markershelper.Marker, rule MarkerScopeRule) { + if err := a.validateFieldTypeConstraint(pass, field, rule, a.allowDangerousTypes); err != nil { + var fixes []analysis.SuggestedFix + + if a.policy == MarkerScopePolicySuggestFix { + // Check if this is a "should be on type definition" error + var moveErr *MarkerShouldBeOnTypeDefinitionError + if errors.As(err, &moveErr) { + // Suggest moving to type definition + fixes = a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule) + } else { + // Type constraint violation - suggest removing the marker + fixes = []analysis.SuggestedFix{ + { + Message: "Remove invalid marker", + TextEdits: []analysis.TextEdit{ + { + Pos: marker.Pos, + End: marker.End + 1, // Include newline + }, + }, + }, + } + } + } + + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), + SuggestedFixes: fixes, + }) + } +} + // reportTypeScopeViolation reports a scope violation for a type marker. func (a *analyzer) reportTypeScopeViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) { var message string @@ -256,7 +281,26 @@ func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *a var fixes []analysis.SuggestedFix if a.policy == MarkerScopePolicySuggestFix { - fixes = a.suggestMoveToField(pass, typeSpec, marker, rule) + // Check if this is a "should be on field" error (though validateTypeSpecTypeConstraint doesn't return this) + // For consistency with checkFieldMarkers, we check the error type + var moveErr *MarkerShouldBeOnTypeDefinitionError + if errors.As(err, &moveErr) { + // This shouldn't happen for type specs, but handle it for consistency + fixes = a.suggestMoveToField(pass, typeSpec, marker, rule) + } else { + // Type constraint violation - suggest removing the marker + fixes = []analysis.SuggestedFix{ + { + Message: "Remove invalid marker", + TextEdits: []analysis.TextEdit{ + { + Pos: marker.Pos, + End: marker.End + 1, // Include newline + }, + }, + }, + } + } } message := fmt.Sprintf("marker %q: %s", marker.Identifier, err) @@ -364,7 +408,6 @@ func (a *analyzer) suggestMoveToField(pass *analysis.Pass, typeSpec *ast.TypeSpe } fieldTypeSpecs := utils.LookupFieldsUsingType(pass, typeSpec) - fmt.Println("fieldTypeSpecs", fieldTypeSpecs) var edits []analysis.TextEdit @@ -422,7 +465,6 @@ func (a *analyzer) suggestMoveToFieldsIfCompatible(pass *analysis.Pass, field *a Pos: marker.Pos, End: marker.End + 1, }) - // Add marker to the line before the type definition markerText := a.extractMarkerText(marker) @@ -445,5 +487,5 @@ func (a *analyzer) suggestMoveToFieldsIfCompatible(pass *analysis.Pass, field *a } func (a *analyzer) extractMarkerText(marker markershelper.Marker) string { - return strings.Split(marker.RawComment, " //")[0] + "\n" + return marker.RawComment + "\n" } diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index 26852525..57938c96 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -30,6 +30,15 @@ func TestAnalyzerWarnOnly(t *testing.T) { analysistest.Run(t, testdata, analyzer, "a") } +func TestAnalyzerSuggestFixes(t *testing.T) { + testdata := analysistest.TestData() + cfg := &MarkerScopeConfig{ + Policy: MarkerScopePolicySuggestFix, + } + analyzer := newAnalyzer(cfg) + analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a") +} + func TestAnalyzerWithCustomMarkers(t *testing.T) { testdata := analysistest.TestData() cfg := &MarkerScopeConfig{ @@ -72,12 +81,3 @@ func TestAnalyzerWithCustomMarkers(t *testing.T) { analyzer := newAnalyzer(cfg) analysistest.Run(t, testdata, analyzer, "b") } - -func TestAnalyzerWithSuggestFix(t *testing.T) { - testdata := analysistest.TestData() - cfg := &MarkerScopeConfig{ - Policy: MarkerScopePolicySuggestFix, - } - analyzer := newAnalyzer(cfg) - analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "c") -} diff --git a/pkg/analysis/markerscope/testdata/src/a/array.go b/pkg/analysis/markerscope/testdata/src/a/array.go index beaf7b8b..da5fc802 100644 --- a/pkg/analysis/markerscope/testdata/src/a/array.go +++ b/pkg/analysis/markerscope/testdata/src/a/array.go @@ -26,6 +26,8 @@ type IntegerArray []int32 // +kubebuilder:validation:MaxItems=100 type BooleanArray []bool +type FieldMarkerOnNamedArray []string + // Type definitions with invalid markers // +kubebuilder:validation:MinItems=1 // want `marker "kubebuilder:validation:MinItems": type string is not allowed \(expected one of: \[array\]\)` type InvalidArrayMarkerOnStringType string @@ -82,8 +84,8 @@ type ArrayMarkersFieldTest struct { ValidBooleanArrayType BooleanArray `json:"validBooleanArrayType"` // Invalid: Field marker on named array type (should use type definition) - // +kubebuilder:validation:MinItems=5 // want `marker "kubebuilder:validation:MinItems": marker should be declared on the type definition of StringArray instead of the field` - InvalidFieldMarkerOnNamedArray StringArray `json:"invalidFieldMarkerOnNamedArray"` + // +kubebuilder:validation:MinItems=5 // want `marker "kubebuilder:validation:MinItems": marker should be declared on the type definition of FieldMarkerOnNamedArray instead of the field` + InvalidFieldMarkerOnNamedArray FieldMarkerOnNamedArray `json:"invalidFieldMarkerOnNamedArray"` // Invalid: Using invalid named type with array marker InvalidNamedStringType InvalidArrayMarkerOnStringType `json:"invalidNamedStringType"` diff --git a/pkg/analysis/markerscope/testdata/src/a/array.go.golden b/pkg/analysis/markerscope/testdata/src/a/array.go.golden new file mode 100644 index 00000000..5c9d93dc --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/array.go.golden @@ -0,0 +1,92 @@ +/* +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 a + +// +kubebuilder:validation:MinItems=1 +// +kubebuilder:validation:MaxItems=10 +// +kubebuilder:validation:UniqueItems=true +type StringArray []string + +// +kubebuilder:validation:MinItems=0 +type IntegerArray []int32 + +// +kubebuilder:validation:MaxItems=100 +type BooleanArray []bool + +// +kubebuilder:validation:MinItems=5 // want `marker "kubebuilder:validation:MinItems": marker should be declared on the type definition of FieldMarkerOnNamedArray instead of the field` +type FieldMarkerOnNamedArray []string + +// Type definitions with invalid markers +type InvalidArrayMarkerOnStringType string + +type InvalidArrayMarkerOnMapType map[string]string + +type ArrayMarkersFieldTest struct { + // Valid: MinItems marker on array field + // +kubebuilder:validation:MinItems=1 + ValidMinItems []string `json:"validMinItems"` + + // Valid: MaxItems marker on array field + // +kubebuilder:validation:MaxItems=10 + ValidMaxItems []string `json:"validMaxItems"` + + // Valid: UniqueItems marker on array field + // +kubebuilder:validation:UniqueItems=true + ValidUniqueItems []string `json:"validUniqueItems"` + + // Valid: All array markers combined on array field + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=10 + // +kubebuilder:validation:UniqueItems=true + ValidAllArrayMarkers []string `json:"validAllArrayMarkers"` + + // Invalid: MinItems marker on string field + InvalidMinItemsOnString string `json:"invalidMinItemsOnString"` + + // Invalid: MaxItems marker on map field + InvalidMaxItemsOnMap map[string]string `json:"invalidMaxItemsOnMap"` + + // Invalid: UniqueItems marker on integer field + InvalidUniqueItemsOnInteger int32 `json:"invalidUniqueItemsOnInteger"` + + // Invalid: MinItems marker on boolean field + InvalidMinItemsOnBoolean bool `json:"invalidMinItemsOnBoolean"` + + // Invalid: MaxItems marker on struct field + InvalidMaxItemsOnStruct ArrayItem `json:"invalidMaxItemsOnStruct"` + + // Valid: Using named array type with markers + ValidNamedArrayType StringArray `json:"validNamedArrayType"` + + // Valid: Using named array type (IntegerArray) + ValidIntegerArrayType IntegerArray `json:"validIntegerArrayType"` + + // Valid: Using named array type (BooleanArray) + ValidBooleanArrayType BooleanArray `json:"validBooleanArrayType"` + + // Invalid: Field marker on named array type (should use type definition) + InvalidFieldMarkerOnNamedArray FieldMarkerOnNamedArray `json:"invalidFieldMarkerOnNamedArray"` + + // Invalid: Using invalid named type with array marker + InvalidNamedStringType InvalidArrayMarkerOnStringType `json:"invalidNamedStringType"` + + // Invalid: Using invalid named type with array marker + InvalidNamedMapType InvalidArrayMarkerOnMapType `json:"invalidNamedMapType"` +} + +type ArrayItem struct { + Name string `json:"name"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/field_only.go.golden b/pkg/analysis/markerscope/testdata/src/a/field_only.go.golden new file mode 100644 index 00000000..5c79c0bb --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/field_only.go.golden @@ -0,0 +1,82 @@ +/* +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 a + +// Invalid: Field-only markers on type +type InvalidFieldOnlyOnType struct { + Name string `json:"name"` +} + +// Invalid: kubebuilder:default on type +type InvalidDefaultOnType string + +// Invalid: kubebuilder:example on type +type InvalidExampleOnType string + +// Invalid: kubebuilder:validation:EmbeddedResource on type +type InvalidEmbeddedResourceOnType struct { + Data map[string]interface{} `json:"data"` +} + +// Invalid: kubebuilder:validation:Schemaless on type +type InvalidSchemalessOnType struct { + Data map[string]interface{} `json:"data"` +} + +type FieldOnlyMarkersTest struct { + // Valid: optional marker + // +optional + ValidOptional string `json:"validOptional,omitempty"` + + // Valid: required marker + // +required + ValidRequired string `json:"validRequired"` + + // Valid: k8s:optional marker + // +k8s:optional + ValidK8sOptional string `json:"validK8sOptional,omitempty"` + + // Valid: k8s:required marker + // +k8s:required + ValidK8sRequired string `json:"validK8sRequired"` + + // Valid: nullable marker + // +nullable + ValidNullable *string `json:"validNullable"` + + // Valid: kubebuilder:default marker + // +kubebuilder:default="default" + ValidDefault string `json:"validDefault"` + + // Valid: kubebuilder:example marker + // +kubebuilder:example="example" + ValidExample string `json:"validExample"` + + // Valid: kubebuilder:validation:EmbeddedResource marker + // +kubebuilder:validation:EmbeddedResource + ValidEmbeddedResource map[string]interface{} `json:"validEmbeddedResource"` + + // Valid: kubebuilder:validation:Schemaless marker + // +kubebuilder:validation:Schemaless + ValidSchemaless map[string]interface{} `json:"validSchemaless"` + + // Valid: All field-only markers combined + // +optional + // +nullable + // +kubebuilder:default="combined" + // +kubebuilder:example="combined-example" + ValidAllFieldOnlyMarkers *string `json:"validAllFieldOnlyMarkers,omitempty"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/general.go.golden b/pkg/analysis/markerscope/testdata/src/a/general.go.golden new file mode 100644 index 00000000..2592d4dd --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/general.go.golden @@ -0,0 +1,98 @@ +/* +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 a + +// +kubebuilder:validation:XValidation:rule="self.size() > 0" // want `marker "kubebuilder:validation:XValidation": marker should be declared on the type definition of GeneralType instead of the field` +type GeneralType string + +// Valid: Enum on type +// +kubebuilder:validation:Enum=A;B;C +type EnumType string + +// Valid: Format on type +// +kubebuilder:validation:Format=email +type FormatType string + +// Valid: Type on type +// +kubebuilder:validation:Type=string +type TypeType string + +// Valid: XValidation on type +// +kubebuilder:validation:XValidation:rule="self.size() > 0" +type XValidationType string + +type GeneralMarkersFieldTest struct { + // Valid: Enum marker on string field + // +kubebuilder:validation:Enum=A;B;C + ValidEnum string `json:"validEnum"` + + // Valid: Format marker on string field + // +kubebuilder:validation:Format=email + ValidFormat string `json:"validFormat"` + + // Valid: Type marker on string field + // +kubebuilder:validation:Type=string + ValidType string `json:"validType"` + + // Valid: XValidation marker on string field + // +kubebuilder:validation:XValidation:rule="self.size() > 0" + ValidXValidation string `json:"validXValidation"` + + // Valid: All general markers on string field + // +kubebuilder:validation:Enum=A;B;C + // +kubebuilder:validation:Format=email + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:XValidation:rule="self.size() > 0" + ValidAllGeneralMarkers string `json:"validAllGeneralMarkers"` + + // Valid: Using named type with general markers + ValidGeneralTyped GeneralType `json:"validGeneralTyped"` + + // Valid: Using EnumType + ValidEnumTyped EnumType `json:"validEnumTyped"` + + // Valid: Using FormatType + ValidFormatTyped FormatType `json:"validFormatTyped"` + + // Valid: Using TypeType + ValidTypeTyped TypeType `json:"validTypeTyped"` + + // Valid: Using XValidationType + ValidXValidationTyped XValidationType `json:"validXValidationTyped"` + + // Valid: General markers can be applied to any type + // +kubebuilder:validation:Enum=1;2;3 + ValidEnumOnInt int32 `json:"validEnumOnInt"` + + // Valid: Format can be applied to any type + // +kubebuilder:validation:Format=int32 + ValidFormatOnInt int32 `json:"validFormatOnInt"` + + // Invalid: Enum marker on named type + // +kubebuilder:validation:Enum=A;B;C + InvalidEnumOnGeneralType GeneralType `json:"invalidEnumOnGeneralType"` + + // Invalid: Format marker on named type + // +kubebuilder:validation:Format=email + InvalidFormatOnGeneralType GeneralType `json:"invalidFormatOnGeneralType"` + + // Invalid: Type marker on named type + // +kubebuilder:validation:Type=string + InvalidTypeOnGeneralType GeneralType `json:"invalidTypeOnGeneralType"` + + // Invalid: XValidation marker on named type + InvalidXValidationOnGeneralType GeneralType `json:"invalidXValidationOnGeneralType"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/items.go b/pkg/analysis/markerscope/testdata/src/a/items.go index b4c40ab9..9d690357 100644 --- a/pkg/analysis/markerscope/testdata/src/a/items.go +++ b/pkg/analysis/markerscope/testdata/src/a/items.go @@ -39,6 +39,13 @@ type ObjectArrayType []map[string]string // +kubebuilder:validation:items:XValidation:rule="self != ”" type GeneralArrayType []string +// Types without markers for testing field markers +type NumericArrayTypeNoMarker []int32 +type StringArrayTypeNoMarker []string +type NestedArrayTypeNoMarker [][]string +type ObjectArrayTypeNoMarker []map[string]string +type GeneralArrayTypeNoMarker []string + // Invalid: items:Maximum on string array type // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)` type InvalidItemsMaximumOnStringArrayType []string @@ -97,28 +104,28 @@ type ArrayItemsMarkersFieldTest struct { ValidObjectArrayTyped ObjectArrayType `json:"validObjectArrayTyped"` // Invalid: items:Maximum marker on named type (should be on type definition) - // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": marker should be declared on the type definition of NumericArrayType instead of the field` - InvalidItemsMaximumOnNumericArrayType NumericArrayType `json:"invalidItemsMaximumOnNumericArrayType"` + // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": marker should be declared on the type definition of NumericArrayTypeNoMarker instead of the field` + InvalidItemsMaximumOnNumericArrayType NumericArrayTypeNoMarker `json:"invalidItemsMaximumOnNumericArrayType"` // Invalid: items:Pattern marker on named type (should be on type definition) - // +kubebuilder:validation:items:Pattern="^[a-z]+$" // want `marker "kubebuilder:validation:items:Pattern": marker should be declared on the type definition of StringArrayType instead of the field` - InvalidItemsPatternOnStringArrayType StringArrayType `json:"invalidItemsPatternOnStringArrayType"` + // +kubebuilder:validation:items:Pattern="^[a-z]+$" // want `marker "kubebuilder:validation:items:Pattern": marker should be declared on the type definition of StringArrayTypeNoMarker instead of the field` + InvalidItemsPatternOnStringArrayType StringArrayTypeNoMarker `json:"invalidItemsPatternOnStringArrayType"` // Invalid: items:MinItems marker on named type (should be on type definition) - // +kubebuilder:validation:items:MinItems=1 // want `marker "kubebuilder:validation:items:MinItems": marker should be declared on the type definition of NestedArrayType instead of the field` - InvalidItemsMinItemsOnNestedArrayType NestedArrayType `json:"invalidItemsMinItemsOnNestedArrayType"` + // +kubebuilder:validation:items:MinItems=1 // want `marker "kubebuilder:validation:items:MinItems": marker should be declared on the type definition of NestedArrayTypeNoMarker instead of the field` + InvalidItemsMinItemsOnNestedArrayType NestedArrayTypeNoMarker `json:"invalidItemsMinItemsOnNestedArrayType"` // Invalid: items:MinProperties marker on named type (should be on type definition) - // +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": marker should be declared on the type definition of ObjectArrayType instead of the field` - InvalidItemsMinPropertiesOnObjectArrayType ObjectArrayType `json:"invalidItemsMinPropertiesOnObjectArrayType"` + // +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": marker should be declared on the type definition of ObjectArrayTypeNoMarker instead of the field` + InvalidItemsMinPropertiesOnObjectArrayType ObjectArrayTypeNoMarker `json:"invalidItemsMinPropertiesOnObjectArrayType"` // Invalid: items:Enum marker on named type (should be on type definition) - // +kubebuilder:validation:items:Enum=A;B;C // want `marker "kubebuilder:validation:items:Enum": marker should be declared on the type definition of GeneralArrayType instead of the field` - InvalidItemsEnumOnGeneralArrayType GeneralArrayType `json:"invalidItemsEnumOnGeneralArrayType"` + // +kubebuilder:validation:items:Enum=A;B;C // want `marker "kubebuilder:validation:items:Enum": marker should be declared on the type definition of GeneralArrayTypeNoMarker instead of the field` + InvalidItemsEnumOnGeneralArrayType GeneralArrayTypeNoMarker `json:"invalidItemsEnumOnGeneralArrayType"` // Invalid: items:Format marker on named type (should be on type definition) - // +kubebuilder:validation:items:Format=uuid // want `marker "kubebuilder:validation:items:Format": marker should be declared on the type definition of GeneralArrayType instead of the field` - InvalidItemsFormatOnGeneralArrayType GeneralArrayType `json:"invalidItemsFormatOnGeneralArrayType"` + // +kubebuilder:validation:items:Format=uuid // want `marker "kubebuilder:validation:items:Format": marker should be declared on the type definition of GeneralArrayTypeNoMarker instead of the field` + InvalidItemsFormatOnGeneralArrayType GeneralArrayTypeNoMarker `json:"invalidItemsFormatOnGeneralArrayType"` // Invalid: items:Maximum on string array (element type mismatch) // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)` diff --git a/pkg/analysis/markerscope/testdata/src/a/items.go.golden b/pkg/analysis/markerscope/testdata/src/a/items.go.golden new file mode 100644 index 00000000..096b22c3 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/items.go.golden @@ -0,0 +1,147 @@ +/* +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 a + +// +kubebuilder:validation:items:Maximum=100 +// +kubebuilder:validation:items:Minimum=0 +// +kubebuilder:validation:items:MultipleOf=5 +type NumericArrayType []int32 + +// +kubebuilder:validation:items:Pattern="^[a-z]+$" +// +kubebuilder:validation:items:MinLength=1 +// +kubebuilder:validation:items:MaxLength=50 +type StringArrayType []string + +// +kubebuilder:validation:items:MinItems=1 +// +kubebuilder:validation:items:MaxItems=5 +type NestedArrayType [][]string + +// +kubebuilder:validation:items:MinProperties=1 +// +kubebuilder:validation:items:MaxProperties=5 +type ObjectArrayType []map[string]string + +// +kubebuilder:validation:items:Enum=A;B;C +// +kubebuilder:validation:items:Format=uuid +// +kubebuilder:validation:items:Type=string +// +kubebuilder:validation:items:XValidation:rule="self != ”" +type GeneralArrayType []string + +// Types without markers for testing field markers +// +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": marker should be declared on the type definition of NumericArrayTypeNoMarker instead of the field` +type NumericArrayTypeNoMarker []int32 +// +kubebuilder:validation:items:Pattern="^[a-z]+$" // want `marker "kubebuilder:validation:items:Pattern": marker should be declared on the type definition of StringArrayTypeNoMarker instead of the field` +type StringArrayTypeNoMarker []string +// +kubebuilder:validation:items:MinItems=1 // want `marker "kubebuilder:validation:items:MinItems": marker should be declared on the type definition of NestedArrayTypeNoMarker instead of the field` +type NestedArrayTypeNoMarker [][]string +// +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": marker should be declared on the type definition of ObjectArrayTypeNoMarker instead of the field` +type ObjectArrayTypeNoMarker []map[string]string +// +kubebuilder:validation:items:Enum=A;B;C // want `marker "kubebuilder:validation:items:Enum": marker should be declared on the type definition of GeneralArrayTypeNoMarker instead of the field` +// +kubebuilder:validation:items:Format=uuid // want `marker "kubebuilder:validation:items:Format": marker should be declared on the type definition of GeneralArrayTypeNoMarker instead of the field` +type GeneralArrayTypeNoMarker []string + +// Invalid: items:Maximum on string array type +type InvalidItemsMaximumOnStringArrayType []string + +// Invalid: items:Pattern on int array type +type InvalidItemsPatternOnIntArrayType []int32 + +// Invalid: items:MinProperties on string array type +type InvalidItemsMinPropertiesOnStringArrayType []string + +type ArrayItemsMarkersFieldTest struct { + // Valid: Numeric element constraints + // +kubebuilder:validation:items:Maximum=100 + // +kubebuilder:validation:items:Minimum=0 + // +kubebuilder:validation:items:MultipleOf=5 + // +kubebuilder:validation:items:ExclusiveMaximum=false + // +kubebuilder:validation:items:ExclusiveMinimum=false + ValidNumericArrayItems []int32 `json:"validNumericArrayItems"` + + // Valid: String element constraints + // +kubebuilder:validation:items:Pattern="^[a-z]+$" + // +kubebuilder:validation:items:MinLength=1 + // +kubebuilder:validation:items:MaxLength=50 + ValidStringArrayItems []string `json:"validStringArrayItems"` + + // Valid: Nested array constraints + // +kubebuilder:validation:items:MinItems=1 + // +kubebuilder:validation:items:MaxItems=5 + // +kubebuilder:validation:items:UniqueItems=true + ValidNestedArrayItems [][]string `json:"validNestedArrayItems"` + + // Valid: Object element constraints + // +kubebuilder:validation:items:MinProperties=1 + // +kubebuilder:validation:items:MaxProperties=5 + ValidObjectArrayItems []map[string]string `json:"validObjectArrayItems"` + + // Valid: General items markers + // +kubebuilder:validation:items:Enum=A;B;C + // +kubebuilder:validation:items:Format=uuid + // +kubebuilder:validation:items:Type=string + // +kubebuilder:validation:items:XValidation:rule="self != ''" + ValidGeneralArrayItems []string `json:"validGeneralArrayItems"` + + // Valid: Using named type with items markers + ValidNumericArrayTyped NumericArrayType `json:"validNumericArrayTyped"` + + // Valid: Using named type with string items + ValidStringArrayTyped StringArrayType `json:"validStringArrayTyped"` + + // Valid: Using named type with nested array items + ValidNestedArrayTyped NestedArrayType `json:"validNestedArrayTyped"` + + // Valid: Using named type with object items + ValidObjectArrayTyped ObjectArrayType `json:"validObjectArrayTyped"` + + // Invalid: items:Maximum marker on named type (should be on type definition) + InvalidItemsMaximumOnNumericArrayType NumericArrayTypeNoMarker `json:"invalidItemsMaximumOnNumericArrayType"` + + // Invalid: items:Pattern marker on named type (should be on type definition) + InvalidItemsPatternOnStringArrayType StringArrayTypeNoMarker `json:"invalidItemsPatternOnStringArrayType"` + + // Invalid: items:MinItems marker on named type (should be on type definition) + InvalidItemsMinItemsOnNestedArrayType NestedArrayTypeNoMarker `json:"invalidItemsMinItemsOnNestedArrayType"` + + // Invalid: items:MinProperties marker on named type (should be on type definition) + InvalidItemsMinPropertiesOnObjectArrayType ObjectArrayTypeNoMarker `json:"invalidItemsMinPropertiesOnObjectArrayType"` + + // Invalid: items:Enum marker on named type (should be on type definition) + InvalidItemsEnumOnGeneralArrayType GeneralArrayTypeNoMarker `json:"invalidItemsEnumOnGeneralArrayType"` + + // Invalid: items:Format marker on named type (should be on type definition) + InvalidItemsFormatOnGeneralArrayType GeneralArrayTypeNoMarker `json:"invalidItemsFormatOnGeneralArrayType"` + + // Invalid: items:Maximum on string array (element type mismatch) + InvalidItemsMaximumOnStringArray []string `json:"invalidItemsMaximumOnStringArray"` + + // Invalid: items:Pattern on int array (element type mismatch) + InvalidItemsPatternOnIntArray []int32 `json:"invalidItemsPatternOnIntArray"` + + // Invalid: items:MinProperties on string array (element type mismatch) + InvalidItemsMinPropertiesOnStringArray []string `json:"invalidItemsMinPropertiesOnStringArray"` + + // Invalid: items marker on non-array field + InvalidItemsMarkerOnNonArray string `json:"invalidItemsMarkerOnNonArray"` + + // Invalid: Using invalid named type + InvalidItemsMaximumOnStringArrayTyped InvalidItemsMaximumOnStringArrayType `json:"invalidItemsMaximumOnStringArrayTyped"` + + // Invalid: Using invalid named type + InvalidItemsPatternOnIntArrayTyped InvalidItemsPatternOnIntArrayType `json:"invalidItemsPatternOnIntArrayTyped"` + + // Invalid: Using invalid named type + InvalidItemsMinPropertiesOnStringArrayTyped InvalidItemsMinPropertiesOnStringArrayType `json:"invalidItemsMinPropertiesOnStringArrayTyped"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/numeric.go.golden b/pkg/analysis/markerscope/testdata/src/a/numeric.go.golden new file mode 100644 index 00000000..847f724e --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/numeric.go.golden @@ -0,0 +1,119 @@ +/* +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 a + +// Valid: All numeric markers on type +// +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": marker should be declared on the type definition of NumericType instead of the field` +// +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": marker should be declared on the type definition of NumericType instead of the field` +// +kubebuilder:validation:ExclusiveMaximum=true // want `marker "kubebuilder:validation:ExclusiveMaximum": marker should be declared on the type definition of NumericType instead of the field` +// +kubebuilder:validation:ExclusiveMinimum=true // want `marker "kubebuilder:validation:ExclusiveMinimum": marker should be declared on the type definition of NumericType instead of the field` +// +kubebuilder:validation:MultipleOf=5 // want `marker "kubebuilder:validation:MultipleOf": marker should be declared on the type definition of NumericType instead of the field` +type NumericType int32 + +// Valid: Minimum on type +// +kubebuilder:validation:Minimum=10 +type MinimumType int32 + +// Valid: Maximum on type +// +kubebuilder:validation:Maximum=50 +type MaximumType int64 + +// Valid: MultipleOf on type +// +kubebuilder:validation:MultipleOf=3 +type MultipleOfType int32 + +// Valid: Float type with numeric markers +type FloatType float64 + +// Invalid: Minimum marker on string type +type InvalidMinimumOnStringType string + +// Invalid: Maximum marker on boolean type +type InvalidMaximumOnBoolType bool + +type NumericMarkersFieldTest struct { + // Valid: Minimum marker on integer field + // +kubebuilder:validation:Minimum=0 + ValidMinimum int32 `json:"validMinimum"` + + // Valid: Maximum marker on integer field + // +kubebuilder:validation:Maximum=100 + ValidMaximum int32 `json:"validMaximum"` + + // Valid: ExclusiveMinimum marker + // +kubebuilder:validation:ExclusiveMinimum=true + // +kubebuilder:validation:Minimum=0 + ValidExclusiveMinimum int32 `json:"validExclusiveMinimum"` + + // Valid: ExclusiveMaximum marker + // +kubebuilder:validation:ExclusiveMaximum=true + // +kubebuilder:validation:Maximum=100 + ValidExclusiveMaximum int32 `json:"validExclusiveMaximum"` + + // Valid: MultipleOf marker + // +kubebuilder:validation:MultipleOf=5 + ValidMultipleOf int32 `json:"validMultipleOf"` + + // Valid: All numeric markers on integer field + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + // +kubebuilder:validation:ExclusiveMinimum=false + // +kubebuilder:validation:ExclusiveMaximum=false + // +kubebuilder:validation:MultipleOf=5 + ValidAllNumericMarkers int32 `json:"validAllNumericMarkers"` + + // Valid: Using MinimumType + ValidMinimumTyped MinimumType `json:"validMinimumTyped"` + + // Valid: Using MaximumType + ValidMaximumTyped MaximumType `json:"validMaximumTyped"` + + // Valid: Using MultipleOfType + ValidMultipleOfTyped MultipleOfType `json:"validMultipleOfTyped"` + + // Valid: Using FloatType + ValidFloatTyped FloatType `json:"validFloatTyped"` + + // Invalid: Maximum marker on named type + InvalidMaximumOnNumericType NumericType `json:"invalidMaximumOnNumericType"` + + // Invalid: Minimum marker on named type + InvalidMinimumOnNumericType NumericType `json:"invalidMinimumOnNumericType"` + + // Invalid: ExclusiveMaximum marker on named type + InvalidExclusiveMaximumOnNumericType NumericType `json:"invalidExclusiveMaximumOnNumericType"` + + // Invalid: ExclusiveMinimum marker on named type + InvalidExclusiveMinimumOnNumericType NumericType `json:"invalidExclusiveMinimumOnNumericType"` + + // Invalid: MultipleOf marker on named type + InvalidMultipleOfOnNumericType NumericType `json:"invalidMultipleOfOnNumericType"` + + // Invalid: Minimum marker on string field + InvalidMinimumOnString string `json:"invalidMinimumOnString"` + + // Invalid: Maximum marker on boolean field + InvalidMaximumOnBool bool `json:"invalidMaximumOnBool"` + + // Invalid: MultipleOf marker on array field + InvalidMultipleOfOnArray []int32 `json:"invalidMultipleOfOnArray"` + + // Invalid: Using invalid named type + InvalidMinimumOnStringTyped InvalidMinimumOnStringType `json:"invalidMinimumOnStringTyped"` + + // Invalid: Using invalid named type + InvalidMaximumOnBoolTyped InvalidMaximumOnBoolType `json:"invalidMaximumOnBoolTyped"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/object.go b/pkg/analysis/markerscope/testdata/src/a/object.go index 0f98eca8..be78b964 100644 --- a/pkg/analysis/markerscope/testdata/src/a/object.go +++ b/pkg/analysis/markerscope/testdata/src/a/object.go @@ -33,6 +33,16 @@ type MaxPropertiesType struct { Field2 string `json:"field2"` } +// Types without markers for testing field markers +type MinPropertiesTypeNoMarker struct { + Field string `json:"field"` +} + +type MaxPropertiesTypeNoMarker struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + // Invalid: MinProperties marker on string type // +kubebuilder:validation:MinProperties=2 // want `marker "kubebuilder:validation:MinProperties": type string is not allowed \(expected one of: \[object\]\)` type InvalidMinPropertiesOnStringType string @@ -62,12 +72,12 @@ type ObjectMarkersFieldTest struct { ValidMaxPropertiesTyped MaxPropertiesType `json:"validMaxPropertiesTyped"` // Invalid: MinProperties marker on named type - // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties": marker should be declared on the type definition of MinPropertiesType instead of the field` - InvalidMinPropertiesOnMinPropertiesType MinPropertiesType `json:"invalidMinPropertiesOnMinPropertiesType"` + // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties": marker should be declared on the type definition of MinPropertiesTypeNoMarker instead of the field` + InvalidMinPropertiesOnMinPropertiesTypeNoMarker MinPropertiesTypeNoMarker `json:"invalidMinPropertiesOnMinPropertiesTypeNoMarker"` // Invalid: MaxProperties marker on named type - // +kubebuilder:validation:MaxProperties=10 // want `marker "kubebuilder:validation:MaxProperties": marker should be declared on the type definition of MaxPropertiesType instead of the field` - InvalidMaxPropertiesOnMaxPropertiesType MaxPropertiesType `json:"invalidMaxPropertiesOnMaxPropertiesType"` + // +kubebuilder:validation:MaxProperties=10 // want `marker "kubebuilder:validation:MaxProperties": marker should be declared on the type definition of MaxPropertiesTypeNoMarker instead of the field` + InvalidMaxPropertiesOnMaxPropertiesTypeNoMarker MaxPropertiesTypeNoMarker `json:"invalidMaxPropertiesOnMaxPropertiesTypeNoMarker"` // Invalid: MinProperties marker on string field // +kubebuilder:validation:MinProperties=2 // want `marker "kubebuilder:validation:MinProperties": type string is not allowed \(expected one of: \[object\]\)` diff --git a/pkg/analysis/markerscope/testdata/src/a/object.go.golden b/pkg/analysis/markerscope/testdata/src/a/object.go.golden new file mode 100644 index 00000000..1f4e772f --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/object.go.golden @@ -0,0 +1,91 @@ +/* +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 a + +type ObjectType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Valid: MinProperties on type +// +kubebuilder:validation:MinProperties=1 +type MinPropertiesType struct { + Field string `json:"field"` +} + +// Valid: MaxProperties on type +// +kubebuilder:validation:MaxProperties=10 +type MaxPropertiesType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Types without markers for testing field markers +// +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties": marker should be declared on the type definition of MinPropertiesTypeNoMarker instead of the field` +type MinPropertiesTypeNoMarker struct { + Field string `json:"field"` +} + +// +kubebuilder:validation:MaxProperties=10 // want `marker "kubebuilder:validation:MaxProperties": marker should be declared on the type definition of MaxPropertiesTypeNoMarker instead of the field` +type MaxPropertiesTypeNoMarker struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Invalid: MinProperties marker on string type +type InvalidMinPropertiesOnStringType string + +// Invalid: MaxProperties marker on array type +type InvalidMaxPropertiesOnArrayType []string + +type ObjectMarkersFieldTest struct { + // Valid: MinProperties marker on map field + // +kubebuilder:validation:MinProperties=1 + ValidMinProperties map[string]string `json:"validMinProperties"` + + // Valid: MaxProperties marker on map field + // +kubebuilder:validation:MaxProperties=10 + ValidMaxProperties map[string]string `json:"validMaxProperties"` + + // Valid: All map markers on map field + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=10 + ValidAllObjectMarkers map[string]string `json:"validAllObjectMarkers"` + + // Valid: Using MinPropertiesType + ValidMinPropertiesTyped MinPropertiesType `json:"validMinPropertiesTyped"` + + // Valid: Using MaxPropertiesType + ValidMaxPropertiesTyped MaxPropertiesType `json:"validMaxPropertiesTyped"` + + // Invalid: MinProperties marker on named type + InvalidMinPropertiesOnMinPropertiesTypeNoMarker MinPropertiesTypeNoMarker `json:"invalidMinPropertiesOnMinPropertiesTypeNoMarker"` + + // Invalid: MaxProperties marker on named type + InvalidMaxPropertiesOnMaxPropertiesTypeNoMarker MaxPropertiesTypeNoMarker `json:"invalidMaxPropertiesOnMaxPropertiesTypeNoMarker"` + + // Invalid: MinProperties marker on string field + InvalidMinPropertiesOnString string `json:"invalidMinPropertiesOnString"` + + // Invalid: MaxProperties marker on array field + InvalidMaxPropertiesOnArray []string `json:"invalidMaxPropertiesOnArray"` + + // Invalid: Using invalid named type + InvalidMinPropertiesOnStringTyped InvalidMinPropertiesOnStringType `json:"invalidMinPropertiesOnStringTyped"` + + // Invalid: Using invalid named type + InvalidMaxPropertiesOnArrayTyped InvalidMaxPropertiesOnArrayType `json:"invalidMaxPropertiesOnArrayTyped"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/scope.go.golden b/pkg/analysis/markerscope/testdata/src/a/scope.go.golden new file mode 100644 index 00000000..fa5b4da7 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/scope.go.golden @@ -0,0 +1,68 @@ +/* +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 a + +// Valid: PreserveUnknownFields on type +// +kubebuilder:pruning:PreserveUnknownFields +type ValidPreserveUnknownFieldsType struct { + Name string `json:"name"` +} + +// Valid: title on type +// +kubebuilder:title="My Title" +type ValidTitleType struct { + Name string `json:"name"` +} + +// Valid: All AnyScope markers on type +// +kubebuilder:pruning:PreserveUnknownFields +// +kubebuilder:title="Combined Title" +type ValidAllAnyScopeType struct { + Name string `json:"name"` +} + +type NoMarkerAllAnyScopeType struct { + Name string `json:"name"` +} + +type AnyScopeOnFieldTest struct { + // Valid: PreserveUnknownFields on field + // +kubebuilder:pruning:PreserveUnknownFields + ValidPreserveUnknownFields map[string]string `json:"validPreserveUnknownFields"` + + // Valid: title on field + // +kubebuilder:title="Field Title" + ValidTitle map[string]string `json:"validTitle"` + + // Valid: All AnyScope markers on field + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:title="Combined Field Title" + ValidAllAnyScopeMarkers map[string]interface{} `json:"validAllAnyScopeMarkers"` + + // Valid: Using type with AnyScope markers + ValidPreserveUnknownFieldsTyped ValidPreserveUnknownFieldsType `json:"validPreserveUnknownFieldsTyped"` + + // Valid: Using type with title marker + ValidTitleTyped ValidTitleType `json:"validTitleTyped"` + + // Valid: Using type with all AnyScope markers + ValidAllAnyScopeTyped ValidAllAnyScopeType `json:"validAllAnyScopeTyped"` + + // Valid: Using type with no AnyScope markers + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:title="Field Combined Title" + NoMarkerAllAnyScopeType NoMarkerAllAnyScopeType `json:"noMarkerAllAnyScopeType"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/string.go.golden b/pkg/analysis/markerscope/testdata/src/a/string.go.golden new file mode 100644 index 00000000..90e8088a --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/string.go.golden @@ -0,0 +1,95 @@ +/* +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 a + +// +kubebuilder:validation:MinLength=1 // want `marker "kubebuilder:validation:MinLength": marker should be declared on the type definition of StringType instead of the field` +// +kubebuilder:validation:MaxLength=100 // want `marker "kubebuilder:validation:MaxLength": marker should be declared on the type definition of StringType instead of the field` +// +kubebuilder:validation:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:Pattern": marker should be declared on the type definition of StringType instead of the field` +type StringType string + +// Valid: Pattern on type +// +kubebuilder:validation:Pattern="^[a-z]+$" +type PatternType string + +// Valid: MinLength on type +// +kubebuilder:validation:MinLength=1 +type MinLengthType string + +// Valid: MaxLength on type +// +kubebuilder:validation:MaxLength=100 +type MaxLengthType string + +// Invalid: Pattern marker on integer type +type InvalidPatternOnIntType int32 + +// Invalid: MinLength marker on boolean type +type InvalidMinLengthOnBoolType bool + +// Invalid: MaxLength marker on array type +type InvalidMaxLengthOnArrayType []string + +type StringMarkersFieldTest struct { + // Valid: Pattern marker on string field + // +kubebuilder:validation:Pattern="^[a-z]+$" + ValidPattern string `json:"validPattern"` + + // Valid: MinLength marker on string field + // +kubebuilder:validation:MinLength=1 + ValidMinLength string `json:"validMinLength"` + + // Valid: MaxLength marker on string field + // +kubebuilder:validation:MaxLength=100 + ValidMaxLength string `json:"validMaxLength"` + + // Valid: All string markers on string field + // +kubebuilder:validation:Pattern="^[a-z]+$" + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=100 + ValidAllStringMarkers string `json:"validAllStringMarkers"` + + // Valid: Using PatternType + ValidPatternTyped PatternType `json:"validPatternTyped"` + + // Valid: Using MinLengthType + ValidMinLengthTyped MinLengthType `json:"validMinLengthTyped"` + + // Valid: Using MaxLengthType + ValidMaxLengthTyped MaxLengthType `json:"validMaxLengthTyped"` + + // Invalid: Pattern marker on integer field + InvalidPatternOnInt int32 `json:"invalidPatternOnInt"` + + // Invalid: MinLength marker on boolean field + InvalidMinLengthOnBool bool `json:"invalidMinLengthOnBool"` + + // Invalid: MaxLength marker on array field + InvalidMaxLengthOnArray []string `json:"invalidMaxLengthOnArray"` + + // Invalid: MinLength marker on named type + InvalidMinLengthOnMinLengthType StringType `json:"invalidMinLengthOnMinLengthType"` + + // Invalid: MaxLength marker on named type + InvalidMaxLengthOnMaxLengthType StringType `json:"invalidMaxLengthOnMaxLengthType"` + + // Invalid: Pattern marker on named type + InvalidPatternOnIntTyped StringType `json:"invalidPatternOnIntTyped"` + + // Invalid: Using invalid named type + InvalidMinLengthOnBoolTyped InvalidMinLengthOnBoolType `json:"invalidMinLengthOnBoolTyped"` + + // Invalid: Using invalid named type + InvalidMaxLengthOnArrayTyped InvalidMaxLengthOnArrayType `json:"invalidMaxLengthOnArrayTyped"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/topology.go b/pkg/analysis/markerscope/testdata/src/a/topology.go index f405ad32..b4168230 100644 --- a/pkg/analysis/markerscope/testdata/src/a/topology.go +++ b/pkg/analysis/markerscope/testdata/src/a/topology.go @@ -35,6 +35,16 @@ type AtomicStruct struct { Field2 string `json:"field2"` } +// Types without markers for testing field markers +type ItemListNoMarker []Item + +type ConfigMapNoMarker map[string]string + +type AtomicStructNoMarker struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + // Invalid: listType on non-array type // +listType=map // want `marker "listType": type object is not allowed \(expected one of: \[array\]\)` type InvalidListTypeOnStruct struct { @@ -80,20 +90,17 @@ type TopologyMarkersFieldTest struct { ValidAtomicStructTyped AtomicStruct `json:"validAtomicStructTyped"` // Invalid: listType marker on named type - // +listType=map // want `marker "listType": marker should be declared on the type definition of ItemList instead of the field` - InvalidListTypeOnItemList ItemList `json:"invalidListTypeOnItemList"` - - // Invalid: listMapKey marker on named type - // +listMapKey=name // want `marker "listMapKey": marker should be declared on the type definition of ItemList instead of the field` - InvalidListMapKeyOnItemList ItemList `json:"invalidListMapKeyOnItemList"` + // +listType=map // want `marker "listType": marker should be declared on the type definition of ItemListNoMarker instead of the field` + // +listMapKey=name // want `marker "listMapKey": marker should be declared on the type definition of ItemListNoMarker instead of the field` + InvalidListTypeOnItemList ItemListNoMarker `json:"invalidListTypeOnItemList"` // Invalid: mapType marker on named type - // +mapType=granular // want `marker "mapType": marker should be declared on the type definition of ConfigMap instead of the field` - InvalidMapTypeOnConfigMap ConfigMap `json:"invalidMapTypeOnConfigMap"` + // +mapType=granular // want `marker "mapType": marker should be declared on the type definition of ConfigMapNoMarker instead of the field` + InvalidMapTypeOnConfigMap ConfigMapNoMarker `json:"invalidMapTypeOnConfigMap"` // Invalid: structType marker on named type // +structType=atomic - InvalidStructTypeOnAtomicStruct AtomicStruct `json:"invalidStructTypeOnAtomicStruct"` + InvalidStructTypeOnAtomicStruct AtomicStructNoMarker `json:"invalidStructTypeOnAtomicStruct"` // Invalid: listType marker on string field // +listType=map // want `marker "listType": type string is not allowed \(expected one of: \[array\]\)` diff --git a/pkg/analysis/markerscope/testdata/src/a/topology.go.golden b/pkg/analysis/markerscope/testdata/src/a/topology.go.golden new file mode 100644 index 00000000..64a3aeea --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/topology.go.golden @@ -0,0 +1,128 @@ +/* +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 a + +// Valid: listType on array type +// +listType=map +type ItemList []Item + +// Valid: listMapKey on array type +// +listType=map +// +listMapKey=name +type ItemListWithKey []Item + +// Valid: mapType on map type +// +mapType=granular +type ConfigMap map[string]string + +// Valid: structType on struct type +// +structType=atomic +type AtomicStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Types without markers for testing field markers +// +listType=map // want `marker "listType": marker should be declared on the type definition of ItemListNoMarker instead of the field` +// +listMapKey=name // want `marker "listMapKey": marker should be declared on the type definition of ItemListNoMarker instead of the field` +type ItemListNoMarker []Item + +// +mapType=granular // want `marker "mapType": marker should be declared on the type definition of ConfigMapNoMarker instead of the field` +type ConfigMapNoMarker map[string]string + +type AtomicStructNoMarker struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Invalid: listType on non-array type +type InvalidListTypeOnStruct struct { + Field string `json:"field"` +} + +// Invalid: mapType on non-map type +type InvalidMapTypeOnArray []string + +// Invalid: structType on non-struct type +type InvalidStructTypeOnString string + +type TopologyMarkersFieldTest struct { + // Valid: listType marker on array field + // +listType=map + ValidListType []Item `json:"validListType"` + + // Valid: listMapKey marker on array field + // +listType=map + // +listMapKey=name + ValidListMapKey []Item `json:"validListMapKey"` + + // Valid: mapType marker on map field + // +mapType=granular + ValidMapType map[string]string `json:"validMapType"` + + // Valid: structType marker on struct field + // +structType=atomic + ValidStructType EmbeddedStruct `json:"validStructType"` + + // Valid: Using named type with listType + ValidItemListTyped ItemList `json:"validItemListTyped"` + + // Valid: Using named type with listMapKey + ValidItemListWithKeyTyped ItemListWithKey `json:"validItemListWithKeyTyped"` + + // Valid: Using named type with mapType + ValidConfigMapTyped ConfigMap `json:"validConfigMapTyped"` + + // Valid: Using named type with structType + ValidAtomicStructTyped AtomicStruct `json:"validAtomicStructTyped"` + + // Invalid: listType marker on named type + InvalidListTypeOnItemList ItemListNoMarker `json:"invalidListTypeOnItemList"` + + // Invalid: mapType marker on named type + InvalidMapTypeOnConfigMap ConfigMapNoMarker `json:"invalidMapTypeOnConfigMap"` + + // Invalid: structType marker on named type + // +structType=atomic + InvalidStructTypeOnAtomicStruct AtomicStructNoMarker `json:"invalidStructTypeOnAtomicStruct"` + + // Invalid: listType marker on string field + InvalidListTypeOnString string `json:"invalidListTypeOnString"` + + // Invalid: mapType marker on array field + InvalidMapTypeOnArray []string `json:"invalidMapTypeOnArray"` + + // Invalid: structType marker on integer field + InvalidStructTypeOnInt int32 `json:"invalidStructTypeOnInt"` + + // Invalid: Using invalid named type + InvalidListTypeOnStructTyped InvalidListTypeOnStruct `json:"invalidListTypeOnStructTyped"` + + // Invalid: Using invalid named type + InvalidMapTypeOnArrayTyped InvalidMapTypeOnArray `json:"invalidMapTypeOnArrayTyped"` + + // Invalid: Using invalid named type + InvalidStructTypeOnStringTyped InvalidStructTypeOnString `json:"invalidStructTypeOnStringTyped"` +} + +type EmbeddedStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +type Item struct { + Name string `json:"name"` +} diff --git a/pkg/analysis/markerscope/testdata/src/a/type_only.go.golden b/pkg/analysis/markerscope/testdata/src/a/type_only.go.golden new file mode 100644 index 00000000..dc643eda --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/a/type_only.go.golden @@ -0,0 +1,76 @@ +/* +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 a + +// Valid: ExactlyOneOf on type +// +kubebuilder:validation:items:ExactlyOneOf={Field1,Field2} +type ValidExactlyOneOfType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Valid: AtMostOneOf on type +// +kubebuilder:validation:items:AtMostOneOf={Field1,Field2} +type ValidAtMostOneOfType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Valid: AtLeastOneOf on type +// +kubebuilder:validation:items:AtLeastOneOf={Field1,Field2} +type ValidAtLeastOneOfType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` +} + +// Valid: All type-only markers combined +// +kubebuilder:validation:items:ExactlyOneOf={Field1,Field2} +// +kubebuilder:validation:items:AtMostOneOf={Field3,Field4} +// +kubebuilder:validation:items:AtLeastOneOf={Field5,Field6} +type ValidAllTypeOnlyMarkers struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + Field4 string `json:"field4"` + Field5 string `json:"field5"` + Field6 string `json:"field6"` +} + +type TypeOnlyMarkersTest struct { + // Invalid: ExactlyOneOf on field + InvalidExactlyOneOfOnField string `json:"invalidExactlyOneOfOnField"` + + // Invalid: AtMostOneOf on field + InvalidAtMostOneOfOnField string `json:"invalidAtMostOneOfOnField"` + + // Invalid: AtLeastOneOf on field + InvalidAtLeastOneOfOnField string `json:"invalidAtLeastOneOfOnField"` + + // Invalid: All type-only markers on field + InvalidAllTypeOnlyOnField string `json:"invalidAllTypeOnlyOnField"` + + // Valid: Using type with type-only markers + ValidExactlyOneOf ValidExactlyOneOfType `json:"validExactlyOneOf"` + + // Valid: Using type with type-only markers + ValidAtMostOneOf ValidAtMostOneOfType `json:"validAtMostOneOf"` + + // Valid: Using type with type-only markers + ValidAtLeastOneOf ValidAtLeastOneOfType `json:"validAtLeastOneOf"` + + // Valid: Using type with all type-only markers + ValidAllTypeOnly ValidAllTypeOnlyMarkers `json:"validAllTypeOnly"` +} diff --git a/pkg/analysis/markerscope/testdata/src/c/c.go b/pkg/analysis/markerscope/testdata/src/c/c.go deleted file mode 100644 index c0b2e7e2..00000000 --- a/pkg/analysis/markerscope/testdata/src/c/c.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -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 c - -// +kubebuilder:validation:MinProperties=1 -type ValidTypeMarkers struct { - // +optional - Name string `json:"name"` -} - -// +kubebuilder:MinProperties=1 -// +required // want `marker "required" can only be applied to fields` -type InvalidTypeMarkers struct { - // +kubebuilder:validation:Required - Name string `json:"name"` -} - -type FieldMarkerTest struct { - // +required - // +kubebuilder:validation:MinProperties=1 - ValidMinPropertiesField map[string]string `json:"validMinPropertiesField"` - - // +required - ValidMinPropertiesField2 []string `json:"validMinPropertiesField2"` - - // +kubebuilder:validation:items:ExactlyOneOf={field1} // want `marker "kubebuilder:validation:items:ExactlyOneOf" can only be applied to types` - InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` -} - -type MapType map[string]string - -type StringType string - -type MapTypeFieldTest struct { - // +kubebuilder:validation:MinProperties=1 // want `marker "kubebuilder:validation:MinProperties": marker should be declared on the type definition of MapType instead of the field` - ValidMinPropertiesField MapType `json:"validMinPropertiesField"` - - // +kubebuilder:validation:MaxLength=10 // want `marker "kubebuilder:validation:MaxLength": marker should be declared on the type definition of StringType instead of the field` - ValidMaxLengthField StringType `json:"validMaxLengthField"` -} diff --git a/pkg/analysis/markerscope/testdata/src/c/c.go.golden b/pkg/analysis/markerscope/testdata/src/c/c.go.golden deleted file mode 100644 index 695a981b..00000000 --- a/pkg/analysis/markerscope/testdata/src/c/c.go.golden +++ /dev/null @@ -1,53 +0,0 @@ -/* -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 c - -// +kubebuilder:validation:MinProperties=1 -type ValidTypeMarkers struct { - // +optional - Name string `json:"name"` -} - -// +kubebuilder:MinProperties=1 -// +kubebuilder:validation:items:ExactlyOneOf={field1} -type InvalidTypeMarkers struct { - // +kubebuilder:validation:Required - Name string `json:"name"` -} - -type FieldMarkerTest struct { - // +required - // +kubebuilder:validation:MinProperties=1 - ValidMinPropertiesField map[string]string `json:"validMinPropertiesField"` - - // +required - ValidMinPropertiesField2 []string `json:"validMinPropertiesField2"` - - // +required - InvalidTypeMarker InvalidTypeMarkers `json:"invalidTypeMarker"` -} - -// +kubebuilder:validation:MinProperties=1 -type MapType map[string]string - -// +kubebuilder:validation:MaxLength=10 -type StringType string - -type MapTypeFieldTest struct { - ValidMinPropertiesField MapType `json:"validMinPropertiesField"` - - ValidMaxLengthField StringType `json:"validMaxLengthField"` -} From f400cf469db8787a86b1f3a5836f31b23942f2e3 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Sun, 26 Oct 2025 16:49:11 +0900 Subject: [PATCH 09/18] docs(markerscope): document strictTypeConstraint and scope exclusions Signed-off-by: nayuta-ai --- docs/linters.md | 42 +++++++++++++++++++++++++++++- pkg/analysis/markerscope/config.go | 8 +++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/docs/linters.md b/docs/linters.md index 1b6d2357..2e8368d4 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -419,6 +419,35 @@ OpenAPI schema types map to Go types as follows: - `array`: []T, [N]T (slices and arrays) - `object`: struct, map[K]V +#### Strict Type Constraints + +For markers with `AnyScope` and type constraints, the `strictTypeConstraint` flag controls where the marker should be declared when used with named types: + +- When `strictTypeConstraint` is `false` (default): The marker can be declared on either the field or the type definition. +- When `strictTypeConstraint` is `true`: The marker must be declared on the type definition, not on fields using that type. + +Example with `strictTypeConstraint: true`: + +```go +// ✅ Valid: marker on type definition +// +kubebuilder:validation:Minimum=0 +type Port int32 + +type Service struct { + Port Port `json:"port"` +} + +// ❌ Invalid: marker on field using named type +type Port int32 + +type Service struct { + // +kubebuilder:validation:Minimum=0 // Error: should be on Port type definition + Port Port `json:"port"` +} +``` + +Most built-in kubebuilder validation markers use `strictTypeConstraint: true` to encourage consistent marker placement on type definitions. + ### Default Marker Rules The linter includes built-in rules for all standard kubebuilder markers and k8s declarative validation markers. Examples: @@ -450,6 +479,7 @@ You can customize marker rules or add support for custom markers: lintersConfig: markerscope: policy: Warn | SuggestFix # The policy for marker scope violations. Defaults to `Warn`. + allowDangerousTypes: false # Allow dangerous number types (float32, float64). Defaults to `false`. markerRules: # Override default rule for a built-in marker "optional": @@ -462,6 +492,7 @@ lintersConfig: # Add a custom marker with scope and type constraints "mycompany:validation:NumericLimit": scope: any + strictTypeConstraint: true # Require declaration on type definition for named types typeConstraint: allowedSchemaTypes: - integer @@ -486,13 +517,22 @@ lintersConfig: **Type constraint fields:** - `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `number`, `string`, `boolean`, `array`, `object`) - `elementConstraint`: Nested constraint for array element types (only valid when `allowedSchemaTypes` includes `array`) +- `strictTypeConstraint`: When `true`, markers with `AnyScope` and type constraints applied to fields using named types must be declared on the type definition instead of the field. Defaults to `false`. If a marker is not in `markerRules` and not in the default rules, no validation is performed for that marker. If a marker is in both `markerRules` and the default rules, your configuration takes precedence. ### Fixes -The `markerscope` linter does not currently provide automatic fixes. It reports violations as warnings or errors based on the configured policy. +When the `policy` is set to `SuggestFix`, the `markerscope` linter provides automatic fix suggestions for marker violations: + +1. **Scope violations**: For markers applied to the wrong scope (field vs type), the linter suggests moving the marker to the correct location. + +2. **Type constraint violations**: For markers applied to incompatible types, the linter suggests removing the invalid marker. + +3. **Named type violations**: For AnyScope markers with type constraints applied to fields using named types, the linter suggests moving the marker to the type definition if the underlying type is compatible with the marker's type constraints. + +When the `policy` is set to `Warn`, violations are reported as warnings without suggesting fixes. **Note**: This linter is not enabled by default and must be explicitly enabled in the configuration. diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index b7b116ea..a9a166e6 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -128,6 +128,11 @@ type MarkerScopeConfig struct { // Users can override these rules or add custom markers by providing a MarkerScopeConfig // with MarkerRules that will be merged with (and take precedence over) these defaults. // +// Note: This function currently covers validation and SSA markers with type and struct constraints. +// Markers from crd.go (e.g., resource, subresource) and pkg.go (e.g., groupName, versionName) +// are not included as they don't have type or struct constraints and are out of scope for +// this linter's current validation capabilities. +// // ref: https://github.com/kubernetes-sigs/controller-tools/blob/v0.19.0/pkg/crd/markers/ func DefaultMarkerRules() map[string]MarkerScopeRule { rules := make(map[string]MarkerScopeRule) @@ -143,9 +148,6 @@ func DefaultMarkerRules() map[string]MarkerScopeRule { addSSATopologyMarkers(rules) addArrayItemsMarkers(rules) - // TODO crd.go - // TODO package.go - return rules } From 09b2fb577807141b2178aad43a80bb4f0c3bd8d6 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Wed, 29 Oct 2025 16:57:28 +0900 Subject: [PATCH 10/18] refactor(markerscope): convert config to PascalCase values and named object lists Signed-off-by: nayuta-ai --- docs/linters.md | 22 +++--- pkg/analysis/markerscope/analyzer.go | 16 ++++- pkg/analysis/markerscope/analyzer_test.go | 17 +++-- pkg/analysis/markerscope/config.go | 50 ++++++-------- pkg/analysis/markerscope/errors.go | 12 +++- pkg/analysis/markerscope/initializer.go | 26 ++++--- pkg/analysis/markerscope/initializer_test.go | 73 +++++++++++--------- 7 files changed, 128 insertions(+), 88 deletions(-) diff --git a/docs/linters.md b/docs/linters.md index 2e8368d4..9a05a206 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -482,16 +482,16 @@ lintersConfig: allowDangerousTypes: false # Allow dangerous number types (float32, float64). Defaults to `false`. markerRules: # Override default rule for a built-in marker - "optional": - scope: field # or: type, any + - name: "optional" + scope: Field # or: Type, Any # Add a custom marker with scope constraint only - "mycompany:validation:CustomMarker": - scope: any + - name: "mycompany:validation:CustomMarker" + scope: Any # Add a custom marker with scope and type constraints - "mycompany:validation:NumericLimit": - scope: any + - name: "mycompany:validation:NumericLimit" + scope: Any strictTypeConstraint: true # Require declaration on type definition for named types typeConstraint: allowedSchemaTypes: @@ -499,8 +499,8 @@ lintersConfig: - number # Add a custom array items marker with element type constraint - "mycompany:validation:items:StringFormat": - scope: any + - name: "mycompany:validation:items:StringFormat" + scope: Any typeConstraint: allowedSchemaTypes: - array @@ -510,9 +510,9 @@ lintersConfig: ``` **Scope values:** -- `field`: FieldScope - marker can only be on fields -- `type`: TypeScope - marker can only be on types -- `any`: AnyScope - marker can be on fields or types +- `Field`: FieldScope - marker can only be on fields +- `Type`: TypeScope - marker can only be on types +- `Any`: AnyScope - marker can be on fields or types **Type constraint fields:** - `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `number`, `string`, `boolean`, `array`, `object`) diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 41c99005..06d035fe 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -60,8 +60,11 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { cfg = &MarkerScopeConfig{} } + // Convert list of marker rules to map + customRules := markerRulesListToMap(cfg.MarkerRules) + a := &analyzer{ - markerRules: mergeMarkerRules(DefaultMarkerRules(), cfg.MarkerRules), + markerRules: mergeMarkerRules(DefaultMarkerRules(), customRules), policy: cfg.Policy, allowDangerousTypes: cfg.AllowDangerousTypes, } @@ -87,6 +90,17 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { } } +// markerRulesListToMap converts a list of marker rules to a map keyed by marker name. +func markerRulesListToMap(rules []MarkerScopeRule) map[string]MarkerScopeRule { + result := make(map[string]MarkerScopeRule, len(rules)) + for _, rule := range rules { + if rule.Name != "" { + result[rule.Name] = rule + } + } + return result +} + // mergeMarkerRules merges custom marker rules with default marker rules. // Custom rules take precedence over default rules for the same marker. func mergeMarkerRules(defaults, custom map[string]MarkerScopeRule) map[string]MarkerScopeRule { diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index 57938c96..9416f01b 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -43,31 +43,36 @@ func TestAnalyzerWithCustomMarkers(t *testing.T) { testdata := analysistest.TestData() cfg := &MarkerScopeConfig{ Policy: MarkerScopePolicyWarn, - MarkerRules: map[string]MarkerScopeRule{ + MarkerRules: []MarkerScopeRule{ // Custom field-only marker - "custom:field-only": { + { + Name: "custom:field-only", Scope: FieldScope, }, // Custom type-only marker - "custom:type-only": { + { + Name: "custom:type-only", Scope: TypeScope, }, // Custom marker with string type constraint - "custom:string-only": { + { + Name: "custom:string-only", Scope: FieldScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, // Custom marker with integer type constraint - "custom:integer-only": { + { + Name: "custom:integer-only", Scope: FieldScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, // Custom marker with array of strings constraint - "custom:string-array": { + { + Name: "custom:string-array", Scope: FieldScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index a9a166e6..c51856b0 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -21,36 +21,24 @@ import ( "sigs.k8s.io/kube-api-linter/pkg/markers" ) -// ScopeConstraint defines where a marker is allowed to be placed using bit flags. -type ScopeConstraint uint8 +// ScopeConstraint defines where a marker is allowed to be placed. +type ScopeConstraint string const ( // FieldScope indicates the marker can be placed on fields. - FieldScope ScopeConstraint = 1 << iota + FieldScope ScopeConstraint = "Field" // TypeScope indicates the marker can be placed on type definitions. - TypeScope - + TypeScope ScopeConstraint = "Type" // AnyScope indicates the marker can be placed on either fields or types. - AnyScope = FieldScope | TypeScope + AnyScope ScopeConstraint = "Any" ) -// String returns a human-readable representation of the scope constraint. -func (s ScopeConstraint) String() string { - switch s { - case FieldScope: - return "field" - case TypeScope: - return "type" - case AnyScope: - return "any" - default: - return "unknown" - } -} - // Allows checks if the given scope is allowed by this constraint. func (s ScopeConstraint) Allows(scope ScopeConstraint) bool { - return s&scope != 0 + if s == AnyScope { + return true + } + return s == scope } // TypeConstraint defines what types a marker can be applied to. @@ -69,6 +57,10 @@ type TypeConstraint struct { // MarkerScopeRule defines comprehensive scope validation rules for a marker. type MarkerScopeRule struct { + // Name is the marker identifier (e.g., "optional", "kubebuilder:validation:Minimum"). + // This field is only used when MarkerScopeRule is part of a list configuration. + Name string `json:"name,omitempty"` + // Scope specifies where the marker can be placed (field vs type). Scope ScopeConstraint @@ -89,29 +81,29 @@ type MarkerScopePolicy string const ( // MarkerScopePolicyWarn only reports warnings without suggesting fixes. - MarkerScopePolicyWarn MarkerScopePolicy = "warn" + MarkerScopePolicyWarn MarkerScopePolicy = "Warn" // MarkerScopePolicySuggestFix reports warnings and suggests automatic fixes. - MarkerScopePolicySuggestFix MarkerScopePolicy = "suggest_fix" + MarkerScopePolicySuggestFix MarkerScopePolicy = "SuggestFix" ) // MarkerScopeConfig contains configuration for marker scope validation. type MarkerScopeConfig struct { - // MarkerRules maps marker names to their scope rules with scope and type constraints. - // This map can be used to: + // MarkerRules is a list of marker rules with scope and type constraints. + // This list can be used to: // - Override default rules for built-in markers (from DefaultMarkerRules) // - Add rules for custom markers not included in DefaultMarkerRules // - // If a marker is not in this map AND not in DefaultMarkerRules(), no scope validation is performed. - // If a marker is in both this map and DefaultMarkerRules(), this map takes precedence. + // If a marker is not in this list AND not in DefaultMarkerRules(), no scope validation is performed. + // If a marker is in both this list and DefaultMarkerRules(), this list takes precedence. // // Example: Adding a custom marker // markerRules: - // "mycompany:validation:CustomMarker": + // - name: "mycompany:validation:CustomMarker" // scope: any // typeConstraint: // allowedSchemaTypes: ["string"] - MarkerRules map[string]MarkerScopeRule `json:"markerRules,omitempty"` + MarkerRules []MarkerScopeRule `json:"markerRules,omitempty"` // AllowDangerousTypes specifies if dangerous types are allowed. // If true, dangerous types are allowed. diff --git a/pkg/analysis/markerscope/errors.go b/pkg/analysis/markerscope/errors.go index 9f637329..bdf52be0 100644 --- a/pkg/analysis/markerscope/errors.go +++ b/pkg/analysis/markerscope/errors.go @@ -21,10 +21,18 @@ import ( ) var ( - errScopeNonZero = errors.New("scope must be non-zero") - errInvalidScopeBits = errors.New("invalid scope bits") + errScopeRequired = errors.New("scope is required") ) +// InvalidScopeConstraintError represents an error when a scope constraint is invalid. +type InvalidScopeConstraintError struct { + Scope string +} + +func (e *InvalidScopeConstraintError) Error() string { + return fmt.Sprintf("invalid scope: %q (must be one of: Field, Type, Any)", e.Scope) +} + // InvalidSchemaTypeError represents an error when a schema type is invalid. type InvalidSchemaTypeError struct { SchemaType string diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index cd0f9e7a..1b49cd3e 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -58,9 +58,17 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList } // Validate marker rules - for marker, rule := range cfg.MarkerRules { + for i, rule := range cfg.MarkerRules { + markerRulePath := fldPath.Child("markerRules").Index(i) + + // Validate that name is not empty + if rule.Name == "" { + fieldErrors = append(fieldErrors, field.Required(markerRulePath.Child("name"), "marker name is required")) + continue + } + if err := validateMarkerRule(rule); err != nil { - fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("markerRules", marker), rule, err.Error())) + fieldErrors = append(fieldErrors, field.Invalid(markerRulePath, rule, err.Error())) } } @@ -69,14 +77,16 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList func validateMarkerRule(rule MarkerScopeRule) error { // Validate scope constraint - if rule.Scope == 0 { - return errScopeNonZero + if rule.Scope == "" { + return errScopeRequired } - // Validate that scope is a valid combination of FieldScope and/or TypeScope - validScopes := FieldScope | TypeScope - if rule.Scope&^validScopes != 0 { - return errInvalidScopeBits + // Validate that scope is a valid value + switch rule.Scope { + case FieldScope, TypeScope, AnyScope: + // Valid scope + default: + return &InvalidScopeConstraintError{Scope: string(rule.Scope)} } // Validate type constraint if present diff --git a/pkg/analysis/markerscope/initializer_test.go b/pkg/analysis/markerscope/initializer_test.go index 3fa4203e..57a998e9 100644 --- a/pkg/analysis/markerscope/initializer_test.go +++ b/pkg/analysis/markerscope/initializer_test.go @@ -73,13 +73,14 @@ var _ = Describe("markerscope initializer", func() { config: markerscope.MarkerScopeConfig{ Policy: "invalid-policy", }, - expectedErr: `markerscope.policy: Invalid value: "invalid-policy": invalid policy, must be one of: "warn", "suggest_fix"`, + expectedErr: `markerscope.policy: Invalid value: "invalid-policy": invalid policy, must be one of: "Warn", "SuggestFix"`, }), Entry("With valid marker rules", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:marker": { + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:marker", Scope: markerscope.FieldScope, }, }, @@ -87,32 +88,35 @@ var _ = Describe("markerscope initializer", func() { expectedErr: "", }), - Entry("With marker rule having zero scope", testCase{ + Entry("With marker rule having empty scope", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:marker": { - Scope: 0, + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:marker", + Scope: "", }, }, }, - expectedErr: `markerscope.markerRules.custom:marker: Invalid value: markerscope.MarkerScopeRule{Scope:0x0, StrictTypeConstraint:false, TypeConstraint:(*markerscope.TypeConstraint)(nil)}: scope must be non-zero`, + expectedErr: `scope is required`, }), - Entry("With marker rule having invalid scope bits", testCase{ + Entry("With marker rule having invalid scope value", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:marker": { - Scope: 8, // Invalid bit + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:marker", + Scope: "invalid", }, }, }, - expectedErr: `markerscope.markerRules.custom:marker: Invalid value: markerscope.MarkerScopeRule{Scope:0x8, StrictTypeConstraint:false, TypeConstraint:(*markerscope.TypeConstraint)(nil)}: invalid scope bits`, + expectedErr: `invalid scope: "invalid" (must be one of: Field, Type, Any)`, }), Entry("With marker rule having invalid schema type", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:marker": { + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:marker", Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{"invalid-type"}, @@ -125,8 +129,9 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid type constraint with string type", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:string-marker": { + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:string-marker", Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeString}, @@ -139,8 +144,9 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid type constraint with integer type", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:integer-marker": { + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:integer-marker", Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeInteger}, @@ -153,8 +159,9 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid type constraint with multiple types", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:numeric-marker": { + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:numeric-marker", Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{ @@ -170,8 +177,9 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid type constraint with element constraint", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:string-array": { + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:string-array", Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeArray}, @@ -187,8 +195,9 @@ var _ = Describe("markerscope initializer", func() { Entry("With invalid element constraint", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:invalid-array": { + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:invalid-array", Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeArray}, @@ -202,11 +211,12 @@ var _ = Describe("markerscope initializer", func() { expectedErr: `invalid type constraint: invalid element constraint: invalid schema type: "invalid-type"`, }), - Entry("With both field and type scope", testCase{ + Entry("With Any scope (field and type)", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:flexible-marker": { - Scope: markerscope.FieldScope | markerscope.TypeScope, + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:flexible-marker", + Scope: markerscope.AnyScope, }, }, }, @@ -233,8 +243,9 @@ var _ = Describe("markerscope initializer", func() { It("should initialize analyzer with custom markers", func() { cfg := &markerscope.MarkerScopeConfig{ Policy: markerscope.MarkerScopePolicyWarn, - MarkerRules: map[string]markerscope.MarkerScopeRule{ - "custom:marker": { + MarkerRules: []markerscope.MarkerScopeRule{ + { + Name: "custom:marker", Scope: markerscope.FieldScope, }, }, From 7945f6038c197a4f59906e98b179cd042d19a2e3 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Thu, 30 Oct 2025 15:02:35 +0900 Subject: [PATCH 11/18] refactor(markerscope): separate overrideMarkers and customMarkers with validation Signed-off-by: nayuta-ai --- docs/linters.md | 55 +++--- pkg/analysis/markerscope/analyzer.go | 37 ++-- pkg/analysis/markerscope/analyzer_test.go | 36 ++-- pkg/analysis/markerscope/config.go | 31 ++-- pkg/analysis/markerscope/initializer.go | 44 ++++- pkg/analysis/markerscope/initializer_test.go | 159 ++++++++++++++---- .../testdata/src/b/{b.go => custom.go} | 0 .../markerscope/testdata/src/b/override.go | 59 +++++++ 8 files changed, 314 insertions(+), 107 deletions(-) rename pkg/analysis/markerscope/testdata/src/b/{b.go => custom.go} (100%) create mode 100644 pkg/analysis/markerscope/testdata/src/b/override.go diff --git a/docs/linters.md b/docs/linters.md index 9a05a206..df356b51 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -473,24 +473,43 @@ The linter includes built-in rules for all standard kubebuilder markers and k8s ### Configuration -You can customize marker rules or add support for custom markers: +You can customize marker rules or add support for custom markers. + +**Scope values:** +- `Field`: Marker can only be applied to struct fields +- `Type`: Marker can only be applied to type definitions +- `Any`: Marker can be applied to either fields or type definitions + +**Type constraints:** + +The `typeConstraint` field allows you to restrict which Go types a marker can be applied to. This ensures that markers are only used with compatible data types (e.g., numeric markers like `Minimum` are only applied to integer/number types). + +**Type constraint fields:** +- `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `number`, `string`, `boolean`, `array`, `object`) +- `elementConstraint`: Nested constraint for array element types (only valid when `allowedSchemaTypes` includes `array`) +- `strictTypeConstraint`: When `true`, markers with `AnyScope` and type constraints applied to fields using named types must be declared on the type definition instead of the field. Defaults to `false`. + +**Configuration example:** ```yaml lintersConfig: markerscope: policy: Warn | SuggestFix # The policy for marker scope violations. Defaults to `Warn`. allowDangerousTypes: false # Allow dangerous number types (float32, float64). Defaults to `false`. - markerRules: - # Override default rule for a built-in marker - - name: "optional" + + # Override default rules for built-in markers + overrideMarkers: + - identifier: "optional" scope: Field # or: Type, Any - # Add a custom marker with scope constraint only - - name: "mycompany:validation:CustomMarker" + # Add rules for custom markers + customMarkers: + # Custom marker with scope constraint only + - identifier: "mycompany:validation:CustomMarker" scope: Any - # Add a custom marker with scope and type constraints - - name: "mycompany:validation:NumericLimit" + # Custom marker with scope and type constraints + - identifier: "mycompany:validation:NumericLimit" scope: Any strictTypeConstraint: true # Require declaration on type definition for named types typeConstraint: @@ -498,8 +517,8 @@ lintersConfig: - integer - number - # Add a custom array items marker with element type constraint - - name: "mycompany:validation:items:StringFormat" + # Custom array items marker with element type constraint + - identifier: "mycompany:validation:items:StringFormat" scope: Any typeConstraint: allowedSchemaTypes: @@ -509,18 +528,10 @@ lintersConfig: - string ``` -**Scope values:** -- `Field`: FieldScope - marker can only be on fields -- `Type`: TypeScope - marker can only be on types -- `Any`: AnyScope - marker can be on fields or types - -**Type constraint fields:** -- `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `number`, `string`, `boolean`, `array`, `object`) -- `elementConstraint`: Nested constraint for array element types (only valid when `allowedSchemaTypes` includes `array`) -- `strictTypeConstraint`: When `true`, markers with `AnyScope` and type constraints applied to fields using named types must be declared on the type definition instead of the field. Defaults to `false`. - -If a marker is not in `markerRules` and not in the default rules, no validation is performed for that marker. -If a marker is in both `markerRules` and the default rules, your configuration takes precedence. +**Configuration notes:** +- Use `overrideMarkers` to customize the behavior of built-in kubebuilder/controller-runtime markers +- Use `customMarkers` to add validation for your own custom markers +- If a marker is not in either list and not in the default rules, no validation is performed for that marker ### Fixes diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 06d035fe..2a0c7585 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -60,11 +60,22 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { cfg = &MarkerScopeConfig{} } - // Convert list of marker rules to map - customRules := markerRulesListToMap(cfg.MarkerRules) + // Convert override and custom marker lists to maps + overrideRules := markerRulesListToMap(cfg.OverrideMarkers) + customRules := markerRulesListToMap(cfg.CustomMarkers) + + // Merge rules: + // 1. Start with default built-in marker rules + // 2. Apply overrides (replaces default rules for built-in markers) + // 3. Add custom markers (new markers not in defaults) + // Note: Validation ensures overrideMarkers only contains built-in markers + // and customMarkers only contains non-built-in markers, so no conflicts. + rules := DefaultMarkerRules() + maps.Copy(rules, overrideRules) // Override built-in markers + maps.Copy(rules, customRules) // Add custom markers a := &analyzer{ - markerRules: mergeMarkerRules(DefaultMarkerRules(), customRules), + markerRules: rules, policy: cfg.Policy, allowDangerousTypes: cfg.AllowDangerousTypes, } @@ -90,31 +101,17 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { } } -// markerRulesListToMap converts a list of marker rules to a map keyed by marker name. +// markerRulesListToMap converts a list of marker rules to a map keyed by marker identifier. func markerRulesListToMap(rules []MarkerScopeRule) map[string]MarkerScopeRule { result := make(map[string]MarkerScopeRule, len(rules)) for _, rule := range rules { - if rule.Name != "" { - result[rule.Name] = rule + if rule.Identifier != "" { + result[rule.Identifier] = rule } } return result } -// mergeMarkerRules merges custom marker rules with default marker rules. -// Custom rules take precedence over default rules for the same marker. -func mergeMarkerRules(defaults, custom map[string]MarkerScopeRule) map[string]MarkerScopeRule { - merged := make(map[string]MarkerScopeRule, len(defaults)+len(custom)) - - // Copy all default rules - maps.Copy(merged, defaults) - - // Override with custom rules - maps.Copy(merged, custom) - - return merged -} - func (a *analyzer) run(pass *analysis.Pass) (any, error) { inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) if !ok { diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index 9416f01b..d2d0f0e5 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -39,41 +39,53 @@ func TestAnalyzerSuggestFixes(t *testing.T) { analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a") } -func TestAnalyzerWithCustomMarkers(t *testing.T) { +func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) { testdata := analysistest.TestData() cfg := &MarkerScopeConfig{ Policy: MarkerScopePolicyWarn, - MarkerRules: []MarkerScopeRule{ + OverrideMarkers: []MarkerScopeRule{ + // Override built-in "optional" to allow on types (default is FieldScope only) + { + Identifier: "optional", + Scope: AnyScope, + }, + // Override built-in "required" to allow on types (default is FieldScope only) + { + Identifier: "required", + Scope: AnyScope, + }, + }, + CustomMarkers: []MarkerScopeRule{ // Custom field-only marker { - Name: "custom:field-only", - Scope: FieldScope, + Identifier: "custom:field-only", + Scope: FieldScope, }, // Custom type-only marker { - Name: "custom:type-only", - Scope: TypeScope, + Identifier: "custom:type-only", + Scope: TypeScope, }, // Custom marker with string type constraint { - Name: "custom:string-only", - Scope: FieldScope, + Identifier: "custom:string-only", + Scope: FieldScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, // Custom marker with integer type constraint { - Name: "custom:integer-only", - Scope: FieldScope, + Identifier: "custom:integer-only", + Scope: FieldScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, // Custom marker with array of strings constraint { - Name: "custom:string-array", - Scope: FieldScope, + Identifier: "custom:string-array", + Scope: FieldScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index c51856b0..66465f6b 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -57,9 +57,8 @@ type TypeConstraint struct { // MarkerScopeRule defines comprehensive scope validation rules for a marker. type MarkerScopeRule struct { - // Name is the marker identifier (e.g., "optional", "kubebuilder:validation:Minimum"). - // This field is only used when MarkerScopeRule is part of a list configuration. - Name string `json:"name,omitempty"` + // Identifier is the marker identifier (e.g., "optional", "kubebuilder:validation:Minimum"). + Identifier string `json:"identifier,omitempty"` // Scope specifies where the marker can be placed (field vs type). Scope ScopeConstraint @@ -89,21 +88,25 @@ const ( // MarkerScopeConfig contains configuration for marker scope validation. type MarkerScopeConfig struct { - // MarkerRules is a list of marker rules with scope and type constraints. - // This list can be used to: - // - Override default rules for built-in markers (from DefaultMarkerRules) - // - Add rules for custom markers not included in DefaultMarkerRules + // OverrideMarkers is a list of marker rules that override default rules for built-in markers. + // Use this to customize the behavior of standard kubebuilder/controller-runtime markers. // - // If a marker is not in this list AND not in DefaultMarkerRules(), no scope validation is performed. - // If a marker is in both this list and DefaultMarkerRules(), this list takes precedence. + // Example: Override the built-in "optional" marker + // overrideMarkers: + // - identifier: "optional" + // scope: Field + OverrideMarkers []MarkerScopeRule `json:"overrideMarkers,omitempty"` + + // CustomMarkers is a list of marker rules for custom markers not included in the default rules. + // Use this to add validation for your own custom markers. // - // Example: Adding a custom marker - // markerRules: - // - name: "mycompany:validation:CustomMarker" - // scope: any + // Example: Add a custom marker + // customMarkers: + // - identifier: "mycompany:validation:CustomMarker" + // scope: Any // typeConstraint: // allowedSchemaTypes: ["string"] - MarkerRules []MarkerScopeRule `json:"markerRules,omitempty"` + CustomMarkers []MarkerScopeRule `json:"customMarkers,omitempty"` // AllowDangerousTypes specifies if dangerous types are allowed. // If true, dangerous types are allowed. diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index 1b49cd3e..460460a0 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -57,13 +57,45 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList fmt.Sprintf("invalid policy, must be one of: %q, %q", MarkerScopePolicyWarn, MarkerScopePolicySuggestFix))) } - // Validate marker rules - for i, rule := range cfg.MarkerRules { - markerRulePath := fldPath.Child("markerRules").Index(i) + // Get default marker rules for validation + defaultRules := DefaultMarkerRules() - // Validate that name is not empty - if rule.Name == "" { - fieldErrors = append(fieldErrors, field.Required(markerRulePath.Child("name"), "marker name is required")) + // Validate override marker rules + for i, rule := range cfg.OverrideMarkers { + markerRulePath := fldPath.Child("overrideMarkers").Index(i) + + // Validate that identifier is not empty + if rule.Identifier == "" { + fieldErrors = append(fieldErrors, field.Required(markerRulePath.Child("identifier"), "marker identifier is required")) + continue + } + + // Validate that override marker exists in default rules + if _, exists := defaultRules[rule.Identifier]; !exists { + fieldErrors = append(fieldErrors, field.Invalid(markerRulePath.Child("identifier"), rule.Identifier, + "override marker must be a built-in marker; use customMarkers for custom markers")) + continue + } + + if err := validateMarkerRule(rule); err != nil { + fieldErrors = append(fieldErrors, field.Invalid(markerRulePath, rule, err.Error())) + } + } + + // Validate custom marker rules + for i, rule := range cfg.CustomMarkers { + markerRulePath := fldPath.Child("customMarkers").Index(i) + + // Validate that identifier is not empty + if rule.Identifier == "" { + fieldErrors = append(fieldErrors, field.Required(markerRulePath.Child("identifier"), "marker identifier is required")) + continue + } + + // Validate that custom marker does not exist in default rules + if _, exists := defaultRules[rule.Identifier]; exists { + fieldErrors = append(fieldErrors, field.Invalid(markerRulePath.Child("identifier"), rule.Identifier, + "custom marker cannot be a built-in marker; use overrideMarkers to override built-in markers")) continue } diff --git a/pkg/analysis/markerscope/initializer_test.go b/pkg/analysis/markerscope/initializer_test.go index 57a998e9..b3a7beb3 100644 --- a/pkg/analysis/markerscope/initializer_test.go +++ b/pkg/analysis/markerscope/initializer_test.go @@ -78,10 +78,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid marker rules", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:marker", - Scope: markerscope.FieldScope, + Identifier: "custom:marker", + Scope: markerscope.FieldScope, }, }, }, @@ -90,10 +90,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With marker rule having empty scope", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:marker", - Scope: "", + Identifier: "custom:marker", + Scope: "", }, }, }, @@ -102,10 +102,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With marker rule having invalid scope value", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:marker", - Scope: "invalid", + Identifier: "custom:marker", + Scope: "invalid", }, }, }, @@ -114,10 +114,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With marker rule having invalid schema type", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:marker", - Scope: markerscope.FieldScope, + Identifier: "custom:marker", + Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{"invalid-type"}, }, @@ -129,10 +129,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid type constraint with string type", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:string-marker", - Scope: markerscope.FieldScope, + Identifier: "custom:string-marker", + Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeString}, }, @@ -144,10 +144,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid type constraint with integer type", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:integer-marker", - Scope: markerscope.FieldScope, + Identifier: "custom:integer-marker", + Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeInteger}, }, @@ -159,10 +159,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid type constraint with multiple types", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:numeric-marker", - Scope: markerscope.FieldScope, + Identifier: "custom:numeric-marker", + Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{ markerscope.SchemaTypeInteger, @@ -177,10 +177,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With valid type constraint with element constraint", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:string-array", - Scope: markerscope.FieldScope, + Identifier: "custom:string-array", + Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeArray}, ElementConstraint: &markerscope.TypeConstraint{ @@ -195,10 +195,10 @@ var _ = Describe("markerscope initializer", func() { Entry("With invalid element constraint", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:invalid-array", - Scope: markerscope.FieldScope, + Identifier: "custom:invalid-array", + Scope: markerscope.FieldScope, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeArray}, ElementConstraint: &markerscope.TypeConstraint{ @@ -213,10 +213,64 @@ var _ = Describe("markerscope initializer", func() { Entry("With Any scope (field and type)", testCase{ config: markerscope.MarkerScopeConfig{ - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:flexible-marker", - Scope: markerscope.AnyScope, + Identifier: "custom:flexible-marker", + Scope: markerscope.AnyScope, + }, + }, + }, + expectedErr: "", + }), + + Entry("With override marker for built-in marker", testCase{ + config: markerscope.MarkerScopeConfig{ + OverrideMarkers: []markerscope.MarkerScopeRule{ + { + Identifier: "optional", + Scope: markerscope.AnyScope, // Override default FieldScope + }, + }, + }, + expectedErr: "", + }), + + Entry("With override marker for non-built-in marker", testCase{ + config: markerscope.MarkerScopeConfig{ + OverrideMarkers: []markerscope.MarkerScopeRule{ + { + Identifier: "custom:nonexistent", + Scope: markerscope.FieldScope, + }, + }, + }, + expectedErr: "override marker must be a built-in marker; use customMarkers for custom markers", + }), + + Entry("With custom marker for built-in marker", testCase{ + config: markerscope.MarkerScopeConfig{ + CustomMarkers: []markerscope.MarkerScopeRule{ + { + Identifier: "optional", // Built-in marker + Scope: markerscope.AnyScope, + }, + }, + }, + expectedErr: "custom marker cannot be a built-in marker; use overrideMarkers to override built-in markers", + }), + + Entry("With both override and custom markers", testCase{ + config: markerscope.MarkerScopeConfig{ + OverrideMarkers: []markerscope.MarkerScopeRule{ + { + Identifier: "optional", + Scope: markerscope.AnyScope, + }, + }, + CustomMarkers: []markerscope.MarkerScopeRule{ + { + Identifier: "custom:marker", + Scope: markerscope.FieldScope, }, }, }, @@ -243,10 +297,49 @@ var _ = Describe("markerscope initializer", func() { It("should initialize analyzer with custom markers", func() { cfg := &markerscope.MarkerScopeConfig{ Policy: markerscope.MarkerScopePolicyWarn, - MarkerRules: []markerscope.MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ + { + Identifier: "custom:marker", + Scope: markerscope.FieldScope, + }, + }, + } + analyzer, err := markerscope.Initializer().Init(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(analyzer).ToNot(BeNil()) + }) + + It("should initialize analyzer with override markers", func() { + cfg := &markerscope.MarkerScopeConfig{ + Policy: markerscope.MarkerScopePolicyWarn, + OverrideMarkers: []markerscope.MarkerScopeRule{ { - Name: "custom:marker", - Scope: markerscope.FieldScope, + Identifier: "optional", + Scope: markerscope.AnyScope, // Override default FieldScope + }, + }, + } + analyzer, err := markerscope.Initializer().Init(cfg) + Expect(err).ToNot(HaveOccurred()) + Expect(analyzer).ToNot(BeNil()) + }) + + It("should initialize analyzer with both override and custom markers", func() { + cfg := &markerscope.MarkerScopeConfig{ + Policy: markerscope.MarkerScopePolicySuggestFix, + OverrideMarkers: []markerscope.MarkerScopeRule{ + { + Identifier: "optional", + Scope: markerscope.AnyScope, + }, + }, + CustomMarkers: []markerscope.MarkerScopeRule{ + { + Identifier: "custom:validation:MyMarker", + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeString}, + }, }, }, } diff --git a/pkg/analysis/markerscope/testdata/src/b/b.go b/pkg/analysis/markerscope/testdata/src/b/custom.go similarity index 100% rename from pkg/analysis/markerscope/testdata/src/b/b.go rename to pkg/analysis/markerscope/testdata/src/b/custom.go diff --git a/pkg/analysis/markerscope/testdata/src/b/override.go b/pkg/analysis/markerscope/testdata/src/b/override.go new file mode 100644 index 00000000..476a72a1 --- /dev/null +++ b/pkg/analysis/markerscope/testdata/src/b/override.go @@ -0,0 +1,59 @@ +/* +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 b + +// ============================================================================ +// Override marker tests +// These test that overrideMarkers configuration works correctly. +// +// The TestAnalyzerWithOverrideMarkers configures: +// - optional: AnyScope (overrides default FieldScope) +// - required: AnyScope (overrides default FieldScope) +// ============================================================================ + +// Built-in markers should work with their default scope on fields +type BuiltInMarkerTest struct { + // Valid: optional is allowed on fields (AnyScope includes FieldScope) + // +optional + ValidOptionalField string `json:"validOptionalField"` + + // Valid: required is allowed on fields (AnyScope includes FieldScope) + // +required + ValidRequiredField string `json:"validRequiredField"` +} + +// Built-in markers on types should now be VALID with overridden AnyScope +// +optional +type ValidOptionalOnType struct { + Name string `json:"name"` +} + +// +required +type ValidRequiredOnType struct { + Name string `json:"name"` +} + +// Test that non-overridden markers still follow default rules +type NonOverriddenMarkerTest struct { + // Valid: nullable is FieldScope by default (not overridden) + // +nullable + ValidNullableField *string `json:"validNullableField"` +} + +// +nullable // want `marker "nullable" can only be applied to fields` +type InvalidNullableOnType struct { + Name string `json:"name"` +} From 963973e5d5e8903341de400c7a15906402649c18 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Thu, 30 Oct 2025 15:36:14 +0900 Subject: [PATCH 12/18] feat(markerscope): add default config and enhance documentation Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 28 ++++++--- pkg/analysis/markerscope/analyzer_test.go | 76 ++++++++++++++--------- pkg/analysis/markerscope/doc.go | 32 ++++++++-- pkg/analysis/markerscope/initializer.go | 2 +- 4 files changed, 96 insertions(+), 42 deletions(-) diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 2a0c7585..340dc268 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -60,6 +60,9 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { cfg = &MarkerScopeConfig{} } + // Apply default configuration + defaultConfig(cfg) + // Convert override and custom marker lists to maps overrideRules := markerRulesListToMap(cfg.OverrideMarkers) customRules := markerRulesListToMap(cfg.CustomMarkers) @@ -87,14 +90,15 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { markershelper.DefaultRegistry().Register(marker) } - // Set default policy if not specified - if a.policy == "" { - a.policy = MarkerScopePolicyWarn - } - return &analysis.Analyzer{ - Name: name, - Doc: "Validates that markers are applied in the correct scope.", + Name: name, + Doc: `Validates that markers are applied in the correct scope and to compatible data types. + This analyzer performs two levels of validation: + 1. Scope validation - ensures markers are placed on the correct location (field vs type) + 2. Type constraint validation - ensures markers are applied to compatible data types + The analyzer includes 100+ built-in kubebuilder marker rules. You can override built-in marker + rules using overrideMarkers configuration, or add custom markers using customMarkers configuration. + `, Run: a.run, Requires: []*analysis.Analyzer{inspect.Analyzer, markershelper.Analyzer}, RunDespiteErrors: true, @@ -112,6 +116,16 @@ func markerRulesListToMap(rules []MarkerScopeRule) map[string]MarkerScopeRule { return result } +// defaultConfig applies default values to the configuration. +func defaultConfig(cfg *MarkerScopeConfig) { + // Set default policy if not specified + if cfg.Policy == "" { + cfg.Policy = MarkerScopePolicyWarn + } + // allowDangerousTypes defaults to false (zero value) + // overrideMarkers and customMarkers default to empty (zero value) +} + func (a *analyzer) run(pass *analysis.Pass) (any, error) { inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) if !ok { diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index d2d0f0e5..bebad633 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -13,88 +13,108 @@ 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 markerscope +package markerscope_test import ( "testing" "golang.org/x/tools/go/analysis/analysistest" + "sigs.k8s.io/kube-api-linter/pkg/analysis/markerscope" ) -func TestAnalyzerWarnOnly(t *testing.T) { +func TestAnalyzerWithDefaultConfig(t *testing.T) { testdata := analysistest.TestData() - cfg := &MarkerScopeConfig{ - Policy: MarkerScopePolicyWarn, + // Test with nil config - should use all defaults: + // - Policy: Warn + // - AllowDangerousTypes: false + // - OverrideMarkers: empty (use built-in defaults) + // - CustomMarkers: empty + analyzer, err := markerscope.Initializer().Init(&markerscope.MarkerScopeConfig{}) + if err != nil { + t.Fatal(err) } - analyzer := newAnalyzer(cfg) analysistest.Run(t, testdata, analyzer, "a") } func TestAnalyzerSuggestFixes(t *testing.T) { testdata := analysistest.TestData() - cfg := &MarkerScopeConfig{ - Policy: MarkerScopePolicySuggestFix, + cfg := &markerscope.MarkerScopeConfig{ + Policy: markerscope.MarkerScopePolicySuggestFix, + } + analyzer, err := markerscope.Initializer().Init(cfg) + if err != nil { + t.Fatal(err) } - analyzer := newAnalyzer(cfg) analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a") } func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) { testdata := analysistest.TestData() - cfg := &MarkerScopeConfig{ - Policy: MarkerScopePolicyWarn, - OverrideMarkers: []MarkerScopeRule{ + cfg := &markerscope.MarkerScopeConfig{ + Policy: markerscope.MarkerScopePolicyWarn, + OverrideMarkers: []markerscope.MarkerScopeRule{ // Override built-in "optional" to allow on types (default is FieldScope only) { Identifier: "optional", - Scope: AnyScope, + Scope: markerscope.AnyScope, }, // Override built-in "required" to allow on types (default is FieldScope only) { Identifier: "required", - Scope: AnyScope, + Scope: markerscope.AnyScope, }, }, - CustomMarkers: []MarkerScopeRule{ + CustomMarkers: []markerscope.MarkerScopeRule{ // Custom field-only marker { Identifier: "custom:field-only", - Scope: FieldScope, + Scope: markerscope.FieldScope, }, // Custom type-only marker { Identifier: "custom:type-only", - Scope: TypeScope, + Scope: markerscope.TypeScope, }, // Custom marker with string type constraint { Identifier: "custom:string-only", - Scope: FieldScope, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{ + markerscope.SchemaTypeString, + }, }, }, // Custom marker with integer type constraint { Identifier: "custom:integer-only", - Scope: FieldScope, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{ + markerscope.SchemaTypeInteger, + }, }, }, // Custom marker with array of strings constraint { Identifier: "custom:string-array", - Scope: FieldScope, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + Scope: markerscope.FieldScope, + TypeConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{ + markerscope.SchemaTypeArray, + }, + ElementConstraint: &markerscope.TypeConstraint{ + AllowedSchemaTypes: []markerscope.SchemaType{ + markerscope.SchemaTypeString, + }, }, }, }, }, } - analyzer := newAnalyzer(cfg) + analyzer, err := markerscope.Initializer().Init(cfg) + if err != nil { + t.Fatal(err) + } analysistest.Run(t, testdata, analyzer, "b") } diff --git a/pkg/analysis/markerscope/doc.go b/pkg/analysis/markerscope/doc.go index 9f6c953b..fc02a824 100644 --- a/pkg/analysis/markerscope/doc.go +++ b/pkg/analysis/markerscope/doc.go @@ -14,13 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package markerscope provides a linter that validates markers are applied in the correct scope. +// Package markerscope provides a linter that validates markers are applied in the correct scope +// and to compatible data types. +// +// # Scope Validation // // Some markers are only valid when applied to specific Go constructs: -// - Field-only markers: optional, required, nullable -// - Type/Struct-only markers: MinProperties, MaxProperties, kubebuilder:object:root, kubebuilder:subresource:status -// - Field or Type markers: default, MinLength, MaxLength, etc. +// - Field-only markers: optional, required, nullable +// - Type/Struct-only markers: MinProperties, MaxProperties, kubebuilder:object:root, kubebuilder:subresource:status +// - Field or Type markers: default, MinLength, MaxLength, etc. +// +// # Type Constraint Validation +// +// Markers are also validated for type correctness to ensure they are applied to compatible data types: +// - Numeric markers (Minimum, Maximum, MultipleOf) must be applied to integer or number types +// - String markers (Pattern, MinLength, MaxLength) must be applied to string types +// - Array markers (MinItems, MaxItems, UniqueItems) must be applied to array types +// - Object markers (MinProperties, MaxProperties) must be applied to object types (struct/map) +// +// For example, applying kubebuilder:validation:Maximum to a string field will be flagged as an error +// since Maximum is only valid for numeric types. +// +// # Array Element Type Constraints +// +// For array types, element-level constraints can be specified using items: prefix markers +// (e.g., items:Minimum, items:Pattern). These validate the array element types rather than +// the array itself. // -// This linter ensures markers are applied in their appropriate contexts to prevent -// configuration errors and improve API consistency. +// This linter ensures markers are applied in their appropriate contexts and to compatible types +// to prevent configuration errors and improve API consistency. package markerscope diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index 460460a0..8f1a66a0 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -34,7 +34,7 @@ func Initializer() initializer.AnalyzerInitializer { return initializer.NewConfigurableInitializer( name, initAnalyzer, - false, // Not enabled by default + true, validateConfig, ) } From 75e9afb6157a44429edd96df1b65d177ed5e4b7b Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Thu, 30 Oct 2025 15:58:26 +0900 Subject: [PATCH 13/18] refactor(markerscope): remove support for float/number types Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 38 +++++++------------ pkg/analysis/markerscope/analyzer_test.go | 1 - pkg/analysis/markerscope/config.go | 25 +++++------- pkg/analysis/markerscope/errors.go | 9 ----- pkg/analysis/markerscope/initializer.go | 2 +- pkg/analysis/markerscope/initializer_test.go | 1 - pkg/analysis/markerscope/schema.go | 6 +-- .../markerscope/testdata/src/a/items.go | 4 +- .../markerscope/testdata/src/a/numeric.go | 20 +++++----- .../testdata/src/a/numeric.go.golden | 2 +- 10 files changed, 39 insertions(+), 69 deletions(-) diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 340dc268..7dd4e86d 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -49,9 +49,8 @@ func init() { } type analyzer struct { - markerRules map[string]MarkerScopeRule - policy MarkerScopePolicy - allowDangerousTypes bool + markerRules map[string]MarkerScopeRule + policy MarkerScopePolicy } // newAnalyzer creates a new analyzer. @@ -78,9 +77,8 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { maps.Copy(rules, customRules) // Add custom markers a := &analyzer{ - markerRules: rules, - policy: cfg.Policy, - allowDangerousTypes: cfg.AllowDangerousTypes, + markerRules: rules, + policy: cfg.Policy, } // Register all markers (both default and custom) with the markers helper @@ -122,7 +120,6 @@ func defaultConfig(cfg *MarkerScopeConfig) { if cfg.Policy == "" { cfg.Policy = MarkerScopePolicyWarn } - // allowDangerousTypes defaults to false (zero value) // overrideMarkers and customMarkers default to empty (zero value) } @@ -211,7 +208,7 @@ func (a *analyzer) checkSingleTypeMarkers(pass *analysis.Pass, typeSpec *ast.Typ } // Check type constraints if present - a.checkTypeConstraintViolation(pass, typeSpec, marker, rule, a.allowDangerousTypes) + a.checkTypeConstraintViolation(pass, typeSpec, marker, rule) } } @@ -242,7 +239,7 @@ func (a *analyzer) reportFieldScopeViolation(pass *analysis.Pass, field *ast.Fie // checkFieldTypeConstraintViolation checks and reports type constraint violations for field markers. func (a *analyzer) checkFieldTypeConstraintViolation(pass *analysis.Pass, field *ast.Field, marker markershelper.Marker, rule MarkerScopeRule) { - if err := a.validateFieldTypeConstraint(pass, field, rule, a.allowDangerousTypes); err != nil { + if err := a.validateFieldTypeConstraint(pass, field, rule); err != nil { var fixes []analysis.SuggestedFix if a.policy == MarkerScopePolicySuggestFix { @@ -301,8 +298,8 @@ func (a *analyzer) reportTypeScopeViolation(pass *analysis.Pass, typeSpec *ast.T } // checkTypeConstraintViolation checks and reports type constraint violations. -func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule, allowDangerousTypes bool) { - if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint, allowDangerousTypes); err != nil { +func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) { + if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint); err != nil { var fixes []analysis.SuggestedFix if a.policy == MarkerScopePolicySuggestFix { @@ -339,14 +336,14 @@ func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *a } // validateFieldTypeConstraint validates that a field's type matches the type constraint. -func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.Field, rule MarkerScopeRule, allowDangerousTypes bool) error { +func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.Field, rule MarkerScopeRule) error { // Get the type of the field tv, ok := pass.TypesInfo.Types[field.Type] if !ok { return nil // Skip if we can't determine the type } - if err := validateTypeAgainstConstraint(tv.Type, rule.TypeConstraint, allowDangerousTypes); err != nil { + if err := validateTypeAgainstConstraint(tv.Type, rule.TypeConstraint); err != nil { return err } @@ -361,7 +358,7 @@ func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.F } // validateTypeSpecTypeConstraint validates that a type spec's type matches the type constraint. -func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec *ast.TypeSpec, tc *TypeConstraint, allowDangerousTypes bool) error { +func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec *ast.TypeSpec, tc *TypeConstraint) error { // Get the type of the type spec obj := pass.TypesInfo.Defs[typeSpec.Name] if obj == nil { @@ -373,21 +370,14 @@ func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec return nil } - return validateTypeAgainstConstraint(typeName.Type(), tc, allowDangerousTypes) + return validateTypeAgainstConstraint(typeName.Type(), tc) } // validateTypeAgainstConstraint validates that a Go type satisfies the type constraint. -func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint, allowDangerousTypes bool) error { +func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { // Get the schema type from the Go type schemaType := getSchemaType(t) - // Check if dangerous types are disallowed - if !allowDangerousTypes && schemaType == SchemaTypeNumber { - // Get the underlying type for better error messages - underlyingType := getUnderlyingType(t) - return &DengerousTypeError{Type: underlyingType.String()} - } - if tc == nil { return nil } @@ -403,7 +393,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint, allowDanger if tc.ElementConstraint != nil && schemaType == SchemaTypeArray { elemType := getElementType(t) if elemType != nil { - if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint, allowDangerousTypes); err != nil { + if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint); err != nil { return &InvalidElementConstraintError{Err: err} } } diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index bebad633..bb25e4e6 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -26,7 +26,6 @@ func TestAnalyzerWithDefaultConfig(t *testing.T) { testdata := analysistest.TestData() // Test with nil config - should use all defaults: // - Policy: Warn - // - AllowDangerousTypes: false // - OverrideMarkers: empty (use built-in defaults) // - CustomMarkers: empty analyzer, err := markerscope.Initializer().Init(&markerscope.MarkerScopeConfig{}) diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 66465f6b..b6403bfa 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -108,11 +108,6 @@ type MarkerScopeConfig struct { // allowedSchemaTypes: ["string"] CustomMarkers []MarkerScopeRule `json:"customMarkers,omitempty"` - // AllowDangerousTypes specifies if dangerous types are allowed. - // If true, dangerous types are allowed. - // If false, dangerous types are not allowed. - AllowDangerousTypes bool `json:"allowDangerousTypes,omitempty"` - // Policy determines whether to suggest fixes or just warn. Policy MarkerScopePolicy `json:"policy,omitempty"` } @@ -196,35 +191,35 @@ func addNumericMarkers(rules map[string]MarkerScopeRule) { Scope: AnyScope, StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderMaximumMarker: { Scope: AnyScope, StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderExclusiveMaximumMarker: { Scope: AnyScope, StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderExclusiveMinimumMarker: { Scope: AnyScope, StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderMultipleOfMarker: { Scope: AnyScope, StrictTypeConstraint: true, TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, } @@ -392,7 +387,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, }, @@ -402,7 +397,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, }, @@ -412,7 +407,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, }, @@ -422,7 +417,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, }, @@ -432,7 +427,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber}, + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, }, diff --git a/pkg/analysis/markerscope/errors.go b/pkg/analysis/markerscope/errors.go index bdf52be0..2df0a598 100644 --- a/pkg/analysis/markerscope/errors.go +++ b/pkg/analysis/markerscope/errors.go @@ -69,15 +69,6 @@ func (e *MarkerShouldBeOnTypeDefinitionError) Error() string { return fmt.Sprintf("marker should be declared on the type definition of %s instead of the field", e.TypeName) } -// DengerousTypeError represents an error when a dangerous type is used. -type DengerousTypeError struct { - Type string -} - -func (e *DengerousTypeError) Error() string { - return fmt.Sprintf("type %s is dangerous and not allowed (set allowDangerousTypes to true to permit)", e.Type) -} - // TypeNotAllowedError represents an error when a type is not allowed. type TypeNotAllowedError struct { Type SchemaType diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index 8f1a66a0..65c1267e 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -157,7 +157,7 @@ func validateTypeConstraint(tc *TypeConstraint) error { func isValidSchemaType(st SchemaType) bool { switch st { - case SchemaTypeInteger, SchemaTypeNumber, SchemaTypeString, SchemaTypeBoolean, SchemaTypeArray, SchemaTypeObject: + case SchemaTypeInteger, SchemaTypeString, SchemaTypeBoolean, SchemaTypeArray, SchemaTypeObject: return true default: return false diff --git a/pkg/analysis/markerscope/initializer_test.go b/pkg/analysis/markerscope/initializer_test.go index b3a7beb3..2054eb8f 100644 --- a/pkg/analysis/markerscope/initializer_test.go +++ b/pkg/analysis/markerscope/initializer_test.go @@ -166,7 +166,6 @@ var _ = Describe("markerscope initializer", func() { TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{ markerscope.SchemaTypeInteger, - markerscope.SchemaTypeNumber, }, }, }, diff --git a/pkg/analysis/markerscope/schema.go b/pkg/analysis/markerscope/schema.go index 8e1329b1..7db40285 100644 --- a/pkg/analysis/markerscope/schema.go +++ b/pkg/analysis/markerscope/schema.go @@ -23,8 +23,6 @@ type SchemaType string const ( // SchemaTypeInteger represents integer types (int, int32, int64, uint, etc.) SchemaTypeInteger SchemaType = "integer" - // SchemaTypeNumber represents floating-point types (float32, float64). - SchemaTypeNumber SchemaType = "number" // SchemaTypeString represents string types. SchemaTypeString SchemaType = "string" // SchemaTypeBoolean represents boolean types. @@ -74,11 +72,9 @@ func getBasicTypeSchema(bt *types.Basic) SchemaType { case types.Int, types.Int8, types.Int16, types.Int32, types.Int64, types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64: return SchemaTypeInteger - case types.Float32, types.Float64: - return SchemaTypeNumber case types.String: return SchemaTypeString - case types.Invalid, types.Uintptr, types.Complex64, types.Complex128, + case types.Float32, types.Float64, types.Invalid, types.Uintptr, types.Complex64, types.Complex128, types.UnsafePointer, types.UntypedBool, types.UntypedInt, types.UntypedRune, types.UntypedFloat, types.UntypedComplex, types.UntypedString, types.UntypedNil: // These types are not supported in OpenAPI schemas diff --git a/pkg/analysis/markerscope/testdata/src/a/items.go b/pkg/analysis/markerscope/testdata/src/a/items.go index 9d690357..22b2e541 100644 --- a/pkg/analysis/markerscope/testdata/src/a/items.go +++ b/pkg/analysis/markerscope/testdata/src/a/items.go @@ -47,7 +47,7 @@ type ObjectArrayTypeNoMarker []map[string]string type GeneralArrayTypeNoMarker []string // Invalid: items:Maximum on string array type -// +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)` +// +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer\]\)` type InvalidItemsMaximumOnStringArrayType []string // Invalid: items:Pattern on int array type @@ -128,7 +128,7 @@ type ArrayItemsMarkersFieldTest struct { InvalidItemsFormatOnGeneralArrayType GeneralArrayTypeNoMarker `json:"invalidItemsFormatOnGeneralArrayType"` // Invalid: items:Maximum on string array (element type mismatch) - // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)` + // +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer\]\)` InvalidItemsMaximumOnStringArray []string `json:"invalidItemsMaximumOnStringArray"` // Invalid: items:Pattern on int array (element type mismatch) diff --git a/pkg/analysis/markerscope/testdata/src/a/numeric.go b/pkg/analysis/markerscope/testdata/src/a/numeric.go index 4b2d2da9..d47b6f30 100644 --- a/pkg/analysis/markerscope/testdata/src/a/numeric.go +++ b/pkg/analysis/markerscope/testdata/src/a/numeric.go @@ -30,17 +30,17 @@ type MaximumType int64 // +kubebuilder:validation:MultipleOf=3 type MultipleOfType int32 -// Valid: Float type with numeric markers -// +kubebuilder:validation:Minimum=0.0 // want `marker "kubebuilder:validation:Minimum": type float64 is dangerous and not allowed \(set allowDangerousTypes to true to permit\)` -// +kubebuilder:validation:Maximum=1.0 // want `marker "kubebuilder:validation:Maximum": type float64 is dangerous and not allowed \(set allowDangerousTypes to true to permit\)` +// Invalid: Float type with numeric markers (float types are not supported) +// +kubebuilder:validation:Minimum=0.0 // want `marker "kubebuilder:validation:Minimum": type is not allowed \(expected one of: \[integer\]\)` +// +kubebuilder:validation:Maximum=1.0 // want `marker "kubebuilder:validation:Maximum": type is not allowed \(expected one of: \[integer\]\)` type FloatType float64 // Invalid: Minimum marker on string type -// +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)` +// +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer\]\)` type InvalidMinimumOnStringType string // Invalid: Maximum marker on boolean type -// +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer number\]\)` +// +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer\]\)` type InvalidMaximumOnBoolType bool type NumericMarkersFieldTest struct { @@ -107,20 +107,20 @@ type NumericMarkersFieldTest struct { InvalidMultipleOfOnNumericType NumericType `json:"invalidMultipleOfOnNumericType"` // Invalid: Minimum marker on string field - // +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)` + // +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer\]\)` InvalidMinimumOnString string `json:"invalidMinimumOnString"` // Invalid: Maximum marker on boolean field - // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer number\]\)` + // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer\]\)` InvalidMaximumOnBool bool `json:"invalidMaximumOnBool"` // Invalid: MultipleOf marker on array field - // +kubebuilder:validation:MultipleOf=5 // want `marker "kubebuilder:validation:MultipleOf": type array is not allowed \(expected one of: \[integer number\]\)` + // +kubebuilder:validation:MultipleOf=5 // want `marker "kubebuilder:validation:MultipleOf": type array is not allowed \(expected one of: \[integer\]\)` InvalidMultipleOfOnArray []int32 `json:"invalidMultipleOfOnArray"` // Invalid: Using invalid named type - // +kubebuilder:validation:Minimum=50 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)` - // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type string is not allowed \(expected one of: \[integer number\]\)` + // +kubebuilder:validation:Minimum=50 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer\]\)` + // +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type string is not allowed \(expected one of: \[integer\]\)` InvalidMinimumOnStringTyped InvalidMinimumOnStringType `json:"invalidMinimumOnStringTyped"` // Invalid: Using invalid named type diff --git a/pkg/analysis/markerscope/testdata/src/a/numeric.go.golden b/pkg/analysis/markerscope/testdata/src/a/numeric.go.golden index 847f724e..e4a51620 100644 --- a/pkg/analysis/markerscope/testdata/src/a/numeric.go.golden +++ b/pkg/analysis/markerscope/testdata/src/a/numeric.go.golden @@ -35,7 +35,7 @@ type MaximumType int64 // +kubebuilder:validation:MultipleOf=3 type MultipleOfType int32 -// Valid: Float type with numeric markers +// Invalid: Float type with numeric markers (float types are not supported) type FloatType float64 // Invalid: Minimum marker on string type From 2c2f8cbe753d1dd3ce9025c9e1ba6cb09b9650a3 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Thu, 30 Oct 2025 16:28:37 +0900 Subject: [PATCH 14/18] refactor(markerscope): replace StrictTypeConstraint with NamedTypeConstraint and improve code quality Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 16 ++- pkg/analysis/markerscope/analyzer_test.go | 5 + pkg/analysis/markerscope/config.go | 159 ++++++++++++---------- pkg/analysis/markerscope/errors.go | 64 ++++----- pkg/analysis/markerscope/initializer.go | 69 ++++++++-- pkg/analysis/markerscope/schema.go | 14 -- 6 files changed, 192 insertions(+), 135 deletions(-) diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 7dd4e86d..cfd6c047 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -106,11 +106,13 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { // markerRulesListToMap converts a list of marker rules to a map keyed by marker identifier. func markerRulesListToMap(rules []MarkerScopeRule) map[string]MarkerScopeRule { result := make(map[string]MarkerScopeRule, len(rules)) + for _, rule := range rules { if rule.Identifier != "" { result[rule.Identifier] = rule } } + return result } @@ -120,7 +122,6 @@ func defaultConfig(cfg *MarkerScopeConfig) { if cfg.Policy == "" { cfg.Policy = MarkerScopePolicyWarn } - // overrideMarkers and customMarkers default to empty (zero value) } func (a *analyzer) run(pass *analysis.Pass) (any, error) { @@ -244,7 +245,7 @@ func (a *analyzer) checkFieldTypeConstraintViolation(pass *analysis.Pass, field if a.policy == MarkerScopePolicySuggestFix { // Check if this is a "should be on type definition" error - var moveErr *MarkerShouldBeOnTypeDefinitionError + var moveErr *markerShouldBeOnTypeDefinitionError if errors.As(err, &moveErr) { // Suggest moving to type definition fixes = a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule) @@ -305,7 +306,7 @@ func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *a if a.policy == MarkerScopePolicySuggestFix { // Check if this is a "should be on field" error (though validateTypeSpecTypeConstraint doesn't return this) // For consistency with checkFieldMarkers, we check the error type - var moveErr *MarkerShouldBeOnTypeDefinitionError + var moveErr *markerShouldBeOnTypeDefinitionError if errors.As(err, &moveErr) { // This shouldn't happen for type specs, but handle it for consistency fixes = a.suggestMoveToField(pass, typeSpec, marker, rule) @@ -347,10 +348,11 @@ func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.F return err } - if rule.StrictTypeConstraint && rule.Scope == AnyScope { + // Check if the marker should be on the type definition instead of the field + if rule.NamedTypeConstraint == NamedTypeConstraintRequireTypeDefinition && rule.Scope == AnyScope { namedType, ok := tv.Type.(*types.Named) if ok { - return &MarkerShouldBeOnTypeDefinitionError{TypeName: namedType.Obj().Name()} + return &markerShouldBeOnTypeDefinitionError{typeName: namedType.Obj().Name()} } } @@ -385,7 +387,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { // Check if the schema type is allowed if len(tc.AllowedSchemaTypes) > 0 { if !slices.Contains(tc.AllowedSchemaTypes, schemaType) { - return &TypeNotAllowedError{Type: schemaType, AllowedTypes: tc.AllowedSchemaTypes} + return &typeNotAllowedError{schemaType: schemaType, allowedTypes: tc.AllowedSchemaTypes} } } @@ -394,7 +396,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { elemType := getElementType(t) if elemType != nil { if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint); err != nil { - return &InvalidElementConstraintError{Err: err} + return &invalidElementConstraintError{err: err} } } } diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index bb25e4e6..1bbc68df 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -32,6 +32,7 @@ func TestAnalyzerWithDefaultConfig(t *testing.T) { if err != nil { t.Fatal(err) } + analysistest.Run(t, testdata, analyzer, "a") } @@ -40,10 +41,12 @@ func TestAnalyzerSuggestFixes(t *testing.T) { cfg := &markerscope.MarkerScopeConfig{ Policy: markerscope.MarkerScopePolicySuggestFix, } + analyzer, err := markerscope.Initializer().Init(cfg) if err != nil { t.Fatal(err) } + analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a") } @@ -111,9 +114,11 @@ func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) { }, }, } + analyzer, err := markerscope.Initializer().Init(cfg) if err != nil { t.Fatal(err) } + analysistest.Run(t, testdata, analyzer, "b") } diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index b6403bfa..44e82ef9 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -38,6 +38,7 @@ func (s ScopeConstraint) Allows(scope ScopeConstraint) bool { if s == AnyScope { return true } + return s == scope } @@ -55,6 +56,19 @@ type TypeConstraint struct { ElementConstraint *TypeConstraint } +// NamedTypeConstraint specifies how markers should be applied to named types. +type NamedTypeConstraint string + +const ( + // NamedTypeConstraintAllowField allows markers on fields with named types. + // The marker can be placed on the field even if the field uses a named type. + NamedTypeConstraintAllowField NamedTypeConstraint = "AllowField" + + // NamedTypeConstraintRequireTypeDefinition requires markers to be on the type definition. + // When a field uses a named type, the marker must be placed on the type definition instead. + NamedTypeConstraintRequireTypeDefinition NamedTypeConstraint = "RequireTypeDefinition" +) + // MarkerScopeRule defines comprehensive scope validation rules for a marker. type MarkerScopeRule struct { // Identifier is the marker identifier (e.g., "optional", "kubebuilder:validation:Minimum"). @@ -63,10 +77,11 @@ type MarkerScopeRule struct { // Scope specifies where the marker can be placed (field vs type). Scope ScopeConstraint - // StrictTypeConstraint specifies if the type constraint is strict. - // If true, the type constraint is strict and only the allowed schema types are allowed. - // If false, the type constraint is not strict and any type is allowed. - StrictTypeConstraint bool + // NamedTypeConstraint specifies how markers should be applied to named types. + // When a field uses a named type (e.g., type CustomInt int32), this determines + // whether the marker can be on the field or must be on the type definition. + // If empty, defaults to AllowField (marker can be placed on either field or type). + NamedTypeConstraint NamedTypeConstraint `json:"namedTypeConstraint,omitempty"` // TypeConstraint specifies what types the marker can be applied to. // NOTE: This is used for both field and type scopes, but typically only enforced @@ -188,36 +203,36 @@ func addNumericMarkers(rules map[string]MarkerScopeRule) { numericMarkers := map[string]MarkerScopeRule{ // numeric markers (field or type, integer or number types) markers.KubebuilderMinimumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderMaximumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderExclusiveMaximumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderExclusiveMinimumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderMultipleOfMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, @@ -232,15 +247,15 @@ func addObjectMarkers(rules map[string]MarkerScopeRule) { objectMarkers := map[string]MarkerScopeRule{ // object markers (field or type, object types) markers.KubebuilderMinPropertiesMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, }, markers.KubebuilderMaxPropertiesMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, @@ -255,22 +270,22 @@ func addStringMarkers(rules map[string]MarkerScopeRule) { stringMarkers := map[string]MarkerScopeRule{ // string markers (field or type, string types) markers.KubebuilderPatternMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, markers.KubebuilderMinLengthMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, markers.KubebuilderMaxLengthMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, @@ -285,22 +300,22 @@ func addArrayMarkers(rules map[string]MarkerScopeRule) { arrayMarkers := map[string]MarkerScopeRule{ // array markers (field or type, array types) markers.KubebuilderMinItemsMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderMaxItemsMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderUniqueItemsMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, @@ -324,8 +339,8 @@ func addGeneralMarkers(rules map[string]MarkerScopeRule) { Scope: AnyScope, }, markers.KubebuilderXValidationMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, }, } @@ -337,22 +352,22 @@ func addSSATopologyMarkers(rules map[string]MarkerScopeRule) { ssaMarkers := map[string]MarkerScopeRule{ // Server-Side Apply topology markers markers.KubebuilderListTypeMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderListMapKeyMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderMapTypeMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, @@ -382,8 +397,8 @@ func addArrayItemsMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { itemsNumericMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMaximumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -392,8 +407,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMinimumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -402,8 +417,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsExclusiveMaximumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -412,8 +427,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsExclusiveMinimumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -422,8 +437,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMultipleOfMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -440,8 +455,8 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { itemsStringMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMinLengthMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -450,8 +465,8 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMaxLengthMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -460,8 +475,8 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsPatternMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -478,8 +493,8 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { itemsArrayMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMaxItemsMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -488,8 +503,8 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMinItemsMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -498,8 +513,8 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsUniqueItemsMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -516,8 +531,8 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { itemsObjectMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMinPropertiesMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -526,8 +541,8 @@ func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMaxPropertiesMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, ElementConstraint: &TypeConstraint{ @@ -544,8 +559,8 @@ func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { itemsGeneralMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsEnumMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, // Enum can apply to any element type @@ -553,8 +568,8 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsFormatMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, // Format can apply to various types @@ -562,8 +577,8 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsTypeMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, // Type marker can override any element type @@ -571,8 +586,8 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsXValidationMarker: { - Scope: AnyScope, - StrictTypeConstraint: true, + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, // CEL validation can apply to any element type diff --git a/pkg/analysis/markerscope/errors.go b/pkg/analysis/markerscope/errors.go index 2df0a598..0ba3efbb 100644 --- a/pkg/analysis/markerscope/errors.go +++ b/pkg/analysis/markerscope/errors.go @@ -24,57 +24,59 @@ var ( errScopeRequired = errors.New("scope is required") ) -// InvalidScopeConstraintError represents an error when a scope constraint is invalid. -type InvalidScopeConstraintError struct { - Scope string +type invalidScopeConstraintError struct { + scope string } -func (e *InvalidScopeConstraintError) Error() string { - return fmt.Sprintf("invalid scope: %q (must be one of: Field, Type, Any)", e.Scope) +func (e *invalidScopeConstraintError) Error() string { + return fmt.Sprintf("invalid scope: %q (must be one of: Field, Type, Any)", e.scope) } -// InvalidSchemaTypeError represents an error when a schema type is invalid. -type InvalidSchemaTypeError struct { - SchemaType string +type invalidNamedTypeConstraintError struct { + constraint string } -func (e *InvalidSchemaTypeError) Error() string { - return fmt.Sprintf("invalid schema type: %q", e.SchemaType) +func (e *invalidNamedTypeConstraintError) Error() string { + return fmt.Sprintf("invalid namedTypeConstraint: %q (must be one of: AllowField, RequireTypeDefinition, or empty)", e.constraint) } -// InvalidTypeConstraintError represents an error when a type constraint is invalid. -type InvalidTypeConstraintError struct { - Err error +type invalidSchemaTypeError struct { + schemaType string } -func (e *InvalidTypeConstraintError) Error() string { - return fmt.Sprintf("invalid type constraint: %v", e.Err) +func (e *invalidSchemaTypeError) Error() string { + return fmt.Sprintf("invalid schema type: %q", e.schemaType) } -// InvalidElementConstraintError represents an error when an element constraint is invalid. -type InvalidElementConstraintError struct { - Err error +type invalidTypeConstraintError struct { + err error } -func (e *InvalidElementConstraintError) Error() string { - return fmt.Sprintf("array element: %v", e.Err) +func (e *invalidTypeConstraintError) Error() string { + return fmt.Sprintf("invalid type constraint: %v", e.err) } -// MarkerShouldBeOnTypeDefinitionError represents an error when a marker should be declared on the type definition. -type MarkerShouldBeOnTypeDefinitionError struct { - TypeName string +type invalidElementConstraintError struct { + err error } -func (e *MarkerShouldBeOnTypeDefinitionError) Error() string { - return fmt.Sprintf("marker should be declared on the type definition of %s instead of the field", e.TypeName) +func (e *invalidElementConstraintError) Error() string { + return fmt.Sprintf("array element: %v", e.err) } -// TypeNotAllowedError represents an error when a type is not allowed. -type TypeNotAllowedError struct { - Type SchemaType - AllowedTypes []SchemaType +type markerShouldBeOnTypeDefinitionError struct { + typeName string } -func (e *TypeNotAllowedError) Error() string { - return fmt.Sprintf("type %s is not allowed (expected one of: %v)", e.Type, e.AllowedTypes) +func (e *markerShouldBeOnTypeDefinitionError) Error() string { + return fmt.Sprintf("marker should be declared on the type definition of %s instead of the field", e.typeName) +} + +type typeNotAllowedError struct { + schemaType SchemaType + allowedTypes []SchemaType +} + +func (e *typeNotAllowedError) Error() string { + return fmt.Sprintf("type %s is not allowed (expected one of: %v)", e.schemaType, e.allowedTypes) } diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index 65c1267e..7b94be2f 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -52,21 +52,41 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList fieldErrors := field.ErrorList{} // Validate policy - if cfg.Policy != "" && cfg.Policy != MarkerScopePolicyWarn && cfg.Policy != MarkerScopePolicySuggestFix { - fieldErrors = append(fieldErrors, field.Invalid(fldPath.Child("policy"), cfg.Policy, - fmt.Sprintf("invalid policy, must be one of: %q, %q", MarkerScopePolicyWarn, MarkerScopePolicySuggestFix))) - } + fieldErrors = append(fieldErrors, validatePolicy(cfg.Policy, fldPath)...) // Get default marker rules for validation defaultRules := DefaultMarkerRules() // Validate override marker rules - for i, rule := range cfg.OverrideMarkers { + fieldErrors = append(fieldErrors, validateOverrideMarkers(cfg.OverrideMarkers, defaultRules, fldPath)...) + + // Validate custom marker rules + fieldErrors = append(fieldErrors, validateCustomMarkers(cfg.CustomMarkers, defaultRules, fldPath)...) + + return fieldErrors +} + +func validatePolicy(policy MarkerScopePolicy, fldPath *field.Path) field.ErrorList { + if policy != "" && policy != MarkerScopePolicyWarn && policy != MarkerScopePolicySuggestFix { + return field.ErrorList{ + field.Invalid(fldPath.Child("policy"), policy, + fmt.Sprintf("invalid policy, must be one of: %q, %q", MarkerScopePolicyWarn, MarkerScopePolicySuggestFix)), + } + } + + return nil +} + +func validateOverrideMarkers(rules []MarkerScopeRule, defaultRules map[string]MarkerScopeRule, fldPath *field.Path) field.ErrorList { + fieldErrors := field.ErrorList{} + + for i, rule := range rules { markerRulePath := fldPath.Child("overrideMarkers").Index(i) // Validate that identifier is not empty if rule.Identifier == "" { fieldErrors = append(fieldErrors, field.Required(markerRulePath.Child("identifier"), "marker identifier is required")) + continue } @@ -74,6 +94,7 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList if _, exists := defaultRules[rule.Identifier]; !exists { fieldErrors = append(fieldErrors, field.Invalid(markerRulePath.Child("identifier"), rule.Identifier, "override marker must be a built-in marker; use customMarkers for custom markers")) + continue } @@ -82,13 +103,19 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList } } - // Validate custom marker rules - for i, rule := range cfg.CustomMarkers { + return fieldErrors +} + +func validateCustomMarkers(rules []MarkerScopeRule, defaultRules map[string]MarkerScopeRule, fldPath *field.Path) field.ErrorList { + fieldErrors := field.ErrorList{} + + for i, rule := range rules { markerRulePath := fldPath.Child("customMarkers").Index(i) // Validate that identifier is not empty if rule.Identifier == "" { fieldErrors = append(fieldErrors, field.Required(markerRulePath.Child("identifier"), "marker identifier is required")) + continue } @@ -96,6 +123,7 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList if _, exists := defaultRules[rule.Identifier]; exists { fieldErrors = append(fieldErrors, field.Invalid(markerRulePath.Child("identifier"), rule.Identifier, "custom marker cannot be a built-in marker; use overrideMarkers to override built-in markers")) + continue } @@ -118,14 +146,19 @@ func validateMarkerRule(rule MarkerScopeRule) error { case FieldScope, TypeScope, AnyScope: // Valid scope default: - return &InvalidScopeConstraintError{Scope: string(rule.Scope)} + return &invalidScopeConstraintError{scope: string(rule.Scope)} + } + + // Validate named type constraint if present + if !isValidNamedTypeConstraint(rule.NamedTypeConstraint) { + return &invalidNamedTypeConstraintError{constraint: string(rule.NamedTypeConstraint)} } // Validate type constraint if present if rule.TypeConstraint != nil { if err := validateTypeConstraint(rule.TypeConstraint); err != nil { - return &InvalidTypeConstraintError{ - Err: err, + return &invalidTypeConstraintError{ + err: err, } } } @@ -141,7 +174,7 @@ func validateTypeConstraint(tc *TypeConstraint) error { // Validate schema types if specified for _, st := range tc.AllowedSchemaTypes { if !isValidSchemaType(st) { - return &InvalidSchemaTypeError{SchemaType: string(st)} + return &invalidSchemaTypeError{schemaType: string(st)} } } @@ -163,3 +196,17 @@ func isValidSchemaType(st SchemaType) bool { return false } } + +func isValidNamedTypeConstraint(ntc NamedTypeConstraint) bool { + // Empty is valid (defaults to AllowField) + if ntc == "" { + return true + } + + switch ntc { + case NamedTypeConstraintAllowField, NamedTypeConstraintRequireTypeDefinition: + return true + default: + return false + } +} diff --git a/pkg/analysis/markerscope/schema.go b/pkg/analysis/markerscope/schema.go index 7db40285..8902cb82 100644 --- a/pkg/analysis/markerscope/schema.go +++ b/pkg/analysis/markerscope/schema.go @@ -105,17 +105,3 @@ func getElementType(t types.Type) types.Type { return nil } - -// getUnderlyingType recursively unwraps type to find the underlying type. -func getUnderlyingType(expr types.Type) types.Type { - switch t := expr.(type) { - case *types.Pointer: - return getUnderlyingType(t.Elem()) - case *types.Named: - return getUnderlyingType(t.Underlying()) - case *types.Alias: - return getUnderlyingType(t.Underlying()) - default: - return expr - } -} From 1d2abb957170763b134c0fc3425f966180bbfd71 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Thu, 30 Oct 2025 16:35:02 +0900 Subject: [PATCH 15/18] refactor(markers): remove Kubebuilder prefix from SSA topology marker constants Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/config.go | 8 ++++---- pkg/analysis/ssatags/analyzer.go | 6 +++--- pkg/markers/markers.go | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 44e82ef9..4ae387e0 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -351,28 +351,28 @@ func addGeneralMarkers(rules map[string]MarkerScopeRule) { func addSSATopologyMarkers(rules map[string]MarkerScopeRule) { ssaMarkers := map[string]MarkerScopeRule{ // Server-Side Apply topology markers - markers.KubebuilderListTypeMarker: { + markers.ListTypeMarker: { Scope: AnyScope, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, - markers.KubebuilderListMapKeyMarker: { + markers.ListMapKeyMarker: { Scope: AnyScope, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, - markers.KubebuilderMapTypeMarker: { + markers.MapTypeMarker: { Scope: AnyScope, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, }, - markers.KubebuilderStructTypeMarker: { + markers.StructTypeMarker: { Scope: AnyScope, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, diff --git a/pkg/analysis/ssatags/analyzer.go b/pkg/analysis/ssatags/analyzer.go index 0ce0f1d8..62f4b93c 100644 --- a/pkg/analysis/ssatags/analyzer.go +++ b/pkg/analysis/ssatags/analyzer.go @@ -85,7 +85,7 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce // If the field is a byte array, we cannot use listType markers with it. if utils.IsByteArray(pass, field) { - listTypeMarkers := fieldMarkers.Get(kubebuildermarkers.KubebuilderListTypeMarker) + listTypeMarkers := fieldMarkers.Get(kubebuildermarkers.ListTypeMarker) for _, marker := range listTypeMarkers { pass.Report(analysis.Diagnostic{ Pos: field.Pos(), @@ -108,7 +108,7 @@ func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAcce return } - listTypeMarkers := fieldMarkers.Get(kubebuildermarkers.KubebuilderListTypeMarker) + listTypeMarkers := fieldMarkers.Get(kubebuildermarkers.ListTypeMarker) if len(listTypeMarkers) == 0 { pass.Report(analysis.Diagnostic{ @@ -146,7 +146,7 @@ func (a *analyzer) checkListTypeMarker(pass *analysis.Pass, listType string, fie } func (a *analyzer) checkListTypeMap(pass *analysis.Pass, fieldMarkers markers.MarkerSet, field *ast.Field, qualifiedFieldName string) { - listMapKeyMarkers := fieldMarkers.Get(kubebuildermarkers.KubebuilderListMapKeyMarker) + listMapKeyMarkers := fieldMarkers.Get(kubebuildermarkers.ListMapKeyMarker) isObjectList := utils.IsObjectList(pass, field) diff --git a/pkg/markers/markers.go b/pkg/markers/markers.go index 99924e80..706d996d 100644 --- a/pkg/markers/markers.go +++ b/pkg/markers/markers.go @@ -150,17 +150,17 @@ const ( // KubebuilderItemsXValidationMarker is the marker used to specify CEL validation rules for entries to a nested array type or field in kubebuilder. KubebuilderItemsXValidationMarker = "kubebuilder:validation:items:XValidation" - // KubebuilderListTypeMarker is the marker used to specify the type of list for server-side apply operations. - KubebuilderListTypeMarker = "listType" + // ListTypeMarker is the marker used to specify the type of list for server-side apply operations. + ListTypeMarker = "listType" - // KubebuilderListMapKeyMarker is the marker used to specify the key field for map-type lists. - KubebuilderListMapKeyMarker = "listMapKey" + // ListMapKeyMarker is the marker used to specify the key field for map-type lists. + ListMapKeyMarker = "listMapKey" - // KubebuilderMapTypeMarker is the marker used to specify the atomicity level of a map. - KubebuilderMapTypeMarker = "mapType" + // MapTypeMarker is the marker used to specify the atomicity level of a map. + MapTypeMarker = "mapType" - // KubebuilderStructTypeMarker is the marker used to specify the atomicity level of a struct. - KubebuilderStructTypeMarker = "structType" + // StructTypeMarker is the marker used to specify the atomicity level of a struct. + StructTypeMarker = "structType" // KubebuilderSchemaLessMarker is the marker that indicates that a struct is schemaless. KubebuilderSchemaLessMarker = "kubebuilder:validation:Schemaless" From c4f233b738cfa7a3a0d71bfd2ce2dcc816aa79f5 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Thu, 30 Oct 2025 17:46:26 +0900 Subject: [PATCH 16/18] refactor(markerscope): improve error handling and code organization Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/analyzer.go | 214 +++++----- pkg/analysis/markerscope/config.go | 478 ---------------------- pkg/analysis/markerscope/errors.go | 6 + pkg/analysis/markerscope/initializer.go | 2 +- pkg/analysis/markerscope/marker_rules.go | 494 +++++++++++++++++++++++ pkg/analysis/markerscope/schema.go | 56 +-- pkg/analysis/utils/utils.go | 45 ++- 7 files changed, 658 insertions(+), 637 deletions(-) create mode 100644 pkg/analysis/markerscope/marker_rules.go diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index cfd6c047..7b689b45 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -24,10 +24,10 @@ import ( "slices" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors" + "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags" + inspectorhelper "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" ) @@ -38,7 +38,7 @@ const ( func init() { // Register all markers we want to validate scope for - defaults := DefaultMarkerRules() + defaults := defaultMarkerRules() markers := make([]string, 0, len(defaults)) for marker := range defaults { @@ -72,7 +72,7 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { // 3. Add custom markers (new markers not in defaults) // Note: Validation ensures overrideMarkers only contains built-in markers // and customMarkers only contains non-built-in markers, so no conflicts. - rules := DefaultMarkerRules() + rules := defaultMarkerRules() maps.Copy(rules, overrideRules) // Override built-in markers maps.Copy(rules, customRules) // Add custom markers @@ -98,7 +98,7 @@ func newAnalyzer(cfg *MarkerScopeConfig) *analysis.Analyzer { rules using overrideMarkers configuration, or add custom markers using customMarkers configuration. `, Run: a.run, - Requires: []*analysis.Analyzer{inspect.Analyzer, markershelper.Analyzer}, + Requires: []*analysis.Analyzer{inspectorhelper.Analyzer}, RunDespiteErrors: true, } } @@ -125,39 +125,47 @@ func defaultConfig(cfg *MarkerScopeConfig) { } func (a *analyzer) run(pass *analysis.Pass) (any, error) { - inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + inspector, ok := pass.ResultOf[inspectorhelper.Analyzer].(inspectorhelper.Inspector) if !ok { return nil, kalerrors.ErrCouldNotGetInspector } - markersAccess, ok := pass.ResultOf[markershelper.Analyzer].(markershelper.Markers) - if !ok { - return nil, kalerrors.ErrCouldNotGetMarkers - } + // Check field markers + inspector.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, _ string) { + a.checkFieldMarkers(pass, field, markersAccess) + }) - // Check field markers and type markers - nodeFilter := []ast.Node{ - (*ast.Field)(nil), - (*ast.GenDecl)(nil), - } + // Check type markers + inspector.InspectTypeSpec(func(typeSpec *ast.TypeSpec, markersAccess markershelper.Markers) { + a.checkTypeSpecMarkers(pass, typeSpec, markersAccess) + }) + + return nil, nil //nolint:nilnil +} - inspect.Preorder(nodeFilter, func(n ast.Node) { - switch node := n.(type) { - case *ast.Field: - a.checkFieldMarkers(pass, node, markersAccess) - case *ast.GenDecl: - a.checkTypeMarkers(pass, node, markersAccess) +// sortMarkersByPosition sorts markers by their position to ensure consistent ordering. +func sortMarkersByPosition(markers []markershelper.Marker) []markershelper.Marker { + slices.SortFunc(markers, func(a, b markershelper.Marker) int { + if a.Pos < b.Pos { + return -1 } + + if a.Pos > b.Pos { + return 1 + } + + return 0 }) - return nil, nil //nolint:nilnil + return markers } // checkFieldMarkers checks markers on fields for violations. func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) { fieldMarkers := markersAccess.FieldMarkers(field) + markers := sortMarkersByPosition(fieldMarkers.UnsortedList()) - for _, marker := range fieldMarkers.UnsortedList() { + for _, marker := range markers { rule, ok := a.markerRules[marker.Identifier] if !ok { // No rule defined for this marker, skip validation @@ -175,27 +183,12 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark } } -// checkTypeMarkers checks markers on types for violations. -func (a *analyzer) checkTypeMarkers(pass *analysis.Pass, genDecl *ast.GenDecl, markersAccess markershelper.Markers) { - if len(genDecl.Specs) == 0 { - return - } - - for i := range genDecl.Specs { - typeSpec, ok := genDecl.Specs[i].(*ast.TypeSpec) - if !ok { - continue - } - - a.checkSingleTypeMarkers(pass, typeSpec, markersAccess) - } -} - -// checkSingleTypeMarkers checks markers on a single type for violations. -func (a *analyzer) checkSingleTypeMarkers(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markershelper.Markers) { +// checkTypeSpecMarkers checks markers on a type spec for violations. +func (a *analyzer) checkTypeSpecMarkers(pass *analysis.Pass, typeSpec *ast.TypeSpec, markersAccess markershelper.Markers) { typeMarkers := markersAccess.TypeMarkers(typeSpec) + markers := sortMarkersByPosition(typeMarkers.UnsortedList()) - for _, marker := range typeMarkers.UnsortedList() { + for _, marker := range markers { rule, ok := a.markerRules[marker.Identifier] if !ok { // No rule defined for this marker, skip validation @@ -241,53 +234,54 @@ func (a *analyzer) reportFieldScopeViolation(pass *analysis.Pass, field *ast.Fie // checkFieldTypeConstraintViolation checks and reports type constraint violations for field markers. func (a *analyzer) checkFieldTypeConstraintViolation(pass *analysis.Pass, field *ast.Field, marker markershelper.Marker, rule MarkerScopeRule) { if err := a.validateFieldTypeConstraint(pass, field, rule); err != nil { - var fixes []analysis.SuggestedFix + a.reportTypeConstraintViolation(pass, field, marker, rule, err) + } +} - if a.policy == MarkerScopePolicySuggestFix { - // Check if this is a "should be on type definition" error - var moveErr *markerShouldBeOnTypeDefinitionError - if errors.As(err, &moveErr) { - // Suggest moving to type definition - fixes = a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule) - } else { - // Type constraint violation - suggest removing the marker - fixes = []analysis.SuggestedFix{ - { - Message: "Remove invalid marker", - TextEdits: []analysis.TextEdit{ - { - Pos: marker.Pos, - End: marker.End + 1, // Include newline - }, +// reportTypeConstraintViolation reports a type constraint violation with appropriate suggested fixes. +func (a *analyzer) reportTypeConstraintViolation(pass *analysis.Pass, field *ast.Field, marker markershelper.Marker, rule MarkerScopeRule, err error) { + var fixes []analysis.SuggestedFix + + if a.policy == MarkerScopePolicySuggestFix { + // Check if this is a "should be on type definition" error + if errors.Is(err, &markerShouldBeOnTypeDefinitionError{}) { + // Suggest moving to type definition + fixes = a.suggestMoveToFieldsIfCompatible(pass, field, marker, rule) + } else { + // Type constraint violation - suggest removing the marker + fixes = []analysis.SuggestedFix{ + { + Message: "Remove invalid marker", + TextEdits: []analysis.TextEdit{ + { + Pos: marker.Pos, + End: marker.End + 1, // Include newline }, }, - } + }, } } - - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), - SuggestedFixes: fixes, - }) } + + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err), + SuggestedFixes: fixes, + }) } // reportTypeScopeViolation reports a scope violation for a type marker. func (a *analyzer) reportTypeScopeViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) { - var message string - var fixes []analysis.SuggestedFix + message := fmt.Sprintf("marker %q cannot be applied to types", marker.Identifier) if rule.Scope == FieldScope { message = fmt.Sprintf("marker %q can only be applied to fields", marker.Identifier) if a.policy == MarkerScopePolicySuggestFix { fixes = a.suggestMoveToField(pass, typeSpec, marker, rule) } - } else { - message = fmt.Sprintf("marker %q cannot be applied to types", marker.Identifier) } pass.Report(analysis.Diagnostic{ @@ -301,39 +295,43 @@ func (a *analyzer) reportTypeScopeViolation(pass *analysis.Pass, typeSpec *ast.T // checkTypeConstraintViolation checks and reports type constraint violations. func (a *analyzer) checkTypeConstraintViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) { if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint); err != nil { - var fixes []analysis.SuggestedFix + a.reportTypeSpecTypeConstraintViolation(pass, typeSpec, marker, rule, err) + } +} - if a.policy == MarkerScopePolicySuggestFix { - // Check if this is a "should be on field" error (though validateTypeSpecTypeConstraint doesn't return this) - // For consistency with checkFieldMarkers, we check the error type - var moveErr *markerShouldBeOnTypeDefinitionError - if errors.As(err, &moveErr) { - // This shouldn't happen for type specs, but handle it for consistency - fixes = a.suggestMoveToField(pass, typeSpec, marker, rule) - } else { - // Type constraint violation - suggest removing the marker - fixes = []analysis.SuggestedFix{ - { - Message: "Remove invalid marker", - TextEdits: []analysis.TextEdit{ - { - Pos: marker.Pos, - End: marker.End + 1, // Include newline - }, +// reportTypeSpecTypeConstraintViolation reports a type constraint violation on a type spec with appropriate suggested fixes. +func (a *analyzer) reportTypeSpecTypeConstraintViolation(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule, err error) { + var fixes []analysis.SuggestedFix + + if a.policy == MarkerScopePolicySuggestFix { + // Check if this is a "should be on field" error (though validateTypeSpecTypeConstraint doesn't return this) + // For consistency with checkFieldMarkers, we check the error type + if errors.Is(err, &markerShouldBeOnTypeDefinitionError{}) { + // This shouldn't happen for type specs, but handle it for consistency + fixes = a.suggestMoveToField(pass, typeSpec, marker, rule) + } else { + // Type constraint violation - suggest removing the marker + fixes = []analysis.SuggestedFix{ + { + Message: "Remove invalid marker", + TextEdits: []analysis.TextEdit{ + { + Pos: marker.Pos, + End: marker.End + 1, // Include newline }, }, - } + }, } } - - message := fmt.Sprintf("marker %q: %s", marker.Identifier, err) - pass.Report(analysis.Diagnostic{ - Pos: marker.Pos, - End: marker.End, - Message: message, - SuggestedFixes: fixes, - }) } + + message := fmt.Sprintf("marker %q: %s", marker.Identifier, err) + pass.Report(analysis.Diagnostic{ + Pos: marker.Pos, + End: marker.End, + Message: message, + SuggestedFixes: fixes, + }) } // validateFieldTypeConstraint validates that a field's type matches the type constraint. @@ -393,7 +391,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { // Validate element constraint for arrays/slices if tc.ElementConstraint != nil && schemaType == SchemaTypeArray { - elemType := getElementType(t) + elemType := utils.UnwrapType(t) if elemType != nil { if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint); err != nil { return &invalidElementConstraintError{err: err} @@ -404,27 +402,13 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { return nil } -// extractIdent extracts an *ast.Ident from an ast.Expr, unwrapping pointers and arrays. -func extractIdent(expr ast.Expr) *ast.Ident { - switch e := expr.(type) { - case *ast.Ident: - return e - case *ast.StarExpr: - return extractIdent(e.X) - case *ast.ArrayType: - return extractIdent(e.Elt) - default: - return nil - } -} - func (a *analyzer) suggestMoveToField(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) []analysis.SuggestedFix { // Only suggest moving to field if FieldScope is allowed if !rule.Scope.Allows(FieldScope) { return nil } - fieldTypeSpecs := utils.LookupFieldsUsingType(pass, typeSpec) + fieldTypeSpecs := utils.LookupTypeSpecUsage(pass, typeSpec) var edits []analysis.TextEdit @@ -465,7 +449,7 @@ func (a *analyzer) suggestMoveToFieldsIfCompatible(pass *analysis.Pass, field *a } // Extract identifier from field type - ident := extractIdent(field.Type) + ident := utils.ExtractIdent(field.Type) if ident == nil { return nil } diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 4ae387e0..0086f818 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -15,12 +15,6 @@ limitations under the License. */ package markerscope -import ( - "maps" - - "sigs.k8s.io/kube-api-linter/pkg/markers" -) - // ScopeConstraint defines where a marker is allowed to be placed. type ScopeConstraint string @@ -126,475 +120,3 @@ type MarkerScopeConfig struct { // Policy determines whether to suggest fixes or just warn. Policy MarkerScopePolicy `json:"policy,omitempty"` } - -// DefaultMarkerRules returns the default marker scope rules with type constraints. -// These rules are based on kubebuilder markers and k8s declarative validation markers. -// -// Users can override these rules or add custom markers by providing a MarkerScopeConfig -// with MarkerRules that will be merged with (and take precedence over) these defaults. -// -// Note: This function currently covers validation and SSA markers with type and struct constraints. -// Markers from crd.go (e.g., resource, subresource) and pkg.go (e.g., groupName, versionName) -// are not included as they don't have type or struct constraints and are out of scope for -// this linter's current validation capabilities. -// -// ref: https://github.com/kubernetes-sigs/controller-tools/blob/v0.19.0/pkg/crd/markers/ -func DefaultMarkerRules() map[string]MarkerScopeRule { - rules := make(map[string]MarkerScopeRule) - - addFieldOnlyMarkers(rules) - addTypeOnlyMarkers(rules) - addFieldOrTypeMarkers(rules) - addNumericMarkers(rules) - addObjectMarkers(rules) - addStringMarkers(rules) - addArrayMarkers(rules) - addGeneralMarkers(rules) - addSSATopologyMarkers(rules) - addArrayItemsMarkers(rules) - - return rules -} - -// addFieldOnlyMarkers adds field-only markers based on controller-tools validation.go. -func addFieldOnlyMarkers(rules map[string]MarkerScopeRule) { - fieldOnlyMarkers := map[string]MarkerScopeRule{ - // Field-only markers (based on controller-tools validation.go) - markers.OptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.RequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.K8sOptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.K8sRequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.NullableMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.DefaultMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.KubebuilderDefaultMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.KubebuilderExampleMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.KubebuilderEmbeddedResourceMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.KubebuilderSchemaLessMarker: {Scope: FieldScope, TypeConstraint: nil}, - } - - maps.Copy(rules, fieldOnlyMarkers) -} - -// addTypeOnlyMarkers adds type-only markers for object-level validation and CRD generation. -func addTypeOnlyMarkers(rules map[string]MarkerScopeRule) { - typeOnlyMarkers := map[string]MarkerScopeRule{ - // Type-only markers (object-level validation and CRD generation) - markers.KubebuilderValidationItemsExactlyOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, - markers.KubebuilderValidationItemsAtMostOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, - markers.KubebuilderValidationItemsAtLeastOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, - } - - maps.Copy(rules, typeOnlyMarkers) -} - -// addFieldOrTypeMarkers adds markers that can be applied to both fields and types. -func addFieldOrTypeMarkers(rules map[string]MarkerScopeRule) { - fieldOrTypeMarkers := map[string]MarkerScopeRule{ - // field-or-type markers - markers.KubebuilderPruningPreserveUnknownFieldsMarker: {Scope: AnyScope, TypeConstraint: nil}, - markers.KubebuilderTitleMarker: {Scope: AnyScope, TypeConstraint: nil}, - } - - maps.Copy(rules, fieldOrTypeMarkers) -} - -// addNumericMarkers adds numeric validation markers for integer and number types. -func addNumericMarkers(rules map[string]MarkerScopeRule) { - numericMarkers := map[string]MarkerScopeRule{ - // numeric markers (field or type, integer or number types) - markers.KubebuilderMinimumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - markers.KubebuilderMaximumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - markers.KubebuilderExclusiveMaximumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - markers.KubebuilderExclusiveMinimumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - markers.KubebuilderMultipleOfMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - } - - maps.Copy(rules, numericMarkers) -} - -// addObjectMarkers adds object validation markers for struct and map types. -func addObjectMarkers(rules map[string]MarkerScopeRule) { - objectMarkers := map[string]MarkerScopeRule{ - // object markers (field or type, object types) - markers.KubebuilderMinPropertiesMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, - }, - }, - markers.KubebuilderMaxPropertiesMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, - }, - }, - } - - maps.Copy(rules, objectMarkers) -} - -// addStringMarkers adds string validation markers. -func addStringMarkers(rules map[string]MarkerScopeRule) { - stringMarkers := map[string]MarkerScopeRule{ - // string markers (field or type, string types) - markers.KubebuilderPatternMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, - }, - }, - markers.KubebuilderMinLengthMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, - }, - }, - markers.KubebuilderMaxLengthMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, - }, - }, - } - - maps.Copy(rules, stringMarkers) -} - -// addArrayMarkers adds array validation markers. -func addArrayMarkers(rules map[string]MarkerScopeRule) { - arrayMarkers := map[string]MarkerScopeRule{ - // array markers (field or type, array types) - markers.KubebuilderMinItemsMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, - }, - markers.KubebuilderMaxItemsMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, - }, - markers.KubebuilderUniqueItemsMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, - }, - } - - maps.Copy(rules, arrayMarkers) -} - -// addGeneralMarkers adds general markers that can apply to any type. -func addGeneralMarkers(rules map[string]MarkerScopeRule) { - generalMarkers := map[string]MarkerScopeRule{ - // general markers (field or type, any type) - markers.KubebuilderEnumMarker: { - Scope: AnyScope, - }, - markers.KubebuilderFormatMarker: { - Scope: AnyScope, - }, - markers.KubebuilderTypeMarker: { - Scope: AnyScope, - }, - markers.KubebuilderXValidationMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - }, - } - - maps.Copy(rules, generalMarkers) -} - -// addSSATopologyMarkers adds Server-Side Apply topology markers. -func addSSATopologyMarkers(rules map[string]MarkerScopeRule) { - ssaMarkers := map[string]MarkerScopeRule{ - // Server-Side Apply topology markers - markers.ListTypeMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, - }, - markers.ListMapKeyMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, - }, - markers.MapTypeMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, - }, - }, - markers.StructTypeMarker: { - Scope: AnyScope, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, - }, - }, - } - - maps.Copy(rules, ssaMarkers) -} - -// addArrayItemsMarkers adds array items markers that validate array elements. -// These validate the ELEMENTS of arrays, not the arrays themselves. -func addArrayItemsMarkers(rules map[string]MarkerScopeRule) { - addArrayItemsNumericMarkers(rules) - addArrayItemsStringMarkers(rules) - addArrayItemsArrayMarkers(rules) - addArrayItemsObjectMarkers(rules) - addArrayItemsGeneralMarkers(rules) -} - -// addArrayItemsNumericMarkers adds items markers for numeric array elements. -func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { - itemsNumericMarkers := map[string]MarkerScopeRule{ - markers.KubebuilderItemsMaximumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - }, - markers.KubebuilderItemsMinimumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - }, - markers.KubebuilderItemsExclusiveMaximumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - }, - markers.KubebuilderItemsExclusiveMinimumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - }, - markers.KubebuilderItemsMultipleOfMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, - }, - }, - }, - } - - maps.Copy(rules, itemsNumericMarkers) -} - -// addArrayItemsStringMarkers adds items markers for string array elements. -func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { - itemsStringMarkers := map[string]MarkerScopeRule{ - markers.KubebuilderItemsMinLengthMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, - }, - }, - }, - markers.KubebuilderItemsMaxLengthMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, - }, - }, - }, - markers.KubebuilderItemsPatternMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeString}, - }, - }, - }, - } - - maps.Copy(rules, itemsStringMarkers) -} - -// addArrayItemsArrayMarkers adds items markers for array-of-arrays. -func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { - itemsArrayMarkers := map[string]MarkerScopeRule{ - markers.KubebuilderItemsMaxItemsMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, - }, - }, - markers.KubebuilderItemsMinItemsMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, - }, - }, - markers.KubebuilderItemsUniqueItemsMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - }, - }, - }, - } - - maps.Copy(rules, itemsArrayMarkers) -} - -// addArrayItemsObjectMarkers adds items markers for arrays of objects. -func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { - itemsObjectMarkers := map[string]MarkerScopeRule{ - markers.KubebuilderItemsMinPropertiesMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, - }, - }, - }, - markers.KubebuilderItemsMaxPropertiesMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - ElementConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, - }, - }, - }, - } - - maps.Copy(rules, itemsObjectMarkers) -} - -// addArrayItemsGeneralMarkers adds general items markers that apply to any element type. -func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { - itemsGeneralMarkers := map[string]MarkerScopeRule{ - markers.KubebuilderItemsEnumMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - // Enum can apply to any element type - ElementConstraint: nil, - }, - }, - markers.KubebuilderItemsFormatMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - // Format can apply to various types - ElementConstraint: nil, - }, - }, - markers.KubebuilderItemsTypeMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - // Type marker can override any element type - ElementConstraint: nil, - }, - }, - markers.KubebuilderItemsXValidationMarker: { - Scope: AnyScope, - NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, - TypeConstraint: &TypeConstraint{ - AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, - // CEL validation can apply to any element type - ElementConstraint: nil, - }, - }, - } - - maps.Copy(rules, itemsGeneralMarkers) -} diff --git a/pkg/analysis/markerscope/errors.go b/pkg/analysis/markerscope/errors.go index 0ba3efbb..3dc6aedf 100644 --- a/pkg/analysis/markerscope/errors.go +++ b/pkg/analysis/markerscope/errors.go @@ -72,6 +72,12 @@ func (e *markerShouldBeOnTypeDefinitionError) Error() string { return fmt.Sprintf("marker should be declared on the type definition of %s instead of the field", e.typeName) } +// Is implements error matching for markerShouldBeOnTypeDefinitionError. +func (e *markerShouldBeOnTypeDefinitionError) Is(target error) bool { + _, ok := target.(*markerShouldBeOnTypeDefinitionError) + return ok +} + type typeNotAllowedError struct { schemaType SchemaType allowedTypes []SchemaType diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index 7b94be2f..c32afd1b 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -55,7 +55,7 @@ func validateConfig(cfg *MarkerScopeConfig, fldPath *field.Path) field.ErrorList fieldErrors = append(fieldErrors, validatePolicy(cfg.Policy, fldPath)...) // Get default marker rules for validation - defaultRules := DefaultMarkerRules() + defaultRules := defaultMarkerRules() // Validate override marker rules fieldErrors = append(fieldErrors, validateOverrideMarkers(cfg.OverrideMarkers, defaultRules, fldPath)...) diff --git a/pkg/analysis/markerscope/marker_rules.go b/pkg/analysis/markerscope/marker_rules.go new file mode 100644 index 00000000..542752b3 --- /dev/null +++ b/pkg/analysis/markerscope/marker_rules.go @@ -0,0 +1,494 @@ +/* +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 markerscope + +import ( + "maps" + + "sigs.k8s.io/kube-api-linter/pkg/markers" +) + +// defaultMarkerRules returns the default marker scope rules with type constraints. +// These rules are based on kubebuilder markers and k8s declarative validation markers. +// +// Users can override these rules or add custom markers by providing a MarkerScopeConfig +// with MarkerRules that will be merged with (and take precedence over) these defaults. +// +// Note: This function currently covers validation and SSA markers with type and struct constraints. +// Markers from crd.go (e.g., resource, subresource) and pkg.go (e.g., groupName, versionName) +// are not included as they don't have type or struct constraints and are out of scope for +// this linter's current validation capabilities. +// +// ref: https://github.com/kubernetes-sigs/controller-tools/blob/v0.19.0/pkg/crd/markers/ +func defaultMarkerRules() map[string]MarkerScopeRule { + rules := make(map[string]MarkerScopeRule) + + addFieldOnlyMarkers(rules) + addTypeOnlyMarkers(rules) + addFieldOrTypeMarkers(rules) + addNumericMarkers(rules) + addObjectMarkers(rules) + addStringMarkers(rules) + addArrayMarkers(rules) + addGeneralMarkers(rules) + addSSATopologyMarkers(rules) + addArrayItemsMarkers(rules) + + return rules +} + +// addFieldOnlyMarkers adds field-only markers based on controller-tools validation.go. +func addFieldOnlyMarkers(rules map[string]MarkerScopeRule) { + fieldOnlyMarkers := map[string]MarkerScopeRule{ + // Field-only markers (based on controller-tools validation.go) + markers.OptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.RequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.K8sOptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.K8sRequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.NullableMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.DefaultMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.KubebuilderDefaultMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.KubebuilderExampleMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.KubebuilderEmbeddedResourceMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.KubebuilderSchemaLessMarker: {Scope: FieldScope, TypeConstraint: nil}, + } + + maps.Copy(rules, fieldOnlyMarkers) +} + +// addTypeOnlyMarkers adds type-only markers for object-level validation and CRD generation. +func addTypeOnlyMarkers(rules map[string]MarkerScopeRule) { + typeOnlyMarkers := map[string]MarkerScopeRule{ + // Type-only markers (object-level validation and CRD generation) + markers.KubebuilderValidationItemsExactlyOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, + markers.KubebuilderValidationItemsAtMostOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, + markers.KubebuilderValidationItemsAtLeastOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, + } + + maps.Copy(rules, typeOnlyMarkers) +} + +// addFieldOrTypeMarkers adds markers that can be applied to both fields and types. +func addFieldOrTypeMarkers(rules map[string]MarkerScopeRule) { + fieldOrTypeMarkers := map[string]MarkerScopeRule{ + // field-or-type markers + markers.KubebuilderPruningPreserveUnknownFieldsMarker: {Scope: AnyScope, TypeConstraint: nil}, + markers.KubebuilderTitleMarker: {Scope: AnyScope, TypeConstraint: nil}, + } + + maps.Copy(rules, fieldOrTypeMarkers) +} + +// addNumericMarkers adds numeric validation markers for integer and number types. +func addNumericMarkers(rules map[string]MarkerScopeRule) { + numericMarkers := map[string]MarkerScopeRule{ + // numeric markers (field or type, integer or number types) + markers.KubebuilderMinimumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + markers.KubebuilderMaximumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + markers.KubebuilderExclusiveMaximumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + markers.KubebuilderExclusiveMinimumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + markers.KubebuilderMultipleOfMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + } + + maps.Copy(rules, numericMarkers) +} + +// addObjectMarkers adds object validation markers for struct and map types. +func addObjectMarkers(rules map[string]MarkerScopeRule) { + objectMarkers := map[string]MarkerScopeRule{ + // object markers (field or type, object types) + markers.KubebuilderMinPropertiesMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + markers.KubebuilderMaxPropertiesMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + } + + maps.Copy(rules, objectMarkers) +} + +// addStringMarkers adds string validation markers. +func addStringMarkers(rules map[string]MarkerScopeRule) { + stringMarkers := map[string]MarkerScopeRule{ + // string markers (field or type, string types) + markers.KubebuilderPatternMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + markers.KubebuilderMinLengthMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + markers.KubebuilderMaxLengthMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + } + + maps.Copy(rules, stringMarkers) +} + +// addArrayMarkers adds array validation markers. +func addArrayMarkers(rules map[string]MarkerScopeRule) { + arrayMarkers := map[string]MarkerScopeRule{ + // array markers (field or type, array types) + markers.KubebuilderMinItemsMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + markers.KubebuilderMaxItemsMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + markers.KubebuilderUniqueItemsMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + } + + maps.Copy(rules, arrayMarkers) +} + +// addGeneralMarkers adds general markers that can apply to any type. +func addGeneralMarkers(rules map[string]MarkerScopeRule) { + generalMarkers := map[string]MarkerScopeRule{ + // general markers (field or type, any type) + markers.KubebuilderEnumMarker: { + Scope: AnyScope, + }, + markers.KubebuilderFormatMarker: { + Scope: AnyScope, + }, + markers.KubebuilderTypeMarker: { + Scope: AnyScope, + }, + markers.KubebuilderXValidationMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + }, + } + + maps.Copy(rules, generalMarkers) +} + +// addSSATopologyMarkers adds Server-Side Apply topology markers. +func addSSATopologyMarkers(rules map[string]MarkerScopeRule) { + ssaMarkers := map[string]MarkerScopeRule{ + // Server-Side Apply topology markers + markers.ListTypeMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + markers.ListMapKeyMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + markers.MapTypeMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + markers.StructTypeMarker: { + Scope: AnyScope, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + } + + maps.Copy(rules, ssaMarkers) +} + +// addArrayItemsMarkers adds array items markers that validate array elements. +// These validate the ELEMENTS of arrays, not the arrays themselves. +func addArrayItemsMarkers(rules map[string]MarkerScopeRule) { + addArrayItemsNumericMarkers(rules) + addArrayItemsStringMarkers(rules) + addArrayItemsArrayMarkers(rules) + addArrayItemsObjectMarkers(rules) + addArrayItemsGeneralMarkers(rules) +} + +// addArrayItemsNumericMarkers adds items markers for numeric array elements. +func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { + itemsNumericMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsMaximumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + }, + markers.KubebuilderItemsMinimumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + }, + markers.KubebuilderItemsExclusiveMaximumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + }, + markers.KubebuilderItemsExclusiveMinimumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + }, + markers.KubebuilderItemsMultipleOfMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, + }, + }, + }, + } + + maps.Copy(rules, itemsNumericMarkers) +} + +// addArrayItemsStringMarkers adds items markers for string array elements. +func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { + itemsStringMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsMinLengthMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + }, + markers.KubebuilderItemsMaxLengthMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + }, + markers.KubebuilderItemsPatternMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeString}, + }, + }, + }, + } + + maps.Copy(rules, itemsStringMarkers) +} + +// addArrayItemsArrayMarkers adds items markers for array-of-arrays. +func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { + itemsArrayMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsMaxItemsMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + }, + markers.KubebuilderItemsMinItemsMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + }, + markers.KubebuilderItemsUniqueItemsMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + }, + }, + }, + } + + maps.Copy(rules, itemsArrayMarkers) +} + +// addArrayItemsObjectMarkers adds items markers for arrays of objects. +func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { + itemsObjectMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsMinPropertiesMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + }, + markers.KubebuilderItemsMaxPropertiesMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + ElementConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, + }, + }, + }, + } + + maps.Copy(rules, itemsObjectMarkers) +} + +// addArrayItemsGeneralMarkers adds general items markers that apply to any element type. +func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { + itemsGeneralMarkers := map[string]MarkerScopeRule{ + markers.KubebuilderItemsEnumMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + // Enum can apply to any element type + ElementConstraint: nil, + }, + }, + markers.KubebuilderItemsFormatMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + // Format can apply to various types + ElementConstraint: nil, + }, + }, + markers.KubebuilderItemsTypeMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + // Type marker can override any element type + ElementConstraint: nil, + }, + }, + markers.KubebuilderItemsXValidationMarker: { + Scope: AnyScope, + NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, + TypeConstraint: &TypeConstraint{ + AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, + // CEL validation can apply to any element type + ElementConstraint: nil, + }, + }, + } + + maps.Copy(rules, itemsGeneralMarkers) +} diff --git a/pkg/analysis/markerscope/schema.go b/pkg/analysis/markerscope/schema.go index 8902cb82..5cca80dd 100644 --- a/pkg/analysis/markerscope/schema.go +++ b/pkg/analysis/markerscope/schema.go @@ -15,7 +15,9 @@ limitations under the License. */ package markerscope -import "go/types" +import ( + "go/types" +) // SchemaType represents OpenAPI schema types that markers can target. type SchemaType string @@ -35,7 +37,15 @@ const ( // getSchemaType converts a Go type to its corresponding OpenAPI schema type. func getSchemaType(t types.Type) SchemaType { - t = unwrapType(t) + // Unwrap pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Unwrap named types to get underlying type + if named, ok := t.(*types.Named); ok { + t = named.Underlying() + } switch ut := t.Underlying().(type) { case *types.Basic: @@ -49,21 +59,6 @@ func getSchemaType(t types.Type) SchemaType { return "" } -// unwrapType unwraps pointer and named types to get the underlying type. -func unwrapType(t types.Type) types.Type { - // Unwrap pointer types - if ptr, ok := t.(*types.Pointer); ok { - t = ptr.Elem() - } - - // Unwrap named types to get underlying type - if named, ok := t.(*types.Named); ok { - t = named.Underlying() - } - - return t -} - // getBasicTypeSchema returns the schema type for a basic Go type. func getBasicTypeSchema(bt *types.Basic) SchemaType { switch bt.Kind() { @@ -74,8 +69,9 @@ func getBasicTypeSchema(bt *types.Basic) SchemaType { return SchemaTypeInteger case types.String: return SchemaTypeString - case types.Float32, types.Float64, types.Invalid, types.Uintptr, types.Complex64, types.Complex128, - types.UnsafePointer, types.UntypedBool, types.UntypedInt, types.UntypedRune, + case types.Invalid, types.Uintptr, types.Float32, types.Float64, + types.Complex64, types.Complex128, types.UnsafePointer, + types.UntypedBool, types.UntypedInt, types.UntypedRune, types.UntypedFloat, types.UntypedComplex, types.UntypedString, types.UntypedNil: // These types are not supported in OpenAPI schemas return "" @@ -83,25 +79,3 @@ func getBasicTypeSchema(bt *types.Basic) SchemaType { return "" } } - -// getElementType returns the element type of an array or slice. -func getElementType(t types.Type) types.Type { - // Unwrap pointer types - if ptr, ok := t.(*types.Pointer); ok { - t = ptr.Elem() - } - - // Unwrap named types - if named, ok := t.(*types.Named); ok { - t = named.Underlying() - } - - switch ut := t.Underlying().(type) { - case *types.Slice: - return ut.Elem() - case *types.Array: - return ut.Elem() - } - - return nil -} diff --git a/pkg/analysis/utils/utils.go b/pkg/analysis/utils/utils.go index 074af20b..b71ec752 100644 --- a/pkg/analysis/utils/utils.go +++ b/pkg/analysis/utils/utils.go @@ -451,8 +451,8 @@ func getFieldTypeName(field *ast.Field) string { return "" } -// LookupFieldsUsingType returns all fields in the package that use the given type. -func LookupFieldsUsingType(pass *analysis.Pass, typeSpec *ast.TypeSpec) []*ast.Field { +// LookupTypeSpecUsage returns all fields in the package that use the given type. +func LookupTypeSpecUsage(pass *analysis.Pass, typeSpec *ast.TypeSpec) []*ast.Field { var fields []*ast.Field // Get the type name @@ -491,3 +491,44 @@ func matchesType(expr ast.Expr, typeName string) bool { return false } } + +// UnwrapType unwraps pointer, named, slice, and array types to get the underlying element type. +// For pointer types, it returns the element type. +// For named types, it returns the underlying type. +// For slice and array types, it recursively unwraps to get the element type. +// Otherwise, it returns the type as-is. +func UnwrapType(t types.Type) types.Type { + // Unwrap pointer types + if ptr, ok := t.(*types.Pointer); ok { + t = ptr.Elem() + } + + // Unwrap named types to get underlying type + if named, ok := t.(*types.Named); ok { + t = named.Underlying() + } + + // Unwrap slice and array types to get element type + switch ut := t.Underlying().(type) { + case *types.Slice: + return ut.Elem() + case *types.Array: + return ut.Elem() + } + + return t +} + +// ExtractIdent extracts an *ast.Ident from an ast.Expr, unwrapping pointers and arrays. +func ExtractIdent(expr ast.Expr) *ast.Ident { + switch e := expr.(type) { + case *ast.Ident: + return e + case *ast.StarExpr: + return ExtractIdent(e.X) + case *ast.ArrayType: + return ExtractIdent(e.Elt) + default: + return nil + } +} From ada18fb0fb533e3421a947bc9a181c09ee90c072 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Thu, 30 Oct 2025 23:57:27 +0900 Subject: [PATCH 17/18] refactor(markerscope): simplify getBasicTypeSchema with nolint:exhaustive Signed-off-by: nayuta-ai --- pkg/analysis/markerscope/schema.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pkg/analysis/markerscope/schema.go b/pkg/analysis/markerscope/schema.go index 5cca80dd..b50195e9 100644 --- a/pkg/analysis/markerscope/schema.go +++ b/pkg/analysis/markerscope/schema.go @@ -61,6 +61,7 @@ func getSchemaType(t types.Type) SchemaType { // getBasicTypeSchema returns the schema type for a basic Go type. func getBasicTypeSchema(bt *types.Basic) SchemaType { + //nolint:exhaustive // Only supporting OpenAPI-compatible types switch bt.Kind() { case types.Bool: return SchemaTypeBoolean @@ -69,13 +70,8 @@ func getBasicTypeSchema(bt *types.Basic) SchemaType { return SchemaTypeInteger case types.String: return SchemaTypeString - case types.Invalid, types.Uintptr, types.Float32, types.Float64, - types.Complex64, types.Complex128, types.UnsafePointer, - types.UntypedBool, types.UntypedInt, types.UntypedRune, - types.UntypedFloat, types.UntypedComplex, types.UntypedString, types.UntypedNil: - // These types are not supported in OpenAPI schemas - return "" default: + // Other types (float, complex, unsafe, untyped) are not supported in OpenAPI schemas return "" } } From c5167b498a1624188b4278a4859ff314ca9741c1 Mon Sep 17 00:00:00 2001 From: nayuta-ai Date: Wed, 26 Nov 2025 00:53:37 +0900 Subject: [PATCH 18/18] refactor(markerscope): update scope handling to support multiple scopes for markers Signed-off-by: nayuta-ai --- docs/linters.md | 39 ++++--- pkg/analysis/markerscope/analyzer.go | 14 +-- pkg/analysis/markerscope/analyzer_test.go | 14 +-- pkg/analysis/markerscope/config.go | 19 ++-- pkg/analysis/markerscope/initializer.go | 16 +-- pkg/analysis/markerscope/initializer_test.go | 38 +++---- pkg/analysis/markerscope/marker_rules.go | 106 +++++++++---------- 7 files changed, 122 insertions(+), 124 deletions(-) diff --git a/docs/linters.md b/docs/linters.md index df356b51..86d39e20 100644 --- a/docs/linters.md +++ b/docs/linters.md @@ -399,13 +399,13 @@ The linter defines different scope types for markers: - **FieldScope**: Can only be applied to struct fields (e.g., `optional`, `required`, `nullable`) - **TypeScope**: Can only be applied to type definitions (e.g., `kubebuilder:validation:items:ExactlyOneOf`) -- **AnyScope**: Can be applied to either fields or type definitions (e.g., `kubebuilder:validation:Minimum`, `kubebuilder:validation:Pattern`) +- **Field and Type**: Markers that can be applied to both fields and type definitions (e.g., `kubebuilder:validation:Minimum`, `kubebuilder:validation:Pattern`) ### Type Constraints The linter validates that markers are applied to compatible OpenAPI schema types: -- **Numeric markers** (`Minimum`, `Maximum`, `MultipleOf`): Only for `integer` or `number` types +- **Numeric markers** (`Minimum`, `Maximum`, `MultipleOf`): Only for `integer` types - **String markers** (`Pattern`, `MinLength`, `MaxLength`): Only for `string` types - **Array markers** (`MinItems`, `MaxItems`, `UniqueItems`): Only for `array` types - **Object markers** (`MinProperties`, `MaxProperties`): Only for `object` types (struct/map) @@ -413,7 +413,6 @@ The linter validates that markers are applied to compatible OpenAPI schema types OpenAPI schema types map to Go types as follows: - `integer`: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 -- `number`: float32, float64 - `string`: string - `boolean`: bool - `array`: []T, [N]T (slices and arrays) @@ -421,7 +420,7 @@ OpenAPI schema types map to Go types as follows: #### Strict Type Constraints -For markers with `AnyScope` and type constraints, the `strictTypeConstraint` flag controls where the marker should be declared when used with named types: +For markers that can be applied to both fields and types with type constraints, the `strictTypeConstraint` flag controls where the marker should be declared when used with named types: - When `strictTypeConstraint` is `false` (default): The marker can be declared on either the field or the type definition. - When `strictTypeConstraint` is `true`: The marker must be declared on the type definition, not on fields using that type. @@ -461,13 +460,13 @@ The linter includes built-in rules for all standard kubebuilder markers and k8s - `kubebuilder:validation:items:AtMostOneOf` - `kubebuilder:validation:items:AtLeastOneOf` -**AnyScope markers with type constraints:** -- `kubebuilder:validation:Minimum` (integer/number types only) +**Field and Type markers with type constraints:** +- `kubebuilder:validation:Minimum` (integer types only) - `kubebuilder:validation:Pattern` (string types only) - `kubebuilder:validation:MinItems` (array types only) - `kubebuilder:validation:MinProperties` (object types only) -**AnyScope markers without type constraints:** +**Field and Type markers without type constraints:** - `kubebuilder:validation:Enum`, `kubebuilder:validation:Format` - `kubebuilder:pruning:PreserveUnknownFields`, `kubebuilder:title` @@ -476,18 +475,20 @@ The linter includes built-in rules for all standard kubebuilder markers and k8s You can customize marker rules or add support for custom markers. **Scope values:** -- `Field`: Marker can only be applied to struct fields -- `Type`: Marker can only be applied to type definitions -- `Any`: Marker can be applied to either fields or type definitions + +The `scopes` field accepts an array of scope constraints: +- `[Field]`: Marker can only be applied to struct fields +- `[Type]`: Marker can only be applied to type definitions +- `[Field, Type]`: Marker can be applied to both fields and type definitions **Type constraints:** -The `typeConstraint` field allows you to restrict which Go types a marker can be applied to. This ensures that markers are only used with compatible data types (e.g., numeric markers like `Minimum` are only applied to integer/number types). +The `typeConstraint` field allows you to restrict which Go types a marker can be applied to. This ensures that markers are only used with compatible data types (e.g., numeric markers like `Minimum` are only applied to integer types). **Type constraint fields:** -- `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `number`, `string`, `boolean`, `array`, `object`) +- `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `string`, `boolean`, `array`, `object`) - `elementConstraint`: Nested constraint for array element types (only valid when `allowedSchemaTypes` includes `array`) -- `strictTypeConstraint`: When `true`, markers with `AnyScope` and type constraints applied to fields using named types must be declared on the type definition instead of the field. Defaults to `false`. +- `strictTypeConstraint`: When `true`, markers that can be applied to both fields and types with type constraints applied to fields using named types must be declared on the type definition instead of the field. Defaults to `false`. **Configuration example:** @@ -495,31 +496,29 @@ The `typeConstraint` field allows you to restrict which Go types a marker can be lintersConfig: markerscope: policy: Warn | SuggestFix # The policy for marker scope violations. Defaults to `Warn`. - allowDangerousTypes: false # Allow dangerous number types (float32, float64). Defaults to `false`. # Override default rules for built-in markers overrideMarkers: - identifier: "optional" - scope: Field # or: Type, Any + scopes: [Field] # Can specify [Field], [Type], or [Field, Type] # Add rules for custom markers customMarkers: # Custom marker with scope constraint only - identifier: "mycompany:validation:CustomMarker" - scope: Any + scopes: [Field, Type] # Custom marker with scope and type constraints - identifier: "mycompany:validation:NumericLimit" - scope: Any + scopes: [Field, Type] strictTypeConstraint: true # Require declaration on type definition for named types typeConstraint: allowedSchemaTypes: - integer - - number # Custom array items marker with element type constraint - identifier: "mycompany:validation:items:StringFormat" - scope: Any + scopes: [Field, Type] typeConstraint: allowedSchemaTypes: - array @@ -541,7 +540,7 @@ When the `policy` is set to `SuggestFix`, the `markerscope` linter provides auto 2. **Type constraint violations**: For markers applied to incompatible types, the linter suggests removing the invalid marker. -3. **Named type violations**: For AnyScope markers with type constraints applied to fields using named types, the linter suggests moving the marker to the type definition if the underlying type is compatible with the marker's type constraints. +3. **Named type violations**: For markers that can be applied to both fields and types with type constraints applied to fields using named types, the linter suggests moving the marker to the type definition if the underlying type is compatible with the marker's type constraints. When the `policy` is set to `Warn`, violations are reported as warnings without suggesting fixes. diff --git a/pkg/analysis/markerscope/analyzer.go b/pkg/analysis/markerscope/analyzer.go index 7b689b45..21253da8 100644 --- a/pkg/analysis/markerscope/analyzer.go +++ b/pkg/analysis/markerscope/analyzer.go @@ -173,7 +173,7 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark } // Check if FieldScope is allowed - if !rule.Scope.Allows(FieldScope) { + if !rule.AllowsScope(FieldScope) { a.reportFieldScopeViolation(pass, field, marker, rule) continue } @@ -196,7 +196,7 @@ func (a *analyzer) checkTypeSpecMarkers(pass *analysis.Pass, typeSpec *ast.TypeS } // Check if TypeScope is allowed - if !rule.Scope.Allows(TypeScope) { + if !rule.AllowsScope(TypeScope) { a.reportTypeScopeViolation(pass, typeSpec, marker, rule) continue } @@ -212,7 +212,7 @@ func (a *analyzer) reportFieldScopeViolation(pass *analysis.Pass, field *ast.Fie var fixes []analysis.SuggestedFix - if rule.Scope == TypeScope { + if rule.AllowsScope(TypeScope) { message = fmt.Sprintf("marker %q can only be applied to types", marker.Identifier) if a.policy == MarkerScopePolicySuggestFix { @@ -276,7 +276,7 @@ func (a *analyzer) reportTypeScopeViolation(pass *analysis.Pass, typeSpec *ast.T var fixes []analysis.SuggestedFix message := fmt.Sprintf("marker %q cannot be applied to types", marker.Identifier) - if rule.Scope == FieldScope { + if rule.AllowsScope(FieldScope) { message = fmt.Sprintf("marker %q can only be applied to fields", marker.Identifier) if a.policy == MarkerScopePolicySuggestFix { @@ -347,7 +347,7 @@ func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.F } // Check if the marker should be on the type definition instead of the field - if rule.NamedTypeConstraint == NamedTypeConstraintRequireTypeDefinition && rule.Scope == AnyScope { + if rule.NamedTypeConstraint == NamedTypeConstraintRequireTypeDefinition && rule.AllowsScope(TypeScope) { namedType, ok := tv.Type.(*types.Named) if ok { return &markerShouldBeOnTypeDefinitionError{typeName: namedType.Obj().Name()} @@ -404,7 +404,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error { func (a *analyzer) suggestMoveToField(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) []analysis.SuggestedFix { // Only suggest moving to field if FieldScope is allowed - if !rule.Scope.Allows(FieldScope) { + if !rule.AllowsScope(FieldScope) { return nil } @@ -444,7 +444,7 @@ func (a *analyzer) suggestMoveToField(pass *analysis.Pass, typeSpec *ast.TypeSpe // suggestMoveToFieldsIfCompatible generates suggested fixes to move a marker from type to compatible fields. func (a *analyzer) suggestMoveToFieldsIfCompatible(pass *analysis.Pass, field *ast.Field, marker markershelper.Marker, rule MarkerScopeRule) []analysis.SuggestedFix { // Only suggest moving to type if TypeScope is allowed - if !rule.Scope.Allows(TypeScope) { + if !rule.AllowsScope(TypeScope) { return nil } diff --git a/pkg/analysis/markerscope/analyzer_test.go b/pkg/analysis/markerscope/analyzer_test.go index 1bbc68df..3e86d937 100644 --- a/pkg/analysis/markerscope/analyzer_test.go +++ b/pkg/analysis/markerscope/analyzer_test.go @@ -58,29 +58,29 @@ func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) { // Override built-in "optional" to allow on types (default is FieldScope only) { Identifier: "optional", - Scope: markerscope.AnyScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope}, }, // Override built-in "required" to allow on types (default is FieldScope only) { Identifier: "required", - Scope: markerscope.AnyScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope}, }, }, CustomMarkers: []markerscope.MarkerScopeRule{ // Custom field-only marker { Identifier: "custom:field-only", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, }, // Custom type-only marker { Identifier: "custom:type-only", - Scope: markerscope.TypeScope, + Scopes: []markerscope.ScopeConstraint{markerscope.TypeScope}, }, // Custom marker with string type constraint { Identifier: "custom:string-only", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{ markerscope.SchemaTypeString, @@ -90,7 +90,7 @@ func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) { // Custom marker with integer type constraint { Identifier: "custom:integer-only", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{ markerscope.SchemaTypeInteger, @@ -100,7 +100,7 @@ func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) { // Custom marker with array of strings constraint { Identifier: "custom:string-array", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{ markerscope.SchemaTypeArray, diff --git a/pkg/analysis/markerscope/config.go b/pkg/analysis/markerscope/config.go index 0086f818..3961e39c 100644 --- a/pkg/analysis/markerscope/config.go +++ b/pkg/analysis/markerscope/config.go @@ -15,6 +15,8 @@ limitations under the License. */ package markerscope +import "slices" + // ScopeConstraint defines where a marker is allowed to be placed. type ScopeConstraint string @@ -23,17 +25,11 @@ const ( FieldScope ScopeConstraint = "Field" // TypeScope indicates the marker can be placed on type definitions. TypeScope ScopeConstraint = "Type" - // AnyScope indicates the marker can be placed on either fields or types. - AnyScope ScopeConstraint = "Any" ) -// Allows checks if the given scope is allowed by this constraint. -func (s ScopeConstraint) Allows(scope ScopeConstraint) bool { - if s == AnyScope { - return true - } - - return s == scope +// AllowsScope checks if the given scope is allowed by this rule. +func (r MarkerScopeRule) AllowsScope(scope ScopeConstraint) bool { + return slices.Contains(r.Scopes, scope) } // TypeConstraint defines what types a marker can be applied to. @@ -68,8 +64,9 @@ type MarkerScopeRule struct { // Identifier is the marker identifier (e.g., "optional", "kubebuilder:validation:Minimum"). Identifier string `json:"identifier,omitempty"` - // Scope specifies where the marker can be placed (field vs type). - Scope ScopeConstraint + // Scopes specifies where the marker can be placed (field, type, or both). + // Can contain FieldScope, TypeScope, or both for markers that can be placed anywhere. + Scopes []ScopeConstraint `json:"scopes,omitempty"` // NamedTypeConstraint specifies how markers should be applied to named types. // When a field uses a named type (e.g., type CustomInt int32), this determines diff --git a/pkg/analysis/markerscope/initializer.go b/pkg/analysis/markerscope/initializer.go index c32afd1b..fa48b90d 100644 --- a/pkg/analysis/markerscope/initializer.go +++ b/pkg/analysis/markerscope/initializer.go @@ -137,16 +137,18 @@ func validateCustomMarkers(rules []MarkerScopeRule, defaultRules map[string]Mark func validateMarkerRule(rule MarkerScopeRule) error { // Validate scope constraint - if rule.Scope == "" { + if len(rule.Scopes) == 0 { return errScopeRequired } - // Validate that scope is a valid value - switch rule.Scope { - case FieldScope, TypeScope, AnyScope: - // Valid scope - default: - return &invalidScopeConstraintError{scope: string(rule.Scope)} + // Validate that each scope is a valid value + for _, scope := range rule.Scopes { + switch scope { + case FieldScope, TypeScope: + // Valid scope + default: + return &invalidScopeConstraintError{scope: string(scope)} + } } // Validate named type constraint if present diff --git a/pkg/analysis/markerscope/initializer_test.go b/pkg/analysis/markerscope/initializer_test.go index 2054eb8f..cb5572c9 100644 --- a/pkg/analysis/markerscope/initializer_test.go +++ b/pkg/analysis/markerscope/initializer_test.go @@ -81,7 +81,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:marker", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, }, }, }, @@ -93,7 +93,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:marker", - Scope: "", + Scopes: []markerscope.ScopeConstraint{}, }, }, }, @@ -105,7 +105,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:marker", - Scope: "invalid", + Scopes: []markerscope.ScopeConstraint{"invalid"}, }, }, }, @@ -117,7 +117,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:marker", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{"invalid-type"}, }, @@ -132,7 +132,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:string-marker", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeString}, }, @@ -147,7 +147,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:integer-marker", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeInteger}, }, @@ -162,7 +162,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:numeric-marker", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{ markerscope.SchemaTypeInteger, @@ -179,7 +179,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:string-array", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeArray}, ElementConstraint: &markerscope.TypeConstraint{ @@ -197,7 +197,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:invalid-array", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeArray}, ElementConstraint: &markerscope.TypeConstraint{ @@ -215,7 +215,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:flexible-marker", - Scope: markerscope.AnyScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope}, }, }, }, @@ -227,7 +227,7 @@ var _ = Describe("markerscope initializer", func() { OverrideMarkers: []markerscope.MarkerScopeRule{ { Identifier: "optional", - Scope: markerscope.AnyScope, // Override default FieldScope + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope}, // Override default [FieldScope] }, }, }, @@ -239,7 +239,7 @@ var _ = Describe("markerscope initializer", func() { OverrideMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:nonexistent", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, }, }, }, @@ -251,7 +251,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "optional", // Built-in marker - Scope: markerscope.AnyScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope}, }, }, }, @@ -263,13 +263,13 @@ var _ = Describe("markerscope initializer", func() { OverrideMarkers: []markerscope.MarkerScopeRule{ { Identifier: "optional", - Scope: markerscope.AnyScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope}, }, }, CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:marker", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, }, }, }, @@ -299,7 +299,7 @@ var _ = Describe("markerscope initializer", func() { CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:marker", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, }, }, } @@ -314,7 +314,7 @@ var _ = Describe("markerscope initializer", func() { OverrideMarkers: []markerscope.MarkerScopeRule{ { Identifier: "optional", - Scope: markerscope.AnyScope, // Override default FieldScope + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope}, // Override default [FieldScope] }, }, } @@ -329,13 +329,13 @@ var _ = Describe("markerscope initializer", func() { OverrideMarkers: []markerscope.MarkerScopeRule{ { Identifier: "optional", - Scope: markerscope.AnyScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope}, }, }, CustomMarkers: []markerscope.MarkerScopeRule{ { Identifier: "custom:validation:MyMarker", - Scope: markerscope.FieldScope, + Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope}, TypeConstraint: &markerscope.TypeConstraint{ AllowedSchemaTypes: []markerscope.SchemaType{markerscope.SchemaTypeString}, }, diff --git a/pkg/analysis/markerscope/marker_rules.go b/pkg/analysis/markerscope/marker_rules.go index 542752b3..03bb892d 100644 --- a/pkg/analysis/markerscope/marker_rules.go +++ b/pkg/analysis/markerscope/marker_rules.go @@ -54,16 +54,16 @@ func defaultMarkerRules() map[string]MarkerScopeRule { func addFieldOnlyMarkers(rules map[string]MarkerScopeRule) { fieldOnlyMarkers := map[string]MarkerScopeRule{ // Field-only markers (based on controller-tools validation.go) - markers.OptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.RequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.K8sOptionalMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.K8sRequiredMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.NullableMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.DefaultMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.KubebuilderDefaultMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.KubebuilderExampleMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.KubebuilderEmbeddedResourceMarker: {Scope: FieldScope, TypeConstraint: nil}, - markers.KubebuilderSchemaLessMarker: {Scope: FieldScope, TypeConstraint: nil}, + markers.OptionalMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.RequiredMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.K8sOptionalMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.K8sRequiredMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.NullableMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.DefaultMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.KubebuilderDefaultMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.KubebuilderExampleMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.KubebuilderEmbeddedResourceMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, + markers.KubebuilderSchemaLessMarker: {Scopes: []ScopeConstraint{FieldScope}, TypeConstraint: nil}, } maps.Copy(rules, fieldOnlyMarkers) @@ -73,9 +73,9 @@ func addFieldOnlyMarkers(rules map[string]MarkerScopeRule) { func addTypeOnlyMarkers(rules map[string]MarkerScopeRule) { typeOnlyMarkers := map[string]MarkerScopeRule{ // Type-only markers (object-level validation and CRD generation) - markers.KubebuilderValidationItemsExactlyOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, - markers.KubebuilderValidationItemsAtMostOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, - markers.KubebuilderValidationItemsAtLeastOneOfMarker: {Scope: TypeScope, TypeConstraint: nil}, + markers.KubebuilderValidationItemsExactlyOneOfMarker: {Scopes: []ScopeConstraint{TypeScope}, TypeConstraint: nil}, + markers.KubebuilderValidationItemsAtMostOneOfMarker: {Scopes: []ScopeConstraint{TypeScope}, TypeConstraint: nil}, + markers.KubebuilderValidationItemsAtLeastOneOfMarker: {Scopes: []ScopeConstraint{TypeScope}, TypeConstraint: nil}, } maps.Copy(rules, typeOnlyMarkers) @@ -85,8 +85,8 @@ func addTypeOnlyMarkers(rules map[string]MarkerScopeRule) { func addFieldOrTypeMarkers(rules map[string]MarkerScopeRule) { fieldOrTypeMarkers := map[string]MarkerScopeRule{ // field-or-type markers - markers.KubebuilderPruningPreserveUnknownFieldsMarker: {Scope: AnyScope, TypeConstraint: nil}, - markers.KubebuilderTitleMarker: {Scope: AnyScope, TypeConstraint: nil}, + markers.KubebuilderPruningPreserveUnknownFieldsMarker: {Scopes: []ScopeConstraint{FieldScope, TypeScope}, TypeConstraint: nil}, + markers.KubebuilderTitleMarker: {Scopes: []ScopeConstraint{FieldScope, TypeScope}, TypeConstraint: nil}, } maps.Copy(rules, fieldOrTypeMarkers) @@ -97,35 +97,35 @@ func addNumericMarkers(rules map[string]MarkerScopeRule) { numericMarkers := map[string]MarkerScopeRule{ // numeric markers (field or type, integer or number types) markers.KubebuilderMinimumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderMaximumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderExclusiveMaximumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderExclusiveMinimumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, }, }, markers.KubebuilderMultipleOfMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeInteger}, @@ -141,14 +141,14 @@ func addObjectMarkers(rules map[string]MarkerScopeRule) { objectMarkers := map[string]MarkerScopeRule{ // object markers (field or type, object types) markers.KubebuilderMinPropertiesMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, }, markers.KubebuilderMaxPropertiesMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, @@ -164,21 +164,21 @@ func addStringMarkers(rules map[string]MarkerScopeRule) { stringMarkers := map[string]MarkerScopeRule{ // string markers (field or type, string types) markers.KubebuilderPatternMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, markers.KubebuilderMinLengthMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, }, }, markers.KubebuilderMaxLengthMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeString}, @@ -194,21 +194,21 @@ func addArrayMarkers(rules map[string]MarkerScopeRule) { arrayMarkers := map[string]MarkerScopeRule{ // array markers (field or type, array types) markers.KubebuilderMinItemsMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderMaxItemsMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.KubebuilderUniqueItemsMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -224,16 +224,16 @@ func addGeneralMarkers(rules map[string]MarkerScopeRule) { generalMarkers := map[string]MarkerScopeRule{ // general markers (field or type, any type) markers.KubebuilderEnumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, }, markers.KubebuilderFormatMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, }, markers.KubebuilderTypeMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, }, markers.KubebuilderXValidationMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, }, } @@ -246,28 +246,28 @@ func addSSATopologyMarkers(rules map[string]MarkerScopeRule) { ssaMarkers := map[string]MarkerScopeRule{ // Server-Side Apply topology markers markers.ListTypeMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.ListMapKeyMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, }, }, markers.MapTypeMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, }, markers.StructTypeMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeObject}, }, @@ -291,7 +291,7 @@ func addArrayItemsMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { itemsNumericMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMaximumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -301,7 +301,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMinimumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -311,7 +311,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsExclusiveMaximumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -321,7 +321,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsExclusiveMinimumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -331,7 +331,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMultipleOfMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -349,7 +349,7 @@ func addArrayItemsNumericMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { itemsStringMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMinLengthMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -359,7 +359,7 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMaxLengthMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -369,7 +369,7 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsPatternMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -387,7 +387,7 @@ func addArrayItemsStringMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { itemsArrayMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMaxItemsMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -397,7 +397,7 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMinItemsMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -407,7 +407,7 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsUniqueItemsMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -425,7 +425,7 @@ func addArrayItemsArrayMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { itemsObjectMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsMinPropertiesMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -435,7 +435,7 @@ func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsMaxPropertiesMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -453,7 +453,7 @@ func addArrayItemsObjectMarkers(rules map[string]MarkerScopeRule) { func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { itemsGeneralMarkers := map[string]MarkerScopeRule{ markers.KubebuilderItemsEnumMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -462,7 +462,7 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsFormatMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -471,7 +471,7 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsTypeMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray}, @@ -480,7 +480,7 @@ func addArrayItemsGeneralMarkers(rules map[string]MarkerScopeRule) { }, }, markers.KubebuilderItemsXValidationMarker: { - Scope: AnyScope, + Scopes: []ScopeConstraint{FieldScope, TypeScope}, NamedTypeConstraint: NamedTypeConstraintRequireTypeDefinition, TypeConstraint: &TypeConstraint{ AllowedSchemaTypes: []SchemaType{SchemaTypeArray},