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 }