diff --git a/rule/struct_tag.go b/rule/struct_tag.go index e6a69e6..5cfa31c 100644 --- a/rule/struct_tag.go +++ b/rule/struct_tag.go @@ -13,7 +13,62 @@ import ( // StructTagRule lints struct tags. type StructTagRule struct { - userDefined map[string][]string // map: key -> []option + userDefined map[tagKey][]string // map: key -> []option +} + +type tagKey string + +const ( + keyASN1 tagKey = "asn1" + keyBSON tagKey = "bson" + keyDatastore tagKey = "datastore" + keyDefault tagKey = "default" + keyJSON tagKey = "json" + keyMapstructure tagKey = "mapstructure" + keyProperties tagKey = "properties" + keyProtobuf tagKey = "protobuf" + keyRequired tagKey = "required" + keyTOML tagKey = "toml" + keyURL tagKey = "url" + keyValidate tagKey = "validate" + keyXML tagKey = "xml" + keyYAML tagKey = "yaml" +) + +type tagChecker func(checkCtx *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) + +// populate tag checkers map +var tagCheckers = map[tagKey]tagChecker{ + keyASN1: checkASN1Tag, + keyBSON: checkBSONTag, + keyDatastore: checkDatastoreTag, + keyDefault: checkDefaultTag, + keyJSON: checkJSONTag, + keyMapstructure: checkMapstructureTag, + keyProperties: checkPropertiesTag, + keyProtobuf: checkProtobufTag, + keyRequired: checkRequiredTag, + keyTOML: checkTOMLTag, + keyURL: checkURLTag, + keyValidate: checkValidateTag, + keyXML: checkXMLTag, + keyYAML: checkYAMLTag, +} + +type checkContext struct { + userDefined map[tagKey][]string // map: key -> []option + usedTagNbr map[int]bool // list of used tag numbers + usedTagName map[string]bool // list of used tag keys + isAtLeastGo124 bool +} + +func (checkCtx checkContext) isUserDefined(key tagKey, opt string) bool { + if checkCtx.userDefined == nil { + return false + } + + options := checkCtx.userDefined[key] + return slices.Contains(options, opt) } // Configure validates the rule configuration, and configures the rule accordingly. @@ -28,7 +83,8 @@ func (r *StructTagRule) Configure(arguments lint.Arguments) error { if err != nil { return err } - r.userDefined = make(map[string][]string, len(arguments)) + + r.userDefined = make(map[tagKey][]string, len(arguments)) for _, arg := range arguments { item, ok := arg.(string) if !ok { @@ -38,12 +94,13 @@ func (r *StructTagRule) Configure(arguments lint.Arguments) error { if len(parts) < 2 { return fmt.Errorf("invalid argument to the %s rule. Expecting a string of the form key[,option]+, got %s", r.Name(), item) } - key := strings.TrimSpace(parts[0]) + key := tagKey(strings.TrimSpace(parts[0])) for i := 1; i < len(parts); i++ { option := strings.TrimSpace(parts[i]) r.userDefined[key] = append(r.userDefined[key], option) } } + return nil } @@ -58,6 +115,7 @@ func (r *StructTagRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure onFailure: onFailure, userDefined: r.userDefined, isAtLeastGo124: file.Pkg.IsAtLeastGoVersion(lint.Go124), + tagCheckers: tagCheckers, } ast.Walk(w, file.AST) @@ -72,10 +130,9 @@ func (*StructTagRule) Name() string { type lintStructTagRule struct { onFailure func(lint.Failure) - userDefined map[string][]string // map: key -> []option - usedTagNbr map[int]bool // list of used tag numbers - usedTagName map[string]bool // list of used tag keys + userDefined map[tagKey][]string // map: key -> []option isAtLeastGo124 bool + tagCheckers map[tagKey]tagChecker } func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor { @@ -86,11 +143,16 @@ func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor { return nil // skip empty structs } - w.usedTagNbr = map[int]bool{} - w.usedTagName = map[string]bool{} + checkCtx := &checkContext{ + userDefined: w.userDefined, + usedTagNbr: map[int]bool{}, + usedTagName: map[string]bool{}, + isAtLeastGo124: w.isAtLeastGo124, + } + for _, f := range n.Fields.List { if f.Tag != nil { - w.checkTaggedField(f) + w.checkTaggedField(checkCtx, f) } } } @@ -98,30 +160,44 @@ func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor { return w } -const ( - keyASN1 = "asn1" - keyBSON = "bson" - keyDatastore = "datastore" - keyDefault = "default" - keyJSON = "json" - keyMapstructure = "mapstructure" - keyProperties = "properties" - keyProtobuf = "protobuf" - keyRequired = "required" - keyTOML = "toml" - keyURL = "url" - keyValidate = "validate" - keyXML = "xml" - keyYAML = "yaml" -) +// checkTaggedField checks the tag of the given field. +// precondition: the field has a tag +func (w lintStructTagRule) checkTaggedField(checkCtx *checkContext, f *ast.Field) { + if len(f.Names) > 0 && !f.Names[0].IsExported() { + w.addFailuref(f, "tag on not-exported field %s", f.Names[0].Name) + } -func (w lintStructTagRule) checkTagNameIfNeed(tag *structtag.Tag) (string, bool) { + tags, err := structtag.Parse(strings.Trim(f.Tag.Value, "`")) + if err != nil || tags == nil { + w.addFailuref(f.Tag, "malformed tag") + return + } + + for _, tag := range tags.Tags() { + if msg, ok := w.checkTagNameIfNeed(checkCtx, tag); !ok { + w.addFailureWithTagKey(f.Tag, msg, tag.Key) + } + + checker, ok := w.tagCheckers[tagKey(tag.Key)] + if !ok { + continue // we don't have a checker for the tag + } + + msg, ok := checker(checkCtx, tag, f.Type) + if !ok { + w.addFailureWithTagKey(f.Tag, msg, tag.Key) + } + } +} + +func (w lintStructTagRule) checkTagNameIfNeed(checkCtx *checkContext, tag *structtag.Tag) (message string, succeeded bool) { isUnnamedTag := tag.Name == "" || tag.Name == "-" if isUnnamedTag { return "", true } - switch tag.Key { + key := tagKey(tag.Key) + switch key { case keyBSON, keyJSON, keyXML, keyYAML, keyProtobuf: default: return "", true @@ -134,22 +210,23 @@ func (w lintStructTagRule) checkTagNameIfNeed(tag *structtag.Tag) (string, bool) // We concat the key and name as the mapping key here // to allow the same tag name in different tag type. - key := tag.Key + ":" + tagName - if _, ok := w.usedTagName[key]; ok { - return fmt.Sprintf("duplicate tag name: '%s'", tagName), false + mapKey := tag.Key + ":" + tagName + if _, ok := checkCtx.usedTagName[mapKey]; ok { + return fmt.Sprintf("duplicated tag name %q", tagName), false } - w.usedTagName[key] = true + checkCtx.usedTagName[mapKey] = true return "", true } func (lintStructTagRule) getTagName(tag *structtag.Tag) string { - switch tag.Key { + key := tagKey(tag.Key) + switch key { case keyProtobuf: for _, option := range tag.Options { - if tagName, found := strings.CutPrefix(option, "name="); found { - return tagName + if tagKey, found := strings.CutPrefix(option, "name="); found { + return tagKey } } return "" // protobuf tag lacks 'name' option @@ -158,218 +235,250 @@ func (lintStructTagRule) getTagName(tag *structtag.Tag) string { } } -// checkTaggedField checks the tag of the given field. -// precondition: the field has a tag -func (w lintStructTagRule) checkTaggedField(f *ast.Field) { - if len(f.Names) > 0 && !f.Names[0].IsExported() { - w.addFailure(f, "tag on not-exported field "+f.Names[0].Name) - } - - tags, err := structtag.Parse(strings.Trim(f.Tag.Value, "`")) - if err != nil || tags == nil { - w.addFailure(f.Tag, "malformed tag") - return - } - - for _, tag := range tags.Tags() { - if msg, ok := w.checkTagNameIfNeed(tag); !ok { - w.addFailure(f.Tag, msg) - } - - switch key := tag.Key; key { - case keyASN1: - msg, ok := w.checkASN1Tag(f.Type, tag) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyBSON: - msg, ok := w.checkBSONTag(tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyDatastore: - msg, ok := w.checkDatastoreTag(tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyDefault: - if !w.typeValueMatch(f.Type, tag.Name) { - w.addFailure(f.Tag, "field's type and default value's type mismatch") - } - case keyJSON: - msg, ok := w.checkJSONTag(tag.Name, tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyMapstructure: - msg, ok := w.checkMapstructureTag(tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyProperties: - msg, ok := w.checkPropertiesTag(f.Type, tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyProtobuf: - msg, ok := w.checkProtobufTag(tag) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyRequired: - if tag.Name != "true" && tag.Name != "false" { - w.addFailure(f.Tag, "required should be 'true' or 'false'") - } - case keyTOML: - msg, ok := w.checkTOMLTag(tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyURL: - msg, ok := w.checkURLTag(tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyValidate: - opts := append([]string{tag.Name}, tag.Options...) - msg, ok := w.checkValidateTag(opts) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyXML: - msg, ok := w.checkXMLTag(tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - case keyYAML: - msg, ok := w.checkYAMLTag(tag.Options) - if !ok { - w.addFailure(f.Tag, msg) - } - default: - // unknown key - } - } -} - -func (w lintStructTagRule) checkASN1Tag(t ast.Expr, tag *structtag.Tag) (string, bool) { +func checkASN1Tag(checkCtx *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) { checkList := append(tag.Options, tag.Name) for _, opt := range checkList { switch opt { case "application", "explicit", "generalized", "ia5", "omitempty", "optional", "set", "utf8": - + // do nothing default: - if strings.HasPrefix(opt, "tag:") { - parts := strings.Split(opt, ":") - tagNumber := parts[1] - number, err := strconv.Atoi(tagNumber) - if err != nil { - return fmt.Sprintf("ASN1 tag must be a number, got '%s'", tagNumber), false - } - if w.usedTagNbr[number] { - return fmt.Sprintf("duplicated tag number %v", number), false - } - w.usedTagNbr[number] = true - - continue + msg, ok := checkCompoundANS1Option(checkCtx, opt, fieldType) + if !ok { + return msg, false } - - if strings.HasPrefix(opt, "default:") { - parts := strings.Split(opt, ":") - if len(parts) < 2 { - return "malformed default for ASN1 tag", false - } - if !w.typeValueMatch(t, parts[1]) { - return "field's type and default value's type mismatch", false - } - - continue - } - - if w.isUserDefined(keyASN1, opt) { - continue - } - - return fmt.Sprintf("unknown option '%s' in ASN1 tag", opt), false } } return "", true } -func (w lintStructTagRule) checkBSONTag(options []string) (string, bool) { - for _, opt := range options { +func checkCompoundANS1Option(checkCtx *checkContext, opt string, fieldType ast.Expr) (message string, succeeded bool) { + key, value, _ := strings.Cut(opt, ":") + switch key { + case "tag": + number, err := strconv.Atoi(value) + if err != nil { + return fmt.Sprintf("tag must be a number but is %q", value), false + } + if checkCtx.usedTagNbr[number] { + return fmt.Sprintf(msgDuplicatedTagNumber, number), false + } + checkCtx.usedTagNbr[number] = true + case "default": + if !typeValueMatch(fieldType, value) { + return msgTypeMismatch, false + } + default: + if !checkCtx.isUserDefined(keyASN1, opt) { + return fmt.Sprintf(msgUnknownOption, opt), false + } + } + return "", true +} + +func checkDatastoreTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + for _, opt := range tag.Options { + switch opt { + case "flatten", "noindex", "omitempty": + default: + if checkCtx.isUserDefined(keyDatastore, opt) { + continue + } + return fmt.Sprintf(msgUnknownOption, opt), false + } + } + + return "", true +} + +func checkDefaultTag(_ *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) { + if !typeValueMatch(fieldType, tag.Name) { + return msgTypeMismatch, false + } + + return "", true +} + +func checkBSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + for _, opt := range tag.Options { switch opt { case "inline", "minsize", "omitempty": default: - if w.isUserDefined(keyBSON, opt) { + if checkCtx.isUserDefined(keyBSON, opt) { continue } - return fmt.Sprintf("unknown option '%s' in BSON tag", opt), false + return fmt.Sprintf(msgUnknownOption, opt), false } } return "", true } -func (w lintStructTagRule) checkJSONTag(name string, options []string) (string, bool) { - for _, opt := range options { +func checkJSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + for _, opt := range tag.Options { switch opt { case "omitempty", "string": case "": // special case for JSON key "-" - if name != "-" { - return "option can not be empty in JSON tag", false + if tag.Name != "-" { + return "option can not be empty", false } case "omitzero": - if w.isAtLeastGo124 { + if checkCtx.isAtLeastGo124 { continue } - fallthrough + return `prior Go 1.24, option "omitzero" is unsupported`, false default: - if w.isUserDefined(keyJSON, opt) { + if checkCtx.isUserDefined(keyJSON, opt) { continue } - return fmt.Sprintf("unknown option '%s' in JSON tag", opt), false + return fmt.Sprintf(msgUnknownOption, opt), false } } return "", true } -func (w lintStructTagRule) checkXMLTag(options []string) (string, bool) { - for _, opt := range options { +func checkMapstructureTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + for _, opt := range tag.Options { switch opt { - case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr": + case "omitempty", "reminder", "squash": default: - if w.isUserDefined(keyXML, opt) { + if checkCtx.isUserDefined(keyMapstructure, opt) { continue } - return fmt.Sprintf("unknown option '%s' in XML tag", opt), false + return fmt.Sprintf(msgUnknownOption, opt), false } } return "", true } -func (w lintStructTagRule) checkYAMLTag(options []string) (string, bool) { +func checkPropertiesTag(_ *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) { + options := tag.Options + if len(options) == 0 { + return "", true + } + + seenOptions := map[string]bool{} for _, opt := range options { - switch opt { - case "flow", "inline", "omitempty": - default: - if w.isUserDefined(keyYAML, opt) { - continue - } - return fmt.Sprintf("unknown option '%s' in YAML tag", opt), false + msg, ok := fmt.Sprintf("unknown or malformed option %q", opt), false + if key, value, found := strings.Cut(opt, "="); found { + msg, ok = checkCompoundPropertiesOption(key, value, fieldType, seenOptions) + } + + if !ok { + return msg, false } } return "", true } -func (w lintStructTagRule) checkURLTag(options []string) (string, bool) { +func checkCompoundPropertiesOption(key, value string, fieldType ast.Expr, seenOptions map[string]bool) (message string, succeeded bool) { + if _, ok := seenOptions[key]; ok { + return fmt.Sprintf(msgDuplicatedOption, key), false + } + seenOptions[key] = true + + if strings.TrimSpace(value) == "" { + return fmt.Sprintf("option %q not of the form %s=value", key, key), false + } + + switch key { + case "default": + if !typeValueMatch(fieldType, value) { + return msgTypeMismatch, false + } + case "layout": + if gofmt(fieldType) != "time.Time" { + return "layout option is only applicable to fields of type time.Time", false + } + } + + return "", true +} + +func checkProtobufTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + // check name + switch tag.Name { + case "bytes", "fixed32", "fixed64", "group", "varint", "zigzag32", "zigzag64": + // do nothing + default: + return fmt.Sprintf("invalid tag name %q", tag.Name), false + } + + return checkProtobufOptions(checkCtx, tag.Options) +} + +func checkProtobufOptions(checkCtx *checkContext, options []string) (message string, succeeded bool) { + seenOptions := map[string]bool{} + hasName := false + for _, opt := range options { + opt := strings.Split(opt, "=")[0] + + if number, err := strconv.Atoi(opt); err == nil { + _, alreadySeen := checkCtx.usedTagNbr[number] + if alreadySeen { + return fmt.Sprintf(msgDuplicatedTagNumber, number), false + } + checkCtx.usedTagNbr[number] = true + continue // option is an integer + } + + switch opt { + case "json", "opt", "proto3", "rep", "req": + // do nothing + case "name": + hasName = true + default: + if checkCtx.isUserDefined(keyProtobuf, opt) { + continue + } + return fmt.Sprintf(msgUnknownOption, opt), false + } + + _, alreadySeen := seenOptions[opt] + if alreadySeen { + return fmt.Sprintf(msgDuplicatedOption, opt), false + } + seenOptions[opt] = true + } + + if !hasName { + return `mandatory option "name" not found`, false + } + + return "", true +} + +func checkRequiredTag(_ *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + switch tag.Name { + case "true", "false": + return "", true + default: + return `required should be "true" or "false"`, false + } +} + +func checkTOMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + for _, opt := range tag.Options { + switch opt { + case "omitempty": + default: + if checkCtx.isUserDefined(keyTOML, opt) { + continue + } + return fmt.Sprintf(msgUnknownOption, opt), false + } + } + + return "", true +} + +func checkURLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { var delimiter = "" - for _, opt := range options { + for _, opt := range tag.Options { switch opt { case "int", "omitempty", "numbered", "brackets": case "unix", "unixmilli", "unixnano": // TODO : check that the field is of type time.Time @@ -378,66 +487,37 @@ func (w lintStructTagRule) checkURLTag(options []string) (string, bool) { delimiter = opt continue } - return fmt.Sprintf("can not set both '%s' and '%s' as delimiters in URL tag", opt, delimiter), false + return fmt.Sprintf("can not set both %q and %q as delimiters", opt, delimiter), false default: - if w.isUserDefined(keyURL, opt) { + if checkCtx.isUserDefined(keyURL, opt) { continue } - return fmt.Sprintf("unknown option '%s' in URL tag", opt), false + return fmt.Sprintf(msgUnknownOption, opt), false } } return "", true } -func (w lintStructTagRule) checkDatastoreTag(options []string) (string, bool) { - for _, opt := range options { - switch opt { - case "flatten", "noindex", "omitempty": - default: - if w.isUserDefined(keyDatastore, opt) { - continue - } - return fmt.Sprintf("unknown option '%s' in Datastore tag", opt), false - } - } - - return "", true -} - -func (w lintStructTagRule) checkMapstructureTag(options []string) (string, bool) { - for _, opt := range options { - switch opt { - case "omitempty", "reminder", "squash": - default: - if w.isUserDefined(keyMapstructure, opt) { - continue - } - return fmt.Sprintf("unknown option '%s' in Mapstructure tag", opt), false - } - } - - return "", true -} - -func (w lintStructTagRule) checkValidateTag(options []string) (string, bool) { +func checkValidateTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { previousOption := "" seenKeysOption := false + options := append([]string{tag.Name}, tag.Options...) for _, opt := range options { switch opt { case "keys": if previousOption != "dive" { - return "option 'keys' must follow a 'dive' option in validate tag", false + return `option "keys" must follow a "dive" option`, false } seenKeysOption = true case "endkeys": if !seenKeysOption { - return "option 'endkeys' without a previous 'keys' option in validate tag", false + return `option "endkeys" without a previous "keys" option`, false } seenKeysOption = false default: parts := strings.Split(opt, "|") - errMsg, ok := w.checkValidateOptionsAlternatives(parts) + errMsg, ok := checkValidateOptionsAlternatives(checkCtx, parts) if !ok { return errMsg, false } @@ -448,47 +528,60 @@ func (w lintStructTagRule) checkValidateTag(options []string) (string, bool) { return "", true } -func (w lintStructTagRule) checkTOMLTag(options []string) (string, bool) { - for _, opt := range options { +func checkXMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + for _, opt := range tag.Options { switch opt { - case "omitempty": + case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr": default: - if w.isUserDefined(keyTOML, opt) { + if checkCtx.isUserDefined(keyXML, opt) { continue } - return fmt.Sprintf("unknown option '%s' in TOML tag", opt), false + return fmt.Sprintf(msgUnknownOption, opt), false } } return "", true } -func (w lintStructTagRule) checkValidateOptionsAlternatives(alternatives []string) (string, bool) { +func checkYAMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { + for _, opt := range tag.Options { + switch opt { + case "flow", "inline", "omitempty": + default: + if checkCtx.isUserDefined(keyYAML, opt) { + continue + } + return fmt.Sprintf(msgUnknownOption, opt), false + } + } + + return "", true +} + +func checkValidateOptionsAlternatives(checkCtx *checkContext, alternatives []string) (message string, succeeded bool) { for _, alternative := range alternatives { alternative := strings.TrimSpace(alternative) - parts := strings.Split(alternative, "=") - switch len(parts) { - case 1: - badOpt, ok := areValidateOpts(parts[0]) - if ok || w.isUserDefined(keyValidate, badOpt) { - continue - } - return fmt.Sprintf("unknown option '%s' in validate tag", badOpt), false - case 2: - lhs := parts[0] + lhs, _, found := strings.Cut(alternative, "=") + if found { _, ok := validateLHS[lhs] - if ok || w.isUserDefined(keyValidate, lhs) { + if ok || checkCtx.isUserDefined(keyValidate, lhs) { continue } - return fmt.Sprintf("unknown option '%s' in validate tag", lhs), false - default: - return fmt.Sprintf("malformed options '%s' in validate tag, not expected more than one '='", alternative), false + return fmt.Sprintf(msgUnknownOption, lhs), false } + + badOpt, ok := areValidateOpts(alternative) + if ok || checkCtx.isUserDefined(keyValidate, badOpt) { + continue + } + + return fmt.Sprintf(msgUnknownOption, badOpt), false } + return "", true } -func (lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool { +func typeValueMatch(t ast.Expr, val string) bool { tID, ok := t.(*ast.Ident) if !ok { return true @@ -512,115 +605,19 @@ func (lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool { return typeMatches } -func (w lintStructTagRule) checkPropertiesTag(t ast.Expr, options []string) (string, bool) { - if len(options) == 0 { - return "", true - } - hasDefault := false - for _, opt := range options { - key, val, found := strings.Cut(opt, "=") - switch key { - case "default": - if hasDefault { - return "properties tag accepts only one default option", false - } - hasDefault = true - - if !found { - return "malformed default for properties tag", false - } - - if !w.typeValueMatch(t, val) { - return "field type and default value type mismatch in properties tag", false - } - case "layout": - if !found || strings.TrimSpace(val) == "" { - return "malformed layout option for properties tag", false - } - - if gofmt(t) != "time.Time" { - return "layout option is only applicable to fields of type time.Time in properties tag", false - } - default: - return fmt.Sprintf("unknown option %q in properties tag", opt), false - } - } - - return "", true +func (w lintStructTagRule) addFailureWithTagKey(n ast.Node, msg string, tagKey string) { + w.addFailuref(n, "%s in %s tag", msg, tagKey) } -func (w lintStructTagRule) checkProtobufTag(tag *structtag.Tag) (string, bool) { - // check name - switch tag.Name { - case "bytes", "fixed32", "fixed64", "group", "varint", "zigzag32", "zigzag64": - // do nothing - default: - return fmt.Sprintf("invalid protobuf tag name '%s'", tag.Name), false - } - - // check options - seenOptions := map[string]bool{} - for _, opt := range tag.Options { - if number, err := strconv.Atoi(opt); err == nil { - _, alreadySeen := w.usedTagNbr[number] - if alreadySeen { - return fmt.Sprintf("duplicated tag number %v", number), false - } - w.usedTagNbr[number] = true - continue // option is an integer - } - - switch { - case opt == "opt" || opt == "proto3" || opt == "rep" || opt == "req": - // do nothing - case strings.Contains(opt, "="): - o := strings.Split(opt, "=")[0] - _, alreadySeen := seenOptions[o] - if alreadySeen { - return fmt.Sprintf("protobuf tag has duplicated option '%s'", o), false - } - seenOptions[o] = true - continue - } - } - _, hasName := seenOptions["name"] - if !hasName { - return "protobuf tag lacks mandatory option 'name'", false - } - - for k := range seenOptions { - switch k { - case "name", "json": - // do nothing - default: - if w.isUserDefined(keyProtobuf, k) { - continue - } - return fmt.Sprintf("unknown option '%s' in protobuf tag", k), false - } - } - - return "", true -} - -func (w lintStructTagRule) addFailure(n ast.Node, msg string) { +func (w lintStructTagRule) addFailuref(n ast.Node, msg string, args ...any) { w.onFailure(lint.Failure{ Node: n, - Failure: msg, + Failure: fmt.Sprintf(msg, args...), Confidence: 1, }) } -func (w lintStructTagRule) isUserDefined(key, opt string) bool { - if w.userDefined == nil { - return false - } - - options := w.userDefined[key] - return slices.Contains(options, opt) -} - func areValidateOpts(opts string) (string, bool) { parts := strings.Split(opts, "|") for _, opt := range parts { @@ -633,6 +630,13 @@ func areValidateOpts(opts string) (string, bool) { return "", true } +const ( + msgDuplicatedOption = "duplicated option %q" + msgDuplicatedTagNumber = "duplicated tag number %v" + msgUnknownOption = "unknown option %q" + msgTypeMismatch = "type mismatch between field type and default value type" +) + var validateSingleOptions = map[string]struct{}{ "alpha": {}, "alphanum": {}, diff --git a/testdata/go1.24/struct_tag.go b/testdata/go1.24/struct_tag.go index 5982c76..91dadc7 100644 --- a/testdata/go1.24/struct_tag.go +++ b/testdata/go1.24/struct_tag.go @@ -1,22 +1,20 @@ package fixtures -import "time" - type decodeAndValidateRequest struct { // BEAWRE : the flag of URLParam should match the const string URLParam URLParam string `json:"-" path:"url_param" validate:"numeric"` Text string `json:"text" validate:"max=10"` - DefaultInt int `json:"defaultInt" default:"10.0"` // MATCH /field's type and default value's type mismatch/ + DefaultInt int `json:"defaultInt" default:"10.0"` // MATCH /type mismatch between field type and default value type in default tag/ DefaultInt2 int `json:"defaultInt2" default:"10"` - // MATCH:12 /unknown option 'inline' in JSON tag/ - DefaultInt3 int `json:"defaultInt2,inline" default:"11"` // MATCH /duplicate tag name: 'defaultInt2'/ + // MATCH:10 /unknown option "inline" in json tag/ + DefaultInt3 int `json:"defaultInt2,inline" default:"11"` // MATCH /duplicated tag name "defaultInt2" in json tag/ DefaultString string `json:"defaultString" default:"foo"` - DefaultBool bool `json:"defaultBool" default:"trues"` // MATCH /field's type and default value's type mismatch/ + DefaultBool bool `json:"defaultBool" default:"trues"` // MATCH /type mismatch between field type and default value type in default tag/ DefaultBool2 bool `json:"defaultBool2" default:"true"` DefaultBool3 bool `json:"defaultBool3" default:"false"` - DefaultFloat float64 `json:"defaultFloat" default:"f10.0"` // MATCH /field's type and default value's type mismatch/ + DefaultFloat float64 `json:"defaultFloat" default:"f10.0"` // MATCH /type mismatch between field type and default value type in default tag/ DefaultFloat2 float64 `json:"defaultFloat2" default:"10.0"` - MandatoryStruct mandatoryStruct `json:"mandatoryStruct" required:"trues"` // MATCH /required should be 'true' or 'false'/ + MandatoryStruct mandatoryStruct `json:"mandatoryStruct" required:"trues"` // MATCH /required should be "true" or "false" in required tag/ MandatoryStruct2 mandatoryStruct `json:"mandatoryStruct2" required:"true"` MandatoryStruct4 mandatoryStruct `json:"mandatoryStruct4" required:"false"` OptionalStruct *optionalStruct `json:"optionalStruct,omitempty"` @@ -24,102 +22,13 @@ type decodeAndValidateRequest struct { optionalQuery string `json:"-" querystring:"queryfoo"` // MATCH /tag on not-exported field optionalQuery/ // No-reg test for bug https://github.com/mgechev/revive/issues/208 Tiret string `json:"-,"` - BadTiret string `json:"other,"` // MATCH /option can not be empty in JSON tag/ - ForOmitzero string `json:"forOmitZero,omitzero"` // 'omitzero' is valid in go 1.24 + BadTiret string `json:"other,"` // MATCH /option can not be empty in json tag/ + ForOmitzero string `json:"forOmitZero,omitzero"` // Go 1.24 introduces omitzero } type RangeAllocation struct { - metav1.TypeMeta `json:",inline"` // MATCH /unknown option 'inline' in JSON tag/ + metav1.TypeMeta `json:",inline"` // MATCH /unknown option "inline" in json tag/ metav1.ObjectMeta `json:"metadata,omitempty"` - Range string `json:"range,flow"` // MATCH /unknown option 'flow' in JSON tag/ - Data []byte `json:"data,inline"` // MATCH /unknown option 'inline' in JSON tag/ -} - -type RangeAllocation struct { - metav1.TypeMeta `bson:",minsize"` - metav1.ObjectMeta `bson:"metadata,omitempty"` - Range string `bson:"range,flow"` // MATCH /unknown option 'flow' in BSON tag/ - Data []byte `bson:"data,inline"` -} - -type TestContextSpecificTags2 struct { - A int `asn1:"explicit,tag:1"` - B int `asn1:"tag:2"` - S string `asn1:"tag:0,utf8"` - Ints []int `asn1:"set"` - Version int `asn1:"optional,explicit,default:0,tag:000"` // MATCH /duplicated tag number 0/ - Time time.Time `asn1:"explicit,tag:4,other"` // MATCH /unknown option 'other' in ASN1 tag/ - X int `asn1:"explicit,tag:invalid"` // MATCH /ASN1 tag must be a number, got 'invalid'/ -} - -type VirtualMachineRelocateSpecDiskLocator struct { - DynamicData - - DiskId int32 `xml:"diskId,attr,cdata"` - Datastore ManagedObjectReference `xml:"datastore,chardata,innerxml"` - DiskMoveType string `xml:"diskMoveType,omitempty,comment"` - DiskBackingInfo BaseVirtualDeviceBackingInfo `xml:"diskBackingInfo,omitempty,any"` - Profile []BaseVirtualMachineProfileSpec `xml:"profile,omitempty,other"` // MATCH /unknown option 'other' in XML tag/ -} - -type TestDuplicatedXMLTags struct { - A int `xml:"a"` - B int `xml:"a"` // MATCH /duplicate tag name: 'a'/ - C int `xml:"c"` -} - -type TestDuplicatedBSONTags struct { - A int `bson:"b"` - B int `bson:"b"` // MATCH /duplicate tag name: 'b'/ - C int `bson:"c"` -} - -type TestDuplicatedYAMLTags struct { - A int `yaml:"b"` - B int `yaml:"c"` - C int `yaml:"c"` // MATCH /duplicate tag name: 'c'/ -} - -type TestDuplicatedProtobufTags struct { - A int `protobuf:"varint,name=b"` - B int `protobuf:"varint,name=c"` - C int `protobuf:"varint,name=c"` // MATCH /duplicate tag name: 'c'/ -} - -// test case from -// sigs.k8s.io/kustomize/api/types/helmchartargs.go - -type HelmChartArgs struct { - ChartName string `json:"chartName,omitempty" yaml:"chartName,omitempty"` - ChartVersion string `json:"chartVersion,omitempty" yaml:"chartVersion,omitempty"` - ChartRepoURL string `json:"chartRepoUrl,omitempty" yaml:"chartRepoUrl,omitempty"` - ChartHome string `json:"chartHome,omitempty" yaml:"chartHome,omitempty"` - ChartRepoName string `json:"chartRepoName,omitempty" yaml:"chartRepoName,omitempty"` - HelmBin string `json:"helmBin,omitempty" yaml:"helmBin,omitempty"` - HelmHome string `json:"helmHome,omitempty" yaml:"helmHome,omitempty"` - Values string `json:"values,omitempty" yaml:"values,omitempty"` - ValuesLocal map[string]interface{} `json:"valuesLocal,omitempty" yaml:"valuesLocal,omitempty"` - ValuesMerge string `json:"valuesMerge,omitempty" yaml:"valuesMerge,omitempty"` - ReleaseName string `json:"releaseName,omitempty" yaml:"releaseName,omitempty"` - ReleaseNamespace string `json:"releaseNamespace,omitempty" yaml:"releaseNamespace,omitempty"` - ExtraArgs []string `json:"extraArgs,omitempty" yaml:"extraArgs,omitempty"` -} - -// Test message for holding primitive types. -type Simple struct { - OBool *bool `protobuf:"varint,1,req,json=oBool"` // MATCH /protobuf tag lacks mandatory option 'name'/ - OInt32 *int32 `protobuf:"varint,2,opt,name=o_int32,jsonx=oInt32"` // MATCH /unknown option 'jsonx' in protobuf tag/ - OInt32Str *int32 `protobuf:"varint,3,rep,name=o_int32_str,name=oInt32Str"` // MATCH /protobuf tag has duplicated option 'name'/ - OInt64 *int64 `protobuf:"varint,4,opt,json=oInt64,name=o_int64,json=oInt64"` // MATCH /protobuf tag has duplicated option 'json'/ - OSint32Str *int32 `protobuf:"zigzag32,11,opt,name=o_sint32_str,json=oSint32Str"` - OSint64Str *int64 `protobuf:"zigzag64,13,opt,name=o_sint32_str,json=oSint64Str"` // MATCH /duplicate tag name: 'o_sint32_str'/ - OFloat *float32 `protobuf:"fixed32,14,opt,name=o_float,json=oFloat"` - ODouble *float64 `protobuf:"fixed64,014,opt,name=o_double,json=oDouble"` // MATCH /duplicated tag number 14/ - ODoubleStr *float64 `protobuf:"fixed6,17,opt,name=o_double_str,json=oDoubleStr"` // MATCH /invalid protobuf tag name 'fixed6'/ - OString *string `protobuf:"bytes,18,opt,name=o_string,json=oString"` - OString2 *string `protobuf:"bytes,name=ameno"` - OString3 *string `protobuf:"bytes,name=ameno"` // MATCH /duplicate tag name: 'ameno'/ - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Range string `json:"range,flow"` // MATCH /unknown option "flow" in json tag/ + Data []byte `json:"data,inline"` // MATCH /unknown option "inline" in json tag/ } diff --git a/testdata/struct_tag.go b/testdata/struct_tag.go index 4518140..d039619 100644 --- a/testdata/struct_tag.go +++ b/testdata/struct_tag.go @@ -6,17 +6,17 @@ type decodeAndValidateRequest struct { // BEAWRE : the flag of URLParam should match the const string URLParam URLParam string `json:"-" path:"url_param" validate:"numeric"` Text string `json:"text" validate:"max=10"` - DefaultInt int `json:"defaultInt" default:"10.0"` // MATCH /field's type and default value's type mismatch/ + DefaultInt int `json:"defaultInt" default:"10.0"` // MATCH /type mismatch between field type and default value type in default tag/ DefaultInt2 int `json:"defaultInt2" default:"10"` - // MATCH:12 /unknown option 'inline' in JSON tag/ - DefaultInt3 int `json:"defaultInt2,inline" default:"11"` // MATCH /duplicate tag name: 'defaultInt2'/ + // MATCH:12 /unknown option "inline" in json tag/ + DefaultInt3 int `json:"defaultInt2,inline" default:"11"` // MATCH /duplicated tag name "defaultInt2" in json tag/ DefaultString string `json:"defaultString" default:"foo"` - DefaultBool bool `json:"defaultBool" default:"trues"` // MATCH /field's type and default value's type mismatch/ + DefaultBool bool `json:"defaultBool" default:"trues"` // MATCH /type mismatch between field type and default value type in default tag/ DefaultBool2 bool `json:"defaultBool2" default:"true"` DefaultBool3 bool `json:"defaultBool3" default:"false"` - DefaultFloat float64 `json:"defaultFloat" default:"f10.0"` // MATCH /field's type and default value's type mismatch/ + DefaultFloat float64 `json:"defaultFloat" default:"f10.0"` // MATCH /type mismatch between field type and default value type in default tag/ DefaultFloat2 float64 `json:"defaultFloat2" default:"10.0"` - MandatoryStruct mandatoryStruct `json:"mandatoryStruct" required:"trues"` // MATCH /required should be 'true' or 'false'/ + MandatoryStruct mandatoryStruct `json:"mandatoryStruct" required:"trues"` // MATCH /required should be "true" or "false" in required tag/ MandatoryStruct2 mandatoryStruct `json:"mandatoryStruct2" required:"true"` MandatoryStruct4 mandatoryStruct `json:"mandatoryStruct4" required:"false"` OptionalStruct *optionalStruct `json:"optionalStruct,omitempty"` @@ -24,21 +24,23 @@ type decodeAndValidateRequest struct { optionalQuery string `json:"-" querystring:"queryfoo"` // MATCH /tag on not-exported field optionalQuery/ // No-reg test for bug https://github.com/mgechev/revive/issues/208 Tiret string `json:"-,"` - BadTiret string `json:"other,"` // MATCH /option can not be empty in JSON tag/ - ForOmitzero string `json:"forOmitZero,omitzero"` // MATCH /unknown option 'omitzero' in JSON tag/ + BadTiret string `json:"other,"` // MATCH /option can not be empty in json tag/ + ForOmitzero string `json:"forOmitZero,omitzero"` // MATCH /prior Go 1.24, option "omitzero" is unsupported in json tag/ + // MATCH:30 /option can not be empty in json tag/ + BadTiret string `json:"other,"` // MATCH /duplicated tag name "other" in json tag/ } type RangeAllocation struct { - metav1.TypeMeta `json:",inline"` // MATCH /unknown option 'inline' in JSON tag/ + metav1.TypeMeta `json:",inline"` // MATCH /unknown option "inline" in json tag/ metav1.ObjectMeta `json:"metadata,omitempty"` - Range string `json:"range,flow"` // MATCH /unknown option 'flow' in JSON tag/ - Data []byte `json:"data,inline"` // MATCH /unknown option 'inline' in JSON tag/ + Range string `json:"range,flow"` // MATCH /unknown option "flow" in json tag/ + Data []byte `json:"data,inline"` // MATCH /unknown option "inline" in json tag/ } type RangeAllocation struct { metav1.TypeMeta `bson:",minsize"` metav1.ObjectMeta `bson:"metadata,omitempty"` - Range string `bson:"range,flow"` // MATCH /unknown option 'flow' in BSON tag/ + Range string `bson:"range,flow"` // MATCH /unknown option "flow" in bson tag/ Data []byte `bson:"data,inline"` } @@ -47,9 +49,9 @@ type TestContextSpecificTags2 struct { B int `asn1:"tag:2"` S string `asn1:"tag:0,utf8"` Ints []int `asn1:"set"` - Version int `asn1:"optional,explicit,default:0,tag:000"` // MATCH /duplicated tag number 0/ - Time time.Time `asn1:"explicit,tag:4,other"` // MATCH /unknown option 'other' in ASN1 tag/ - X int `asn1:"explicit,tag:invalid"` // MATCH /ASN1 tag must be a number, got 'invalid'/ + Version int `asn1:"optional,explicit,default:0,tag:000"` // MATCH /duplicated tag number 0 in asn1 tag/ + Time time.Time `asn1:"explicit,tag:4,other"` // MATCH /unknown option "other" in asn1 tag/ + X int `asn1:"explicit,tag:invalid"` // MATCH /tag must be a number but is "invalid" in asn1 tag/ } type VirtualMachineRelocateSpecDiskLocator struct { @@ -59,31 +61,31 @@ type VirtualMachineRelocateSpecDiskLocator struct { Datastore ManagedObjectReference `xml:"datastore,chardata,innerxml"` DiskMoveType string `xml:"diskMoveType,omitempty,comment"` DiskBackingInfo BaseVirtualDeviceBackingInfo `xml:"diskBackingInfo,omitempty,any"` - Profile []BaseVirtualMachineProfileSpec `xml:"profile,omitempty,other"` // MATCH /unknown option 'other' in XML tag/ + Profile []BaseVirtualMachineProfileSpec `xml:"profile,omitempty,other"` // MATCH /unknown option "other" in xml tag/ } -type TestDuplicatedXMLTags struct { +type TestDuplicatedxmlTags struct { A int `xml:"a"` - B int `xml:"a"` // MATCH /duplicate tag name: 'a'/ + B int `xml:"a"` // MATCH /duplicated tag name "a" in xml tag/ C int `xml:"c"` } -type TestDuplicatedBSONTags struct { +type TestDuplicatedbsonTags struct { A int `bson:"b"` - B int `bson:"b"` // MATCH /duplicate tag name: 'b'/ + B int `bson:"b"` // MATCH /duplicated tag name "b" in bson tag/ C int `bson:"c"` } type TestDuplicatedYAMLTags struct { A int `yaml:"b"` B int `yaml:"c"` - C int `yaml:"c"` // MATCH /duplicate tag name: 'c'/ + C int `yaml:"c"` // MATCH /duplicated tag name "c" in yaml tag/ } type TestDuplicatedProtobufTags struct { A int `protobuf:"varint,name=b"` B int `protobuf:"varint,name=c"` - C int `protobuf:"varint,name=c"` // MATCH /duplicate tag name: 'c'/ + C int `protobuf:"varint,name=c"` // MATCH /duplicated tag name "c" in protobuf tag/ } // test case from @@ -107,18 +109,18 @@ type HelmChartArgs struct { // Test message for holding primitive types. type Simple struct { - OBool *bool `protobuf:"varint,1,req,json=oBool"` // MATCH /protobuf tag lacks mandatory option 'name'/ - OInt32 *int32 `protobuf:"varint,2,opt,name=o_int32,jsonx=oInt32"` // MATCH /unknown option 'jsonx' in protobuf tag/ - OInt32Str *int32 `protobuf:"varint,3,rep,name=o_int32_str,name=oInt32Str"` // MATCH /protobuf tag has duplicated option 'name'/ - OInt64 *int64 `protobuf:"varint,4,opt,json=oInt64,name=o_int64,json=oInt64"` // MATCH /protobuf tag has duplicated option 'json'/ + OBool *bool `protobuf:"varint,1,req,json=oBool"` // MATCH /mandatory option "name" not found in protobuf tag/ + OInt32 *int32 `protobuf:"varint,2,opt,name=o_int32,jsonx=oInt32"` // MATCH /unknown option "jsonx" in protobuf tag/ + OInt32Str *int32 `protobuf:"varint,3,rep,name=o_int32_str,name=oInt32Str"` // MATCH /duplicated option "name" in protobuf tag/ + OInt64 *int64 `protobuf:"varint,4,opt,json=oInt64,name=o_int64,json=oInt64"` // MATCH /duplicated option "json" in protobuf tag/ OSint32Str *int32 `protobuf:"zigzag32,11,opt,name=o_sint32_str,json=oSint32Str"` - OSint64Str *int64 `protobuf:"zigzag64,13,opt,name=o_sint32_str,json=oSint64Str"` // MATCH /duplicate tag name: 'o_sint32_str'/ + OSint64Str *int64 `protobuf:"zigzag64,13,opt,name=o_sint32_str,json=oSint64Str"` // MATCH /duplicated tag name "o_sint32_str" in protobuf tag/ OFloat *float32 `protobuf:"fixed32,14,opt,name=o_float,json=oFloat"` - ODouble *float64 `protobuf:"fixed64,014,opt,name=o_double,json=oDouble"` // MATCH /duplicated tag number 14/ - ODoubleStr *float64 `protobuf:"fixed6,17,opt,name=o_double_str,json=oDoubleStr"` // MATCH /invalid protobuf tag name 'fixed6'/ + ODouble *float64 `protobuf:"fixed64,014,opt,name=o_double,json=oDouble"` // MATCH /duplicated tag number 14 in protobuf tag/ + ODoubleStr *float64 `protobuf:"fixed6,17,opt,name=o_double_str,json=oDoubleStr"` // MATCH /invalid tag name "fixed6" in protobuf tag/ OString *string `protobuf:"bytes,18,opt,name=o_string,json=oString"` OString2 *string `protobuf:"bytes,name=ameno"` - OString3 *string `protobuf:"bytes,name=ameno"` // MATCH /duplicate tag name: 'ameno'/ + OString3 *string `protobuf:"bytes,name=ameno"` // MATCH /duplicated tag name "ameno" in protobuf tag/ XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -131,20 +133,20 @@ type RequestQueryOption struct { Associations2 []string `url:"associations2,semicolon,omitempty"` Associations3 []string `url:"associations3,space,brackets,omitempty"` Associations4 []string `url:"associations4,numbered,omitempty"` - Associations5 []string `url:"associations5,space,semicolon,omitempty"` // MATCH /can not set both 'semicolon' and 'space' as delimiters in URL tag/ + Associations5 []string `url:"associations5,space,semicolon,omitempty"` // MATCH /can not set both "semicolon" and "space" as delimiters in url tag/ PaginateAssociations bool `url:"paginateAssociations,int,omitempty"` - Archived bool `url:"archived,myURLOption"` // MATCH /unknown option 'myURLOption' in URL tag/ + Archived bool `url:"archived,myURLOption"` // MATCH /unknown option "myURLOption" in url tag/ IDProperty string `url:"idProperty,omitempty"` } type Fields struct { Field string `datastore:",noindex,flatten,omitempty"` - OtherField string `datastore:",unknownOption"` // MATCH /unknown option 'unknownOption' in Datastore tag/ + OtherField string `datastore:",unknownOption"` // MATCH /unknown option "unknownOption" in datastore tag/ } type MapStruct struct { Field1 string `mapstructure:",squash,reminder,omitempty"` - OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option 'unknownOption' in Mapstructure tag/ + OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option "unknownOption" in mapstructure tag/ } type ValidateUser struct { @@ -152,30 +154,30 @@ type ValidateUser struct { Email string `validate:"required,email"` Password string `validate:"required,min=8,max=32"` Biography string `validate:"min=0,max=1000"` - DisplayName string `validate:"displayName,min=3,max=32"` // MATCH /unknown option 'displayName' in validate tag/ + DisplayName string `validate:"displayName,min=3,max=32"` // MATCH /unknown option "displayName" in validate tag/ Complex string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"` - BadComplex string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"` // MATCH /option 'keys' must follow a 'dive' option in validate tag/ - BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"` // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/ - BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/ + BadComplex string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"` // MATCH /option "keys" must follow a "dive" option in validate tag/ + BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"` // MATCH /option "endkeys" without a previous "keys" option in validate tag/ + BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option "endkeys" without a previous "keys" option in validate tag/ } type TomlUser struct { Username string `toml:"username,omitempty"` - Location string `toml:"location,unknown"` // MATCH /unknown option 'unknown' in TOML tag/ + Location string `toml:"location,unknown"` // MATCH /unknown option "unknown" in toml tag/ } type PropertiesTags struct { Field int `properties:"-"` Field int `properties:"myName"` Field int `properties:"myName,default=15"` - Field int `properties:"myName,default=sString"` // MATCH /field type and default value type mismatch in properties tag/ - Field int `properties:",default:15"` // MATCH /unknown option "default:15" in properties tag/ - Field int `properties:",default=15,default=2"` // MATCH /properties tag accepts only one default option/ + Field int `properties:"myName,default=sString"` // MATCH /type mismatch between field type and default value type in properties tag/ + Field int `properties:",default:15"` // MATCH /unknown or malformed option "default:15" in properties tag/ + Field int `properties:",default=15,default=2"` // MATCH /duplicated option "default" in properties tag/ Field time.Time `properties:"date,layout=2006-01-02"` Field time.Time `properties:",layout=2006-01-02"` - Field time.Time `properties:"date,layout"` // MATCH /malformed layout option for properties tag/ - Field time.Time `properties:"date,layout= "` // MATCH /malformed layout option for properties tag/ + Field time.Time `properties:"date,layout"` // MATCH /unknown or malformed option "layout" in properties tag/ + Field time.Time `properties:"date,layout= "` // MATCH /option "layout" not of the form layout=value in properties tag/ Field string `properties:"date,layout=2006-01-02"` // MATCH /layout option is only applicable to fields of type time.Time in properties tag/ Field []string `properties:",default=a;b;c"` - Field map[string]string `properties:"myName,omitempty"` // MATCH /unknown option "omitempty" in properties tag/ + Field map[string]string `properties:"myName,omitempty"` // MATCH /unknown or malformed option "omitempty" in properties tag/ } diff --git a/testdata/struct_tag_user_options.go b/testdata/struct_tag_user_options.go index 66585e6..f2a925b 100644 --- a/testdata/struct_tag_user_options.go +++ b/testdata/struct_tag_user_options.go @@ -4,30 +4,30 @@ type RangeAllocation struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Range string `json:"range,outline"` - Data []byte `json:"data,flow"` // MATCH /unknown option 'flow' in JSON tag/ + Data []byte `json:"data,flow"` // MATCH /unknown option "flow" in json tag/ } type RangeAllocation struct { metav1.TypeMeta `bson:",minsize,gnu"` metav1.ObjectMeta `bson:"metadata,omitempty"` - Range string `bson:"range,flow"` // MATCH /unknown option 'flow' in BSON tag/ + Range string `bson:"range,flow"` // MATCH /unknown option "flow" in bson tag/ Data []byte `bson:"data,inline"` } type RequestQueryOptions struct { - Properties []string `url:"properties,commmma,omitempty"` // MATCH /unknown option 'commmma' in URL tag/ + Properties []string `url:"properties,commmma,omitempty"` // MATCH /unknown option "commmma" in url tag/ CustomProperties []string `url:"-"` Archived bool `url:"archived,myURLOption"` } type Fields struct { Field string `datastore:",noindex,flatten,omitempty,myDatastoreOption"` - OtherField string `datastore:",unknownOption"` // MATCH /unknown option 'unknownOption' in Datastore tag/ + OtherField string `datastore:",unknownOption"` // MATCH /unknown option "unknownOption" in datastore tag/ } type MapStruct struct { Field1 string `mapstructure:",squash,reminder,omitempty,myMapstructureOption"` - OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option 'unknownOption' in Mapstructure tag/ + OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option "unknownOption" in mapstructure tag/ } type ValidateUser struct { @@ -37,9 +37,9 @@ type ValidateUser struct { Biography string `validate:"min=0,max=1000"` DisplayName string `validate:"displayName,min=3,max=32"` Complex string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"` - BadComplex string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"` // MATCH /option 'keys' must follow a 'dive' option in validate tag/ - BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"` // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/ - BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/ + BadComplex string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"` // MATCH /option "keys" must follow a "dive" option in validate tag/ + BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"` // MATCH /option "endkeys" without a previous "keys" option in validate tag/ + BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option "endkeys" without a previous "keys" option in validate tag/ } type TomlUser struct {