Skip to content

Commit cf1ca33

Browse files
committed
refactor(markerscope): update scope handling to support multiple scopes for markers
Signed-off-by: nayuta-ai <nayuta723@gmail.com>
1 parent ada18fb commit cf1ca33

File tree

7 files changed

+124
-123
lines changed

7 files changed

+124
-123
lines changed

docs/linters.md

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -399,29 +399,28 @@ The linter defines different scope types for markers:
399399

400400
- **FieldScope**: Can only be applied to struct fields (e.g., `optional`, `required`, `nullable`)
401401
- **TypeScope**: Can only be applied to type definitions (e.g., `kubebuilder:validation:items:ExactlyOneOf`)
402-
- **AnyScope**: Can be applied to either fields or type definitions (e.g., `kubebuilder:validation:Minimum`, `kubebuilder:validation:Pattern`)
402+
- **Field and Type**: Markers that can be applied to both fields and type definitions (e.g., `kubebuilder:validation:Minimum`, `kubebuilder:validation:Pattern`)
403403

404404
### Type Constraints
405405

406406
The linter validates that markers are applied to compatible OpenAPI schema types:
407407

408-
- **Numeric markers** (`Minimum`, `Maximum`, `MultipleOf`): Only for `integer` or `number` types
408+
- **Numeric markers** (`Minimum`, `Maximum`, `MultipleOf`): Only for `integer` types
409409
- **String markers** (`Pattern`, `MinLength`, `MaxLength`): Only for `string` types
410410
- **Array markers** (`MinItems`, `MaxItems`, `UniqueItems`): Only for `array` types
411411
- **Object markers** (`MinProperties`, `MaxProperties`): Only for `object` types (struct/map)
412412
- **Array items markers** (`items:Minimum`, `items:Pattern`, etc.): Apply constraints to array element types
413413

414414
OpenAPI schema types map to Go types as follows:
415415
- `integer`: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
416-
- `number`: float32, float64
417416
- `string`: string
418417
- `boolean`: bool
419418
- `array`: []T, [N]T (slices and arrays)
420419
- `object`: struct, map[K]V
421420

422421
#### Strict Type Constraints
423422

424-
For markers with `AnyScope` and type constraints, the `strictTypeConstraint` flag controls where the marker should be declared when used with named types:
423+
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:
425424

426425
- When `strictTypeConstraint` is `false` (default): The marker can be declared on either the field or the type definition.
427426
- 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
461460
- `kubebuilder:validation:items:AtMostOneOf`
462461
- `kubebuilder:validation:items:AtLeastOneOf`
463462

464-
**AnyScope markers with type constraints:**
465-
- `kubebuilder:validation:Minimum` (integer/number types only)
463+
**Field and Type markers with type constraints:**
464+
- `kubebuilder:validation:Minimum` (integer types only)
466465
- `kubebuilder:validation:Pattern` (string types only)
467466
- `kubebuilder:validation:MinItems` (array types only)
468467
- `kubebuilder:validation:MinProperties` (object types only)
469468

470-
**AnyScope markers without type constraints:**
469+
**Field and Type markers without type constraints:**
471470
- `kubebuilder:validation:Enum`, `kubebuilder:validation:Format`
472471
- `kubebuilder:pruning:PreserveUnknownFields`, `kubebuilder:title`
473472

@@ -476,50 +475,50 @@ The linter includes built-in rules for all standard kubebuilder markers and k8s
476475
You can customize marker rules or add support for custom markers.
477476

478477
**Scope values:**
479-
- `Field`: Marker can only be applied to struct fields
480-
- `Type`: Marker can only be applied to type definitions
481-
- `Any`: Marker can be applied to either fields or type definitions
478+
479+
The `scopes` field accepts an array of scope constraints:
480+
- `[Field]`: Marker can only be applied to struct fields
481+
- `[Type]`: Marker can only be applied to type definitions
482+
- `[Field, Type]`: Marker can be applied to both fields and type definitions
482483

483484
**Type constraints:**
484485

485-
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).
486+
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).
486487

487488
**Type constraint fields:**
488-
- `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `number`, `string`, `boolean`, `array`, `object`)
489+
- `allowedSchemaTypes`: List of allowed OpenAPI schema types (`integer`, `string`, `boolean`, `array`, `object`)
489490
- `elementConstraint`: Nested constraint for array element types (only valid when `allowedSchemaTypes` includes `array`)
490-
- `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`.
491+
- `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`.
491492

492493
**Configuration example:**
493494

494495
```yaml
495496
lintersConfig:
496497
markerscope:
497498
policy: Warn | SuggestFix # The policy for marker scope violations. Defaults to `Warn`.
498-
allowDangerousTypes: false # Allow dangerous number types (float32, float64). Defaults to `false`.
499499

500500
# Override default rules for built-in markers
501501
overrideMarkers:
502502
- identifier: "optional"
503-
scope: Field # or: Type, Any
503+
scopes: [Field] # Can specify [Field], [Type], or [Field, Type]
504504

