Skip to content

Commit df6a56e

Browse files
committed
feat: support ExactlyOneOf marker in arrayofstruct analyzer
1 parent 5fca1e8 commit df6a56e

File tree

2 files changed

+54
-0
lines changed

2 files changed

+54
-0
lines changed

pkg/analysis/arrayofstruct/analyzer.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ package arrayofstruct
1818
import (
1919
"fmt"
2020
"go/ast"
21+
"regexp"
22+
"slices"
2123

2224
"golang.org/x/tools/go/analysis"
25+
2326
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
2427
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
2528
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
@@ -74,6 +77,13 @@ func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelp
7477
return
7578
}
7679

80+
// Check if the struct has union markers that satisfy the required constraint
81+
if hasExactlyOneOfMarker(structType) {
82+
// ExactlyOneOf marker enforces that exactly one field is set,
83+
// so we don't need to report an error
84+
return
85+
}
86+
7787
// Check if at least one field in the struct has a required marker
7888
if hasRequiredField(structType, markersAccess) {
7989
return
@@ -208,3 +218,35 @@ func hasRequiredField(structType *ast.StructType, markersAccess markershelper.Ma
208218

209219
return false
210220
}
221+
222+
// hasExactlyOneOfMarker checks if the struct has an ExactlyOneOf marker,
223+
// which satisfies the required field constraint by ensuring exactly one field is set.
224+
func hasExactlyOneOfMarker(structType *ast.StructType) bool {
225+
if structType.Fields == nil {
226+
return false
227+
}
228+
229+
for _, field := range structType.Fields.List {
230+
var markers []string
231+
232+
if field.Doc != nil {
233+
for _, comment := range field.Doc.List {
234+
markers = append(markers, comment.Text)
235+
}
236+
}
237+
// Check for ExactlyOneOf marker
238+
if hasMarkerPattern(markers, "ExactlyOneOf") {
239+
return true
240+
}
241+
}
242+
243+
return false
244+
}
245+
246+
// hasMarkerPattern checks if any of the markers match the given pattern.
247+
func hasMarkerPattern(markers []string, markerName string) bool {
248+
pattern := fmt.Sprintf(`\+kubebuilder:validation:%s=`, markerName)
249+
re := regexp.MustCompile(pattern)
250+
251+
return slices.ContainsFunc(markers, re.MatchString)
252+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,15 @@ type ValidStructWithCustomBasicType struct {
150150
// This should not trigger the linter because CustomString is based on string, a basic type
151151
Items []CustomString
152152
}
153+
154+
// Valid case - struct with ExactlyOneOf marker
155+
type ValidWithExactlyOneOf struct {
156+
Items []ValidExactlyOneOfItem
157+
}
158+
159+
type ValidExactlyOneOfItem struct {
160+
// +kubebuilder:validation:ExactlyOneOf=FieldA;FieldB
161+
// No required fields, but the marker enforces exactly one is set
162+
FieldA *string `json:"fieldA,omitempty"`
163+
FieldB *string `json:"fieldB,omitempty"`
164+
}

0 commit comments

Comments
 (0)