mirror of
https://github.com/mgechev/revive.git
synced 2025-11-23 22:04:49 +02:00
feature: add suport of validate struct tags in struct-tag rule (#1244)
This commit is contained in:
@@ -107,6 +107,7 @@ const (
|
|||||||
keyProtobuf = "protobuf"
|
keyProtobuf = "protobuf"
|
||||||
keyRequired = "required"
|
keyRequired = "required"
|
||||||
keyURL = "url"
|
keyURL = "url"
|
||||||
|
keyValidate = "validate"
|
||||||
keyXML = "xml"
|
keyXML = "xml"
|
||||||
keyYAML = "yaml"
|
keyYAML = "yaml"
|
||||||
)
|
)
|
||||||
@@ -216,6 +217,12 @@ func (w lintStructTagRule) checkTaggedField(f *ast.Field) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
w.addFailure(f.Tag, msg)
|
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:
|
case keyXML:
|
||||||
msg, ok := w.checkXMLTag(tag.Options)
|
msg, ok := w.checkXMLTag(tag.Options)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -400,6 +407,59 @@ func (w lintStructTagRule) checkMapstructureTag(options []string) (string, bool)
|
|||||||
return "", true
|
return "", true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w lintStructTagRule) checkValidateTag(options []string) (string, bool) {
|
||||||
|
previousOption := ""
|
||||||
|
seenKeysOption := false
|
||||||
|
for _, opt := range options {
|
||||||
|
switch opt {
|
||||||
|
case "keys":
|
||||||
|
if previousOption != "dive" {
|
||||||
|
return "option 'keys' must follow a 'dive' option in validate tag", false
|
||||||
|
}
|
||||||
|
seenKeysOption = true
|
||||||
|
case "endkeys":
|
||||||
|
if !seenKeysOption {
|
||||||
|
return "option 'endkeys' without a previous 'keys' option in validate tag", false
|
||||||
|
}
|
||||||
|
seenKeysOption = false
|
||||||
|
default:
|
||||||
|
parts := strings.Split(opt, "|")
|
||||||
|
errMsg, ok := w.checkValidateOptionsAlternatives(parts)
|
||||||
|
if !ok {
|
||||||
|
return errMsg, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousOption = opt
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w lintStructTagRule) checkValidateOptionsAlternatives(alternatives []string) (string, 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]
|
||||||
|
_, ok := validateLHS[lhs]
|
||||||
|
if ok || w.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 "", true
|
||||||
|
}
|
||||||
|
|
||||||
func (lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool {
|
func (lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool {
|
||||||
tID, ok := t.(*ast.Ident)
|
tID, ok := t.(*ast.Ident)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -500,3 +560,154 @@ func (w lintStructTagRule) isUserDefined(key, opt string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func areValidateOpts(opts string) (string, bool) {
|
||||||
|
parts := strings.Split(opts, "|")
|
||||||
|
for _, opt := range parts {
|
||||||
|
_, ok := validateSingleOptions[opt]
|
||||||
|
if !ok {
|
||||||
|
return opt, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
|
||||||
|
var validateSingleOptions = map[string]struct{}{
|
||||||
|
"alpha": {},
|
||||||
|
"alphanum": {},
|
||||||
|
"alphanumunicode": {},
|
||||||
|
"alphaunicode": {},
|
||||||
|
"ascii": {},
|
||||||
|
"base32": {},
|
||||||
|
"base64": {},
|
||||||
|
"base64url": {},
|
||||||
|
"bcp47_language_tag": {},
|
||||||
|
"boolean": {},
|
||||||
|
"bic": {},
|
||||||
|
"btc_addr": {},
|
||||||
|
"btc_addr_bech32": {},
|
||||||
|
"cidr": {},
|
||||||
|
"cidrv4": {},
|
||||||
|
"cidrv6": {},
|
||||||
|
"country_code": {},
|
||||||
|
"credit_card": {},
|
||||||
|
"cron": {},
|
||||||
|
"cve": {},
|
||||||
|
"datauri": {},
|
||||||
|
"dir": {},
|
||||||
|
"dirpath": {},
|
||||||
|
"dive": {},
|
||||||
|
"dns_rfc1035_label": {},
|
||||||
|
"e164": {},
|
||||||
|
"email": {},
|
||||||
|
"eth_addr": {},
|
||||||
|
"file": {},
|
||||||
|
"filepath": {},
|
||||||
|
"fqdn": {},
|
||||||
|
"hexadecimal": {},
|
||||||
|
"hexcolor": {},
|
||||||
|
"hostname": {},
|
||||||
|
"hostname_port": {},
|
||||||
|
"hostname_rfc1123": {},
|
||||||
|
"hsl": {},
|
||||||
|
"hsla": {},
|
||||||
|
"html": {},
|
||||||
|
"html_encoded": {},
|
||||||
|
"image": {},
|
||||||
|
"ip": {},
|
||||||
|
"ip4_addr": {},
|
||||||
|
"ip6_addr": {},
|
||||||
|
"ip_addr": {},
|
||||||
|
"ipv4": {},
|
||||||
|
"ipv6": {},
|
||||||
|
"isbn": {},
|
||||||
|
"isbn10": {},
|
||||||
|
"isbn13": {},
|
||||||
|
"isdefault": {},
|
||||||
|
"iso3166_1_alpha2": {},
|
||||||
|
"iso3166_1_alpha3": {},
|
||||||
|
"iscolor": {},
|
||||||
|
"json": {},
|
||||||
|
"jwt": {},
|
||||||
|
"latitude": {},
|
||||||
|
"longitude": {},
|
||||||
|
"lowercase": {},
|
||||||
|
"luhn_checksum": {},
|
||||||
|
"mac": {},
|
||||||
|
"mongodb": {},
|
||||||
|
"mongodb_connection_string": {},
|
||||||
|
"multibyte": {},
|
||||||
|
"nostructlevel": {},
|
||||||
|
"number": {},
|
||||||
|
"numeric": {},
|
||||||
|
"omitempty": {},
|
||||||
|
"printascii": {},
|
||||||
|
"required": {},
|
||||||
|
"rgb": {},
|
||||||
|
"rgba": {},
|
||||||
|
"semver": {},
|
||||||
|
"ssn": {},
|
||||||
|
"structonly": {},
|
||||||
|
"tcp_addr": {},
|
||||||
|
"tcp4_addr": {},
|
||||||
|
"tcp6_addr": {},
|
||||||
|
"timezone": {},
|
||||||
|
"udp4_addr": {},
|
||||||
|
"udp6_addr": {},
|
||||||
|
"ulid": {},
|
||||||
|
"unique": {},
|
||||||
|
"unix_addr": {},
|
||||||
|
"uppercase": {},
|
||||||
|
"uri": {},
|
||||||
|
"url": {},
|
||||||
|
"url_encoded": {},
|
||||||
|
"urn_rfc2141": {},
|
||||||
|
"uuid": {},
|
||||||
|
"uuid3": {},
|
||||||
|
"uuid4": {},
|
||||||
|
"uuid5": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
var validateLHS = map[string]struct{}{
|
||||||
|
"contains": {},
|
||||||
|
"containsany": {},
|
||||||
|
"containsfield": {},
|
||||||
|
"containsrune": {},
|
||||||
|
"datetime": {},
|
||||||
|
"endsnotwith": {},
|
||||||
|
"endswith": {},
|
||||||
|
"eq": {},
|
||||||
|
"eqfield": {},
|
||||||
|
"eqcsfield": {},
|
||||||
|
"excluded_if": {},
|
||||||
|
"excluded_unless": {},
|
||||||
|
"excludes": {},
|
||||||
|
"excludesall": {},
|
||||||
|
"excludesfield": {},
|
||||||
|
"excludesrune": {},
|
||||||
|
"gt": {},
|
||||||
|
"gtcsfield": {},
|
||||||
|
"gtecsfield": {},
|
||||||
|
"len": {},
|
||||||
|
"lt": {},
|
||||||
|
"lte": {},
|
||||||
|
"ltcsfield": {},
|
||||||
|
"ltecsfield": {},
|
||||||
|
"max": {},
|
||||||
|
"min": {},
|
||||||
|
"ne": {},
|
||||||
|
"necsfield": {},
|
||||||
|
"oneof": {},
|
||||||
|
"oneofci": {},
|
||||||
|
"required_if": {},
|
||||||
|
"required_unless": {},
|
||||||
|
"required_with": {},
|
||||||
|
"required_with_all": {},
|
||||||
|
"required_without": {},
|
||||||
|
"required_without_all": {},
|
||||||
|
"spicedb": {},
|
||||||
|
"startsnotwith": {},
|
||||||
|
"startswith": {},
|
||||||
|
"unique": {},
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func TestStructTagWithUserOptions(t *testing.T) {
|
|||||||
"url,myURLOption",
|
"url,myURLOption",
|
||||||
"datastore,myDatastoreOption",
|
"datastore,myDatastoreOption",
|
||||||
"mapstructure,myMapstructureOption",
|
"mapstructure,myMapstructureOption",
|
||||||
|
"validate,displayName",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
12
testdata/struct_tag.go
vendored
12
testdata/struct_tag.go
vendored
@@ -146,3 +146,15 @@ type MapStruct struct {
|
|||||||
Field1 string `mapstructure:",squash,reminder,omitempty"`
|
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 {
|
||||||
|
Username string `validate:"required,min=3,max=32"`
|
||||||
|
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/
|
||||||
|
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/
|
||||||
|
}
|
||||||
|
|||||||
12
testdata/struct_tag_user_options.go
vendored
12
testdata/struct_tag_user_options.go
vendored
@@ -29,3 +29,15 @@ type MapStruct struct {
|
|||||||
Field1 string `mapstructure:",squash,reminder,omitempty,myMapstructureOption"`
|
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 {
|
||||||
|
Username string `validate:"required,min=3,max=32"`
|
||||||
|
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"`
|
||||||
|
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/
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user