2018-07-28 18:07:31 +02:00
|
|
|
package rule
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"go/ast"
|
2025-03-28 01:32:30 -07:00
|
|
|
"slices"
|
2018-07-28 18:07:31 +02:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
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 00:16:12 +01:00
|
|
|
type StructTagRule struct {
|
2025-04-10 07:43:35 +02:00
|
|
|
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)
|
2023-03-15 00:16:12 +01:00
|
|
|
}
|
|
|
|
|
|
2024-12-13 21:38:46 +01:00
|
|
|
// Configure validates the rule configuration, and configures the rule accordingly.
|
|
|
|
|
//
|
|
|
|
|
// Configuration implements the [lint.ConfigurableRule] interface.
|
|
|
|
|
func (r *StructTagRule) Configure(arguments lint.Arguments) error {
|
2024-11-15 13:03:59 +02:00
|
|
|
if len(arguments) == 0 {
|
2024-12-11 19:35:58 +01:00
|
|
|
return nil
|
2024-10-01 12:14:02 +02:00
|
|
|
}
|
|
|
|
|
|
2024-12-11 19:35:58 +01:00
|
|
|
err := checkNumberOfArguments(1, arguments, r.Name())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
|
|
|
|
|
r.userDefined = make(map[tagKey][]string, len(arguments))
|
2024-10-01 12:14:02 +02:00
|
|
|
for _, arg := range arguments {
|
|
|
|
|
item, ok := arg.(string)
|
|
|
|
|
if !ok {
|
2024-12-11 19:35:58 +01:00
|
|
|
return fmt.Errorf("invalid argument to the %s rule. Expecting a string, got %v (of type %T)", r.Name(), arg, arg)
|
2024-10-01 12:14:02 +02:00
|
|
|
}
|
|
|
|
|
parts := strings.Split(item, ",")
|
|
|
|
|
if len(parts) < 2 {
|
2024-12-11 19:35:58 +01:00
|
|
|
return fmt.Errorf("invalid argument to the %s rule. Expecting a string of the form key[,option]+, got %s", r.Name(), item)
|
2024-10-01 12:14:02 +02:00
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
key := tagKey(strings.TrimSpace(parts[0]))
|
2024-10-01 12:14:02 +02:00
|
|
|
for i := 1; i < len(parts); i++ {
|
|
|
|
|
option := strings.TrimSpace(parts[i])
|
|
|
|
|
r.userDefined[key] = append(r.userDefined[key], option)
|
2023-03-15 00:16:12 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
|
2024-12-11 19:35:58 +01:00
|
|
|
return nil
|
2023-03-15 00:16:12 +01:00
|
|
|
}
|
2018-07-28 18:07:31 +02:00
|
|
|
|
|
|
|
|
// Apply applies the rule to given file.
|
2024-12-13 21:38:46 +01:00
|
|
|
func (r *StructTagRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
|
2023-03-15 00:16:12 +01: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 00:16:12 +01:00
|
|
|
w := lintStructTagRule{
|
2025-02-16 11:31:17 +01:00
|
|
|
onFailure: onFailure,
|
|
|
|
|
userDefined: r.userDefined,
|
2025-03-16 18:18:05 +01:00
|
|
|
isAtLeastGo124: file.Pkg.IsAtLeastGoVersion(lint.Go124),
|
2025-04-10 07:43:35 +02:00
|
|
|
tagCheckers: tagCheckers,
|
2023-03-15 00:16:12 +01:00
|
|
|
}
|
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 {
|
2025-02-16 11:31:17 +01:00
|
|
|
onFailure func(lint.Failure)
|
2025-04-10 07:43:35 +02:00
|
|
|
userDefined map[tagKey][]string // map: key -> []option
|
2025-02-16 11:31:17 +01:00
|
|
|
isAtLeastGo124 bool
|
2025-04-10 07:43:35 +02:00
|
|
|
tagCheckers map[tagKey]tagChecker
|
2018-07-28 18:07:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor {
|
2025-05-01 12:40:29 +03:00
|
|
|
if n, ok := node.(*ast.StructType); ok {
|
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
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
checkCtx := &checkContext{
|
|
|
|
|
userDefined: w.userDefined,
|
|
|
|
|
usedTagNbr: map[int]bool{},
|
|
|
|
|
usedTagName: map[string]bool{},
|
|
|
|
|
isAtLeastGo124: w.isAtLeastGo124,
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-28 18:07:31 +02:00
|
|
|
for _, f := range n.Fields.List {
|
|
|
|
|
if f.Tag != nil {
|
2025-04-10 07:43:35 +02:00
|
|
|
w.checkTaggedField(checkCtx, f)
|
2018-07-28 18:07:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return w
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
// 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)
|
|
|
|
|
}
|
2023-03-15 00:16:12 +01:00
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
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) {
|
2022-07-14 02:26:44 +08:00
|
|
|
isUnnamedTag := tag.Name == "" || tag.Name == "-"
|
|
|
|
|
if isUnnamedTag {
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
key := tagKey(tag.Key)
|
|
|
|
|
switch key {
|
2024-12-12 18:42:09 +02:00
|
|
|
case keyBSON, keyJSON, keyXML, keyYAML, keyProtobuf:
|
|
|
|
|
default:
|
2022-07-14 02:26:44 +08:00
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tagName := w.getTagName(tag)
|
2022-07-15 20:15:55 +02:00
|
|
|
if tagName == "" {
|
|
|
|
|
return "", true // No tag name found
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-14 02:26:44 +08:00
|
|
|
// We concat the key and name as the mapping key here
|
|
|
|
|
// to allow the same tag name in different tag type.
|
2025-04-10 07:43:35 +02:00
|
|
|
mapKey := tag.Key + ":" + tagName
|
|
|
|
|
if _, ok := checkCtx.usedTagName[mapKey]; ok {
|
|
|
|
|
return fmt.Sprintf("duplicated tag name %q", tagName), false
|
2022-07-14 02:26:44 +08:00
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
checkCtx.usedTagName[mapKey] = true
|
2022-07-14 02:26:44 +08:00
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lintStructTagRule) getTagName(tag *structtag.Tag) string {
|
2025-04-10 07:43:35 +02:00
|
|
|
key := tagKey(tag.Key)
|
|
|
|
|
switch key {
|
2023-03-15 00:16:12 +01:00
|
|
|
case keyProtobuf:
|
2022-07-14 02:26:44 +08:00
|
|
|
for _, option := range tag.Options {
|
2025-04-10 07:43:35 +02:00
|
|
|
if tagKey, found := strings.CutPrefix(option, "name="); found {
|
|
|
|
|
return tagKey
|
2022-07-14 02:26:44 +08:00
|
|
|
}
|
|
|
|
|
}
|
2023-09-23 10:41:34 +02:00
|
|
|
return "" // protobuf tag lacks 'name' option
|
2022-07-14 02:26:44 +08:00
|
|
|
default:
|
|
|
|
|
return tag.Name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
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:
|
|
|
|
|
msg, ok := checkCompoundANS1Option(checkCtx, opt, fieldType)
|
|
|
|
|
if !ok {
|
|
|
|
|
return msg, false
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-01 10:34:43 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
return "", true
|
|
|
|
|
}
|
2018-07-28 18:07:31 +02:00
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
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
|
2022-07-14 02:26:44 +08:00
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
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
|
2018-07-28 18:07:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return "", true
|
2018-07-28 18:07:31 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func checkDatastoreTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
|
|
|
|
|
for _, opt := range tag.Options {
|
2018-08-07 20:28:45 +02:00
|
|
|
switch opt {
|
2025-04-10 07:43:35 +02:00
|
|
|
case "flatten", "noindex", "omitempty":
|
2018-08-07 20:28:45 +02:00
|
|
|
default:
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isUserDefined(keyDatastore, opt) {
|
2018-08-07 20:28:45 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, opt), false
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-07 20:28:45 +02:00
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
return "", true
|
|
|
|
|
}
|
2023-03-15 00:16:12 +01:00
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func checkDefaultTag(_ *checkContext, tag *structtag.Tag, fieldType ast.Expr) (message string, succeeded bool) {
|
|
|
|
|
if !typeValueMatch(fieldType, tag.Name) {
|
|
|
|
|
return msgTypeMismatch, false
|
2018-08-07 20:28:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func checkBSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
|
|
|
|
|
for _, opt := range tag.Options {
|
2018-08-07 20:28:45 +02:00
|
|
|
switch opt {
|
|
|
|
|
case "inline", "minsize", "omitempty":
|
|
|
|
|
default:
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isUserDefined(keyBSON, opt) {
|
2023-03-15 00:16:12 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, opt), false
|
2018-08-07 20:28:45 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func checkJSONTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
|
|
|
|
|
for _, opt := range tag.Options {
|
2018-07-28 18:07:31 +02:00
|
|
|
switch opt {
|
|
|
|
|
case "omitempty", "string":
|
2019-08-05 18:21:20 +02:00
|
|
|
case "":
|
|
|
|
|
// special case for JSON key "-"
|
2025-04-10 07:43:35 +02:00
|
|
|
if tag.Name != "-" {
|
|
|
|
|
return "option can not be empty", false
|
2019-08-05 18:21:20 +02:00
|
|
|
}
|
2025-02-16 11:31:17 +01:00
|
|
|
case "omitzero":
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isAtLeastGo124 {
|
2025-02-16 11:31:17 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return `prior Go 1.24, option "omitzero" is unsupported`, false
|
2018-07-28 18:07:31 +02:00
|
|
|
default:
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isUserDefined(keyJSON, opt) {
|
2023-03-15 00:16:12 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, opt), false
|
2018-07-28 18:07:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func checkMapstructureTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
|
|
|
|
|
for _, opt := range tag.Options {
|
2018-07-28 18:07:31 +02:00
|
|
|
switch opt {
|
2025-04-10 07:43:35 +02:00
|
|
|
case "omitempty", "reminder", "squash":
|
2018-07-28 18:07:31 +02:00
|
|
|
default:
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isUserDefined(keyMapstructure, opt) {
|
2023-03-15 00:16:12 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, opt), false
|
2018-07-28 18:07:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
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{}
|
2018-07-28 18:07:31 +02:00
|
|
|
for _, opt := range options {
|
2025-04-10 07:43:35 +02:00
|
|
|
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
|
2018-07-28 18:07:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
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
|
2025-02-17 07:18:30 +01:00
|
|
|
for _, opt := range options {
|
2025-04-10 07:43:35 +02:00
|
|
|
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
|
2025-02-17 07:18:30 +01:00
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
checkCtx.usedTagNbr[number] = true
|
|
|
|
|
continue // option is an integer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch opt {
|
|
|
|
|
case "json", "opt", "proto3", "rep", "req":
|
|
|
|
|
// do nothing
|
|
|
|
|
case "name":
|
|
|
|
|
hasName = true
|
2025-02-17 07:18:30 +01:00
|
|
|
default:
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isUserDefined(keyProtobuf, opt) {
|
2025-02-17 07:18:30 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, opt), false
|
2025-02-17 07:18:30 +01:00
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
|
|
|
|
|
_, alreadySeen := seenOptions[opt]
|
|
|
|
|
if alreadySeen {
|
|
|
|
|
return fmt.Sprintf(msgDuplicatedOption, opt), false
|
|
|
|
|
}
|
|
|
|
|
seenOptions[opt] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !hasName {
|
|
|
|
|
return `mandatory option "name" not found`, false
|
2025-02-17 07:18:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
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 {
|
2025-02-18 13:09:31 +01:00
|
|
|
switch opt {
|
2025-04-10 07:43:35 +02:00
|
|
|
case "omitempty":
|
2025-02-18 13:09:31 +01:00
|
|
|
default:
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isUserDefined(keyTOML, opt) {
|
2025-02-18 13:09:31 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, opt), false
|
2025-02-18 13:09:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func checkURLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
|
|
|
|
|
var delimiter = ""
|
|
|
|
|
for _, opt := range tag.Options {
|
2025-02-19 14:30:29 +01:00
|
|
|
switch opt {
|
2025-04-10 07:43:35 +02:00
|
|
|
case "int", "omitempty", "numbered", "brackets":
|
|
|
|
|
case "unix", "unixmilli", "unixnano": // TODO : check that the field is of type time.Time
|
|
|
|
|
case "comma", "semicolon", "space":
|
|
|
|
|
if delimiter == "" {
|
|
|
|
|
delimiter = opt
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("can not set both %q and %q as delimiters", opt, delimiter), false
|
2025-02-19 14:30:29 +01:00
|
|
|
default:
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isUserDefined(keyURL, opt) {
|
2025-02-19 14:30:29 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, opt), false
|
2025-02-19 14:30:29 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func checkValidateTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
|
2025-02-20 19:48:35 +01:00
|
|
|
previousOption := ""
|
|
|
|
|
seenKeysOption := false
|
2025-04-10 07:43:35 +02:00
|
|
|
options := append([]string{tag.Name}, tag.Options...)
|
2025-02-20 19:48:35 +01:00
|
|
|
for _, opt := range options {
|
|
|
|
|
switch opt {
|
|
|
|
|
case "keys":
|
|
|
|
|
if previousOption != "dive" {
|
2025-04-10 07:43:35 +02:00
|
|
|
return `option "keys" must follow a "dive" option`, false
|
2025-02-20 19:48:35 +01:00
|
|
|
}
|
|
|
|
|
seenKeysOption = true
|
|
|
|
|
case "endkeys":
|
|
|
|
|
if !seenKeysOption {
|
2025-04-10 07:43:35 +02:00
|
|
|
return `option "endkeys" without a previous "keys" option`, false
|
2025-02-20 19:48:35 +01:00
|
|
|
}
|
|
|
|
|
seenKeysOption = false
|
|
|
|
|
default:
|
|
|
|
|
parts := strings.Split(opt, "|")
|
2025-04-10 07:43:35 +02:00
|
|
|
errMsg, ok := checkValidateOptionsAlternatives(checkCtx, parts)
|
2025-02-20 19:48:35 +01:00
|
|
|
if !ok {
|
|
|
|
|
return errMsg, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
previousOption = opt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func checkXMLTag(checkCtx *checkContext, tag *structtag.Tag, _ ast.Expr) (message string, succeeded bool) {
|
|
|
|
|
for _, opt := range tag.Options {
|
2025-03-03 13:19:40 +01:00
|
|
|
switch opt {
|
2025-04-10 07:43:35 +02:00
|
|
|
case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr":
|
2025-03-03 13:19:40 +01:00
|
|
|
default:
|
2025-04-10 07:43:35 +02:00
|
|
|
if checkCtx.isUserDefined(keyXML, opt) {
|
2025-03-03 13:19:40 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, opt), false
|
2025-03-03 13:19:40 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
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) {
|
2025-02-20 19:48:35 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
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)
|
|
|
|
|
lhs, _, found := strings.Cut(alternative, "=")
|
|
|
|
|
if found {
|
2025-02-20 19:48:35 +01:00
|
|
|
_, ok := validateLHS[lhs]
|
2025-04-10 07:43:35 +02:00
|
|
|
if ok || checkCtx.isUserDefined(keyValidate, lhs) {
|
2025-02-20 19:48:35 +01:00
|
|
|
continue
|
|
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
return fmt.Sprintf(msgUnknownOption, lhs), false
|
2025-02-20 19:48:35 +01:00
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
|
|
|
|
|
badOpt, ok := areValidateOpts(alternative)
|
|
|
|
|
if ok || checkCtx.isUserDefined(keyValidate, badOpt) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf(msgUnknownOption, badOpt), false
|
2025-02-20 19:48:35 +01:00
|
|
|
}
|
2025-04-10 07:43:35 +02:00
|
|
|
|
2025-02-20 19:48:35 +01:00
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func 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
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func (w lintStructTagRule) addFailureWithTagKey(n ast.Node, msg string, tagKey string) {
|
|
|
|
|
w.addFailuref(n, "%s in %s tag", msg, tagKey)
|
2022-07-15 20:15:55 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
func (w lintStructTagRule) addFailuref(n ast.Node, msg string, args ...any) {
|
2018-07-28 18:07:31 +02:00
|
|
|
w.onFailure(lint.Failure{
|
|
|
|
|
Node: n,
|
2025-04-10 07:43:35 +02:00
|
|
|
Failure: fmt.Sprintf(msg, args...),
|
2018-07-28 18:07:31 +02:00
|
|
|
Confidence: 1,
|
|
|
|
|
})
|
|
|
|
|
}
|
2023-03-15 00:16:12 +01:00
|
|
|
|
2025-02-20 19:48:35 +01:00
|
|
|
func areValidateOpts(opts string) (string, bool) {
|
|
|
|
|
parts := strings.Split(opts, "|")
|
|
|
|
|
for _, opt := range parts {
|
|
|
|
|
_, ok := validateSingleOptions[opt]
|
|
|
|
|
if !ok {
|
|
|
|
|
return opt, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 07:43:35 +02:00
|
|
|
const (
|
|
|
|
|
msgDuplicatedOption = "duplicated option %q"
|
|
|
|
|
msgDuplicatedTagNumber = "duplicated tag number %v"
|
|
|
|
|
msgUnknownOption = "unknown option %q"
|
|
|
|
|
msgTypeMismatch = "type mismatch between field type and default value type"
|
|
|
|
|
)
|
|
|
|
|
|
2025-02-20 19:48:35 +01:00
|
|
|
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": {},
|
|
|
|
|
}
|