1
0
mirror of https://github.com/mgechev/revive.git synced 2025-10-30 23:37:49 +02:00

struct-tag: support codec struct tag (#1507)

This commit is contained in:
Artem
2025-09-08 10:20:24 +03:00
committed by GitHub
parent b24647aaec
commit b827c22c4d
4 changed files with 131 additions and 30 deletions

View File

@@ -23,6 +23,7 @@ type tagKey string
const ( const (
keyASN1 tagKey = "asn1" keyASN1 tagKey = "asn1"
keyBSON tagKey = "bson" keyBSON tagKey = "bson"
keyCodec tagKey = "codec"
keyDatastore tagKey = "datastore" keyDatastore tagKey = "datastore"
keyDefault tagKey = "default" keyDefault tagKey = "default"
keyJSON tagKey = "json" keyJSON tagKey = "json"
@@ -38,11 +39,12 @@ const (
keyYAML tagKey = "yaml" keyYAML tagKey = "yaml"
) )
type tagChecker func(checkCtx *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) type tagChecker func(checkCtx *checkContext, tag *structtag.Tag, field *ast.Field) (message string, succeeded bool)
var tagCheckers = map[tagKey]tagChecker{ var tagCheckers = map[tagKey]tagChecker{
keyASN1: checkASN1Tag, keyASN1: checkASN1Tag,
keyBSON: checkBSONTag, keyBSON: checkBSONTag,
keyCodec: checkCodecTag,
keyDatastore: checkDatastoreTag, keyDatastore: checkDatastoreTag,
keyDefault: checkDefaultTag, keyDefault: checkDefaultTag,
keyJSON: checkJSONTag, keyJSON: checkJSONTag,
@@ -62,6 +64,7 @@ type checkContext struct {
userDefined map[tagKey][]string // map: key -> []option userDefined map[tagKey][]string // map: key -> []option
usedTagNbr map[int]bool // list of used tag numbers usedTagNbr map[int]bool // list of used tag numbers
usedTagName map[string]bool // list of used tag keys usedTagName map[string]bool // list of used tag keys
commonOptions map[string]bool // list of options defined for all fields
isAtLeastGo124 bool isAtLeastGo124 bool
} }
@@ -74,6 +77,23 @@ func (checkCtx checkContext) isUserDefined(key tagKey, opt string) bool {
return slices.Contains(options, opt) return slices.Contains(options, opt)
} }
func (checkCtx *checkContext) isCommonOption(opt string) bool {
if checkCtx.commonOptions == nil {
return false
}
_, ok := checkCtx.commonOptions[opt]
return ok
}
func (checkCtx *checkContext) addCommonOption(opt string) {
if checkCtx.commonOptions == nil {
checkCtx.commonOptions = map[string]bool{}
}
checkCtx.commonOptions[opt] = true
}
// Configure validates the rule configuration, and configures the rule accordingly. // Configure validates the rule configuration, and configures the rule accordingly.
// //
// Configuration implements the [lint.ConfigurableRule] interface. // Configuration implements the [lint.ConfigurableRule] interface.
@@ -164,36 +184,66 @@ func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor {
// checkTaggedField checks the tag of the given field. // checkTaggedField checks the tag of the given field.
// precondition: the field has a tag // precondition: the field has a tag
func (w lintStructTagRule) checkTaggedField(checkCtx *checkContext, f *ast.Field) { func (w lintStructTagRule) checkTaggedField(checkCtx *checkContext, field *ast.Field) {
if len(f.Names) > 0 && !f.Names[0].IsExported() { tags, err := structtag.Parse(strings.Trim(field.Tag.Value, "`"))
w.addFailuref(f, "tag on not-exported field %s", f.Names[0].Name)
}
tags, err := structtag.Parse(strings.Trim(f.Tag.Value, "`"))
if err != nil || tags == nil { if err != nil || tags == nil {
w.addFailuref(f.Tag, "malformed tag") w.addFailuref(field.Tag, "malformed tag")
return return
} }
analyzedTags := map[tagKey]struct{}{}
for _, tag := range tags.Tags() { for _, tag := range tags.Tags() {
if msg, ok := w.checkTagNameIfNeed(checkCtx, tag); !ok { if msg, ok := w.checkTagNameIfNeed(checkCtx, tag); !ok {
w.addFailureWithTagKey(f.Tag, msg, tag.Key) w.addFailureWithTagKey(field.Tag, msg, tag.Key)
} }
if msg, ok := checkOptionsOnIgnoredField(tag); !ok { if msg, ok := checkOptionsOnIgnoredField(tag); !ok {
w.addFailureWithTagKey(f.Tag, msg, tag.Key) w.addFailureWithTagKey(field.Tag, msg, tag.Key)
} }
checker, ok := w.tagCheckers[tagKey(tag.Key)] key := tagKey(tag.Key)
checker, ok := w.tagCheckers[key]
if !ok { if !ok {
continue // we don't have a checker for the tag continue // we don't have a checker for the tag
} }
msg, ok := checker(checkCtx, tag, f.Type) msg, ok := checker(checkCtx, tag, field)
if !ok { if !ok {
w.addFailureWithTagKey(f.Tag, msg, tag.Key) w.addFailureWithTagKey(field.Tag, msg, tag.Key)
}
analyzedTags[key] = struct{}{}
}
if w.shallWarnOnUnexportedField(field.Names, analyzedTags) {
w.addFailuref(field, "tag on not-exported field %s", field.Names[0].Name)
}
}
// tagKeyToSpecialField maps tag keys to their "special" meaning struct fields.
var tagKeyToSpecialField = map[tagKey]string{
"codec": structTagCodecSpecialField,
}
func (lintStructTagRule) shallWarnOnUnexportedField(fieldNames []*ast.Ident, tags map[tagKey]struct{}) bool {
if len(fieldNames) != 1 { // only handle the case of single field name (99.999% of cases)
return false
}
if fieldNames[0].IsExported() {
return false
}
fieldNameStr := fieldNames[0].Name
for key := range tags {
specialField, ok := tagKeyToSpecialField[key]
if ok && specialField == fieldNameStr {
return false
} }
} }
return true
} }
func (w lintStructTagRule) checkTagNameIfNeed(checkCtx *checkContext, tag *structtag.Tag) (message string, succeeded bool) { func (w lintStructTagRule) checkTagNameIfNeed(checkCtx *checkContext, tag *structtag.Tag) (message string, succeeded bool) {
@@ -204,7 +254,7 @@ func (w lintStructTagRule) checkTagNameIfNeed(checkCtx *checkContext, tag *struc
key := tagKey(tag.Key) key := tagKey(tag.Key)
switch key { switch key {
case keyBSON, keyJSON, keyXML, keyYAML, keyProtobuf, keySpanner: case keyBSON, keyCodec, keyJSON, keyProtobuf, keySpanner, keyXML, keyYAML: // keys that need to check for duplicated tags
default: default:
return "", true return "", true
} }
@@ -241,7 +291,8 @@ func (lintStructTagRule) getTagName(tag *structtag.Tag) string {
} }
} }
func checkASN1Tag(checkCtx *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) { func checkASN1Tag(checkCtx *checkContext, tag *structtag.Tag, field *ast.Field) (message string, succeeded bool) {
fieldType := field.Type
checkList := slices.Concat(tag.Options, []string{tag.Name}) checkList := slices.Concat(tag.Options, []string{tag.Name})
for _, opt := range checkList { for _, opt := range checkList {
switch opt { switch opt {
@@ -282,7 +333,7 @@ func checkCompoundANS1Option(checkCtx *checkContext, opt string, fieldType ast.E
return "", true return "", true
} }
func checkDatastoreTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkDatastoreTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
for _, opt := range tag.Options { for _, opt := range tag.Options {
switch opt { switch opt {
case "flatten", "noindex", "omitempty": case "flatten", "noindex", "omitempty":
@@ -297,15 +348,15 @@ func checkDatastoreTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (
return "", true return "", true
} }
func checkDefaultTag(_ *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) { func checkDefaultTag(_ *checkContext, tag *structtag.Tag, field *ast.Field) (message string, succeeded bool) {
if !typeValueMatch(fieldType, tag.Name) { if !typeValueMatch(field.Type, tag.Name) {
return msgTypeMismatch, false return msgTypeMismatch, false
} }
return "", true return "", true
} }
func checkBSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkBSONTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
for _, opt := range tag.Options { for _, opt := range tag.Options {
switch opt { switch opt {
case "inline", "minsize", "omitempty": case "inline", "minsize", "omitempty":
@@ -320,7 +371,32 @@ func checkBSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (messa
return "", true return "", true
} }
func checkJSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { const structTagCodecSpecialField = "_struct"
func checkCodecTag(checkCtx *checkContext, tag *structtag.Tag, field *ast.Field) (message string, succeeded bool) {
fieldNames := field.Names
mustAddToCommonOptions := len(fieldNames) == 1 && fieldNames[0].Name == structTagCodecSpecialField // see https://github.com/mgechev/revive/issues/1477#issuecomment-3191493076
for _, opt := range tag.Options {
if mustAddToCommonOptions {
checkCtx.addCommonOption(opt)
} else if checkCtx.isCommonOption(opt) {
return fmt.Sprintf("redundant option %q, already set for all fields", opt), false
}
switch opt {
case "omitempty", "toarray", "int", "uint", "float", "-", "omitemptyarray":
default:
if checkCtx.isUserDefined(keyCodec, opt) {
continue
}
return fmt.Sprintf(msgUnknownOption, opt), false
}
}
return "", true
}
func checkJSONTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
for _, opt := range tag.Options { for _, opt := range tag.Options {
switch opt { switch opt {
case "omitempty", "string": case "omitempty", "string":
@@ -345,7 +421,7 @@ func checkJSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (messa
return "", true return "", true
} }
func checkMapstructureTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkMapstructureTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
for _, opt := range tag.Options { for _, opt := range tag.Options {
switch opt { switch opt {
case "omitempty", "reminder", "squash": case "omitempty", "reminder", "squash":
@@ -360,13 +436,14 @@ func checkMapstructureTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr
return "", true return "", true
} }
func checkPropertiesTag(_ *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) { func checkPropertiesTag(_ *checkContext, tag *structtag.Tag, field *ast.Field) (message string, succeeded bool) {
options := tag.Options options := tag.Options
if len(options) == 0 { if len(options) == 0 {
return "", true return "", true
} }
seenOptions := map[string]bool{} seenOptions := map[string]bool{}
fieldType := field.Type
for _, opt := range options { for _, opt := range options {
msg, ok := fmt.Sprintf("unknown or malformed option %q", opt), false msg, ok := fmt.Sprintf("unknown or malformed option %q", opt), false
if key, value, found := strings.Cut(opt, "="); found { if key, value, found := strings.Cut(opt, "="); found {
@@ -405,7 +482,7 @@ func checkCompoundPropertiesOption(key, value string, fieldType ast.Expr, seenOp
return "", true return "", true
} }
func checkProtobufTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkProtobufTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
// check name // check name
switch tag.Name { switch tag.Name {
case "bytes", "fixed32", "fixed64", "group", "varint", "zigzag32", "zigzag64": case "bytes", "fixed32", "fixed64", "group", "varint", "zigzag32", "zigzag64":
@@ -458,7 +535,7 @@ func checkProtobufOptions(checkCtx *checkContext, options []string) (message str
return "", true return "", true
} }
func checkRequiredTag(_ *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkRequiredTag(_ *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
switch tag.Name { switch tag.Name {
case "true", "false": case "true", "false":
return "", true return "", true
@@ -467,7 +544,7 @@ func checkRequiredTag(_ *checkContext, tag *structtag.Tag, _ ast.Expr) (message
} }
} }
func checkTOMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkTOMLTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
for _, opt := range tag.Options { for _, opt := range tag.Options {
switch opt { switch opt {
case "omitempty": case "omitempty":
@@ -482,7 +559,7 @@ func checkTOMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (messa
return "", true return "", true
} }
func checkURLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkURLTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
var delimiter = "" var delimiter = ""
for _, opt := range tag.Options { for _, opt := range tag.Options {
switch opt { switch opt {
@@ -505,7 +582,7 @@ func checkURLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (messag
return "", true return "", true
} }
func checkValidateTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkValidateTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
previousOption := "" previousOption := ""
seenKeysOption := false seenKeysOption := false
options := append([]string{tag.Name}, tag.Options...) options := append([]string{tag.Name}, tag.Options...)
@@ -534,7 +611,7 @@ func checkValidateTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (m
return "", true return "", true
} }
func checkXMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkXMLTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
for _, opt := range tag.Options { for _, opt := range tag.Options {
switch opt { switch opt {
case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr": case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr":
@@ -549,7 +626,7 @@ func checkXMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (messag
return "", true return "", true
} }
func checkYAMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkYAMLTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
for _, opt := range tag.Options { for _, opt := range tag.Options {
switch opt { switch opt {
case "flow", "inline", "omitempty": case "flow", "inline", "omitempty":
@@ -564,7 +641,7 @@ func checkYAMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (messa
return "", true return "", true
} }
func checkSpannerTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) { func checkSpannerTag(checkCtx *checkContext, tag *structtag.Tag, _ *ast.Field) (message string, succeeded bool) {
for _, opt := range tag.Options { for _, opt := range tag.Options {
if !checkCtx.isUserDefined(keySpanner, opt) { if !checkCtx.isUserDefined(keySpanner, opt) {
return fmt.Sprintf(msgUnknownOption, opt), false return fmt.Sprintf(msgUnknownOption, opt), false

View File

@@ -22,6 +22,7 @@ func TestStructTagWithUserOptions(t *testing.T) {
"validate,displayName", "validate,displayName",
"toml,unknown", "toml,unknown",
"spanner,mySpannerOption", "spanner,mySpannerOption",
"codec,myCodecOption",
}, },
}) })
} }

View File

@@ -196,3 +196,21 @@ type SpannerUser struct {
CreatedAt time.Time `spanner:"created_at"` CreatedAt time.Time `spanner:"created_at"`
UpdatedAt time.Time `spanner:"updated_at,unknown"` // MATCH /unknown option "unknown" in spanner tag/ UpdatedAt time.Time `spanner:"updated_at,unknown"` // MATCH /unknown option "unknown" in spanner tag/
} }
type Codec struct {
_something struct{} `codec:",omitempty,int"` // MATCH /tag on not-exported field _something/
_struct struct{} `codec:",omitempty,int"` // do not match, _struct has special meaning for codec tag
Field1 string `codec:"-"`
Field2 int `codec:"myName"`
Field3 int32 `codec:",omitempty"` // MATCH /redundant option "omitempty", already set for all fields in codec tag/
Field4 bool `codec:"f4,int"` // MATCH /redundant option "int", already set for all fields in codec tag/
field5 bool // unexported, so skipped
Anon
}
type TestDuplicatedCodecTags struct {
_struct struct{} `json:",omitempty"` // MATCH /tag on not-exported field _struct/
A int `codec:"field_a"`
B int `codec:"field_a"` // MATCH /duplicated tag name "field_a" in codec tag/
C int `codec:"field_c"`
}

View File

@@ -97,3 +97,8 @@ type uselessOptions struct {
// MATCH:83 /unknown option "" in xml tag/ // MATCH:83 /unknown option "" in xml tag/
// MATCH:86 /unknown option "" in yaml tag/ // MATCH:86 /unknown option "" in yaml tag/
} }
type CodecUserOptions struct {
ID int `codec:"user_id,myCodecOption"`
Name string `codec:"full_name,unknownOption"` // MATCH /unknown option "unknownOption" in codec tag/
}