Skip to content

Commit 77257d4

Browse files
committed
maxLength: add support for k8s declarative validation markers and configurability
1 parent de8f856 commit 77257d4

9 files changed

Lines changed: 628 additions & 60 deletions

File tree

pkg/analysis/maxlength/analyzer.go

Lines changed: 152 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,84 @@ const (
3232
name = "maxlength"
3333
)
3434

35-
// Analyzer is the analyzer for the maxlength package.
35+
func init() {
36+
markershelper.DefaultRegistry().Register(
37+
markers.K8sMaxLengthMarker,
38+
markers.K8sMaxBytesMarker,
39+
markers.K8sMaxItemsMarker,
40+
markers.K8sMaxPropertiesMarker,
41+
markers.K8sEnumMarker,
42+
markers.K8sFormatMarker,
43+
)
44+
}
45+
46+
// Analyzer is the analyzer for the maxlength package with the default (kubebuilder-preferred) configuration.
3647
// It checks that strings and arrays have maximum lengths and maximum items respectively.
37-
var Analyzer = &analysis.Analyzer{
38-
Name: name,
39-
Doc: "Checks that all strings formatted fields are marked with a maximum length, and that arrays are marked with max items.",
40-
Run: run,
41-
Requires: []*analysis.Analyzer{inspector.Analyzer},
48+
var Analyzer = newAnalyzer(nil)
49+
50+
type analyzer struct {
51+
preferredMaxLengthMarker string
52+
preferredMaxItemsMarker string
53+
preferredMaxPropertiesMarker string
54+
}
55+
56+
// newAnalyzer creates a new analysis.Analyzer for the given MaxLengthConfig.
57+
// If cfg is nil, the default configuration is used (kubebuilder markers preferred).
58+
func newAnalyzer(cfg *MaxLengthConfig) *analysis.Analyzer {
59+
if cfg == nil {
60+
cfg = &MaxLengthConfig{}
61+
}
62+
63+
defaultConfig(cfg)
64+
65+
a := &analyzer{
66+
preferredMaxLengthMarker: cfg.PreferredMaxLengthMarker,
67+
preferredMaxItemsMarker: cfg.PreferredMaxItemsMarker,
68+
preferredMaxPropertiesMarker: cfg.PreferredMaxPropertiesMarker,
69+
}
70+
71+
return &analysis.Analyzer{
72+
Name: name,
73+
Doc: "Checks that all string fields are marked with a maximum length, arrays are marked with max items, and maps are marked with max properties.",
74+
Run: a.run,
75+
Requires: []*analysis.Analyzer{inspector.Analyzer},
76+
}
4277
}
4378

44-
func run(pass *analysis.Pass) (any, error) {
79+
func defaultConfig(cfg *MaxLengthConfig) {
80+
if cfg.PreferredMaxLengthMarker == "" {
81+
cfg.PreferredMaxLengthMarker = markers.KubebuilderMaxLengthMarker
82+
}
83+
84+
if cfg.PreferredMaxItemsMarker == "" {
85+
cfg.PreferredMaxItemsMarker = markers.KubebuilderMaxItemsMarker
86+
}
87+
88+
if cfg.PreferredMaxPropertiesMarker == "" {
89+
cfg.PreferredMaxPropertiesMarker = markers.KubebuilderMaxPropertiesMarker
90+
}
91+
}
92+
93+
func (a *analyzer) run(pass *analysis.Pass) (any, error) {
4594
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
4695
if !ok {
4796
return nil, kalerrors.ErrCouldNotGetInspector
4897
}
4998

5099
inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markershelper.Markers, qualifiedFieldName string) {
51-
checkField(pass, field, markersAccess, qualifiedFieldName)
100+
a.checkField(pass, field, markersAccess, qualifiedFieldName)
52101
})
53102

54103
return nil, nil //nolint:nilnil
55104
}
56105

57-
func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) {
106+
func (a *analyzer) checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers, qualifiedFieldName string) {
58107
prefix := fmt.Sprintf("field %s", qualifiedFieldName)
59108

60-
checkTypeExpr(pass, field.Type, field, nil, markersAccess, prefix, markers.KubebuilderMaxLengthMarker, needsStringMaxLength)
109+
a.checkTypeExpr(pass, field.Type, field, nil, markersAccess, prefix, a.preferredMaxLengthMarker, a.needsStringMaxLength)
61110
}
62111

