diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/options/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/options/doc.go index 4e3895b1c213a..a8f0ecab8647d 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/options/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/options/doc.go @@ -27,13 +27,13 @@ var localSchemeBuilder = testscheme.New() type T1 struct { TypeMeta int - // +k8s:ifOptionEnabled=FeatureX=+k8s:validateFalse="field T1.S1" + // +k8s:ifOptionEnabled(FeatureX)=+k8s:validateFalse="field T1.S1" S1 string `json:"s1"` - // +k8s:ifOptionDisabled=FeatureX=+k8s:validateFalse="field T1.S2" + // +k8s:ifOptionDisabled(FeatureX)=+k8s:validateFalse="field T1.S2" S2 string `json:"s2"` - // +k8s:ifOptionEnabled=FeatureX=+k8s:validateFalse="field T1.S3.FeatureX" - // +k8s:ifOptionDisabled=FeatureY=+k8s:validateFalse="field T1.S3.FeatureY" + // +k8s:ifOptionEnabled(FeatureX)=+k8s:validateFalse="field T1.S3.FeatureX" + // +k8s:ifOptionDisabled(FeatureY)=+k8s:validateFalse="field T1.S3.FeatureY" S3 string `json:"s3"` } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/options.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/options.go index 165ac4db1407e..70a527776b864 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/options.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/options.go @@ -18,7 +18,6 @@ package validators import ( "fmt" - "strings" "k8s.io/gengo/v2" "k8s.io/gengo/v2/types" @@ -42,14 +41,20 @@ const ( ) func (o optionDeclarativeValidator) ExtractValidations(t *types.Type, comments []string) (Validations, error) { - enabledTagValues, hasEnabledTags := gengo.ExtractCommentTags("+", comments)[ifOptionEnabledTag] - disabledTagValues, hasDisabledTags := gengo.ExtractCommentTags("+", comments)[ifOptionDisabledTag] + tags, err := gengo.ExtractFunctionalCommentTags("+", []string{ifOptionEnabledTag, ifOptionDisabledTag}, comments) + if err != nil { + return Validations{}, err + } + + enabledTags, hasEnabledTags := tags[ifOptionEnabledTag] + disabledTags, hasDisabledTags := tags[ifOptionDisabledTag] if !hasEnabledTags && !hasDisabledTags { return Validations{}, nil } + var functions []FunctionGen var variables []VariableGen - for _, v := range enabledTagValues { + for _, v := range enabledTags { optionName, validations, err := o.parseIfOptionsTag(t, v) if err != nil { return Validations{}, err @@ -59,7 +64,7 @@ func (o optionDeclarativeValidator) ExtractValidations(t *types.Type, comments [ } variables = append(variables, validations.Variables...) } - for _, v := range disabledTagValues { + for _, v := range disabledTags { optionName, validations, err := o.parseIfOptionsTag(t, v) if err != nil { return Validations{}, err @@ -75,18 +80,15 @@ func (o optionDeclarativeValidator) ExtractValidations(t *types.Type, comments [ }, nil } -func (o optionDeclarativeValidator) parseIfOptionsTag(t *types.Type, tagValue string) (string, Validations, error) { - parts := strings.SplitN(tagValue, "=", 2) - if len(parts) != 2 { - return "", Validations{}, fmt.Errorf("invalid value %q for option %q", tagValue, parts[0]) +func (o optionDeclarativeValidator) parseIfOptionsTag(t *types.Type, tag gengo.Tag) (string, Validations, error) { + if len(tag.Args) != 1 { + return "", Validations{}, fmt.Errorf("tag %q requires 1 argument", tag.Name) } - optionName := parts[0] - embeddedValidation := parts[1] - validations, err := o.cfg.EmbedValidator.ExtractValidations(t, []string{embeddedValidation}) + validations, err := o.cfg.EmbedValidator.ExtractValidations(t, []string{tag.Value}) if err != nil { return "", Validations{}, err } - return optionName, validations, nil + return tag.Args[0], validations, nil } func (optionDeclarativeValidator) Docs() []TagDoc { diff --git a/vendor/k8s.io/gengo/v2/comments.go b/vendor/k8s.io/gengo/v2/comments.go index ba49c432be74b..966a61a983cb8 100644 --- a/vendor/k8s.io/gengo/v2/comments.go +++ b/vendor/k8s.io/gengo/v2/comments.go @@ -17,8 +17,10 @@ limitations under the License. package gengo import ( + "bytes" "fmt" "strings" + "unicode" ) // ExtractCommentTags parses comments for lines of the form: @@ -81,3 +83,199 @@ func ExtractSingleBoolCommentTag(marker string, key string, defaultVal bool, lin } return false, fmt.Errorf("tag value for %q is not boolean: %q", key, values[0]) } + +// ExtractFunctionalCommentTags parses comments for special metadata tags. The +// marger argument should be unique enough to identify the tags needed, and +// should not be a marker for tags you don't want, or else the caller takes +// responsibility for making that distinction. +// +// The prefixes argument further selects among comment lines which match the +// marker. If this is nil or empty, all lines which match the marker are +// considered. If this is specified, only lines with begin with marker + +// prefix will be considered. This is useful when a common marker is used which +// may match lines which fail this syntax (e.g. which predate this definition). +// +// This function looks for input lines of the following forms: +// - 'marker' + "key=value" +// - 'marker' + "key()=value" +// - 'marker' + "key(arg)=value" +// +// The arg is optional. If not specified (either as "key=value" or as +// "key()=value"), the resulting Tag will have an empty Args list. +// +// The value is optional. If not specified, the resulting Tag will have "" as +// the value. +// +// Tag comment-lines may have a trailing end-of-line comment. +// +// The map returned here is keyed by the Tag's name without args. +// +// A tag can be specified more than one time and all values are returned. If +// the resulting map has an entry for a key, the value (a slice) is guaranteed +// to have at least 1 element. +// +// Example: if you pass "+" as the marker, and the following lines are in +// the comments: +// +// +foo=val1 // foo +// +bar +// +foo=val2 // also foo +// +baz="qux" +// +foo(arg) // still foo +// +// Then this function will return: +// +// map[string][]Tag{ +// "foo": []Tag{{ +// Name: "foo", +// Args: nil, +// Value: "val1", +// }, { +// Name: "foo", +// Args: nil, +// Value: "val2", +// }, { +// Name: "foo", +// Args: []string{"arg"}, +// Value: "", +// }, { +// Name: "bar", +// Args: nil, +// Value: "" +// }, { +// Name: "baz", +// Args: nil, +// Value: "\"qux\"" +// }} +func ExtractFunctionalCommentTags(marker string, prefixes []string, lines []string) (map[string][]Tag, error) { + stripComment := func(in string) string { + parts := strings.SplitN(in, "//", 2) + return strings.TrimSpace(parts[0]) + } + + matches := func(line string) bool { + if len(line) == 0 { + return false + } + if !strings.HasPrefix(line, marker) { + return false + } + if len(prefixes) == 0 { + return true + } + for _, pfx := range prefixes { + if strings.HasPrefix(line, marker+pfx) { + return true + } + } + return false + } + + out := map[string][]Tag{} + for _, line := range lines { + line = strings.TrimSpace(line) + if !matches(line) { + continue + } + line = stripComment(line) + kv := strings.SplitN(line[len(marker):], "=", 2) + key := kv[0] + val := "" + if len(kv) == 2 { + val = kv[1] + } + + tag := Tag{} + if name, args, err := parseTagKey(key); err != nil { + return nil, err + } else { + tag.Name, tag.Args = name, args + } + tag.Value = val + + out[tag.Name] = append(out[tag.Name], tag) + } + return out, nil +} + +// Tag represents a single comment tag. +type Tag struct { + // Name is the name of the tag with no arguments. + Name string + // Args is a list of optional arguments to the tag. + Args []string + // Value is the value of the tag. + Value string +} + +func (t Tag) String() string { + buf := bytes.Buffer{} + buf.WriteString(t.Name) + if len(t.Args) > 0 { + buf.WriteString("(") + for i, a := range t.Args { + if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(a) + } + buf.WriteString(")") + } + return buf.String() +} + +// parseTagKey parses the key part of an extended comment tag, including +// optional arguments. The input is assumed to be the entire text of the +// original input after the marker, up to the '=' or end-of-line. +// +// At the moment, arguments are very strictly formatted (see parseTagArgs) and +// whitespace is not allowed. +// +// This function returns the key name and arguments. +func parseTagKey(input string) (string, []string, error) { + parts := strings.SplitN(input, "(", 2) + key := parts[0] + var args []string + if len(parts) == 2 { + if ret, err := parseTagArgs(parts[1]); err != nil { + return key, nil, fmt.Errorf("failed to parse tag args: %v", err) + } else { + args = ret + } + } + return key, args, nil +} + +// parseTagArgs parses the arguments part of an extended comment tag. The input +// is assumed to be the entire text of the original input after the opening +// '(', including the trailing ')'. +// +// At the moment this assumes that the entire string between the opening '(' +// and the trailing ')' is a single Go-style identifier token, but in the +// future could be extended to have multiple arguments with actual syntax. The +// single token may consist only of letters and digits. Whitespace is not +// allowed. +func parseTagArgs(input string) ([]string, error) { + // This is really dumb, but should be extendable to a "real" parser if + // needed. + runes := []rune(input) + for i, r := range runes { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + continue + } + if r == ',' { + return nil, fmt.Errorf("multiple arguments are not supported: %q", input) + } + if r == ')' { + if i != len(runes)-1 { + return nil, fmt.Errorf("unexpected characters after ')': %q", string(runes[i:])) + } + if i == 0 { + return nil, nil + } + return []string{string(runes[:i])}, nil + } + return nil, fmt.Errorf("unsupported character: %q", string(r)) + } + return nil, fmt.Errorf("no closing ')' found: %q", input) +} diff --git a/vendor/k8s.io/gengo/v2/comments_test.go b/vendor/k8s.io/gengo/v2/comments_test.go new file mode 100644 index 0000000000000..938596b150800 --- /dev/null +++ b/vendor/k8s.io/gengo/v2/comments_test.go @@ -0,0 +1,239 @@ +package gengo + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestExtractExtendedCommentTags(t *testing.T) { + mktags := func(t ...Tag) []Tag { return t } + mkstrs := func(s ...string) []string { return s } + + cases := []struct { + name string + comments []string + prefixes []string + expect map[string][]Tag + }{{ + name: "no args", + comments: []string{ + "Human comment that is ignored", + "+simpleNoVal", + "+simpleWithVal=val", + "+duplicateNoVal", + "+duplicateNoVal", + "+duplicateWithVal=val1", + "+duplicateWithVal=val2", + }, + expect: map[string][]Tag{ + "simpleNoVal": mktags(Tag{"simpleNoVal", nil, ""}), + "simpleWithVal": mktags(Tag{"simpleWithVal", nil, "val"}), + "duplicateNoVal": mktags( + Tag{"duplicateNoVal", nil, ""}, + Tag{"duplicateNoVal", nil, ""}), + "duplicateWithVal": mktags( + Tag{"duplicateWithVal", nil, "val1"}, + Tag{"duplicateWithVal", nil, "val2"}), + }, + }, { + name: "empty parens", + comments: []string{ + "Human comment that is ignored", + "+simpleNoVal()", + "+simpleWithVal()=val", + "+duplicateNoVal()", + "+duplicateNoVal()", + "+duplicateWithVal()=val1", + "+duplicateWithVal()=val2", + }, + expect: map[string][]Tag{ + "simpleNoVal": mktags(Tag{"simpleNoVal", nil, ""}), + "simpleWithVal": mktags(Tag{"simpleWithVal", nil, "val"}), + "duplicateNoVal": mktags( + Tag{"duplicateNoVal", nil, ""}, + Tag{"duplicateNoVal", nil, ""}), + "duplicateWithVal": mktags( + Tag{"duplicateWithVal", nil, "val1"}, + Tag{"duplicateWithVal", nil, "val2"}), + }, + }, { + name: "mixed no args and empty parens", + comments: []string{ + "Human comment that is ignored", + "+noVal", + "+withVal=val1", + "+noVal()", + "+withVal()=val2", + }, + expect: map[string][]Tag{ + "noVal": mktags( + Tag{"noVal", nil, ""}, + Tag{"noVal", nil, ""}), + "withVal": mktags( + Tag{"withVal", nil, "val1"}, + Tag{"withVal", nil, "val2"}), + }, + }, { + name: "with args", + comments: []string{ + "Human comment that is ignored", + "+simpleNoVal(arg)", + "+simpleWithVal(arg)=val", + "+duplicateNoVal(arg1)", + "+duplicateNoVal(arg2)", + "+duplicateWithVal(arg1)=val1", + "+duplicateWithVal(arg2)=val2", + }, + expect: map[string][]Tag{ + "simpleNoVal": mktags(Tag{"simpleNoVal", mkstrs("arg"), ""}), + "simpleWithVal": mktags(Tag{"simpleWithVal", mkstrs("arg"), "val"}), + "duplicateNoVal": mktags( + Tag{"duplicateNoVal", mkstrs("arg1"), ""}, + Tag{"duplicateNoVal", mkstrs("arg2"), ""}), + "duplicateWithVal": mktags( + Tag{"duplicateWithVal", mkstrs("arg1"), "val1"}, + Tag{"duplicateWithVal", mkstrs("arg2"), "val2"}), + }, + }, { + name: "mixed no args and empty parens", + comments: []string{ + "Human comment that is ignored", + "+noVal", + "+withVal=val1", + "+noVal(arg)", + "+withVal(arg)=val2", + }, + expect: map[string][]Tag{ + "noVal": mktags( + Tag{"noVal", nil, ""}, + Tag{"noVal", mkstrs("arg"), ""}), + "withVal": mktags( + Tag{"withVal", nil, "val1"}, + Tag{"withVal", mkstrs("arg"), "val2"}), + }, + }, { + name: "prefixes", + comments: []string{ + "Human comment that is ignored", + "+pfx1Foo", + "+pfx2Foo=val1", + "+pfx3Bar", + "+pfx4Bar=val", + "+pfx1Foo(arg)", + "+pfx2Foo(arg)=val2", + "+pfx3Bar(arg)", + "+pfx4Bar(arg)=val", + }, + prefixes: []string{"pfx1", "pfx2"}, + expect: map[string][]Tag{ + "pfx1Foo": mktags( + Tag{"pfx1Foo", nil, ""}, + Tag{"pfx1Foo", mkstrs("arg"), ""}), + "pfx2Foo": mktags( + Tag{"pfx2Foo", nil, "val1"}, + Tag{"pfx2Foo", mkstrs("arg"), "val2"}), + }, + }} + + for _, tc := range cases { + result, _ := ExtractExtendedCommentTags("+", tc.prefixes, tc.comments) + if !reflect.DeepEqual(result, tc.expect) { + t.Errorf("case %q: wrong result:\n%v", tc.name, cmp.Diff(tc.expect, result)) + } + } +} + +func TestParseTagKey(t *testing.T) { + mkss := func(s ...string) []string { return s } + + cases := []struct { + input string + expectKey string + expectArgs []string + err bool + }{ + {"simple", "simple", nil, false}, + {"parens()", "parens", nil, false}, + {"withArgLower(arg)", "withArgLower", mkss("arg"), false}, + {"withArgUpper(ARG)", "withArgUpper", mkss("ARG"), false}, + {"withArgMixed(ArG)", "withArgMixed", mkss("ArG"), false}, + {"withArgs(arg1, arg2)", "", nil, true}, + {"trailingParen(arg))", "", nil, true}, + {"trailingSpace(arg) ", "", nil, true}, + {"argWithDash(arg-name) ", "", nil, true}, + {"argWithUnder(arg_name) ", "", nil, true}, + } + for _, tc := range cases { + key, args, err := parseTagKey(tc.input) + if err != nil && tc.err == false { + t.Errorf("[%q]: expected success, got: %v", tc.input, err) + continue + } + if err == nil { + if tc.err == true { + t.Errorf("[%q]: expected failure, got: %v(%v)", tc.input, key, args) + continue + } + if key != tc.expectKey { + t.Errorf("[%q]\nexpected key: %q, got: %q", tc.input, tc.expectKey, key) + } + if len(args) != len(tc.expectArgs) { + t.Errorf("[%q]: expected %d args, got: %q", tc.input, len(tc.expectArgs), args) + continue + } + for i := range tc.expectArgs { + if want, got := tc.expectArgs[i], args[i]; got != want { + t.Errorf("[%q]\nexpected %q, got %q", tc.input, want, got) + } + } + } + } +} + +func TestParseTagArgs(t *testing.T) { + mkss := func(s ...string) []string { return s } + + cases := []struct { + input string + expect []string + err bool + }{ + {")", nil, false}, + {"lower)", mkss("lower"), false}, + {"CAPITAL)", mkss("CAPITAL"), false}, + {"MiXeD)", mkss("MiXeD"), false}, + {"mIxEd)", mkss("mIxEd"), false}, + {"_under)", nil, true}, + {"has space", nil, true}, + {"has-dash", nil, true}, + {`"hasQuotes"`, nil, true}, + {"multiple, args)", nil, true}, + {"noClosingParen", nil, true}, + {"extraParen))", nil, true}, + {"trailingSpace) ", nil, true}, + } + for _, tc := range cases { + ret, err := parseTagArgs(tc.input) + if err != nil && tc.err == false { + t.Errorf("[%q]: expected success, got: %v", tc.input, err) + continue + } + if err == nil { + if tc.err == true { + t.Errorf("[%q]: expected failure, got: %q", tc.input, ret) + continue + } + if len(ret) != len(tc.expect) { + t.Errorf("[%q]: expected %d results, got: %q", tc.input, len(tc.expect), ret) + continue + } + for i := range tc.expect { + if want, got := tc.expect[i], ret[i]; got != want { + t.Errorf("[%q]\nexpected %q, got %q", tc.input, want, got) + } + } + } + } +}