@@ -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
78127func 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
157237func 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 {
211316func 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