aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/structtag/structtag.go
blob: f0b15051c52eacc1cc5fd6912bfa03dcb3ea7558 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package structtag defines an Analyzer that checks struct field tags
// are well formed.
package structtag

import (
	"errors"
	"go/ast"
	"go/token"
	"go/types"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/passes/inspect"
	"golang.org/x/tools/go/ast/inspector"
)

const Doc = `check that struct field tags conform to reflect.StructTag.Get

Also report certain struct tags (json, xml) used with unexported fields.`

var Analyzer = &analysis.Analyzer{
	Name:             "structtag",
	Doc:              Doc,
	Requires:         []*analysis.Analyzer{inspect.Analyzer},
	RunDespiteErrors: true,
	Run:              run,
}

func run(pass *analysis.Pass) (interface{}, error) {
	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	nodeFilter := []ast.Node{
		(*ast.StructType)(nil),
	}
	inspect.Preorder(nodeFilter, func(n ast.Node) {
		styp, ok := pass.TypesInfo.Types[n.(*ast.StructType)].Type.(*types.Struct)
		// Type information may be incomplete.
		if !ok {
			return
		}
		var seen namesSeen
		for i := 0; i < styp.NumFields(); i++ {
			field := styp.Field(i)
			tag := styp.Tag(i)
			checkCanonicalFieldTag(pass, field, tag, &seen)
		}
	})
	return nil, nil
}

// namesSeen keeps track of encoding tags by their key, name, and nested level
// from the initial struct. The level is taken into account because equal
// encoding key names only conflict when at the same level; otherwise, the lower
// level shadows the higher level.
type namesSeen map[uniqueName]token.Pos

type uniqueName struct {
	key   string // "xml" or "json"
	name  string // the encoding name
	level int    // anonymous struct nesting level
}

func (s *namesSeen) Get(key, name string, level int) (token.Pos, bool) {
	if *s == nil {
		*s = make(map[uniqueName]token.Pos)
	}
	pos, ok := (*s)[uniqueName{key, name, level}]
	return pos, ok
}

func (s *namesSeen) Set(key, name string, level int, pos token.Pos) {
	if *s == nil {
		*s = make(map[uniqueName]token.Pos)
	}
	(*s)[uniqueName{key, name, level}] = pos
}

var checkTagDups = []string{"json", "xml"}
var checkTagSpaces = map[string]bool{"json": true, "xml": true, "asn1": true}

// checkCanonicalFieldTag checks a single struct field tag.
func checkCanonicalFieldTag(pass *analysis.Pass, field *types.Var, tag string, seen *namesSeen) {
	switch pass.Pkg.Path() {
	case "encoding/json", "encoding/xml":
		// These packages know how to use their own APIs.
		// Sometimes they are testing what happens to incorrect programs.
		return
	}

	for _, key := range checkTagDups {
		checkTagDuplicates(pass, tag, key, field, field, seen, 1)
	}

	if err := validateStructTag(tag); err != nil {
		pass.Reportf(field.Pos(), "struct field tag %#q not compatible with reflect.StructTag.Get: %s", tag, err)
	}

	// Check for use of json or xml tags with unexported fields.

	// Embedded struct. Nothing to do for now, but that
	// may change, depending on what happens with issue 7363.
	// TODO(adonovan): investigate, now that that issue is fixed.
	if field.Anonymous() {
		return
	}

	if field.Exported() {
		return
	}

	for _, enc := range [...]string{"json", "xml"} {
		switch reflect.StructTag(tag).Get(enc) {
		// Ignore warning if the field not exported and the tag is marked as
		// ignored.
		case "", "-":
		default:
			pass.Reportf(field.Pos(), "struct field %s has %s tag but is not exported", field.Name(), enc)
			return
		}
	}
}

