Skip to content

Commit c2d07a1

Browse files
committed
Add TypeViolation
Signed-off-by: nayuta-ai <nayuta723@gmail.com>
1 parent c905a92 commit c2d07a1

File tree

3 files changed

+204
-29
lines changed

3 files changed

+204
-29
lines changed

pkg/analysis/markerscope/analyzer.go

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package markerscope
1818
import (
1919
"fmt"
2020
"go/ast"
21+
"go/types"
2122
"strings"
2223

2324
"golang.org/x/tools/go/analysis"
@@ -144,8 +145,19 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark
144145
// Check if FieldScope is allowed
145146
if !rule.Scope.Allows(FieldScope) {
146147
a.reportScopeViolation(pass, marker, rule)
148+
continue
149+
}
150+
151+
// Check type constraints if present
152+
if rule.TypeConstraint != nil {
153+
if err := a.validateFieldTypeConstraint(pass, field, rule.TypeConstraint); err != nil {
154+
pass.Report(analysis.Diagnostic{
155+
Pos: marker.Pos,
156+
End: marker.End,
157+
Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err),
158+
})
159+
}
147160
}
148-
// TODO: Add type constraint validation here
149161
}
150162
}
151163

@@ -173,9 +185,138 @@ func (a *analyzer) checkTypeMarkers(pass *analysis.Pass, genDecl *ast.GenDecl, m
173185
// Check if TypeScope is allowed
174186
if !rule.Scope.Allows(TypeScope) {
175187
a.reportScopeViolation(pass, marker, rule)
188+
continue
189+
}
190+
191+
// Check type constraints if present
192+
if rule.TypeConstraint != nil {
193+
if err := a.validateTypeSpecTypeConstraint(pass, typeSpec, rule.TypeConstraint); err != nil {
194+
pass.Report(analysis.Diagnostic{
195+
Pos: marker.Pos,
196+
End: marker.End,
197+
Message: fmt.Sprintf("marker %q: %s", marker.Identifier, err),
198+
})
199+
}
176200
}
177-
// TODO: Add type constraint validation here
178201
}
179202
}
180203
}
181204

205+
// validateFieldTypeConstraint validates that a field's type matches the type constraint
206+
func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.Field, tc *TypeConstraint) error {
207+
// Get the type of the field
208+
tv, ok := pass.TypesInfo.Types[field.Type]
209+
if !ok {
210+
return nil // Skip if we can't determine the type
211+
}
212+
213+
return validateTypeAgainstConstraint(tv.Type, tc)
214+
}
215+
216+
// validateTypeSpecTypeConstraint validates that a type spec's type matches the type constraint
217+
func (a *analyzer) validateTypeSpecTypeConstraint(pass *analysis.Pass, typeSpec *ast.TypeSpec, tc *TypeConstraint) error {
218+
// Get the type of the type spec
219+
obj := pass.TypesInfo.Defs[typeSpec.Name]
220+
if obj == nil {
221+
return nil // Skip if we can't determine the type
222+
}
223+
224+
typeName, ok := obj.(*types.TypeName)
225+
if !ok {
226+
return nil
227+
}
228+
229+
return validateTypeAgainstConstraint(typeName.Type(), tc)
230+
}
231+
232+
// validateTypeAgainstConstraint validates that a Go type satisfies the type constraint
233+
func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error {
234+
if tc == nil {
235+
return nil
236+
}
237+
238+
// Get the schema type from the Go type
239+
schemaType := getSchemaType(t)
240+
241+
// Check if the schema type is allowed
242+
if len(tc.AllowedSchemaTypes) > 0 {
243+
allowed := false
244+
for _, allowedType := range tc.AllowedSchemaTypes {
245+
if schemaType == allowedType {
246+
allowed = true
247+
break
248+
}
249+
}
250+
if !allowed {
251+
return fmt.Errorf("type %s is not allowed (expected one of: %v)", schemaType, tc.AllowedSchemaTypes)
252+
}
253+
}
254+
255+
// Validate element constraint for arrays/slices
256+
if tc.ElementConstraint != nil && schemaType == SchemaTypeArray {
257+
elemType := getElementType(t)
258+
if elemType != nil {
259+
if err := validateTypeAgainstConstraint(elemType, tc.ElementConstraint); err != nil {
260+
return fmt.Errorf("array element: %w", err)
261+
}
262+
}
263+
}
264+
265+
return nil
266+
}
267+
268+
// getSchemaType converts a Go type to an OpenAPI schema type
269+
func getSchemaType(t types.Type) SchemaType {
270+
// Unwrap pointer types
271+
if ptr, ok := t.(*types.Pointer); ok {
272+
t = ptr.Elem()
273+
}
274+
275+
// Unwrap named types to get underlying type
276+
if named, ok := t.(*types.Named); ok {
277+
t = named.Underlying()
278+
}
279+
280+
switch ut := t.Underlying().(type) {
281+
case *types.Basic:
282+
switch ut.Kind() {
283+
case types.Bool:
284+
return SchemaTypeBoolean
285+
case types.Int, types.Int8, types.Int16, types.Int32, types.Int64,
286+
types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64:
287+
return SchemaTypeInteger
288+
case types.Float32, types.Float64:
289+
return SchemaTypeNumber
290+
case types.String:
291+
return SchemaTypeString
292+
}
293+
case *types.Slice, *types.Array:
294+
return SchemaTypeArray
295+
case *types.Map, *types.Struct:
296+
return SchemaTypeObject
297+
}
298+
299+
return ""
300+
}
301+
302+
// getElementType returns the element type of an array or slice
303+
func getElementType(t types.Type) types.Type {
304+
// Unwrap pointer types
305+
if ptr, ok := t.(*types.Pointer); ok {
306+
t = ptr.Elem()
307+
}
308+
309+
// Unwrap named types to get underlying type
310+
if named, ok := t.(*types.Named); ok {
311+
t = named.Underlying()
312+
}
313+
314+
switch ut := t.(type) {
315+
case *types.Slice:
316+
return ut.Elem()
317+
case *types.Array:
318+
return ut.Elem()
319+
}
320+
321+
return nil
322+
}

