github.com/songshiyun/revive@v1.1.5-0.20220323112655-f8433a19b3c5/rule/struct-tag.go (about)

     1  package rule
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/fatih/structtag"
    10  	"github.com/songshiyun/revive/lint"
    11  )
    12  
    13  // StructTagRule lints struct tags.
    14  type StructTagRule struct{}
    15  
    16  // Apply applies the rule to given file.
    17  func (r *StructTagRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
    18  	var failures []lint.Failure
    19  
    20  	onFailure := func(failure lint.Failure) {
    21  		failures = append(failures, failure)
    22  	}
    23  
    24  	w := lintStructTagRule{onFailure: onFailure}
    25  
    26  	ast.Walk(w, file.AST)
    27  
    28  	return failures
    29  }
    30  
    31  // Name returns the rule name.
    32  func (r *StructTagRule) Name() string {
    33  	return "struct-tag"
    34  }
    35  
    36  type lintStructTagRule struct {
    37  	onFailure  func(lint.Failure)
    38  	usedTagNbr map[string]bool // list of used tag numbers
    39  }
    40  
    41  func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor {
    42  	switch n := node.(type) {
    43  	case *ast.StructType:
    44  		if n.Fields == nil || n.Fields.NumFields() < 1 {
    45  			return nil // skip empty structs
    46  		}
    47  		w.usedTagNbr = map[string]bool{} // init
    48  		for _, f := range n.Fields.List {
    49  			if f.Tag != nil {
    50  				w.checkTaggedField(f)
    51  			}
    52  		}
    53  	}
    54  
    55  	return w
    56  }
    57  
    58  // checkTaggedField checks the tag of the given field.
    59  // precondition: the field has a tag
    60  func (w lintStructTagRule) checkTaggedField(f *ast.Field) {
    61  	if len(f.Names) > 0 && !f.Names[0].IsExported() {
    62  		w.addFailure(f, "tag on not-exported field "+f.Names[0].Name)
    63  	}
    64  
    65  	tags, err := structtag.Parse(strings.Trim(f.Tag.Value, "`"))
    66  	if err != nil || tags == nil {
    67  		w.addFailure(f.Tag, "malformed tag")
    68  		return
    69  	}
    70  
    71  	for _, tag := range tags.Tags() {
    72  		switch key := tag.Key; key {
    73  		case "asn1":
    74  			msg, ok := w.checkASN1Tag(f.Type, tag)
    75  			if !ok {
    76  				w.addFailure(f.Tag, msg)
    77  			}
    78  		case "bson":
    79  			msg, ok := w.checkBSONTag(tag.Options)
    80  			if !ok {
    81  				w.addFailure(f.Tag, msg)
    82  			}
    83  		case "default":
    84  			if !w.typeValueMatch(f.Type, tag.Name) {
    85  				w.addFailure(f.Tag, "field's type and default value's type mismatch")
    86  			}
    87  		case "json":
    88  			msg, ok := w.checkJSONTag(tag.Name, tag.Options)
    89  			if !ok {
    90  				w.addFailure(f.Tag, msg)
    91  			}
    92  		case "protobuf":
    93  			// Not implemented yet
    94  		case "required":
    95  			if tag.Name != "true" && tag.Name != "false" {
    96  				w.addFailure(f.Tag, "required should be 'true' or 'false'")
    97  			}
    98  		case "xml":
    99  			msg, ok := w.checkXMLTag(tag.Options)
   100  			if !ok {
   101  				w.addFailure(f.Tag, msg)
   102  			}
   103  		case "yaml":
   104  			msg, ok := w.checkYAMLTag(tag.Options)
   105  			if !ok {
   106  				w.addFailure(f.Tag, msg)
   107  			}
   108  		default:
   109  			// unknown key
   110  		}
   111  	}
   112  }
   113  
   114  func (w lintStructTagRule) checkASN1Tag(t ast.Expr, tag *structtag.Tag) (string, bool) {
   115  	checkList := append(tag.Options, tag.Name)
   116  	for _, opt := range checkList {
   117  		switch opt {
   118  		case "application", "explicit", "generalized", "ia5", "omitempty", "optional", "set", "utf8":
   119  
   120  		default:
   121  			if strings.HasPrefix(opt, "tag:") {
   122  				parts := strings.Split(opt, ":")
   123  				tagNumber := parts[1]
   124  				if w.usedTagNbr[tagNumber] {
   125  					return fmt.Sprintf("duplicated tag number %s", tagNumber), false
   126  				}
   127  				w.usedTagNbr[tagNumber] = true
   128  
   129  				continue
   130  			}
   131  
   132  			if strings.HasPrefix(opt, "default:") {
   133  				parts := strings.Split(opt, ":")
   134  				if len(parts) < 2 {
   135  					return "malformed default for ASN1 tag", false
   136  				}
   137  				if !w.typeValueMatch(t, parts[1]) {
   138  					return "field's type and default value's type mismatch", false
   139  				}
   140  
   141  				continue
   142  			}
   143  
   144  			return fmt.Sprintf("unknown option '%s' in ASN1 tag", opt), false
   145  		}
   146  	}
   147  
   148  	return "", true
   149  }
   150  
   151  func (w lintStructTagRule) checkBSONTag(options []string) (string, bool) {
   152  	for _, opt := range options {
   153  		switch opt {
   154  		case "inline", "minsize", "omitempty":
   155  		default:
   156  			return fmt.Sprintf("unknown option '%s' in BSON tag", opt), false
   157  		}
   158  	}
   159  
   160  	return "", true
   161  }
   162  
   163  func (w lintStructTagRule) checkJSONTag(name string, options []string) (string, bool) {
   164  	for _, opt := range options {
   165  		switch opt {
   166  		case "omitempty", "string":
   167  		case "":
   168  			// special case for JSON key "-"
   169  			if name != "-" {
   170  				return "option can not be empty in JSON tag", false
   171  			}
   172  		default:
   173  			return fmt.Sprintf("unknown option '%s' in JSON tag", opt), false
   174  		}
   175  	}
   176  
   177  	return "", true
   178  }
   179  
   180  func (w lintStructTagRule) checkXMLTag(options []string) (string, bool) {
   181  	for _, opt := range options {
   182  		switch opt {
   183  		case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr":
   184  		default:
   185  			return fmt.Sprintf("unknown option '%s' in XML tag", opt), false
   186  		}
   187  	}
   188  
   189  	return "", true
   190  }
   191  
   192  func (w lintStructTagRule) checkYAMLTag(options []string) (string, bool) {
   193  	for _, opt := range options {
   194  		switch opt {
   195  		case "flow", "inline", "omitempty":
   196  		default:
   197  			return fmt.Sprintf("unknown option '%s' in YAML tag", opt), false
   198  		}
   199  	}
   200  
   201  	return "", true
   202  }
   203  
   204  func (w lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool {
   205  	tID, ok := t.(*ast.Ident)
   206  	if !ok {
   207  		return true
   208  	}
   209  
   210  	typeMatches := true
   211  	switch tID.Name {
   212  	case "bool":
   213  		typeMatches = val == "true" || val == "false"
   214  	case "float64":
   215  		_, err := strconv.ParseFloat(val, 64)
   216  		typeMatches = err == nil
   217  	case "int":
   218  		_, err := strconv.ParseInt(val, 10, 64)
   219  		typeMatches = err == nil
   220  	case "string":
   221  	case "nil":
   222  	default:
   223  		// unchecked type
   224  	}
   225  
   226  	return typeMatches
   227  }
   228  
   229  func (w lintStructTagRule) addFailure(n ast.Node, msg string) {
   230  	w.onFailure(lint.Failure{
   231  		Node:       n,
   232  		Failure:    msg,
   233  		Confidence: 1,
   234  	})
   235  }