505505
# Add rules for custom markers
506506
customMarkers:
507507
# Custom marker with scope constraint only
508508
- identifier: "mycompany:validation:CustomMarker"
509-
scope: Any
509+
scopes: [Field, Type]
510510

511511
# Custom marker with scope and type constraints
512512
- identifier: "mycompany:validation:NumericLimit"
513-
scope: Any
513+
scopes: [Field, Type]
514514
strictTypeConstraint: true # Require declaration on type definition for named types
515515
typeConstraint:
516516
allowedSchemaTypes:
517517
- integer
518-
- number
519518

520519
# Custom array items marker with element type constraint
521520
- identifier: "mycompany:validation:items:StringFormat"
522-
scope: Any
521+
scopes: [Field, Type]
523522
typeConstraint:
524523
allowedSchemaTypes:
525524
- array
@@ -541,7 +540,7 @@ When the `policy` is set to `SuggestFix`, the `markerscope` linter provides auto
541540

542541
2. **Type constraint violations**: For markers applied to incompatible types, the linter suggests removing the invalid marker.
543542

544-
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.
543+
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.
545544

546545
When the `policy` is set to `Warn`, violations are reported as warnings without suggesting fixes.
547546

pkg/analysis/markerscope/analyzer.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func (a *analyzer) checkFieldMarkers(pass *analysis.Pass, field *ast.Field, mark
173173
}
174174

