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 // This file contains the test for canonical struct tags.
19 register("structtags",
20 "check that struct field tags have canonical format and apply to exported fields as needed",
25 // checkStructFieldTags checks all the field tags of a struct, including checking for duplicates.
26 func checkStructFieldTags(f
*File
, node ast
.Node
) {
27 var seen
map[[2]string]token
.Pos
28 for _
, field
:= range node
.(*ast
.StructType
).Fields
.List
{
29 checkCanonicalFieldTag(f
, field
, &seen
)
33 var checkTagDups
= []string{"json", "xml"}
34 var checkTagSpaces
= map[string]bool{"json": true, "xml": true, "asn1": true}
36 // checkCanonicalFieldTag checks a single struct field tag.
37 func checkCanonicalFieldTag(f
*File
, field
*ast
.Field
, seen
*map[[2]string]token
.Pos
) {
42 tag
, err
:= strconv
.Unquote(field
.Tag
.Value
)
44 f
.Badf(field
.Pos(), "unable to read struct tag %s", field
.Tag
.Value
)
48 if err
:= validateStructTag(tag
); err
!= nil {
49 raw
, _
:= strconv
.Unquote(field
.Tag
.Value
) // field.Tag.Value is known to be a quoted string
50 f
.Badf(field
.Pos(), "struct field tag %#q not compatible with reflect.StructTag.Get: %s", raw
, err
)
53 for _
, key
:= range checkTagDups
{
54 val
:= reflect
.StructTag(tag
).Get(key
)
55 if val
== "" || val
== "-" || val
[0] == ',' {
58 if key
== "xml" && len(field
.Names
) > 0 && field
.Names
[0].Name
== "XMLName" {
59 // XMLName defines the XML element name of the struct being
60 // checked. That name cannot collide with element or attribute
61 // names defined on other fields of the struct. Vet does not have a
62 // check for untagged fields of type struct defining their own name
63 // by containing a field named XMLName; see issue 18256.
66 if i
:= strings
.Index(val
, ","); i
>= 0 {
68 // Use a separate namespace for XML attributes.
69 for _
, opt
:= range strings
.Split(val
[i
:], ",") {
71 key
+= " attribute" // Key is part of the error message.
79 *seen
= map[[2]string]token
.Pos
{}
81 if pos
, ok
:= (*seen
)[[2]string{key
, val
}]; ok
{
83 if len(field
.Names
) > 0 {
84 name
= field
.Names
[0].Name
86 name
= field
.Type
.(*ast
.Ident
).Name
88 f
.Badf(field
.Pos(), "struct field %s repeats %s tag %q also at %s", name
, key
, val
, f
.loc(pos
))
90 (*seen
)[[2]string{key
, val
}] = field
.Pos()
94 // Check for use of json or xml tags with unexported fields.
96 // Embedded struct. Nothing to do for now, but that
97 // may change, depending on what happens with issue 7363.
98 if len(field
.Names
) == 0 {
102 if field
.Names
[0].IsExported() {
106 for _
, enc
:= range [...]string{"json", "xml"} {
107 if reflect
.StructTag(tag
).Get(enc
) != "" {
108 f
.Badf(field
.Pos(), "struct field %s has %s tag but is not exported", field
.Names
[0].Name
, enc
)
115 errTagSyntax
= errors
.New("bad syntax for struct tag pair")
116 errTagKeySyntax
= errors
.New("bad syntax for struct tag key")
117 errTagValueSyntax
= errors
.New("bad syntax for struct tag value")
118 errTagValueSpace
= errors
.New("suspicious space in struct tag value")
119 errTagSpace
= errors
.New("key:\"value\" pairs not separated by spaces")
122 // validateStructTag parses the struct tag and returns an error if it is not
123 // in the canonical format, which is a space-separated list of key:"value"
124 // settings. The value may contain spaces.
125 func validateStructTag(tag
string) error
{
126 // This code is based on the StructTag.Get code in package reflect.
129 for ; tag
!= ""; n
++ {
130 if n
> 0 && tag
!= "" && tag
[0] != ' ' {
131 // More restrictive than reflect, but catches likely mistakes
132 // like `x:"foo",y:"bar"`, which parses as `x:"foo" ,y:"bar"` with second key ",y".
135 // Skip leading space.
137 for i
< len(tag
) && tag
[i
] == ' ' {
145 // Scan to colon. A space, a quote or a control character is a syntax error.
146 // Strictly speaking, control chars include the range [0x7f, 0x9f], not just
147 // [0x00, 0x1f], but in practice, we ignore the multi-byte control characters
148 // as it is simpler to inspect the tag's bytes than the tag's runes.
150 for i
< len(tag
) && tag
[i
] > ' ' && tag
[i
] != ':' && tag
[i
] != '"' && tag
[i
] != 0x7f {
154 return errTagKeySyntax
156 if i
+1 >= len(tag
) || tag
[i
] != ':' {
160 return errTagValueSyntax
165 // Scan quoted string to find value.
167 for i
< len(tag
) && tag
[i
] != '"' {
174 return errTagValueSyntax
179 value
, err
:= strconv
.Unquote(qvalue
)
181 return errTagValueSyntax
184 if !checkTagSpaces
[key
] {
190 // If the first or last character in the XML tag is a space, it is
192 if strings
.Trim(value
, " ") != value
{
193 return errTagValueSpace
196 // If there are multiple spaces, they are suspicious.
197 if strings
.Count(value
, " ") > 1 {
198 return errTagValueSpace
201 // If there is no comma, skip the rest of the checks.
202 comma
:= strings
.IndexRune(value
, ',')
207 // If the character before a comma is a space, this is suspicious.
208 if comma
> 0 && value
[comma
-1] == ' ' {
209 return errTagValueSpace
211 value
= value
[comma
+1:]
213 // JSON allows using spaces in the name, so skip it.
214 comma
:= strings
.IndexRune(value
, ',')
218 value
= value
[comma
+1:]
221 if strings
.IndexByte(value
, ' ') >= 0 {
222 return errTagValueSpace