github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/go/analysis/passes/structtag/structtag.go (about) 1 // Copyright 2010 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package structtag defines an Analyzer that checks struct field tags 6 // are well formed. 7 package structtag 8 9 import ( 10 "errors" 11 "go/ast" 12 "go/token" 13 "go/types" 14 "path/filepath" 15 "reflect" 16 "strconv" 17 "strings" 18 19 "github.com/powerman/golang-tools/go/analysis" 20 "github.com/powerman/golang-tools/go/analysis/passes/inspect" 21 "github.com/powerman/golang-tools/go/ast/inspector" 22 ) 23 24 const Doc = `check that struct field tags conform to reflect.StructTag.Get 25 26 Also report certain struct tags (json, xml) used with unexported fields.` 27 28 var Analyzer = &analysis.Analyzer{ 29 Name: "structtag", 30 Doc: Doc, 31 Requires: []*analysis.Analyzer{inspect.Analyzer}, 32 RunDespiteErrors: true, 33 Run: run, 34 } 35 36 func run(pass *analysis.Pass) (interface{}, error) { 37 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 38 39 nodeFilter := []ast.Node{ 40 (*ast.StructType)(nil), 41 } 42 inspect.Preorder(nodeFilter, func(n ast.Node) { 43 styp, ok := pass.TypesInfo.Types[n.(*ast.StructType)].Type.(*types.Struct) 44 // Type information may be incomplete. 45 if !ok { 46 return 47 } 48 var seen namesSeen 49 for i := 0; i < styp.NumFields(); i++ { 50 field := styp.Field(i) 51 tag := styp.Tag(i) 52 checkCanonicalFieldTag(pass, field, tag, &seen) 53 } 54 }) 55 return nil, nil 56 } 57 58 // namesSeen keeps track of encoding tags by their key, name, and nested level 59 // from the initial struct. The level is taken into account because equal 60 // encoding key names only conflict when at the same level; otherwise, the lower 61 // level shadows the higher level. 62 type namesSeen map[uniqueName]token.Pos 63 64 type uniqueName struct { 65 key string // "xml" or "json" 66 name string // the encoding name 67 level int // anonymous struct nesting level 68 } 69 70 func (s *namesSeen) Get(key, name string, level int) (token.Pos, bool) { 71 if *s == nil { 72 *s = make(map[uniqueName]token.Pos) 73 } 74 pos, ok := (*s)[uniqueName{key, name, level}] 75 return pos, ok 76 } 77 78 func (s *namesSeen) Set(key, name string, level int, pos token.Pos) { 79 if *s == nil { 80 *s = make(map[uniqueName]token.Pos) 81 } 82 (*s)[uniqueName{key, name, level}] = pos 83 } 84 85 var checkTagDups = []string{"json", "xml"} 86 var checkTagSpaces = map[string]bool{"json": true, "xml": true, "asn1": true} 87 88 // checkCanonicalFieldTag checks a single struct field tag. 89 func checkCanonicalFieldTag(pass *analysis.Pass, field *types.Var, tag string, seen *namesSeen) { 90 switch pass.Pkg.Path() { 91 case "encoding/json", "encoding/xml": 92 // These packages know how to use their own APIs. 93 // Sometimes they are testing what happens to incorrect programs. 94 return 95 } 96 97 for _, key := range checkTagDups { 98 checkTagDuplicates(pass, tag, key, field, field, seen, 1) 99 } 100 101 if err := validateStructTag(tag); err != nil { 102 pass.Reportf(field.Pos(), "struct field tag %#q not compatible with reflect.StructTag.Get: %s", tag, err) 103 } 104 105 // Check for use of json or xml tags with unexported fields. 106 107 // Embedded struct. Nothing to do for now, but that 108 // may change, depending on what happens with issue 7363. 109 // TODO(adonovan): investigate, now that that issue is fixed. 110 if field.Anonymous() { 111 return 112 } 113 114 if field.Exported() { 115 return 116 } 117 118 for _, enc := range [...]string{"json", "xml"} { 119 switch reflect.StructTag(tag).Get(enc) { 120 // Ignore warning if the field not exported and the tag is marked as 121 // ignored. 122 case "", "-": 123 default: 124 pass.Reportf(field.Pos(), "struct field %s has %s tag but is not exported", field.Name(), enc) 125 return 126 } 127 } 128 } 129 130 // checkTagDuplicates checks a single struct field tag to see if any tags are 131 // duplicated. nearest is the field that's closest to the field being checked, 132 // while still being part of the top-level struct type. 133 func checkTagDuplicates(pass *analysis.Pass, tag, key string, nearest, field *types.Var, seen *namesSeen, level int) { 134 val := reflect.StructTag(tag).Get(key) 135 if val == "-" { 136 // Ignored, even if the field is anonymous. 137 return 138 } 139 if val == "" || val[0] == ',' { 140 if !field.Anonymous() { 141 // Ignored if the field isn't anonymous. 142 return 143 } 144 typ, ok := field.Type().Underlying().(*types.Struct) 145 if !ok { 146 return 147 } 148 for i := 0; i < typ.NumFields(); i++ { 149 field := typ.Field(i) 150 if !field.Exported() { 151 continue 152 } 153 tag := typ.Tag(i) 154 checkTagDuplicates(pass, tag, key, nearest, field, seen, level+1) 155 } 156 return 157 } 158 if key == "xml" && field.Name() == "XMLName" { 159 // XMLName defines the XML element name of the struct being 160 // checked. That name cannot collide with element or attribute 161 // names defined on other fields of the struct. Vet does not have a 162 // check for untagged fields of type struct defining their own name 163 // by containing a field named XMLName; see issue 18256. 164 return 165 } 166 if i := strings.Index(val, ","); i >= 0 { 167 if key == "xml" { 168 // Use a separate namespace for XML attributes. 169 for _, opt := range strings.Split(val[i:], ",") { 170 if opt == "attr" { 171 key += " attribute" // Key is part of the error message. 172 break 173 } 174 } 175 } 176 val = val[:i] 177 } 178 if pos, ok := seen.Get(key, val, level); ok { 179 alsoPos := pass.Fset.Position(pos) 180 alsoPos.Column = 0 181 182 // Make the "also at" position relative to the current position, 183 // to ensure that all warnings are unambiguous and correct. For 184 // example, via anonymous struct fields, it's possible for the 185 // two fields to be in different packages and directories. 186 thisPos := pass.Fset.Position(field.Pos()) 187 rel, err := filepath.Rel(filepath.Dir(thisPos.Filename), alsoPos.Filename) 188 if err != nil { 189 // Possibly because the paths are relative; leave the 190 // filename alone. 191 } else { 192 alsoPos.Filename = rel 193 } 194 195 pass.Reportf(nearest.Pos(), "struct field %s repeats %s tag %q also at %s", field.Name(), key, val, alsoPos) 196 } else { 197 seen.Set(key, val, level, field.Pos()) 198 } 199 } 200 201 var ( 202 errTagSyntax = errors.New("bad syntax for struct tag pair") 203 errTagKeySyntax = errors.New("bad syntax for struct tag key") 204 errTagValueSyntax = errors.New("bad syntax for struct tag value") 205 errTagValueSpace = errors.New("suspicious space in struct tag value") 206 errTagSpace = errors.New("key:\"value\" pairs not separated by spaces") 207 ) 208 209 // validateStructTag parses the struct tag and returns an error if it is not 210 // in the canonical format, which is a space-separated list of key:"value" 211 // settings. The value may contain spaces. 212 func validateStructTag(tag string) error { 213 // This code is based on the StructTag.Get code in package reflect. 214 215 n := 0 216 for ; tag != ""; n++ { 217 if n > 0 && tag != "" && tag[0] != ' ' { 218 // More restrictive than reflect, but catches likely mistakes 219 // like `x:"foo",y:"bar"`, which parses as `x:"foo" ,y:"bar"` with second key ",y". 220 return errTagSpace 221 } 222 // Skip leading space. 223 i := 0 224 for i < len(tag) && tag[i] == ' ' { 225 i++ 226 } 227 tag = tag[i:] 228 if tag == "" { 229 break 230 } 231 232 // Scan to colon. A space, a quote or a control character is a syntax error. 233 // Strictly speaking, control chars include the range [0x7f, 0x9f], not just 234 // [0x00, 0x1f], but in practice, we ignore the multi-byte control characters 235 // as it is simpler to inspect the tag's bytes than the tag's runes. 236 i = 0 237 for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f { 238 i++ 239 } 240 if i == 0 { 241 return errTagKeySyntax 242 } 243 if i+1 >= len(tag) || tag[i] != ':' { 244 return errTagSyntax 245 } 246 if tag[i+1] != '"' { 247 return errTagValueSyntax 248 } 249 key := tag[:i] 250 tag = tag[i+1:] 251 252 // Scan quoted string to find value. 253 i = 1 254 for i < len(tag) && tag[i] != '"' { 255 if tag[i] == '\\' { 256 i++ 257 } 258 i++ 259 } 260 if i >= len(tag) { 261 return errTagValueSyntax 262 } 263 qvalue := tag[:i+1] 264 tag = tag[i+1:] 265 266 value, err := strconv.Unquote(qvalue) 267 if err != nil { 268 return errTagValueSyntax 269 } 270 271 if !checkTagSpaces[key] { 272 continue 273 } 274 275 switch key { 276 case "xml": 277 // If the first or last character in the XML tag is a space, it is 278 // suspicious. 279 if strings.Trim(value, " ") != value { 280 return errTagValueSpace 281 } 282 283 // If there are multiple spaces, they are suspicious. 284 if strings.Count(value, " ") > 1 { 285 return errTagValueSpace 286 } 287 288 // If there is no comma, skip the rest of the checks. 289 comma := strings.IndexRune(value, ',') 290 if comma < 0 { 291 continue 292 } 293 294 // If the character before a comma is a space, this is suspicious. 295 if comma > 0 && value[comma-1] == ' ' { 296 return errTagValueSpace 297 } 298 value = value[comma+1:] 299 case "json": 300 // JSON allows using spaces in the name, so skip it. 301 comma := strings.IndexRune(value, ',') 302 if comma < 0 { 303 continue 304 } 305 value = value[comma+1:] 306 } 307 308 if strings.IndexByte(value, ' ') >= 0 { 309 return errTagValueSpace 310 } 311 } 312 return nil 313 }