Skip to content

Commit 82d79d9

Browse files
authored
Merge pull request #149 from JoelSpeed/minlength
Add MinLength linter rule
2 parents 4699749 + eb6c656 commit 82d79d9

File tree

10 files changed

+592
-14
lines changed

10 files changed

+592
-14
lines changed

pkg/analysis/minlength/analyzer.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package minlength
17+
18+
import (
19+
"fmt"
20+
"go/ast"
21+
22+
"golang.org/x/tools/go/analysis"
23+
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
24+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/extractjsontags"
25+
"sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/inspector"
26+
markershelper "sigs.k8s.io/kube-api-linter/pkg/analysis/helpers/markers"
27+
"sigs.k8s.io/kube-api-linter/pkg/analysis/utils"
28+
"sigs.k8s.io/kube-api-linter/pkg/markers"
29+
)
30+
31+
const (
32+
name = "minlength"
33+
)
34+
35+
// Analyzer is the analyzer for the minlength package.
36+
// It checks that strings and arrays have minimum lengths and minimum items respectively.
37+
var Analyzer = &analysis.Analyzer{
38+
Name: name,
39+
Doc: "Checks that all strings formatted fields are marked with a minimum length, and that arrays are marked with min items, maps are marked with min properties, and structs that do not have required fields are marked with min properties",
40+
Run: run,
41+
Requires: []*analysis.Analyzer{inspector.Analyzer},
42+
}
43+
44+
func run(pass *analysis.Pass) (any, error) {
45+
inspect, ok := pass.ResultOf[inspector.Analyzer].(inspector.Inspector)
46+
if !ok {
47+
return nil, kalerrors.ErrCouldNotGetInspector
48+
}
49+
50+
inspect.InspectFields(func(field *ast.Field, _ extractjsontags.FieldTagInfo, markersAccess markershelper.Markers) {
51+
checkField(pass, field, markersAccess)
52+
})
53+
54+
return nil, nil //nolint:nilnil
55+
}
56+
57+
func checkField(pass *analysis.Pass, field *ast.Field, markersAccess markershelper.Markers) {
58+
fieldName := utils.FieldName(field)
59+
if fieldName == "" {
60+
return
61+
}
62+
63+
prefix := fmt.Sprintf("field %s", fieldName)
64+
65+
checkTypeExpr(pass, field.Type, field, nil, markersAccess, prefix, markers.KubebuilderMinLengthMarker, needsStringMinLength)
66+
}
67+
68+
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) {
69+
if utils.IsBasicType(pass, ident) { // Built-in type
70+
checkString(pass, ident, node, aliases, markersAccess, prefix, marker, needsMaxLength)
71+
72+
return
73+
}
74+
75+
tSpec, ok := utils.LookupTypeSpec(pass, ident)
76+
if !ok {
77+
return
78+
}
79+
80+
checkTypeSpec(pass, tSpec, node, append(aliases, tSpec), markersAccess, fmt.Sprintf("%s type", prefix), marker, needsMaxLength)
81+
}
82+
83+
func checkString(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMinLength func(markershelper.MarkerSet) bool) {
84+
if ident.Name != "string" {
85+
return
86+
}
87+
88+
markers := getCombinedMarkers(markersAccess, node, aliases)
89+
90+
if needsMinLength(markers) {
91+
pass.Reportf(node.Pos(), "%s must have a minimum length, add %s marker", prefix, marker)
92+
}
93+
}
94+
95+
func checkTypeSpec(pass *analysis.Pass, tSpec *ast.TypeSpec, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMinLength func(markershelper.MarkerSet) bool) {
96+
if tSpec.Name == nil {
97+
return
98+
}
99+
100+
typeName := tSpec.Name.Name
101+
prefix = fmt.Sprintf("%s %s", prefix, typeName)
102+
103+
checkTypeExpr(pass, tSpec.Type, node, aliases, markersAccess, prefix, marker, needsMinLength)
104+
}
105+
106+
func checkTypeExpr(pass *analysis.Pass, typeExpr ast.Expr, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix, marker string, needsMinLength func(markershelper.MarkerSet) bool) {
107+
switch typ := typeExpr.(type) {
108+
case *ast.Ident:
109+
checkIdent(pass, typ, node, aliases, markersAccess, prefix, marker, needsMinLength)
110+
case *ast.StarExpr:
111+
checkTypeExpr(pass, typ.X, node, aliases, markersAccess, prefix, marker, needsMinLength)
112+
case *ast.ArrayType:
113+
checkArrayType(pass, typ, node, aliases, markersAccess, prefix)
114+
case *ast.MapType:
115+
checkMapType(pass, node, aliases, markersAccess, prefix)
116+
case *ast.StructType:
117+
checkStructType(pass, typ, node, aliases, markersAccess, prefix)
118+
}
119+
}
120+
121+
func checkArrayType(pass *analysis.Pass, arrayType *ast.ArrayType, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
122+
if arrayType.Elt != nil {
123+
if ident, ok := arrayType.Elt.(*ast.Ident); ok {
124+
if ident.Name == "byte" {
125+
// byte slices are a special case as they are treated as strings.
126+
// Pretend the ident is a string so that checkString can process it as expected.
127+
i := &ast.Ident{
128+
NamePos: ident.NamePos,
129+
Name: "string",
130+
}
131+
checkString(pass, i, node, aliases, markersAccess, prefix, markers.KubebuilderMinLengthMarker, needsStringMinLength)
132+
133+
return
134+
}
135+
136+
checkArrayElementIdent(pass, ident, node, aliases, markersAccess, fmt.Sprintf("%s array element", prefix))
137+
}
138+
}
139+
140+
markerSet := getCombinedMarkers(markersAccess, node, aliases)
141+
142+
if !markerSet.Has(markers.KubebuilderMinItemsMarker) {
143+
pass.Reportf(node.Pos(), "%s must have a minimum items, add %s marker", prefix, markers.KubebuilderMinItemsMarker)
144+
}
145+
}
146+
147+
func checkArrayElementIdent(pass *analysis.Pass, ident *ast.Ident, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
148+
if ident.Obj == nil { // Built-in type
149+
checkString(pass, ident, node, aliases, markersAccess, prefix, markers.KubebuilderItemsMinLengthMarker, needsItemsMinLength)
150+
151+
return
152+
}
153+
154+
tSpec, ok := ident.Obj.Decl.(*ast.TypeSpec)
155+
if !ok {
156+
return
157+
}
158+
159+
// If the array element wasn't directly a string, allow a string alias to be used
160+
// with either the items style markers or the on alias style markers.
161+
checkTypeSpec(pass, tSpec, node, append(aliases, tSpec), markersAccess, fmt.Sprintf("%s type", prefix), markers.KubebuilderMinLengthMarker, func(ms markershelper.MarkerSet) bool {
162+
return needsStringMinLength(ms) && needsItemsMinLength(ms)
163+
})
164+
}
165+
166+
func checkMapType(pass *analysis.Pass, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
167+
markerSet := getCombinedMarkers(markersAccess, node, aliases)
168+
169+
if !markerSet.Has(markers.KubebuilderMinPropertiesMarker) {
170+
pass.Reportf(node.Pos(), "%s must have a minimum properties, add %s marker", prefix, markers.KubebuilderMinPropertiesMarker)
171+
}
172+
}
173+
174+
func checkStructType(pass *analysis.Pass, structType *ast.StructType, node ast.Node, aliases []*ast.TypeSpec, markersAccess markershelper.Markers, prefix string) {
175+
markerSet := getCombinedMarkers(markersAccess, node, aliases)
176+
177+
minProperties, err := utils.GetMinProperties(markerSet)
178+
if err != nil {
179+
pass.Reportf(node.Pos(), "could not get min properties for struct: %v", err)
180+
return
181+
}
182+
183+
if minProperties != nil {
184+
// There's already a min properties specified.
185+
return
186+
}
187+
188+
for _, field := range structType.Fields.List {
189+
if utils.IsFieldRequired(field, markersAccess) {
190+
// The struct has at least one required field,
191+
// this means that `{}` is not valid.
192+
return
193+
}
194+
}
195+
196+
// The field does not have a min properties, and does not have any required fields.
197+
pass.Reportf(node.Pos(), "%s must have a minimum properties, add %s marker", prefix, markers.KubebuilderMinPropertiesMarker)
198+
}
199+
200+
func getCombinedMarkers(markersAccess markershelper.Markers, node ast.Node, aliases []*ast.TypeSpec) markershelper.MarkerSet {
201+
base := markershelper.NewMarkerSet(getMarkers(markersAccess, node).UnsortedList()...)
202+
203+
for _, a := range aliases {
204+
base.Insert(getMarkers(markersAccess, a).UnsortedList()...)
205+
}
206+
207+
return base
208+
}
209+
210+
func getMarkers(markersAccess markershelper.Markers, node ast.Node) markershelper.MarkerSet {
211+
switch t := node.(type) {
212+
case *ast.Field:
213+
return markersAccess.FieldMarkers(t)
214+
case *ast.TypeSpec:
215+
return markersAccess.TypeMarkers(t)
216+
}
217+
218+
return nil
219+
}
220+
221+
// needsMinLength returns true if the field needs a minimum length.
222+
// Fields do not need a minimum length if they are already marked with a minimum length,
223+
// or if they are an enum, or if they are a date, date-time or duration.
224+
func needsStringMinLength(markerSet markershelper.MarkerSet) bool {
225+
switch {
226+
case markerSet.Has(markers.KubebuilderMinLengthMarker),
227+
markerSet.Has(markers.KubebuilderEnumMarker),
228+
markerSet.HasWithValue(kubebuilderFormatWithValue("date")),
229+
markerSet.HasWithValue(kubebuilderFormatWithValue("date-time")),
230+
markerSet.HasWithValue(kubebuilderFormatWithValue("duration")):
231+
return false
232+
}
233+
234+
return true
235+
}
236+
237+
func needsItemsMinLength(markerSet markershelper.MarkerSet) bool {
238+
switch {
239+
case markerSet.Has(markers.KubebuilderItemsMinLengthMarker),
240+
markerSet.Has(markers.KubebuilderItemsEnumMarker),
241+
markerSet.HasWithValue(kubebuilderItemsFormatWithValue("date")),
242+
markerSet.HasWithValue(kubebuilderItemsFormatWithValue("date-time")),
243+
markerSet.HasWithValue(kubebuilderItemsFormatWithValue("duration")):
244+
return false
245+
}
246+
247+
return true
248+
}
249+
250+
func kubebuilderFormatWithValue(value string) string {
251+
return fmt.Sprintf("%s:=%s", markers.KubebuilderFormatMarker, value)
252+
}
253+
254+
func kubebuilderItemsFormatWithValue(value string) string {
255+
return fmt.Sprintf("%s:=%s", markers.KubebuilderItemsFormatMarker, value)
256+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package minlength_test
17+
18+
import (
19+
"testing"
20+
21+
"golang.org/x/tools/go/analysis/analysistest"
22+
"sigs.k8s.io/kube-api-linter/pkg/analysis/minlength"
23+
)
24+
25+
func TestMinLength(t *testing.T) {
26+
testdata := analysistest.TestData()
27+
28+
analysistest.Run(t, testdata, minlength.Analyzer, "a")
29+
}

pkg/analysis/minlength/doc.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/*
18+
minlength is an analyzer that checks that all string fields have a minimum length, and that all array fields have a minimum number of items,
19+
that maps have a minimum number of properties, and that structs that do not have required fields have a minimum number of fields.
20+
21+
String fields that are not otherwise bound in length, through being an enum or formatted in a certain way, should have a minimum length.
22+
This ensures that authors make a choice about whether or not the empty string is a valid choice for users.
23+
24+
Array fields should have a minimum number of items.
25+
This ensures that empty arrays are not allowed.
26+
Empty arrays are generally not recommended and API authors should generally not distinguish between empty and omitted arrays.
27+
When the empty array is a valid choice, setting the minimum items marker to 0 can be used to indicate that this is an explicit choice.
28+
29+
Maps should have a minimum number of properties.
30+
This ensures that empty maps are not allowed.
31+
Empty maps are generally not recommended and API authors should generally not distinguish between empty and omitted maps.
32+
When the empty map is a valid choice, setting the minimum properties marker to 0 can be used to indicate that this is an explicit choice.
33+
34+
Structs that do not have required fields should have a minimum number of fields.
35+
This ensures that empty structs are not allowed.
36+
Empty structs are generally not recommended and API authors should generally not distinguish between empty and omitted structs.
37+
When the empty struct is a valid choice, setting the minimum properties marker to 0 can be used to indicate that this is an explicit choice.
38+
39+
For strings, the minimum length can be set using the `kubebuilder:validation:MinLength` tag.
40+
For arrays, the minimum number of items can be set using the `kubebuilder:validation:MinItems` tag.
41+
For maps, the minimum number of properties can be set using the `kubebuilder:validation:MinProperties` tag.
42+
For structs, the minimum number of fields can be set using the `kubebuilder:validation:MinProperties` tag.
43+
44+
For arrays of strings, the minimum length of each string can be set using the `kubebuilder:validation:items:MinLength` tag,
45+
on the array field itself.
46+
Or, if the array uses a string type alias, the `kubebuilder:validation:MinLength` tag can be used on the alias.
47+
*/
48+
package minlength
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package minlength
17+
18+
import (
19+
"sigs.k8s.io/kube-api-linter/pkg/analysis/initializer"
20+
"sigs.k8s.io/kube-api-linter/pkg/analysis/registry"
21+
)
22+
23+
func init() {
24+
registry.DefaultRegistry().RegisterLinter(Initializer())
25+
}
26+
27+
// Initializer returns the AnalyzerInitializer for this
28+
// Analyzer so that it can be added to the registry.
29+
func Initializer() initializer.AnalyzerInitializer {
30+
return initializer.NewInitializer(
31+
name,
32+
Analyzer,
33+
false, // For now, CRD only, and so not on by default.
34+
)
35+
}

0 commit comments

Comments
 (0)