Skip to content

Commit 1a694bf

Browse files
committed
Make validation-gen lint errors easier to read
This started with fixing the "+default" tag, but I realized the errors could be more readable. * Move lint rules to lint_rules.go * Make lintErrors a map of type -> []error * Keep 1 linter for all pkgs, rather than create/destroy * Make conflictingTagsRule() return a lintRule, making the default rules simpler and fail at startup if too few tags * Added a human-friendly "message" arg to conflictingTagsRule() to be printed when the rule fails Now we get: ``` go run k8s.io/code-generator/cmd/validation-gen --output-file zz_generated.validations.go --go-header-file=../../../examples/hack/boilerplate.go.txt ./... F1228 15:38:48.953915 82030 targets.go:309] lint failed: type k8s.io/code-generator/cmd/validation-gen/output_tests/optional.T1: field S: conflicting tags: {+k8s:optional, +k8s:required}: fields cannot be both optional and required field S: conflicting tags: {+k8s:required, +default}: fields with default values are always optional ```
1 parent 71eb042 commit 1a694bf

File tree

5 files changed

+147
-91
lines changed

5 files changed

+147
-91
lines changed

staging/src/k8s.io/code-generator/cmd/validation-gen/lint.go

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"fmt"
21+
"sort"
2122
"strings"
2223

