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 }