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"
2025-07-12 17:32:39 +03:00
2025-05-26 13:18:38 +02:00
"github.com/mgechev/revive/internal/astutils"
2019-06-01 10:34:43 +02:00
"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
2025-09-19 07:10:52 +02:00
omittedTags map [ tagKey ] struct { } // set of tags that must not be analyzed
2025-04-10 07:43:35 +02:00
}
type tagKey string
const (
keyASN1 tagKey = "asn1"
keyBSON tagKey = "bson"
2025-09-22 11:03:36 +02:00
keyCbor tagKey = "cbor"
2025-09-08 10:20:24 +03:00
keyCodec tagKey = "codec"
2025-04-10 07:43:35 +02:00
keyDatastore tagKey = "datastore"
keyDefault tagKey = "default"
keyJSON tagKey = "json"
keyMapstructure tagKey = "mapstructure"
keyProperties tagKey = "properties"
keyProtobuf tagKey = "protobuf"
keyRequired tagKey = "required"
2025-08-16 01:00:40 +05:30
keySpanner tagKey = "spanner"
2025-04-10 07:43:35 +02:00
keyTOML tagKey = "toml"
keyURL tagKey = "url"
keyValidate tagKey = "validate"
keyXML tagKey = "xml"
keyYAML tagKey = "yaml"
)
2025-09-08 10:20:24 +03:00
type tagChecker func ( checkCtx * checkContext , tag * structtag . Tag , field * ast . Field ) ( message string , succeeded bool )
2025-04-10 07:43:35 +02:00
var tagCheckers = map [ tagKey ] tagChecker {
keyASN1 : checkASN1Tag ,
keyBSON : checkBSONTag ,
2025-09-22 11:03:36 +02:00
keyCbor : checkCborTag ,
2025-09-08 10:20:24 +03:00
keyCodec : checkCodecTag ,
2025-04-10 07:43:35 +02:00
keyDatastore : checkDatastoreTag ,
keyDefault : checkDefaultTag ,
keyJSON : checkJSONTag ,
keyMapstructure : checkMapstructureTag ,
keyProperties : checkPropertiesTag ,
keyProtobuf : checkProtobufTag ,
keyRequired : checkRequiredTag ,
2025-08-16 01:00:40 +05:30
keySpanner : checkSpannerTag ,
2025-04-10 07:43:35 +02:00
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
2025-09-08 10:20:24 +03:00
commonOptions map [ string ] bool // list of options defined for all fields
2025-04-10 07:43:35 +02:00
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
}
2025-09-08 10:20:24 +03:00
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
}
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
}
2025-09-19 07:10:52 +02:00
r . userDefined = map [ tagKey ] [ ] string { }
r . omittedTags = map [ tagKey ] struct { } { }
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
}
2025-09-19 07:10:52 +02:00
2024-10-01 12:14:02 +02:00
parts := strings . Split ( item , "," )
2025-09-19 07:10:52 +02:00
keyStr := strings . TrimSpace ( parts [ 0 ] )
keyStr , isOmitted := strings . CutPrefix ( keyStr , "!" )
key := tagKey ( keyStr )
if isOmitted {
r . omittedTags [ key ] = struct { } { }
continue
2024-10-01 12:14:02 +02:00
}
2025-09-19 07:10:52 +02:00
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-09-19 07:10:52 +02:00
omittedTags : r . omittedTags ,
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-09-19 07:10:52 +02:00
omittedTags map [ tagKey ] struct { }
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
2025-09-08 10:20:24 +03:00
func ( w lintStructTagRule ) checkTaggedField ( checkCtx * checkContext , field * ast . Field ) {
tags , err := structtag . Parse ( strings . Trim ( field . Tag . Value , "`" ) )
2025-04-10 07:43:35 +02:00
if err != nil || tags == nil {
2025-09-08 10:20:24 +03:00
w . addFailuref ( field . Tag , "malformed tag" )
2025-04-10 07:43:35 +02:00
return
}
2025-09-08 10:20:24 +03:00
analyzedTags := map [ tagKey ] struct { } { }
2025-04-10 07:43:35 +02:00
for _ , tag := range tags . Tags ( ) {
2025-09-19 07:10:52 +02:00
_ , mustOmit := w . omittedTags [ tagKey ( tag . Key ) ]
if mustOmit {
continue
}
2025-04-10 07:43:35 +02:00
if msg , ok := w . checkTagNameIfNeed ( checkCtx , tag ) ; ! ok {
2025-09-08 10:20:24 +03:00
w . addFailureWithTagKey ( field . Tag , msg , tag . Key )
2025-04-10 07:43:35 +02:00
}
2025-08-24 07:17:09 +02:00
if msg , ok := checkOptionsOnIgnoredField ( tag ) ; ! ok {
2025-09-08 10:20:24 +03:00
w . addFailureWithTagKey ( field . Tag , msg , tag . Key )
2025-08-24 07:17:09 +02:00
}
2025-09-08 10:20:24 +03:00
key := tagKey ( tag . Key )
checker , ok := w . tagCheckers [ key ]
2025-04-10 07:43:35 +02:00
if ! ok {
continue // we don't have a checker for the tag
}
2025-09-08 10:20:24 +03:00
msg , ok := checker ( checkCtx , tag , field )
2025-04-10 07:43:35 +02:00
if ! ok {
2025-09-08 10:20:24 +03:00
w . addFailureWithTagKey ( field . Tag , msg , tag . Key )
2025-04-10 07:43:35 +02:00
}
2025-09-08 10:20:24 +03:00
analyzedTags [ key ] = struct { } { }
}
if w . shallWarnOnUnexportedField ( field . Names , analyzedTags ) {
w . addFailuref ( field , "tag on not-exported field %s" , field . Names [ 0 ] . Name )
2025-04-10 07:43:35 +02:00
}
}
2025-09-08 10:20:24 +03:00
// 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
}
2025-04-10 07:43:35 +02:00
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 {
2025-09-08 10:20:24 +03:00
case keyBSON , keyCodec , keyJSON , keyProtobuf , keySpanner , keyXML , keyYAML : // keys that need to check for duplicated tags
2024-12-12 18:42:09 +02:00
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-09-08 10:20:24 +03:00
func checkASN1Tag ( checkCtx * checkContext , tag * structtag . Tag , field * ast . Field ) ( message string , succeeded bool ) {
fieldType := field . Type
2025-05-26 12:40:17 +03:00
checkList := slices . Concat ( tag . Options , [ ] string { tag . Name } )
2025-04-10 07:43:35 +02:00
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-09-08 10:20:24 +03:00
func checkDatastoreTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
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-09-08 10:20:24 +03:00
func checkDefaultTag ( _ * checkContext , tag * structtag . Tag , field * ast . Field ) ( message string , succeeded bool ) {
if ! typeValueMatch ( field . Type , tag . Name ) {
2025-04-10 07:43:35 +02:00
return msgTypeMismatch , false
2018-08-07 20:28:45 +02:00
}
return "" , true
}
2025-09-08 10:20:24 +03:00
func checkBSONTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
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-09-22 11:03:36 +02:00
func checkCborTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
hasToArray := false
hasOmitEmptyOrZero := false
hasKeyAsInt := false
for _ , opt := range tag . Options {
switch opt {
case "omitempty" , "omitzero" :
hasOmitEmptyOrZero = true
case "toarray" :
if tag . Name != "" {
return ` tag name for option "toarray" should be empty ` , false
}
hasToArray = true
case "keyasint" :
intKey , err := strconv . Atoi ( tag . Name )
if err != nil {
return ` tag name for option "keyasint" should be an integer ` , false
}
_ , ok := checkCtx . usedTagNbr [ intKey ]
if ok {
return fmt . Sprintf ( "duplicated integer key %d" , intKey ) , false
}
checkCtx . usedTagNbr [ intKey ] = true
hasKeyAsInt = true
continue
default :
if ! checkCtx . isUserDefined ( keyCbor , opt ) {
return fmt . Sprintf ( msgUnknownOption , opt ) , false
}
}
}
// Check for duplicated tag names
if tag . Name != "" {
_ , ok := checkCtx . usedTagName [ tag . Name ]
if ok {
return fmt . Sprintf ( "duplicated tag name %s" , tag . Name ) , false
}
checkCtx . usedTagName [ tag . Name ] = true
}
// Check for integer tag names without keyasint option
if ! hasKeyAsInt {
_ , err := strconv . Atoi ( tag . Name )
if err == nil {
return ` integer tag names are only allowed in presence of "keyasint" option ` , false
}
}
if hasToArray && hasOmitEmptyOrZero {
return ` options "omitempty" and "omitzero" are ignored in presence of "toarray" option ` , false
}
return "" , true
}
2025-09-08 10:20:24 +03:00
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 ) {
2025-04-10 07:43:35 +02:00
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-09-08 10:20:24 +03:00
func checkMapstructureTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
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-09-08 10:20:24 +03:00
func checkPropertiesTag ( _ * checkContext , tag * structtag . Tag , field * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
options := tag . Options
if len ( options ) == 0 {
return "" , true
}
seenOptions := map [ string ] bool { }
2025-09-08 10:20:24 +03:00
fieldType := field . Type
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" :
2025-05-26 13:18:38 +02:00
if astutils . GoFmt ( fieldType ) != "time.Time" {
2025-04-10 07:43:35 +02:00
return "layout option is only applicable to fields of type time.Time" , false
}
}
return "" , true
}
2025-09-08 10:20:24 +03:00
func checkProtobufTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
// 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-09-08 10:20:24 +03:00
func checkRequiredTag ( _ * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
switch tag . Name {
case "true" , "false" :
return "" , true
default :
return ` required should be "true" or "false" ` , false
}
}
2025-09-08 10:20:24 +03:00
func checkTOMLTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
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-09-08 10:20:24 +03:00
func checkURLTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
var delimiter = ""
for _ , opt := range tag . Options {
2025-02-19 14:30:29 +01:00
switch opt {
2025-07-30 15:55:03 +02:00
case "int" , "omitempty" , "numbered" , "brackets" ,
"unix" , "unixmilli" , "unixnano" : // TODO : check that the field is of type time.Time
2025-04-10 07:43:35 +02:00
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-09-08 10:20:24 +03:00
func checkValidateTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( 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-09-08 10:20:24 +03:00
func checkXMLTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
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-09-08 10:20:24 +03:00
func checkYAMLTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-04-10 07:43:35 +02:00
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
}
2025-09-08 10:20:24 +03:00
func checkSpannerTag ( checkCtx * checkContext , tag * structtag . Tag , _ * ast . Field ) ( message string , succeeded bool ) {
2025-08-16 01:00:40 +05:30
for _ , opt := range tag . Options {
if ! checkCtx . isUserDefined ( keySpanner , opt ) {
return fmt . Sprintf ( msgUnknownOption , opt ) , false
}
}
2025-08-17 07:50:54 +02:00
2025-08-16 01:00:40 +05:30
return "" , true
}
2025-08-24 07:17:09 +02:00
// checkOptionsOnIgnoredField checks if an ignored struct field (tag name "-") has any options specified.
// It returns a message and false if there are useless options present, or an empty message and true if valid.
func checkOptionsOnIgnoredField ( tag * structtag . Tag ) ( message string , succeeded bool ) {
if tag . Name != "-" {
return "" , true
}
switch len ( tag . Options ) {
case 0 :
return "" , true
case 1 :
opt := strings . TrimSpace ( tag . Options [ 0 ] )
if opt == "" {
return "" , true // accept "-," as options
}
return fmt . Sprintf ( "useless option %s for ignored field" , opt ) , false
default :
return fmt . Sprintf ( "useless options %s for ignored field" , strings . Join ( tag . Options , "," ) ) , false
}
}
2025-04-10 07:43:35 +02:00
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
2025-07-30 15:55:03 +02:00
default : // "string", "nil", ...
2018-07-28 18:07:31 +02:00
// unchecked type
}
return typeMatches
}
2022-07-15 20:15:55 +02:00
2025-05-26 12:40:17 +03:00
func ( w lintStructTagRule ) addFailureWithTagKey ( n ast . Node , msg , tagKey string ) {
2025-04-10 07:43:35 +02:00
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 { } {
2025-05-22 20:53:10 +02:00
"alpha" : { } ,
"alphanum" : { } ,
"alphanumunicode" : { } ,
"alphaunicode" : { } ,
"ascii" : { } ,
"base32" : { } ,
"base64" : { } ,
"base64rawurl" : { } ,
"base64url" : { } ,
"bcp47_language_tag" : { } ,
"bic" : { } ,
"boolean" : { } ,
"btc_addr" : { } ,
"btc_addr_bech32" : { } ,
"cidr" : { } ,
"cidrv4" : { } ,
"cidrv6" : { } ,
2025-09-25 22:40:51 -06:00
"contains" : { } ,
"containsany" : { } ,
"containsrune" : { } ,
2025-05-22 20:53:10 +02:00
"credit_card" : { } ,
"cron" : { } ,
"cve" : { } ,
"datauri" : { } ,
2025-09-25 22:40:51 -06:00
"datetime" : { } ,
2025-05-22 20:53:10 +02:00
"dir" : { } ,
"dirpath" : { } ,
"dive" : { } ,
"dns_rfc1035_label" : { } ,
"e164" : { } ,
"ein" : { } ,
"email" : { } ,
2025-09-25 22:40:51 -06:00
"endsnotwith" : { } ,
"endswith" : { } ,
"eq" : { } ,
"eq_ignore_case" : { } ,
"eqcsfield" : { } ,
"eqfield" : { } ,
2025-05-22 20:53:10 +02:00
"eth_addr" : { } ,
"eth_addr_checksum" : { } ,
2025-09-25 22:40:51 -06:00
"excluded_if" : { } ,
"excluded_unless" : { } ,
"excluded_with" : { } ,
"excluded_with_all" : { } ,
"excluded_without" : { } ,
"excluded_without_all" : { } ,
"excludes" : { } ,
"excludesall" : { } ,
"excludesrune" : { } ,
"fieldcontains" : { } ,
"fieldexcludes" : { } ,
2025-05-22 20:53:10 +02:00
"file" : { } ,
"filepath" : { } ,
"fqdn" : { } ,
2025-09-25 22:40:51 -06:00
"gt" : { } ,
"gtcsfield" : { } ,
"gte" : { } ,
"gtecsfield" : { } ,
"gtefield" : { } ,
"gtfield" : { } ,
2025-05-22 20:53:10 +02:00
"hexadecimal" : { } ,
"hexcolor" : { } ,
"hostname" : { } ,
"hostname_port" : { } ,
"hostname_rfc1123" : { } ,
"hsl" : { } ,
"hsla" : { } ,
"html" : { } ,
"html_encoded" : { } ,
"http_url" : { } ,
"image" : { } ,
"ip" : { } ,
"ip4_addr" : { } ,
"ip6_addr" : { } ,
2025-09-25 22:40:51 -06:00
"ip_addr" : { } ,
2025-05-22 20:53:10 +02:00
"ipv4" : { } ,
"ipv6" : { } ,
"isbn" : { } ,
"isbn10" : { } ,
"isbn13" : { } ,
"isdefault" : { } ,
"iso3166_1_alpha2" : { } ,
"iso3166_1_alpha2_eu" : { } ,
"iso3166_1_alpha3" : { } ,
"iso3166_1_alpha3_eu" : { } ,
2025-09-25 22:40:51 -06:00
"iso3166_1_alpha_numeric" : { } ,
"iso3166_1_alpha_numeric_eu" : { } ,
2025-05-22 20:53:10 +02:00
"iso3166_2" : { } ,
"iso4217" : { } ,
"iso4217_numeric" : { } ,
"issn" : { } ,
"json" : { } ,
"jwt" : { } ,
"latitude" : { } ,
2025-09-25 22:40:51 -06:00
"len" : { } ,
2025-05-22 20:53:10 +02:00
"longitude" : { } ,
"lowercase" : { } ,
2025-09-25 22:40:51 -06:00
"lt" : { } ,
"ltcsfield" : { } ,
"lte" : { } ,
"ltecsfield" : { } ,
"ltefield" : { } ,
"ltfield" : { } ,
2025-05-22 20:53:10 +02:00
"luhn_checksum" : { } ,
"mac" : { } ,
2025-09-25 22:40:51 -06:00
"max" : { } ,
2025-05-22 20:53:10 +02:00
"md4" : { } ,
"md5" : { } ,
2025-09-25 22:40:51 -06:00
"min" : { } ,
2025-05-22 20:53:10 +02:00
"mongodb" : { } ,
"mongodb_connection_string" : { } ,
"multibyte" : { } ,
2025-09-25 22:40:51 -06:00
"ne" : { } ,
"ne_ignore_case" : { } ,
"necsfield" : { } ,
"nefield" : { } ,
2025-05-22 20:53:10 +02:00
"number" : { } ,
"numeric" : { } ,
2025-09-20 12:16:31 +02:00
"omitempty" : { } ,
2025-09-25 22:40:51 -06:00
"omitnil" : { } ,
"omitzero" : { } ,
"oneof" : { } ,
"oneofci" : { } ,
2025-05-22 20:53:10 +02:00
"port" : { } ,
"postcode_iso3166_alpha2" : { } ,
"postcode_iso3166_alpha2_field" : { } ,
"printascii" : { } ,
"required" : { } ,
2025-09-25 22:40:51 -06:00
"required_if" : { } ,
"required_unless" : { } ,
"required_with" : { } ,
"required_with_all" : { } ,
"required_without" : { } ,
"required_without_all" : { } ,
2025-05-22 20:53:10 +02:00
"rgb" : { } ,
"rgba" : { } ,
"ripemd128" : { } ,
"ripemd160" : { } ,
"semver" : { } ,
"sha256" : { } ,
"sha384" : { } ,
"sha512" : { } ,
2025-09-25 22:40:51 -06:00
"skip_unless" : { } ,
"spicedb" : { } ,
2025-05-22 20:53:10 +02:00
"ssn" : { } ,
2025-09-25 22:40:51 -06:00
"startsnotwith" : { } ,
"startswith" : { } ,
2025-05-22 20:53:10 +02:00
"tcp4_addr" : { } ,
"tcp6_addr" : { } ,
2025-09-25 22:40:51 -06:00
"tcp_addr" : { } ,
2025-05-22 20:53:10 +02:00
"tiger128" : { } ,
"tiger160" : { } ,
"tiger192" : { } ,
"timezone" : { } ,
"udp4_addr" : { } ,
"udp6_addr" : { } ,
2025-09-25 22:40:51 -06:00
"udp_addr" : { } ,
2025-05-22 20:53:10 +02:00
"ulid" : { } ,
2025-09-25 22:40:51 -06:00
"unique" : { } ,
2025-05-22 20:53:10 +02:00
"unix_addr" : { } ,
"uppercase" : { } ,
"uri" : { } ,
"url" : { } ,
"url_encoded" : { } ,
"urn_rfc2141" : { } ,
"uuid" : { } ,
"uuid3" : { } ,
"uuid3_rfc4122" : { } ,
"uuid4" : { } ,
"uuid4_rfc4122" : { } ,
"uuid5" : { } ,
"uuid5_rfc4122" : { } ,
2025-09-25 22:40:51 -06:00
"uuid_rfc4122" : { } ,
"validateFn" : { } ,
2025-05-22 20:53:10 +02:00
}
2025-11-14 18:02:48 +02:00
// validateLHS are options that are used in expressions of the form:
2025-05-27 08:44:24 +03:00
//
// <option> = <RHS>
2025-02-20 19:48:35 +01:00
var validateLHS = map [ string ] struct { } {
"contains" : { } ,
"containsany" : { } ,
"containsfield" : { } ,
"containsrune" : { } ,
"datetime" : { } ,
"endsnotwith" : { } ,
"endswith" : { } ,
"eq" : { } ,
2025-05-22 20:53:10 +02:00
"eq_ignore_case" : { } ,
2025-02-20 19:48:35 +01:00
"eqcsfield" : { } ,
2025-05-22 20:53:10 +02:00
"eqfield" : { } ,
2025-02-20 19:48:35 +01:00
"excluded_if" : { } ,
"excluded_unless" : { } ,
2025-05-22 20:53:10 +02:00
"excluded_with" : { } ,
"excluded_with_all" : { } ,
"excluded_without" : { } ,
"excluded_without_all" : { } ,
2025-02-20 19:48:35 +01:00
"excludes" : { } ,
"excludesall" : { } ,
"excludesfield" : { } ,
"excludesrune" : { } ,
2025-05-22 20:53:10 +02:00
"fieldcontains" : { } ,
"fieldexcludes" : { } ,
2025-02-20 19:48:35 +01:00
"gt" : { } ,
"gtcsfield" : { } ,
2025-05-22 20:53:10 +02:00
"gte" : { } ,
2025-02-20 19:48:35 +01:00
"gtecsfield" : { } ,
2025-05-22 20:53:10 +02:00
"gtefield" : { } ,
"gtfield" : { } ,
2025-02-20 19:48:35 +01:00
"len" : { } ,
"lt" : { } ,
"ltcsfield" : { } ,
2025-05-22 20:53:10 +02:00
"lte" : { } ,
2025-02-20 19:48:35 +01:00
"ltecsfield" : { } ,
2025-05-22 20:53:10 +02:00
"ltefield" : { } ,
"ltfield" : { } ,
2025-02-20 19:48:35 +01:00
"max" : { } ,
"min" : { } ,
"ne" : { } ,
2025-05-22 20:53:10 +02:00
"ne_ignore_case" : { } ,
2025-02-20 19:48:35 +01:00
"necsfield" : { } ,
2025-05-22 20:53:10 +02:00
"nefield" : { } ,
2025-02-20 19:48:35 +01:00
"oneof" : { } ,
"oneofci" : { } ,
"required_if" : { } ,
"required_unless" : { } ,
"required_with" : { } ,
"required_with_all" : { } ,
"required_without" : { } ,
"required_without_all" : { } ,
2025-05-22 20:53:10 +02:00
"skip_unless" : { } ,
2025-02-20 19:48:35 +01:00
"spicedb" : { } ,
"startsnotwith" : { } ,
"startswith" : { } ,
"unique" : { } ,
2025-05-22 20:53:10 +02:00
"validateFn" : { } ,
2025-02-20 19:48:35 +01:00
}