GoPLS Viewer

Home|gopls/go/analysis/passes/structtag/structtag.go
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.
7package structtag
8
9import (
10    "errors"
11    "go/ast"
12    "go/token"
13    "go/types"
14    "path/filepath"
15    "reflect"
16    "strconv"
17    "strings"
18
19    "golang.org/x/tools/go/analysis"
20    "golang.org/x/tools/go/analysis/passes/inspect"
21    "golang.org/x/tools/go/ast/inspector"
22)
23
24const Doc = `check that struct field tags conform to reflect.StructTag.Get
25
26Also report certain struct tags (json, xml) used with unexported fields.`
27
28var Analyzer = &analysis.Analyzer{
29    Name:             "structtag",
30    Doc:              Doc,
31    Requires:         []*analysis.Analyzer{inspect.Analyzer},
32    RunDespiteErrorstrue,
33    Run:              run,
34}
35
36func 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        stypok := 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 := 0i < styp.NumFields(); i++ {
50            field := styp.Field(i)
51            tag := styp.Tag(i)
52            checkCanonicalFieldTag(passfieldtag, &seen)
53        }
54    })
55    return nilnil
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.
62type namesSeen map[uniqueName]token.Pos
63
64type uniqueName struct {
65    key   string // "xml" or "json"
66    name  string // the encoding name
67    level int    // anonymous struct nesting level
68}
69
70func (s *namesSeenGet(keyname stringlevel int) (token.Posbool) {
71    if *s == nil {
72        *s = make(map[uniqueName]token.Pos)
73    }
74    posok := (*s)[uniqueName{keynamelevel}]
75    return posok
76}
77
78func (s *namesSeenSet(keyname stringlevel intpos token.Pos) {
79    if *s == nil {
80        *s = make(map[uniqueName]token.Pos)
81    }
82    (*s)[uniqueName{keynamelevel}] = pos
83}
84
85var checkTagDups = []string{"json""xml"}
86var checkTagSpaces = map[string]bool{"json"true"xml"true"asn1"true}
87
88// checkCanonicalFieldTag checks a single struct field tag.
89func checkCanonicalFieldTag(pass *analysis.Passfield *types.Vartag stringseen *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(passtagkeyfieldfieldseen1)
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"tagerr)
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.
133func checkTagDuplicates(pass *analysis.Passtagkey stringnearestfield *types.Varseen *namesSeenlevel 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        typok := field.Type().Underlying().(*types.Struct)
145        if !ok {
146            return
147        }
148        for i := 0i < typ.NumFields(); i++ {
149            field := typ.Field(i)
150            if !field.Exported() {
151                continue
152            }
153            tag := typ.Tag(i)
154            checkTagDuplicates(passtagkeynearestfieldseenlevel+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 posok := seen.Get(keyvallevel); 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        relerr := 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(), keyvalalsoPos)
196    } else {
197        seen.Set(keyvallevelfield.Pos())
198    }
199}
200
201var (
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.
212func validateStructTag(tag stringerror {
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        valueerr := 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}
314
MembersX
checkTagDuplicates
checkTagDuplicates.BlockStmt.thisPos
validateStructTag.BlockStmt.BlockStmt.comma
namesSeen.Set.key
namesSeen.Set.pos
run.BlockStmt.BlockStmt.tag
checkTagDuplicates.pass
checkTagDuplicates.field
checkTagDuplicates.seen
checkTagDuplicates.level
types
uniqueName
run.nodeFilter
run
uniqueName.key
namesSeen.Get.s
namesSeen.Set.s
checkTagDuplicates.val
inspector
checkCanonicalFieldTag.tag
checkCanonicalFieldTag.seen
checkCanonicalFieldTag.RangeStmt_2713.key
checkTagDuplicates.tag
checkTagDuplicates.i
validateStructTag.n
checkCanonicalFieldTag
namesSeen.Get.level
namesSeen.Set
reflect
run.BlockStmt.i
uniqueName.level
checkCanonicalFieldTag.err
checkTagDuplicates.key
checkTagDuplicates.BlockStmt.BlockStmt.field
checkTagDuplicates.BlockStmt.BlockStmt.tag
validateStructTag.tag
Doc
validateStructTag.BlockStmt.value
run.BlockStmt.BlockStmt.field
checkTagDuplicates.BlockStmt.i
checkTagDuplicates.BlockStmt.alsoPos
filepath
analysis
inspect
namesSeen.Get.key
ast
namesSeen.Set.name
namesSeen.Set.level
namesSeen.Get.name
checkTagDuplicates.BlockStmt.BlockStmt.RangeStmt_4969.opt
checkTagDuplicates.ok
validateStructTag.BlockStmt.i
validateStructTag.BlockStmt.err
strconv
checkCanonicalFieldTag.field
checkTagDuplicates.nearest
checkCanonicalFieldTag.pass
uniqueName.name
checkCanonicalFieldTag.RangeStmt_3284.enc
checkTagDuplicates.pos
strings
run.pass
namesSeen
namesSeen.Get
checkTagDuplicates.BlockStmt.err
validateStructTag
errors
run.BlockStmt.seen
checkTagDuplicates.BlockStmt.rel
token
Members
X