175175
// Check if FieldScope is allowed
176-
if !rule.Scope.Allows(FieldScope) {
176+
if !rule.AllowsScope(FieldScope) {
177177
a.reportFieldScopeViolation(pass, field, marker, rule)
178178
continue
179179
}
@@ -196,7 +196,7 @@ func (a *analyzer) checkTypeSpecMarkers(pass *analysis.Pass, typeSpec *ast.TypeS
196196
}
197197

198198
// Check if TypeScope is allowed
199-
if !rule.Scope.Allows(TypeScope) {
199+
if !rule.AllowsScope(TypeScope) {
200200
a.reportTypeScopeViolation(pass, typeSpec, marker, rule)
201201
continue
202202
}
@@ -212,7 +212,7 @@ func (a *analyzer) reportFieldScopeViolation(pass *analysis.Pass, field *ast.Fie
212212

213213
var fixes []analysis.SuggestedFix
214214

215-
if rule.Scope == TypeScope {
215+
if rule.AllowsScope(TypeScope) {
216216
message = fmt.Sprintf("marker %q can only be applied to types", marker.Identifier)
217217

218218
if a.policy == MarkerScopePolicySuggestFix {
@@ -276,7 +276,7 @@ func (a *analyzer) reportTypeScopeViolation(pass *analysis.Pass, typeSpec *ast.T
276276
var fixes []analysis.SuggestedFix
277277

278278
message := fmt.Sprintf("marker %q cannot be applied to types", marker.Identifier)
279-
if rule.Scope == FieldScope {
279+
if rule.AllowsScope(FieldScope) {
280280
message = fmt.Sprintf("marker %q can only be applied to fields", marker.Identifier)
281281

282282
if a.policy == MarkerScopePolicySuggestFix {
@@ -347,7 +347,7 @@ func (a *analyzer) validateFieldTypeConstraint(pass *analysis.Pass, field *ast.F
347347
}
348348

349349
// Check if the marker should be on the type definition instead of the field
350-
if rule.NamedTypeConstraint == NamedTypeConstraintRequireTypeDefinition && rule.Scope == AnyScope {
350+
if rule.NamedTypeConstraint == NamedTypeConstraintRequireTypeDefinition && rule.AllowsScope(TypeScope) {
351351
namedType, ok := tv.Type.(*types.Named)
352352
if ok {
353353
return &markerShouldBeOnTypeDefinitionError{typeName: namedType.Obj().Name()}
@@ -404,7 +404,7 @@ func validateTypeAgainstConstraint(t types.Type, tc *TypeConstraint) error {
404404

405405
func (a *analyzer) suggestMoveToField(pass *analysis.Pass, typeSpec *ast.TypeSpec, marker markershelper.Marker, rule MarkerScopeRule) []analysis.SuggestedFix {
406406
// Only suggest moving to field if FieldScope is allowed
407-
if !rule.Scope.Allows(FieldScope) {
407+
if !rule.AllowsScope(FieldScope) {
408408
return nil
409409
}
410410

@@ -444,7 +444,7 @@ func (a *analyzer) suggestMoveToField(pass *analysis.Pass, typeSpec *ast.TypeSpe
444444
// suggestMoveToFieldsIfCompatible generates suggested fixes to move a marker from type to compatible fields.
445445
func (a *analyzer) suggestMoveToFieldsIfCompatible(pass *analysis.Pass, field *ast.Field, marker markershelper.Marker, rule MarkerScopeRule) []analysis.SuggestedFix {
446446
// Only suggest moving to type if TypeScope is allowed
447-
if !rule.Scope.Allows(TypeScope) {
447+
if !rule.AllowsScope(TypeScope) {
448448
return nil
449449
}
450450

pkg/analysis/markerscope/analyzer_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,29 +58,29 @@ func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) {
5858
// Override built-in "optional" to allow on types (default is FieldScope only)
5959
{
6060
Identifier: "optional",
61-
Scope: markerscope.AnyScope,
61+
Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope},
6262
},
6363
// Override built-in "required" to allow on types (default is FieldScope only)
6464
{
6565
Identifier: "required",
66-
Scope: markerscope.AnyScope,
66+
Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope, markerscope.TypeScope},
6767
},
6868
},
6969
CustomMarkers: []markerscope.MarkerScopeRule{
7070
// Custom field-only marker
7171
{
7272
Identifier: "custom:field-only",
73-
Scope: markerscope.FieldScope,
73+
Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope},
7474
},
7575
// Custom type-only marker
7676
{
7777
Identifier: "custom:type-only",
78-
Scope: markerscope.TypeScope,
78+
Scopes: []markerscope.ScopeConstraint{markerscope.TypeScope},
7979
},
8080
// Custom marker with string type constraint
8181
{
8282
Identifier: "custom:string-only",
83-
Scope: markerscope.FieldScope,
83+
Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope},
8484
TypeConstraint: &markerscope.TypeConstraint{
8585
AllowedSchemaTypes: []markerscope.SchemaType{
8686
markerscope.SchemaTypeString,
@@ -90,7 +90,7 @@ func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) {
9090
// Custom marker with integer type constraint
9191
{
9292
Identifier: "custom:integer-only",
93-
Scope: markerscope.FieldScope,
93+
Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope},
9494
TypeConstraint: &markerscope.TypeConstraint{
9595
AllowedSchemaTypes: []markerscope.SchemaType{
9696
markerscope.SchemaTypeInteger,
@@ -100,7 +100,7 @@ func TestAnalyzerWithCustomAndOverrideMarkers(t *testing.T) {
100100
// Custom marker with array of strings constraint
101101
{
102102
Identifier: "custom:string-array",
103-
Scope: markerscope.FieldScope,
103+
Scopes: []markerscope.ScopeConstraint{markerscope.FieldScope},
104104
TypeConstraint: &markerscope.TypeConstraint{
105105
AllowedSchemaTypes: []markerscope.SchemaType{
106106
markerscope.SchemaTypeArray,

pkg/analysis/markerscope/config.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,16 @@ const (
2323
FieldScope ScopeConstraint = "Field"
2424
// TypeScope indicates the marker can be placed on type definitions.
2525
TypeScope ScopeConstraint = "Type"
26-
// AnyScope indicates the marker can be placed on either fields or types.
27-
AnyScope ScopeConstraint = "Any"
2826
)
2927

30-
// Allows checks if the given scope is allowed by this constraint.
31-
func (s ScopeConstraint) Allows(scope ScopeConstraint) bool {
32-
if s == AnyScope {
33-
return true
28+
// AllowsScope checks if the given scope is allowed by this rule.
29+
func (r MarkerScopeRule) AllowsScope(scope ScopeConstraint) bool {
30+
for _, s := range r.Scopes {
31+
if s == scope {
32+
return true
33+
}
3434
}
35-
36-
return s == scope
35+
return false
3736
}
3837

3938
// TypeConstraint defines what types a marker can be applied to.
@@ -68,8 +67,9 @@ type MarkerScopeRule struct {
6867
// Identifier is the marker identifier (e.g., "optional", "kubebuilder:validation:Minimum").
6968
Identifier string `json:"identifier,omitempty"`
7069

71-
// Scope specifies where the marker can be placed (field vs type).
72-
Scope ScopeConstraint
70+
// Scopes specifies where the marker can be placed (field, type, or both).
71+
// Can contain FieldScope, TypeScope, or both for markers that can be placed anywhere.
72+
Scopes []ScopeConstraint `json:"scopes,omitempty"`
7373

7474
// NamedTypeConstraint specifies how markers should be applied to named types.
7575
// When a field uses a named type (e.g., type CustomInt int32), this determines

pkg/analysis/markerscope/initializer.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,18 @@ func validateCustomMarkers(rules []MarkerScopeRule, defaultRules map[string]Mark
137137

138138
func validateMarkerRule(rule MarkerScopeRule) error {
139139
// Validate scope constraint
140-
if rule.Scope == "" {
140+
if len(rule.Scopes) == 0 {
141141
return errScopeRequired
142142
}
143143

144-
// Validate that scope is a valid value
145-
switch rule.Scope {
146-
case FieldScope, TypeScope, AnyScope:
147-
// Valid scope
148-
default:
149-
return &invalidScopeConstraintError{scope: string(rule.Scope)}
144+
// Validate that each scope is a valid value
145+
for _, scope := range rule.Scopes {
146+
switch scope {
147+
case FieldScope, TypeScope:
148+
// Valid scope
149+
default:
150+
return &invalidScopeConstraintError{scope: string(scope)}
151+
}
150152
}
151153

152154
// Validate named type constraint if present

0 commit comments

Comments
 (0)