2018-07-28 18:07:31 +02:00
|
|
|
package rule
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"go/ast"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2023-03-15 01:16:12 +02:00
|
|
|
"sync"
|
2019-06-01 10:34:43 +02:00
|
|
|
|
|
|
|
"github.com/fatih/structtag"
|
|
|
|
"github.com/mgechev/revive/lint"
|
2018-07-28 18:07:31 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// StructTagRule lints struct tags.
|
2023-03-15 01:16:12 +02:00
|
|
|
type StructTagRule struct {
|
|
|
|
userDefined map[string][]string // map: key -> []option
|
|
|
|
sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *StructTagRule) configure(arguments lint.Arguments) {
|
|
|
|
r.Lock()
|
|
|
|
defer r.Unlock()
|
2024-10-01 12:14:02 +02:00
|
|
|
|
|
|
|
mustConfigure := r.userDefined == nil && len(arguments) > 0
|
|
|
|
if !mustConfigure {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
checkNumberOfArguments(1, arguments, r.Name())
|
|
|
|
r.userDefined = make(map[string][]string, len(arguments))
|
|
|
|
for _, arg := range arguments {
|
|
|
|
item, ok := arg.(string)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("Invalid argument to the %s rule. Expecting a string, got %v (of type %T)", r.Name(), arg, arg))
|
|
|
|
}
|
|
|
|
parts := strings.Split(item, ",")
|
|
|
|
if len(parts) < 2 {
|
|
|
|
panic(fmt.Sprintf("Invalid argument to the %s rule. Expecting a string of the form key[,option]+, got %s", r.Name(), item))
|
|
|
|
}
|
|
|
|
key := strings.TrimSpace(parts[0])
|
|
|
|
for i := 1; i < len(parts); i++ {
|
|
|
|
option := strings.TrimSpace(parts[i])
|
|
|
|
r.userDefined[key] = append(r.userDefined[key], option)
|
2023-03-15 01:16:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-07-28 18:07:31 +02:00
|
|
|
|
|
|
|
// Apply applies the rule to given file.
|
2023-03-15 01:16:12 +02:00
|
|
|
func (r *StructTagRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
|
|
|
|
r.configure(args)
|
2018-07-28 18:07:31 +02:00
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
var failures []lint.Failure
|
2018-07-28 18:07:31 +02:00
|
|
|
onFailure := func(failure lint.Failure) {
|
|
|
|
failures = append(failures, failure)
|
|
|
|
}
|
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
w := lintStructTagRule{
|
|
|
|
onFailure: onFailure,
|
|
|
|
userDefined: r.userDefined,
|
|
|
|
}
|
2018-07-28 18:07:31 +02:00
|
|
|
|
|
|
|
ast.Walk(w, file.AST)
|
|
|
|
|
|
|
|
return failures
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the rule name.
|
2022-04-10 11:55:13 +02:00
|
|
|
func (*StructTagRule) Name() string {
|
2018-07-28 18:07:31 +02:00
|
|
|
return "struct-tag"
|
|
|
|
}
|
|
|
|
|
|
|
|
type lintStructTagRule struct {
|
2022-07-13 20:26:44 +02:00
|
|
|
onFailure func(lint.Failure)
|
2023-03-15 01:16:12 +02:00
|
|
|
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
|
2018-07-28 18:07:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor {
|
|
|
|
switch n := node.(type) {
|
|
|
|
case *ast.StructType:
|
2024-10-01 12:14:02 +02:00
|
|
|
isEmptyStruct := n.Fields == nil || n.Fields.NumFields() < 1
|
|
|
|
if isEmptyStruct {
|
2018-07-28 18:07:31 +02:00
|
|
|
return nil // skip empty structs
|
|
|
|
}
|
2024-10-01 12:14:02 +02:00
|
|
|
|
|
|
|
w.usedTagNbr = map[int]bool{}
|
|
|
|
w.usedTagName = map[string]bool{}
|
2018-07-28 18:07:31 +02:00
|
|
|
for _, f := range n.Fields.List {
|
|
|
|
if f.Tag != nil {
|
|
|
|
w.checkTaggedField(f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
const keyASN1 = "asn1"
|
|
|
|
const keyBSON = "bson"
|
|
|
|
const keyDefault = "default"
|
|
|
|
const keyJSON = "json"
|
|
|
|
const keyProtobuf = "protobuf"
|
|
|
|
const keyRequired = "required"
|
|
|
|
const keyXML = "xml"
|
|
|
|
const keyYAML = "yaml"
|
|
|
|
|
2022-07-13 20:26:44 +02:00
|
|
|
func (w lintStructTagRule) checkTagNameIfNeed(tag *structtag.Tag) (string, bool) {
|
|
|
|
isUnnamedTag := tag.Name == "" || tag.Name == "-"
|
|
|
|
if isUnnamedTag {
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
needsToCheckTagName := tag.Key == keyBSON ||
|
|
|
|
tag.Key == keyJSON ||
|
|
|
|
tag.Key == keyXML ||
|
|
|
|
tag.Key == keyYAML ||
|
|
|
|
tag.Key == keyProtobuf
|
2022-07-13 20:26:44 +02:00
|
|
|
|
|
|
|
if !needsToCheckTagName {
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
|
|
|
tagName := w.getTagName(tag)
|
2022-07-15 20:15:55 +02:00
|
|
|
if tagName == "" {
|
|
|
|
return "", true // No tag name found
|
|
|
|
}
|
|
|
|
|
2022-07-13 20:26:44 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
w.usedTagName[key] = true
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lintStructTagRule) getTagName(tag *structtag.Tag) string {
|
|
|
|
switch tag.Key {
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyProtobuf:
|
2022-07-13 20:26:44 +02:00
|
|
|
for _, option := range tag.Options {
|
|
|
|
if strings.HasPrefix(option, "name=") {
|
2023-05-16 08:09:20 +02:00
|
|
|
return strings.TrimPrefix(option, "name=")
|
2022-07-13 20:26:44 +02:00
|
|
|
}
|
|
|
|
}
|
2023-09-23 10:41:34 +02:00
|
|
|
return "" // protobuf tag lacks 'name' option
|
2022-07-13 20:26:44 +02:00
|
|
|
default:
|
|
|
|
return tag.Name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-28 18:07:31 +02:00
|
|
|
// checkTaggedField checks the tag of the given field.
|
|
|
|
// precondition: the field has a tag
|
|
|
|
func (w lintStructTagRule) checkTaggedField(f *ast.Field) {
|
2019-06-01 10:34:43 +02:00
|
|
|
if len(f.Names) > 0 && !f.Names[0].IsExported() {
|
|
|
|
w.addFailure(f, "tag on not-exported field "+f.Names[0].Name)
|
|
|
|
}
|
|
|
|
|
2018-07-28 18:07:31 +02:00
|
|
|
tags, err := structtag.Parse(strings.Trim(f.Tag.Value, "`"))
|
|
|
|
if err != nil || tags == nil {
|
2018-08-07 20:28:45 +02:00
|
|
|
w.addFailure(f.Tag, "malformed tag")
|
2018-07-28 18:07:31 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tag := range tags.Tags() {
|
2022-07-13 20:26:44 +02:00
|
|
|
if msg, ok := w.checkTagNameIfNeed(tag); !ok {
|
|
|
|
w.addFailure(f.Tag, msg)
|
|
|
|
}
|
|
|
|
|
2018-07-28 18:07:31 +02:00
|
|
|
switch key := tag.Key; key {
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyASN1:
|
2018-08-07 20:28:45 +02:00
|
|
|
msg, ok := w.checkASN1Tag(f.Type, tag)
|
|
|
|
if !ok {
|
|
|
|
w.addFailure(f.Tag, msg)
|
|
|
|
}
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyBSON:
|
2018-08-07 20:28:45 +02:00
|
|
|
msg, ok := w.checkBSONTag(tag.Options)
|
|
|
|
if !ok {
|
|
|
|
w.addFailure(f.Tag, msg)
|
|
|
|
}
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyDefault:
|
2018-07-28 18:07:31 +02:00
|
|
|
if !w.typeValueMatch(f.Type, tag.Name) {
|
|
|
|
w.addFailure(f.Tag, "field's type and default value's type mismatch")
|
|
|
|
}
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyJSON:
|
2019-08-05 18:21:20 +02:00
|
|
|
msg, ok := w.checkJSONTag(tag.Name, tag.Options)
|
2018-07-28 18:07:31 +02:00
|
|
|
if !ok {
|
|
|
|
w.addFailure(f.Tag, msg)
|
|
|
|
}
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyProtobuf:
|
2022-07-15 20:15:55 +02:00
|
|
|
msg, ok := w.checkProtobufTag(tag)
|
|
|
|
if !ok {
|
|
|
|
w.addFailure(f.Tag, msg)
|
|
|
|
}
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyRequired:
|
2018-07-28 18:07:31 +02:00
|
|
|
if tag.Name != "true" && tag.Name != "false" {
|
|
|
|
w.addFailure(f.Tag, "required should be 'true' or 'false'")
|
|
|
|
}
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyXML:
|
2018-07-28 18:07:31 +02:00
|
|
|
msg, ok := w.checkXMLTag(tag.Options)
|
|
|
|
if !ok {
|
|
|
|
w.addFailure(f.Tag, msg)
|
|
|
|
}
|
2023-03-15 01:16:12 +02:00
|
|
|
case keyYAML:
|
2018-07-28 18:07:31 +02:00
|
|
|
msg, ok := w.checkYAMLTag(tag.Options)
|
|
|
|
if !ok {
|
|
|
|
w.addFailure(f.Tag, msg)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
// unknown key
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-07 20:28:45 +02:00
|
|
|
func (w lintStructTagRule) checkASN1Tag(t ast.Expr, tag *structtag.Tag) (string, bool) {
|
|
|
|
checkList := append(tag.Options, tag.Name)
|
|
|
|
for _, opt := range checkList {
|
|
|
|
switch opt {
|
|
|
|
case "application", "explicit", "generalized", "ia5", "omitempty", "optional", "set", "utf8":
|
|
|
|
|
|
|
|
default:
|
|
|
|
if strings.HasPrefix(opt, "tag:") {
|
|
|
|
parts := strings.Split(opt, ":")
|
|
|
|
tagNumber := parts[1]
|
2022-07-15 20:15:55 +02:00
|
|
|
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
|
2018-08-07 20:28:45 +02:00
|
|
|
}
|
2022-07-15 20:15:55 +02:00
|
|
|
w.usedTagNbr[number] = true
|
2018-08-07 20:28:45 +02:00
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
if w.isUserDefined(keyASN1, opt) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-08-07 20:28:45 +02:00
|
|
|
return fmt.Sprintf("unknown option '%s' in ASN1 tag", opt), false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
func (w lintStructTagRule) checkBSONTag(options []string) (string, bool) {
|
2018-08-07 20:28:45 +02:00
|
|
|
for _, opt := range options {
|
|
|
|
switch opt {
|
|
|
|
case "inline", "minsize", "omitempty":
|
|
|
|
default:
|
2023-03-15 01:16:12 +02:00
|
|
|
if w.isUserDefined(keyBSON, opt) {
|
|
|
|
continue
|
|
|
|
}
|
2018-08-07 20:28:45 +02:00
|
|
|
return fmt.Sprintf("unknown option '%s' in BSON tag", opt), false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
func (w lintStructTagRule) checkJSONTag(name string, options []string) (string, bool) {
|
2018-07-28 18:07:31 +02:00
|
|
|
for _, opt := range options {
|
|
|
|
switch opt {
|
|
|
|
case "omitempty", "string":
|
2019-08-05 18:21:20 +02:00
|
|
|
case "":
|
|
|
|
// special case for JSON key "-"
|
|
|
|
if name != "-" {
|
2021-03-19 00:10:22 +02:00
|
|
|
return "option can not be empty in JSON tag", false
|
2019-08-05 18:21:20 +02:00
|
|
|
}
|
2018-07-28 18:07:31 +02:00
|
|
|
default:
|
2023-03-15 01:16:12 +02:00
|
|
|
if w.isUserDefined(keyJSON, opt) {
|
|
|
|
continue
|
|
|
|
}
|
2018-07-28 18:07:31 +02:00
|
|
|
return fmt.Sprintf("unknown option '%s' in JSON tag", opt), false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
func (w lintStructTagRule) checkXMLTag(options []string) (string, bool) {
|
2018-07-28 18:07:31 +02:00
|
|
|
for _, opt := range options {
|
|
|
|
switch opt {
|
2018-08-07 20:28:45 +02:00
|
|
|
case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr":
|
2018-07-28 18:07:31 +02:00
|
|
|
default:
|
2023-03-15 01:16:12 +02:00
|
|
|
if w.isUserDefined(keyXML, opt) {
|
|
|
|
continue
|
|
|
|
}
|
2018-07-28 18:07:31 +02:00
|
|
|
return fmt.Sprintf("unknown option '%s' in XML tag", opt), false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
2023-03-15 01:16:12 +02:00
|
|
|
func (w lintStructTagRule) checkYAMLTag(options []string) (string, bool) {
|
2018-07-28 18:07:31 +02:00
|
|
|
for _, opt := range options {
|
|
|
|
switch opt {
|
|
|
|
case "flow", "inline", "omitempty":
|
|
|
|
default:
|
2023-03-15 01:16:12 +02:00
|
|
|
if w.isUserDefined(keyYAML, opt) {
|
|
|
|
continue
|
|
|
|
}
|
2018-07-28 18:07:31 +02:00
|
|
|
return fmt.Sprintf("unknown option '%s' in YAML tag", opt), false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
2022-04-10 11:55:13 +02:00
|
|
|
func (lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool {
|
2018-07-28 18:07:31 +02:00
|
|
|
tID, ok := t.(*ast.Ident)
|
|
|
|
if !ok {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
typeMatches := true
|
|
|
|
switch tID.Name {
|
|
|
|
case "bool":
|
|
|
|
typeMatches = val == "true" || val == "false"
|
|
|
|
case "float64":
|
|
|
|
_, err := strconv.ParseFloat(val, 64)
|
|
|
|
typeMatches = err == nil
|
|
|
|
case "int":
|
|
|
|
_, err := strconv.ParseInt(val, 10, 64)
|
|
|
|
typeMatches = err == nil
|
|
|
|
case "string":
|
|
|
|
case "nil":
|
|
|
|
default:
|
|
|
|
// unchecked type
|
|
|
|
}
|
|
|
|
|
|
|
|
return typeMatches
|
|
|
|
}
|
|
|
|
|
2022-07-15 20:15:55 +02:00
|
|
|
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:
|
2023-03-15 01:16:12 +02:00
|
|
|
if w.isUserDefined(keyProtobuf, k) {
|
|
|
|
continue
|
|
|
|
}
|
2022-07-15 20:15:55 +02:00
|
|
|
return fmt.Sprintf("unknown option '%s' in protobuf tag", k), false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
}
|
|
|
|
|
2018-07-28 18:07:31 +02:00
|
|
|
func (w lintStructTagRule) addFailure(n ast.Node, msg string) {
|
|
|
|
w.onFailure(lint.Failure{
|
|
|
|
Node: n,
|
|
|
|
Failure: msg,
|
|
|
|
Confidence: 1,
|
|
|
|
})
|
|
|
|
}
|
2023-03-15 01:16:12 +02:00
|
|
|
|
|
|
|
func (w lintStructTagRule) isUserDefined(key, opt string) bool {
|
|
|
|
if w.userDefined == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
options := w.userDefined[key]
|
|
|
|
for _, o := range options {
|
|
|
|
if opt == o {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|