1
2
3
4
5
6
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 "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
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
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
59
60
61
62 type namesSeen map[uniqueName]token.Pos
63
64 type uniqueName struct {
65 key string
66 name string
67 level int
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
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
93
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
106
107
108
109
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
121
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
131
132
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
137 return
138 }
139 if val == "" || val[0] == ',' {
140 if !field.Anonymous() {
141
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
160
161
162
163
164 return
165 }
166 if i := strings.Index(val, ","); i >= 0 {
167 if key == "xml" {
168
169 for _, opt := range strings.Split(val[i:], ",") {
170 if opt == "attr" {
171 key += " attribute"
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
183
184
185
186 thisPos := pass.Fset.Position(field.Pos())
187 rel, err := filepath.Rel(filepath.Dir(thisPos.Filename), alsoPos.Filename)
188 if err != nil {
189
190
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
210
211
212 func validateStructTag(tag string) error {
213
214
215 n := 0
216 for ; tag != ""; n++ {
217 if n > 0 && tag != "" && tag[0] != ' ' {
218
219
220 return errTagSpace
221 }
222
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
233
234
235
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
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
278
279 if strings.Trim(value, " ") != value {
280 return errTagValueSpace
281 }
282
283
284 if strings.Count(value, " ") > 1 {
285 return errTagValueSpace
286 }
287
288
289 comma := strings.IndexRune(value, ',')
290 if comma < 0 {
291 continue
292 }
293
294
295 if comma > 0 && value[comma-1] == ' ' {
296 return errTagValueSpace
297 }
298 value = value[comma+1:]
299 case "json":
300
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
View as plain text