1
0
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:
chavacava
2025-02-20 19:48:35 +01:00
committed by GitHub
parent 4f342352f0
commit 3bbfedbf43
4 changed files with 236 additions and 0 deletions

View File

@@ -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": {},
}

View File

@@ -19,6 +19,7 @@ func TestStructTagWithUserOptions(t *testing.T) {
"url,myURLOption", "url,myURLOption",
"datastore,myDatastoreOption", "datastore,myDatastoreOption",
"mapstructure,myMapstructureOption", "mapstructure,myMapstructureOption",
"validate,displayName",
}, },
}) })
} }

View File

@@ -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/
}

View File

@@ -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/
}