2324
"k8s.io/gengo/v2/types"
@@ -30,30 +31,34 @@ import (
3031
type linter struct {
3132
linted map[*types.Type]bool
3233
rules []lintRule
33-
// lintErrors is a list of errors that occurred during the linting process.
34-
// lintErrors would be in the format:
35-
// field <field_name>: <lint broken message>
36-
// type <type_name>: <lint broken message>
37-
lintErrors []error
34+
// lintErrors is all the errors, grouped by type, that occurred during the
35+
// linting process.
36+
lintErrors map[*types.Type][]error
3837
}
3938

40-
var defaultRules = []lintRule{
41-
ruleOptionalAndRequired,
42-
ruleRequiredAndDefault,
43-
}
39+
// lintRule is a function that validates a slice of comments.
40+
// It returns a string as an error message if the comments are invalid,
41+
// and an error there is an error happened during the linting process.
42+
type lintRule func(comments []string) (string, error)
4443

45-
func (l *linter) AddError(field, msg string) {
46-
l.lintErrors = append(l.lintErrors, fmt.Errorf("%s: %s", field, msg))
44+
func (l *linter) AddError(t *types.Type, field, msg string) {
45+
var err error
46+
if field == "" {
47+
err = fmt.Errorf("%s", msg)
48+
} else {
49+
err = fmt.Errorf("field %s: %s", field, msg)
50+
}
51+
l.lintErrors[t] = append(l.lintErrors[t], err)
4752
}
4853

4954
func newLinter(rules ...lintRule) *linter {
5055
if len(rules) == 0 {
51-
rules = defaultRules
56+
rules = defaultLintRules
5257
}
5358
return &linter{
5459
linted: make(map[*types.Type]bool),
5560
rules: rules,
56-
lintErrors: []error{},
61+
lintErrors: map[*types.Type][]error{},
5762
}
5863
}
5964

@@ -70,7 +75,7 @@ func (l *linter) lintType(t *types.Type) error {
7075
return err
7176
}
7277
for _, lintErr := range lintErrs {
73-
l.AddError("type "+t.Name.String(), lintErr)
78+
l.AddError(t, "", lintErr)
7479
}
7580
}
7681
switch t.Kind {
@@ -88,7 +93,7 @@ func (l *linter) lintType(t *types.Type) error {
8893
return err
8994
}
9095
for _, lintErr := range lintErrs {
91-
l.AddError("type "+t.Name.String(), lintErr)
96+
l.AddError(t, member.Name, lintErr)
9297
}
9398
if err := l.lintType(member.Type); err != nil {
9499
return err
@@ -111,11 +116,6 @@ func (l *linter) lintType(t *types.Type) error {
111116
return nil
112117
}
113118

114-
// lintRule is a function that validates a slice of comments.
115-
// It returns a string as an error message if the comments are invalid,
116-
// and an error there is an error happened during the linting process.
117-
type lintRule func(comments []string) (string, error)
118-
119119
// lintComments runs all registered rules on a slice of comments.
120120
func (l *linter) lintComments(comments []string) ([]string, error) {
121121
var lintErrs []string
@@ -130,31 +130,29 @@ func (l *linter) lintComments(comments []string) ([]string, error) {
130130
return lintErrs, nil
131131
}
132132

133-
// conflictingTagsRule checks for conflicting tags in a slice of comments.
134-
func conflictingTagsRule(comments []string, tags ...string) (string, error) {
133+
// conflictingTagsRule creates a lintRule which checks for conflicting tags.
134+
func conflictingTagsRule(msg string, tags ...string) lintRule {
135135
if len(tags) < 2 {
136-
return "", fmt.Errorf("at least two tags must be provided")
136+
panic("conflictingTagsRule: at least 2 tags must be specified")
137137
}
138-
tagCount := make(map[string]bool)
139-
for _, comment := range comments {
140-
for _, tag := range tags {
141-
if strings.HasPrefix(comment, tag) {
142-
tagCount[tag] = true
138+
139+
return func(comments []string) (string, error) {
140+
found := make(map[string]bool)
141+
for _, comment := range comments {
142+
for _, tag := range tags {
143+
if strings.HasPrefix(comment, tag) {
144+
found[tag] = true
145+
}
143146
}
144147
}
148+
if len(found) > 1 {
149+
keys := make([]string, 0, len(found))
150+
for k := range found {
151+
keys = append(keys, k)
152+
}
153+
sort.Strings(keys)
154+
return fmt.Sprintf("conflicting tags: {%s}: %s", strings.Join(keys, ", "), msg), nil
155+
}
156+
return "", nil
145157
}
146-
if len(tagCount) > 1 {
147-
return fmt.Sprintf("conflicting tags: {%s}", strings.Join(tags, ", ")), nil
148-
}
149-
return "", nil
150-
}
151-
152-
// ruleOptionalAndRequired checks for conflicting tags +k8s:optional and +k8s:required in a slice of comments.
153-
func ruleOptionalAndRequired(comments []string) (string, error) {
154-
return conflictingTagsRule(comments, "+k8s:optional", "+k8s:required")
155-
}
156-
157-
// ruleRequiredAndDefault checks for conflicting tags +k8s:required and +default in a slice of comments.
158-
func ruleRequiredAndDefault(comments []string) (string, error) {
159-
return conflictingTagsRule(comments, "+k8s:required", "+default")
160158
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
package main
18+
19+
var ruleOptionalAndRequired = conflictingTagsRule(
20+
"fields cannot be both optional and required",
21+
"+k8s:optional", "+k8s:required")
22+
23+
var ruleRequiredAndDefault = conflictingTagsRule(
24+
"fields with default values are always optional",
25+
"+k8s:required", "+default")
26+
27+
var defaultLintRules = []lintRule{
28+
ruleOptionalAndRequired,
29+
ruleRequiredAndDefault,
30+
}

staging/src/k8s.io/code-generator/cmd/validation-gen/lint_test.go

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"errors"
21+
"regexp"
2122
"testing"
2223

2324
"k8s.io/gengo/v2/types"
@@ -121,7 +122,6 @@ func TestRuleOptionalAndRequired(t *testing.T) {
121122
name string
122123
comments []string
123124
wantMsg string
124-
wantErr bool
125125
}{
126126
{
127127
name: "no comments",
@@ -139,22 +139,37 @@ func TestRuleOptionalAndRequired(t *testing.T) {
139139
wantMsg: "",
140140
},
141141
{
142-
name: "optional and required",
142+
name: "optional required",
143143
comments: []string{"+k8s:optional", "+k8s:required"},
144-
wantMsg: "conflicting tags: {+k8s:optional, +k8s:required}",
144+
wantMsg: `conflicting tags: {\+k8s:optional, \+k8s:required}`,
145145
},
146146
{
147-
name: "optional, empty, required",
147+
name: "required optional",
148+
comments: []string{"+k8s:optional", "+k8s:required"},
149+
wantMsg: `conflicting tags: {\+k8s:optional, \+k8s:required}`,
150+
},
151+
{
152+
name: "optional empty required",
148153
comments: []string{"+k8s:optional", "", "+k8s:required"},
149-
wantMsg: "conflicting tags: {+k8s:optional, +k8s:required}",
154+
wantMsg: `conflicting tags: {\+k8s:optional, \+k8s:required}`,
155+
},
156+
{
157+
name: "empty required empty empty optional empty",
158+
comments: []string{"", "+k8s:optional", "", "", "+k8s:required", ""},
159+
wantMsg: `conflicting tags: {\+k8s:optional, \+k8s:required}`,
150160
},
151161
}
152162

153163
for _, tt := range tests {
154164
t.Run(tt.name, func(t *testing.T) {
155-
msg, _ := ruleOptionalAndRequired(tt.comments)
156-
if msg != tt.wantMsg {
157-
t.Errorf("ruleOptionalAndRequired() msg = %v, wantMsg %v", msg, tt.wantMsg)
165+
msg, err := ruleOptionalAndRequired(tt.comments)
166+
if err != nil {
167+
t.Errorf("unexpected error: %v", err)
168+
} else if tt.wantMsg != "" {
169+
re := regexp.MustCompile(tt.wantMsg)
170+
if !re.MatchString(msg) {
171+
t.Errorf("message:\n\t%s\ndoes not match:\n\t%s", msg, re.String())
172+
}
158173
}
159174
})
160175
}
@@ -182,22 +197,37 @@ func TestRuleRequiredAndDefault(t *testing.T) {
182197
wantMsg: "",
183198
},
184199
{
185-
name: "required and default",
200+
name: "required default",
186201
comments: []string{"+k8s:required", "+default=somevalue"},
187-
wantMsg: "conflicting tags: {+k8s:required, +default}",
202+
wantMsg: `conflicting tags: {\+default, \+k8s:required}`,
203+
},
204+
{
205+
name: "default required",
206+
comments: []string{"+default=somevalue", "+k8s:required"},
207+
wantMsg: `conflicting tags: {\+default, \+k8s:required}`,
188208
},
189209
{
190-
name: "required, empty, default",
210+
name: "required empty default",
191211
comments: []string{"+k8s:required", "", "+default=somevalue"},
192-
wantMsg: "conflicting tags: {+k8s:required, +default}",
212+
wantMsg: `conflicting tags: {\+default, \+k8s:required}`,
213+
},
214+
{
215+
name: "empty default empty empty required empty",
216+
comments: []string{"", "+default=somevalue", "", "", "+k8s:required", ""},
217+
wantMsg: `conflicting tags: {\+default, \+k8s:required}`,
193218
},
194219
}
195220

196221
for _, tt := range tests {
197222
t.Run(tt.name, func(t *testing.T) {
198-
msg, _ := ruleRequiredAndDefault(tt.comments)
199-
if msg != tt.wantMsg {
200-
t.Errorf("ruleRequiredAndDefault() msg = %v, wantMsg %v", msg, tt.wantMsg)
223+
msg, err := ruleRequiredAndDefault(tt.comments)
224+
if err != nil {
225+
t.Errorf("unexpected error: %v", err)
226+
} else if tt.wantMsg != "" {
227+
re := regexp.MustCompile(tt.wantMsg)
228+
if !re.MatchString(msg) {
229+
t.Errorf("message:\n\t%s\ndoes not match:\n\t%s", msg, re.String())
230+
}
201231
}
202232
})
203233
}
@@ -209,7 +239,6 @@ func TestConflictingTagsRule(t *testing.T) {
209239
comments []string
210240
tags []string
211241
wantMsg string
212-
wantErr bool
213242
}{
214243
{
215244
name: "no comments",
@@ -227,32 +256,32 @@ func TestConflictingTagsRule(t *testing.T) {
227256
name: "tag1, empty, tag2",
228257
comments: []string{"+tag1", "", "+tag2"},
229258
tags: []string{"+tag1", "+tag2"},
230-
wantMsg: "conflicting tags: {+tag1, +tag2}",
259+
wantMsg: `conflicting tags: {\+tag1, \+tag2}`,
231260
},
232261
{
233-
name: "3 tags",
234-
comments: []string{"tag1", "+tag2", "+tag3=value"},
262+
name: "3 lines 2 tags match",
263+
comments: []string{"tag3", "+tag1", "+tag2=value"},
235264
tags: []string{"+tag1", "+tag2", "+tag3"},
236-
wantMsg: "conflicting tags: {+tag1, +tag2, +tag3}",
265+
wantMsg: `conflicting tags: {\+tag1, \+tag2}`,
237266
},
238267
{
239-
name: "less than 2 tags",
240-
comments: []string{"+tag1"},
241-
tags: []string{"+tag1"},
242-
wantMsg: "",
243-
wantErr: true,
268+
name: "3 tags all match",
269+
comments: []string{"+tag3", "+tag1", "+tag2=value"},
270+
tags: []string{"+tag1", "+tag2", "+tag3"},
271+
wantMsg: `conflicting tags: {\+tag1, \+tag2, \+tag3}`,
244272
},
245273
}
246274

247275
for _, tt := range tests {
248276
t.Run(tt.name, func(t *testing.T) {
249-
msg, err := conflictingTagsRule(tt.comments, tt.tags...)
250-
if (err != nil) != tt.wantErr {
251-
t.Errorf("conflictingTagsRule() error = %v, wantErr %v", err, tt.wantErr)
252-
return
253-
}
254-
if msg != tt.wantMsg {
255-
t.Errorf("conflictingTagsRule() msg = %v, wantMsg %v", msg, tt.wantMsg)
277+
msg, err := conflictingTagsRule("test", tt.tags...)(tt.comments)
278+
if err != nil {
279+
t.Errorf("unexpected error: %v", err)
280+
} else if tt.wantMsg != "" {
281+
re := regexp.MustCompile(tt.wantMsg)
282+
if !re.MatchString(msg) {
283+
t.Errorf("message:\n\t%s\ndoes not match:\n\t%s", msg, re.String())
284+
}
256285
}
257286
})
258287
}

staging/src/k8s.io/code-generator/cmd/validation-gen/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ type Args struct {
7878
ReadOnlyPkgs []string // Always consider these as last-ditch possibilities for validations.
7979
GoHeaderFile string
8080
PrintDocs bool
81-
Lint bool
81+
LintOnly bool
8282
}
8383

8484
// AddFlags add the generator flags to the flag set.
@@ -91,7 +91,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) {
9191
"the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year")
9292
fs.BoolVar(&args.PrintDocs, "docs", false,
9393
"print documentation for supported declarative validations, and then exit")
94-
fs.BoolVar(&args.Lint, "lint", false,
94+
fs.BoolVar(&args.LintOnly, "lint", false,
9595
"only run linting checks, do not generate code")
9696
}
9797

0 commit comments

Comments
 (0)