pkg/analysis/markerscope/config.go

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,6 @@ type MarkerScopeRule struct {
9595
TypeConstraint *TypeConstraint
9696
}
9797

98-
// MarkerScope defines where a marker is allowed to be placed (legacy).
99-
// Deprecated: Use MarkerScopeRule with ScopeConstraint instead.
100-
type MarkerScope string
101-
102-
const (
103-
// ScopeField indicates the marker can only be placed on fields.
104-
ScopeField MarkerScope = "field"
105-
106-
// ScopeType indicates the marker can only be placed on type definitions.
107-
ScopeType MarkerScope = "type"
108-
109-
// ScopeFieldOrType indicates the marker can be placed on either fields or types.
110-
ScopeFieldOrType MarkerScope = "field_or_type"
111-
112-
// ScopeTypeOrObjectField indicates the marker can be placed on type definitions or object fields (struct/map).
113-
ScopeTypeOrObjectField MarkerScope = "type_or_object_field"
114-
)
115-
11698
// MarkerScopePolicy defines how the linter should handle violations.
11799
type MarkerScopePolicy string
118100

@@ -175,13 +157,13 @@ func DefaultMarkerRules() map[string]MarkerScopeRule {
175157
markers.KubebuilderExclusiveMaximumMarker: {
176158
Scope: AnyScope,
177159
TypeConstraint: &TypeConstraint{
178-
AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean},
160+
AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber},
179161
},
180162
},
181163
markers.KubebuilderExclusiveMinimumMarker: {
182164
Scope: AnyScope,
183165
TypeConstraint: &TypeConstraint{
184-
AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean},
166+
AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber},
185167
},
186168
},
187169
markers.KubebuilderMultipleOfMarker: {
@@ -354,7 +336,7 @@ func DefaultMarkerRules() map[string]MarkerScopeRule {
354336
TypeConstraint: &TypeConstraint{
355337
AllowedSchemaTypes: []SchemaType{SchemaTypeArray},
356338
ElementConstraint: &TypeConstraint{
357-
AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean},
339+
AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber},
358340
},
359341
},
360342
},
@@ -363,7 +345,7 @@ func DefaultMarkerRules() map[string]MarkerScopeRule {
363345
TypeConstraint: &TypeConstraint{
364346
AllowedSchemaTypes: []SchemaType{SchemaTypeArray},
365347
ElementConstraint: &TypeConstraint{
366-
AllowedSchemaTypes: []SchemaType{SchemaTypeBoolean},
348+
AllowedSchemaTypes: []SchemaType{SchemaTypeInteger, SchemaTypeNumber},
367349
},
368350
},
369351
},