// checkTagDuplicates checks a single struct field tag to see if any tags are
// duplicated. nearest is the field that's closest to the field being checked,
// while still being part of the top-level struct type.
func checkTagDuplicates(pass *analysis.Pass, tag, key string, nearest, field *types.Var, seen *namesSeen, level int) {
	val := reflect.StructTag(tag).Get(key)
	if val == "-" {
		// Ignored, even if the field is anonymous.
		return
	}
	if val == "" || val[0] == ',' {
		if !field.Anonymous() {
			// Ignored if the field isn't anonymous.
			return
		}
		typ, ok := field.Type().Underlying().(*types.Struct)
		if !ok {
			return
		}
		for i := 0; i < typ.NumFields(); i++ {
			field := typ.Field(i)
			if !field.Exported() {
				continue
			}
			tag := typ.Tag(i)
			checkTagDuplicates(pass, tag, key, nearest, field, seen, level+1)
		}
		return
	}
	if key == "xml" && field.Name() == "XMLName" {
		// XMLName defines the XML element name of the struct being
		// checked. That name cannot collide with element or attribute
		// names defined on other fields of the struct. Vet does not have a
		// check for untagged fields of type struct defining their own name
		// by containing a field named XMLName; see issue 18256.
		return
	}
	if i := strings.Index(val, ","); i >= 0 {
		if key == "xml" {
			// Use a separate namespace for XML attributes.
			for _, opt := range strings.Split(val[i:], ",") {
				if opt == "attr" {
					key += " attribute" // Key is part of the error message.
					break
				}
			}
		}
		val = val[:i]
	}
	if pos, ok := seen.Get(key, val, level); ok {
		alsoPos := pass.Fset.Position(pos)
		alsoPos.Column = 0

		// Make the "also at" position relative to the current position,
		// to ensure that all warnings are unambiguous and correct. For
		// example, via anonymous struct fields, it's possible for the
		// two fields to be in different packages and directories.
		thisPos := pass.Fset.Position(field.Pos())
		rel, err := filepath.Rel(filepath.Dir(thisPos.Filename), alsoPos.Filename)
		if err != nil {
			// Possibly because the paths are relative; leave the
			// filename alone.
		} else {
			alsoPos.Filename = rel
		}

		pass.Reportf(nearest.Pos(), "struct field %s repeats %s tag %q also at %s", field.Name(), key, val, alsoPos)
	} else {
		seen.Set(key, val, level, field.Pos())
	}
}

var (
	errTagSyntax      = errors.New("bad syntax for struct tag pair")
	errTagKeySyntax   = errors.New("bad syntax for struct tag key")
	errTagValueSyntax = errors.New("bad syntax for struct tag value")
	errTagValueSpace  = errors.New("suspicious space in struct tag value")
	errTagSpace       = errors.New("key:\"value\" pairs not separated by spaces")
)

// validateStructTag parses the struct tag and returns an error if it is not
// in the canonical format, which is a space-separated list of key:"value"
// settings. The value may contain spaces.
func validateStructTag(tag string) error {
	// This code is based on the StructTag.Get code in package reflect.

	n := 0
	for ; tag != ""; n++ {
		if n > 0 && tag != "" && tag[0] != ' ' {
			// More restrictive than reflect, but catches likely mistakes
			// like `x:"foo",y:"bar"`, which parses as `x:"foo" ,y:"bar"` with second key ",y".
			return errTagSpace
		}
		// Skip leading space.
		i := 0
		for i < len(tag) && tag[i] == ' ' {
			i++
		}
		tag = tag[i:]
		if tag == "" {
			break
		}

		// Scan to colon. A space, a quote or a control character is a syntax error.
		// Strictly speaking, control chars include the range [0x7f, 0x9f], not just
		// [0x00, 0x1f], but in practice, we ignore the multi-byte control characters
		// as it is simpler to inspect the tag's bytes than the tag's runes.
		i = 0
		for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
			i++
		}
		if i == 0 {
			return errTagKeySyntax
		}
		if i+1 >= len(tag) || tag[i] != ':' {
			return errTagSyntax
		}
		if tag[i+1] != '"' {
			return errTagValueSyntax
		}
		key := tag[:i]
		tag = tag[i+1:]

		// Scan quoted string to find value.
		i = 1
		for i < len(tag) && tag[i] != '"' {
			if tag[i] == '\\' {
				i++
			}
			i++
		}
		if i >= len(tag) {
			return errTagValueSyntax
		}
		qvalue := tag[:i+1]
		tag = tag[i+1:]

		value, err := strconv.Unquote(qvalue)
		if err != nil {
			return errTagValueSyntax
		}

		if !checkTagSpaces[key] {
			continue
		}

		switch key {
		case "xml":
			// If the first or last character in the XML tag is a space, it is
			// suspicious.
			if strings.Trim(value, " ") != value {
				return errTagValueSpace
			}

			// If there are multiple spaces, they are suspicious.
			if strings.Count(value, " ") > 1 {
				return errTagValueSpace
			}

			// If there is no comma, skip the rest of the checks.
			comma := strings.IndexRune(value, ',')
			if comma < 0 {
				continue
			}

			// If the character before a comma is a space, this is suspicious.
			if comma > 0 && value[comma-1] == ' ' {
				return errTagValueSpace
			}
			value = value[comma+1:]
		case "json":
			// JSON allows using spaces in the name, so skip it.
			comma := strings.IndexRune(value, ',')
			if comma < 0 {
				continue
			}
			value = value[comma+1:]
		}

		if strings.IndexByte(value, ' ') >= 0 {
			return errTagValueSpace
		}
	}
	return nil
}