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.
5 // Package structtag defines an Analyzer that checks struct field tags
19 "golang.org/x/tools/go/analysis"
20 "golang.org/x/tools/go/analysis/passes/inspect"
21 "golang.org/x/tools/go/ast/inspector"
24 const Doc = `check that struct field tags conform to reflect.StructTag.Get
26 Also report certain struct tags (json, xml) used with unexported fields.`
28 var Analyzer = &analysis.Analyzer{
31 Requires: []*analysis.Analyzer{inspect.Analyzer},
32 RunDespiteErrors: true,
36 func run(pass *analysis.Pass) (interface{}, error) {
37 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
39 nodeFilter := []ast.Node{
40 (*ast.StructType)(nil),
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.
49 for i := 0; i < styp.NumFields(); i++ {
50 field := styp.Field(i)
52 checkCanonicalFieldTag(pass, field, tag, &seen)
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
64 type uniqueName struct {
65 key string // "xml" or "json"
66 name string // the encoding name
67 level int // anonymous struct nesting level
70 func (s *namesSeen) Get(key, name string, level int) (token.Pos, bool) {
72 *s = make(map[uniqueName]token.Pos)
74 pos, ok := (*s)[uniqueName{key, name, level}]
78 func (s *namesSeen) Set(key, name string, level int, pos token.Pos) {
80 *s = make(map[uniqueName]token.Pos)
82 (*s)[uniqueName{key, name, level}] = pos
85 var checkTagDups = []string{"json", "xml"}
86 var checkTagSpaces = map[string]bool{"json": true, "xml": true, "asn1": true}
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.
97 for _, key := range checkTagDups {
98 checkTagDuplicates(pass, tag, key, field, field, seen, 1)
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)
105 // Check for use of json or xml tags with unexported fields.
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() {
114 if field.Exported() {
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
124 pass.Reportf(field.Pos(), "struct field %s has %s tag but is not exported", field.Name(), enc)
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)
136 // Ignored, even if the field is anonymous.
139 if val == "" || val[0] == ',' {
140 if !field.Anonymous() {
141 // Ignored if the field isn't anonymous.
144 typ, ok := field.Type().Underlying().(*types.Struct)
148 for i := 0; i < typ.NumFields(); i++ {
149 field := typ.Field(i)
150 if !field.Exported() {
154 checkTagDuplicates(pass, tag, key, nearest, field, seen, level+1)
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.
166 if i := strings.Index(val, ","); i >= 0 {
168 // Use a separate namespace for XML attributes.
169 for _, opt := range strings.Split(val[i:], ",") {
171 key += " attribute" // Key is part of the error message.
178 if pos, ok := seen.Get(key, val, level); ok {
179 alsoPos := pass.Fset.Position(pos)
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)
189 // Possibly because the paths are relative; leave the
192 alsoPos.Filename = rel
195 pass.Reportf(nearest.Pos(), "struct field %s repeats %s tag %q also at %s", field.Name(), key, val, alsoPos)
197 seen.Set(key, val, level, field.Pos())
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")
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.
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".
222 // Skip leading space.
224 for i < len(tag) && tag[i] == ' ' {
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.
237 for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
241 return errTagKeySyntax
243 if i+1 >= len(tag) || tag[i] != ':' {
247 return errTagValueSyntax
252 // Scan quoted string to find value.
254 for i < len(tag) && tag[i] != '"' {
261 return errTagValueSyntax
266 value, err := strconv.Unquote(qvalue)
268 return errTagValueSyntax
271 if !checkTagSpaces[key] {
277 // If the first or last character in the XML tag is a space, it is
279 if strings.Trim(value, " ") != value {
280 return errTagValueSpace
283 // If there are multiple spaces, they are suspicious.
284 if strings.Count(value, " ") > 1 {
285 return errTagValueSpace
288 // If there is no comma, skip the rest of the checks.
289 comma := strings.IndexRune(value, ',')
294 // If the character before a comma is a space, this is suspicious.
295 if comma > 0 && value[comma-1] == ' ' {
296 return errTagValueSpace
298 value = value[comma+1:]
300 // JSON allows using spaces in the name, so skip it.
301 comma := strings.IndexRune(value, ',')
305 value = value[comma+1:]
308 if strings.IndexByte(value, ' ') >= 0 {
309 return errTagValueSpace