pkg/analysis/markerscope/testdata/src/a/a.go

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ type AnyScopeOnFieldTest struct {
8989
type NumericType int32
9090

9191
type NumericMarkersFieldTest struct {
92+
// Valid: numeric markers on numeric types
9293
// +kubebuilder:validation:Minimum=0
9394
// +kubebuilder:validation:Maximum=100
9495
// +kubebuilder:validation:ExclusiveMinimum=false
@@ -99,6 +100,14 @@ type NumericMarkersFieldTest struct {
99100
// +kubebuilder:validation:Minimum=0.0
100101
// +kubebuilder:validation:Maximum=1.0
101102
ValidFloatField float64 `json:"validFloatField"`
103+
104+
// Invalid: numeric marker on string field
105+
// +kubebuilder:validation:Minimum=0 // want `marker "kubebuilder:validation:Minimum": type string is not allowed \(expected one of: \[integer number\]\)`
106+
InvalidMinimumOnString string `json:"invalidMinimumOnString"`
107+
108+
// Invalid: numeric marker on bool field
109+
// +kubebuilder:validation:Maximum=100 // want `marker "kubebuilder:validation:Maximum": type boolean is not allowed \(expected one of: \[integer number\]\)`
110+
InvalidMaximumOnBool bool `json:"invalidMaximumOnBool"`
102111
}
103112

104113
// ============================================================================
@@ -111,10 +120,19 @@ type NumericMarkersFieldTest struct {
111120
type StringType string
112121

113122
type StringMarkersFieldTest struct {
123+
// Valid: string markers on string field
114124
// +kubebuilder:validation:Pattern="^[a-z]+$"
115125
// +kubebuilder:validation:MinLength=1
116126
// +kubebuilder:validation:MaxLength=100
117127
ValidStringField string `json:"validStringField"`
128+
129+
// Invalid: string marker on int field
130+
// +kubebuilder:validation:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:Pattern": type integer is not allowed \(expected one of: \[string\]\)`
131+
InvalidPatternOnInt int32 `json:"invalidPatternOnInt"`
132+
133+
// Invalid: string marker on array field
134+
// +kubebuilder:validation:MinLength=5 // want `marker "kubebuilder:validation:MinLength": type array is not allowed \(expected one of: \[string\]\)`
135+
InvalidMinLengthOnArray []string `json:"invalidMinLengthOnArray"`
118136
}
119137

120138
// ============================================================================
@@ -127,10 +145,19 @@ type StringMarkersFieldTest struct {
127145
type StringArray []string
128146

129147
type ArrayMarkersFieldTest struct {
148+
// Valid: array markers on array field
130149
// +kubebuilder:validation:MinItems=1
131150
// +kubebuilder:validation:MaxItems=10
132151
// +kubebuilder:validation:UniqueItems=true
133152
ValidArrayField []string `json:"validArrayField"`
153+
154+
// Invalid: array marker on string field
155+
// +kubebuilder:validation:MinItems=1 // want `marker "kubebuilder:validation:MinItems": type string is not allowed \(expected one of: \[array\]\)`
156+
InvalidMinItemsOnString string `json:"invalidMinItemsOnString"`
157+
158+
// Invalid: array marker on object field
159+
// +kubebuilder:validation:MaxItems=10 // want `marker "kubebuilder:validation:MaxItems": type object is not allowed \(expected one of: \[array\]\)`
160+
InvalidMaxItemsOnObject map[string]string `json:"invalidMaxItemsOnObject"`
134161
}
135162

136163
// ============================================================================
@@ -145,9 +172,18 @@ type ObjectType struct {
145172
}
146173

147174
type ObjectMarkersFieldTest struct {
175+
// Valid: object markers on map field
148176
// +kubebuilder:validation:MinProperties=1
149177
// +kubebuilder:validation:MaxProperties=10
150178
ValidObjectField map[string]string `json:"validObjectField"`
179+
180+
// Invalid: object marker on string field
181+
// +kubebuilder:validation:MinProperties=2 // want `marker "kubebuilder:validation:MinProperties": type string is not allowed \(expected one of: \[object\]\)`
182+
InvalidMinPropertiesOnString string `json:"invalidMinPropertiesOnString"`
183+
184+
// Invalid: object marker on array field
185+
// +kubebuilder:validation:MaxProperties=5 // want `marker "kubebuilder:validation:MaxProperties": type array is not allowed \(expected one of: \[object\]\)`
186+
InvalidMaxPropertiesOnArray []string `json:"invalidMaxPropertiesOnArray"`
151187
}
152188

153189
// ============================================================================
@@ -228,35 +264,51 @@ type NestedArrayType [][]string
228264
type ObjectArrayType []map[string]string
229265

230266
type ArrayItemsMarkersFieldTest struct {
231-
// Numeric element constraints
267+
// Valid: Numeric element constraints
232268
// +kubebuilder:validation:items:Maximum=100
233269
// +kubebuilder:validation:items:Minimum=0
234270
// +kubebuilder:validation:items:MultipleOf=5
235271
// +kubebuilder:validation:items:ExclusiveMaximum=false
236272
// +kubebuilder:validation:items:ExclusiveMinimum=false
237273
ValidNumericArrayItems []int32 `json:"validNumericArrayItems"`
238274

239-
// String element constraints
275+
// Valid: String element constraints
240276
// +kubebuilder:validation:items:Pattern="^[a-z]+$"
241277
// +kubebuilder:validation:items:MinLength=1
242278
// +kubebuilder:validation:items:MaxLength=50
243279
ValidStringArrayItems []string `json:"validStringArrayItems"`
244280

245-
// Nested array constraints
281+
// Valid: Nested array constraints
246282
// +kubebuilder:validation:items:MinItems=1
247283
// +kubebuilder:validation:items:MaxItems=5
248284
// +kubebuilder:validation:items:UniqueItems=true
249285
ValidNestedArrayItems [][]string `json:"validNestedArrayItems"`
250286

251-
// Object element constraints
287+
// Valid: Object element constraints
252288
// +kubebuilder:validation:items:MinProperties=1
253289
// +kubebuilder:validation:items:MaxProperties=5
254290
ValidObjectArrayItems []map[string]string `json:"validObjectArrayItems"`
255291

256-
// General items markers
292+
// Valid: General items markers
257293
// +kubebuilder:validation:items:Enum=A;B;C
258294
// +kubebuilder:validation:items:Format=uuid
259295
// +kubebuilder:validation:items:Type=string
260296
// +kubebuilder:validation:items:XValidation:rule="self != ''"
261297
ValidGeneralArrayItems []string `json:"validGeneralArrayItems"`
298+
299+
// Invalid: items:Maximum on string array (element type mismatch)
300+
// +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": array element: type string is not allowed \(expected one of: \[integer number\]\)`
301+
InvalidItemsMaximumOnStringArray []string `json:"invalidItemsMaximumOnStringArray"`
302+
303+
// Invalid: items:Pattern on int array (element type mismatch)
304+
// +kubebuilder:validation:items:Pattern="^[0-9]+$" // want `marker "kubebuilder:validation:items:Pattern": array element: type integer is not allowed \(expected one of: \[string\]\)`
305+
InvalidItemsPatternOnIntArray []int32 `json:"invalidItemsPatternOnIntArray"`
306+
307+
// Invalid: items:MinProperties on string array (element type mismatch)
308+
// +kubebuilder:validation:items:MinProperties=1 // want `marker "kubebuilder:validation:items:MinProperties": array element: type string is not allowed \(expected one of: \[object\]\)`
309+
InvalidItemsMinPropertiesOnStringArray []string `json:"invalidItemsMinPropertiesOnStringArray"`
310+
311+
// Invalid: items marker on non-array field
312+
// +kubebuilder:validation:items:Maximum=100 // want `marker "kubebuilder:validation:items:Maximum": type string is not allowed \(expected one of: \[array\]\)`
313+
InvalidItemsMarkerOnNonArray string `json:"invalidItemsMarkerOnNonArray"`
262314
}

0 commit comments

Comments
 (0)