63-
func checkIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMaxLength func(markershelper.MarkerSet) bool) {
112+
func (a *analyzer) checkIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMaxLength func(markershelper.MarkerSet) bool) {
64113
if utils.IsBasicType(pass, ident) { // Built-in type
65114
checkString(pass, ident, node, aliases, markersAccess, prefix, marker, needsMaxLength)
66115

@@ -72,72 +121,103 @@ func checkIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []
72121
return
73122
}
74123

75-
checkTypeSpec(pass, tSpec, node, append(aliases, tSpec), markersAccess, fmt.Sprintf("%s type", prefix), marker, needsMaxLength)
124+
a.checkTypeSpec(pass, tSpec, node, append(aliases, tSpec), markersAccess, fmt.Sprintf("%s type", prefix), marker, needsMaxLength)
76125
}
77126

78127
func checkString(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMaxLength func(markershelper.MarkerSet) bool) {
79128
if ident.Name != "string" {
80129
return
81130
}
82131

83-
markers := getCombinedMarkers(markersAccess, node, aliases)
132+
markerSet := getCombinedMarkers(markersAccess, node, aliases)
84133

85-
if needsMaxLength(markers) {
134+
if needsMaxLength(markerSet) {
86135
pass.Reportf(node.Pos(), "%s must have a maximum length, add %s marker", prefix, marker)
87136
}
88137
}
89138

90-
func checkTypeSpec(pass *analysis.Pass, tSpec *ast.TypeSpec, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMaxLength func(markershelper.MarkerSet) bool) {
139+
func (a *analyzer) checkTypeSpec(pass *analysis.Pass, tSpec *ast.TypeSpec, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMaxLength func(markershelper.MarkerSet) bool) {
91140
if tSpec.Name == nil {
92141
return
93142
}
94143

95144
typeName := tSpec.Name.Name
96145
prefix = fmt.Sprintf("%s %s", prefix, typeName)
97146

98-
checkTypeExpr(pass, tSpec.Type, node, aliases, markersAccess, prefix, marker, needsMaxLength)
147+
a.checkTypeExpr(pass, tSpec.Type, node, aliases, markersAccess, prefix, marker, needsMaxLength)
99148
}
100149

101-
func checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMaxLength func(markershelper.MarkerSet) bool) {
150+
func (a *analyzer) checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMaxLength func(markershelper.MarkerSet) bool) {
102151
switch typ := typeExpr.(type) {
103152
case *ast.Ident:
104-
checkIdent(pass, typ, node, aliases, markersAccess, prefix, marker, needsMaxLength)
153+
a.checkIdent(pass, typ, node, aliases, markersAccess, prefix, marker, needsMaxLength)
105154
case *ast.StarExpr:
106-
checkTypeExpr(pass, typ.X, node, aliases, markersAccess, prefix, marker, needsMaxLength)
155+
a.checkTypeExpr(pass, typ.X, node, aliases, markersAccess, prefix, marker, needsMaxLength)
107156
case *ast.ArrayType:
108-
checkArrayType(pass, typ, node, aliases, markersAccess, prefix)
157+
a.checkArrayType(pass, typ, node, aliases, markersAccess, prefix)
158+
case *ast.MapType:
159+
a.checkMapType(pass, typ, node, aliases, markersAccess, prefix)
109160
}
110161
}
111162

112-
func checkArrayType(pass *analysis.Pass, arrayType *ast.ArrayType, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
163+
func (a *analyzer) checkArrayType(pass *analysis.Pass, arrayType *ast.ArrayType, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
113164
if arrayType.Elt != nil {
114165
if ident, ok := arrayType.Elt.(*ast.Ident); ok {
115166
if ident.Name == "byte" {
116-
// byte slices are a special case as they are treated as strings.
117-
// Pretend the ident is a string so that checkString can process it as expected.
118-
i := &ast.Ident{
119-
NamePos: ident.NamePos,
120-
Name: "string",
121-
}
122-
checkString(pass, i, node, aliases, markersAccess, prefix, markers.KubebuilderMaxLengthMarker, needsStringMaxLength)
167+
a.checkByteSlice(pass, ident, node, aliases, markersAccess, prefix)
123168

124169
return
125170
}
126171

127-
checkArrayElementIdent(pass, ident, node, aliases, markersAccess, fmt.Sprintf("%s array element", prefix))
172+
a.checkArrayElementIdent(pass, ident, node, aliases, markersAccess, fmt.Sprintf("%s array element", prefix))
128173
}
129174
}
130175

131176
markerSet := getCombinedMarkers(markersAccess, node, aliases)
132177

133-
if !markerSet.Has(markers.KubebuilderMaxItemsMarker) {
134-
pass.Reportf(node.Pos(), "%s must have a maximum items, add %s marker", prefix, markers.KubebuilderMaxItemsMarker)
178+
if !markerSet.Has(markers.KubebuilderMaxItemsMarker) && !markerSet.Has(markers.K8sMaxItemsMarker) {
179+
pass.Reportf(node.Pos(), "%s must have a maximum items, add %s marker", prefix, a.preferredMaxItemsMarker)
180+
}
181+
}
182+
183+
func (a *analyzer) checkByteSlice(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
184+
// byte slices are a special case as they are treated as strings.
185+
// Pretend the ident is a string so that checkString can process it as expected.
186+
// In DV, k8s:maxBytes (not k8s:maxLength) is the correct tag for []byte fields,
187+
// as it constrains byte count rather than Unicode character count.
188+
i := &ast.Ident{
189+
NamePos: ident.NamePos,
190+
Name: "string",
191+
}
192+
193+
suggestedMarker := a.preferredMaxLengthMarker
194+
if suggestedMarker == markers.K8sMaxLengthMarker {
195+
suggestedMarker = markers.K8sMaxBytesMarker
196+
}
197+
198+
checkString(pass, i, node, aliases, markersAccess, prefix, suggestedMarker, a.needsByteSliceMaxLength)
199+
}
200+
201+
// checkMapType checks that map[string]V fields have a maximum properties marker.
202+
// Only string-keyed maps are checked, mirroring the constraint that maxProperties
203+
// validation only applies to map[string]V types.
204+
func (a *analyzer) checkMapType(pass *analysis.Pass, mapType *ast.MapType, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
205+
// Map maxProperties validation only supports string-keyed maps.
206+
// Skip non-string-keyed maps (e.g. map[int]string) to avoid false positives.
207+
if !utils.IsStringType(pass, mapType.Key) {
208+
return
209+
}
210+
211+
markerSet := getCombinedMarkers(markersAccess, node, aliases)
212+
213+
if !markerSet.Has(markers.KubebuilderMaxPropertiesMarker) && !markerSet.Has(markers.K8sMaxPropertiesMarker) {
214+
pass.Reportf(node.Pos(), "%s must have a maximum properties, add %s marker", prefix, a.preferredMaxPropertiesMarker)
135215
}
136216
}
137217

138-
func checkArrayElementIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
218+
func (a *analyzer) checkArrayElementIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
139219
if ident.Obj == nil { // Built-in type
140-
checkString(pass, ident, node, aliases, markersAccess, prefix, markers.KubebuilderItemsMaxLengthMarker, needsItemsMaxLength)
220+
checkString(pass, ident, node, aliases, markersAccess, prefix, markers.KubebuilderItemsMaxLengthMarker, a.needsItemsMaxLength)
141221

142222
return
143223
}
@@ -149,16 +229,16 @@ func checkArrayElementIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node
149229

150230
// If the array element wasn't directly a string, allow a string alias to be used
151231
// with either the items style markers or the on alias style markers.
152-
checkTypeSpec(pass, tSpec, node, append(aliases, tSpec), markersAccess, fmt.Sprintf("%s type", prefix), markers.KubebuilderMaxLengthMarker, func(ms markershelper.MarkerSet) bool {
153-
return needsStringMaxLength(ms) && needsItemsMaxLength(ms)
232+
a.checkTypeSpec(pass, tSpec, node, append(aliases, tSpec), markersAccess, fmt.Sprintf("%s type", prefix), markers.KubebuilderMaxLengthMarker, func(ms markershelper.MarkerSet) bool {
233+
return a.needsStringMaxLength(ms) && a.needsItemsMaxLength(ms)
154234
})
155235
}
156236

157237
func getCombinedMarkers(markersAccess markershelper.Markers, node ast.Node, aliases []*ast.TypeSpec) markershelper.MarkerSet {
158238
base := markershelper.NewMarkerSet(getMarkers(markersAccess, node).UnsortedList()...)
159239

160-
for _, a := range aliases {
161-
base.Insert(getMarkers(markersAccess, a).UnsortedList()...)
240+
for _, alias := range aliases {
241+
base.Insert(getMarkers(markersAccess, alias).UnsortedList()...)
162242
}
163243

164244
return base
@@ -175,23 +255,48 @@ func getMarkers(markersAccess markershelper.Markers, node ast.Node) markershelpe
175255
return nil
176256
}
177257

178-
// needsMaxLength returns true if the field needs a maximum length.
179-
// Fields do not need a maximum length if they are already marked with a maximum length,
180-
// or if they are an enum, or if they are a date, date-time or duration.
181-
func needsStringMaxLength(markerSet markershelper.MarkerSet) bool {
258+
// needsStringMaxLength returns true if the field needs a maximum length.
259+
// Returns false if either a kubebuilder or DV max-length marker is already present,
260+
// or if the field is an enum, or is formatted as a date, date-time or duration.
261+
func (a *analyzer) needsStringMaxLength(markerSet markershelper.MarkerSet) bool {
182262
switch {
183263
case markerSet.Has(markers.KubebuilderMaxLengthMarker),
264+
markerSet.Has(markers.K8sMaxLengthMarker),
184265
markerSet.Has(markers.KubebuilderEnumMarker),
266+
markerSet.Has(markers.K8sEnumMarker),
185267
markerSet.HasWithValue(kubebuilderFormatWithValue("date")),
186268
markerSet.HasWithValue(kubebuilderFormatWithValue("date-time")),
187-
markerSet.HasWithValue(kubebuilderFormatWithValue("duration")):
269+
markerSet.HasWithValue(kubebuilderFormatWithValue("duration")),
270+
markerSet.HasWithValue(k8sFormatWithValue("date")),
271+
markerSet.HasWithValue(k8sFormatWithValue("date-time")),
272+
markerSet.HasWithValue(k8sFormatWithValue("duration")):
188273
return false
189274
}
190275

191276
return true
192277
}
193278

194-
func needsItemsMaxLength(markerSet markershelper.MarkerSet) bool {
279+
// needsByteSliceMaxLength is like needsStringMaxLength but enforces that for DV markers,
280+
// k8s:maxBytes is used instead of k8s:maxLength (which counts characters).
281+
func (a *analyzer) needsByteSliceMaxLength(markerSet markershelper.MarkerSet) bool {
282+
switch {
283+
case markerSet.Has(markers.KubebuilderMaxLengthMarker),
284+
markerSet.Has(markers.K8sMaxBytesMarker),
285+
markerSet.Has(markers.KubebuilderEnumMarker),
286+
markerSet.Has(markers.K8sEnumMarker),
287+
markerSet.HasWithValue(kubebuilderFormatWithValue("date")),
288+
markerSet.HasWithValue(kubebuilderFormatWithValue("date-time")),
289+
markerSet.HasWithValue(kubebuilderFormatWithValue("duration")),
290+
markerSet.HasWithValue(k8sFormatWithValue("date")),
291+
markerSet.HasWithValue(k8sFormatWithValue("date-time")),
292+
markerSet.HasWithValue(k8sFormatWithValue("duration")):
293+
return false
294+
}
295+
296+
return true
297+
}
298+
299+
func (a *analyzer) needsItemsMaxLength(markerSet markershelper.MarkerSet) bool {
195300
switch {
196301
case markerSet.Has(markers.KubebuilderItemsMaxLengthMarker),
197302
markerSet.Has(markers.KubebuilderItemsEnumMarker),
@@ -211,3 +316,7 @@ func kubebuilderFormatWithValue(value string) string {
211316
func kubebuilderItemsFormatWithValue(value string) string {
212317
return fmt.Sprintf("%s:=%s", markers.KubebuilderItemsFormatMarker, value)
213318
}
319+
320+
func k8sFormatWithValue(value string) string {
321+
return fmt.Sprintf("%s=%s", markers.K8sFormatMarker, value)
322+
}

0 commit comments

Comments
 (0)