github.com/Johnny2210/revive@v1.0.8-0.20210625134200-febf37ccd0f5/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/mgechev/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  
    59  // checkTaggedField checks the tag of the given field.
    60  // precondition: the field has a tag
    61  func (w lintStructTagRule) checkTaggedField(f *ast.Field) {
    62  	if len(f.Names) > 0 && !f.Names[0].IsExported() {
    63  		w.addFailure(f, "tag on not-exported field "+f.Names[0].Name)
    64  	}
    65  
    66  	tags, err := structtag.Parse(strings.Trim(f.Tag.Value, "`"))
    67  	if err != nil || tags == nil {
    68  		w.addFailure(f.Tag, "malformed tag")
    69  		return
    70  	}
    71  
    72  	for _, tag := range tags.Tags() {
    73  		switch key := tag.Key; key {
    74  		case "asn1":
    75  			msg, ok := w.checkASN1Tag(f.Type, tag)
    76  			if !ok {
    77  				w.addFailure(f.Tag, msg)
    78  			}
    79  		case "bson":
    80  			msg, ok := w.checkBSONTag(tag.Options)
    81  			if !ok {
    82  				w.addFailure(f.Tag, msg)
    83  			}
    84  		case "default":
    85  			if !w.typeValueMatch(f.Type, tag.Name) {
    86  				w.addFailure(f.Tag, "field's type and default value's type mismatch")
    87  			}
    88  		case "json":
    89  			msg, ok := w.checkJSONTag(tag.Name, tag.Options)
    90  			if !ok {
    91  				w.addFailure(f.Tag, msg)
    92  			}
    93  		case "protobuf":
    94  			// Not implemented yet
    95  		case "required":
    96  			if tag.Name != "true" && tag.Name != "false" {
    97  				w.addFailure(f.Tag, "required should be 'true' or 'false'")
    98  			}
    99  		case "xml":
   100  			msg, ok := w.checkXMLTag(tag.Options)
   101  			if !ok {
   102  				w.addFailure(f.Tag, msg)
   103  			}
   104  		case "yaml":
   105  			msg, ok := w.checkYAMLTag(tag.Options)
   106  			if !ok {
   107  				w.addFailure(f.Tag, msg)
   108  			}
   109  		default:
   110  			// unknown key
   111  		}
   112  	}
   113  }
   114  
   115  func (w lintStructTagRule) checkASN1Tag(t ast.Expr, tag *structtag.Tag) (string, bool) {
   116  	checkList := append(tag.Options, tag.Name)
   117  	for _, opt := range checkList {
   118  		switch opt {
   119  		case "application", "explicit", "generalized", "ia5", "omitempty", "optional", "set", "utf8":
   120  
   121  		default:
   122  			if strings.HasPrefix(opt, "tag:") {
   123  				parts := strings.Split(opt, ":")
   124  				tagNumber := parts[1]
   125  				if w.usedTagNbr[tagNumber] {
   126  					return fmt.Sprintf("duplicated tag number %s", tagNumber), false
   127  				}
   128  				w.usedTagNbr[tagNumber] = true
   129  
   130  				continue
   131  			}
   132  
   133  			if strings.HasPrefix(opt, "default:") {
   134  				parts := strings.Split(opt, ":")
   135  				if len(parts) < 2 {
   136  					return "malformed default for ASN1 tag", false
   137  				}
   138  				if !w.typeValueMatch(t, parts[1]) {
   139  					return "field's type and default value's type mismatch", false
   140  				}
   141  
   142  				continue
   143  			}
   144  
   145  			return fmt.Sprintf("unknown option '%s' in ASN1 tag", opt), false
   146  		}
   147  	}
   148  
   149  	return "", true
   150  }
   151  
   152  func (w lintStructTagRule) checkBSONTag(options []string) (string, bool) {
   153  	for _, opt := range options {
   154  		switch opt {
   155  		case "inline", "minsize", "omitempty":
   156  		default:
   157  			return fmt.Sprintf("unknown option '%s' in BSON tag", opt), false
   158  		}
   159  	}
   160  
   161  	return "", true
   162  }
   163  
   164  func (w lintStructTagRule) checkJSONTag(name string, options []string) (string, bool) {
   165  	for _, opt := range options {
   166  		switch opt {
   167  		case "omitempty", "string":
   168  		case "":
   169  			// special case for JSON key "-"
   170  			if name != "-" {
   171  				return "option can not be empty in JSON tag", false
   172  			}
   173  		default:
   174  			return fmt.Sprintf("unknown option '%s' in JSON tag", opt), false
   175  		}
   176  	}
   177  
   178  	return "", true
   179  }
   180  
   181  func (w lintStructTagRule) checkXMLTag(options []string) (string, bool) {
   182  	for _, opt := range options {
   183  		switch opt {
   184  		case "any", "attr", "cdata", "chardata", "comment", "innerxml", "omitempty", "typeattr":
   185  		default:
   186  			return fmt.Sprintf("unknown option '%s' in XML tag", opt), false
   187  		}
   188  	}
   189  
   190  	return "", true
   191  }
   192  
   193  func (w lintStructTagRule) checkYAMLTag(options []string) (string, bool) {
   194  	for _, opt := range options {
   195  		switch opt {
   196  		case "flow", "inline", "omitempty":
   197  		default:
   198  			return fmt.Sprintf("unknown option '%s' in YAML tag", opt), false
   199  		}
   200  	}
   201  
   202  	return "", true
   203  }
   204  
   205  func (w lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool {
   206  	tID, ok := t.(*ast.Ident)
   207  	if !ok {
   208  		return true
   209  	}
   210  
   211  	typeMatches := true
   212  	switch tID.Name {
   213  	case "bool":
   214  		typeMatches = val == "true" || val == "false"
   215  	case "float64":
   216  		_, err := strconv.ParseFloat(val, 64)
   217  		typeMatches = err == nil
   218  	case "int":
   219  		_, err := strconv.ParseInt(val, 10, 64)
   220  		typeMatches = err == nil
   221  	case "string":
   222  	case "nil":
   223  	default:
   224  		// unchecked type
   225  	}
   226  
   227  	return typeMatches
   228  }
   229  
   230  func (w lintStructTagRule) addFailure(n ast.Node, msg string) {
   231  	w.onFailure(lint.Failure{
   232  		Node:       n,
   233  		Failure:    msg,
   234  		Confidence: 1,
   235  	})
   236  }