github.com/stingnevermore/go@v0.0.0-20180120041312-3810f5bfed72/src/cmd/vet/structtag.go (about) 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 // This file contains the test for canonical struct tags. 6 7 package main 8 9 import ( 10 "errors" 11 "go/ast" 12 "go/token" 13 "reflect" 14 "strconv" 15 "strings" 16 ) 17 18 func init() { 19 register("structtags", 20 "check that struct field tags have canonical format and apply to exported fields as needed", 21 checkStructFieldTags, 22 structType) 23 } 24 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) 30 } 31 } 32 33 var checkTagDups = []string{"json", "xml"} 34 var checkTagSpaces = map[string]bool{"json": true, "xml": true, "asn1": true} 35 36 // checkCanonicalFieldTag checks a single struct field tag. 37 func checkCanonicalFieldTag(f *File, field *ast.Field, seen *map[[2]string]token.Pos) { 38 if field.Tag == nil { 39 return 40 } 41 42 tag, err := strconv.Unquote(field.Tag.Value) 43 if err != nil { 44 f.Badf(field.Pos(), "unable to read struct tag %s", field.Tag.Value) 45 return 46 } 47 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) 51 } 52 53 for _, key := range checkTagDups { 54 val := reflect.StructTag(tag).Get(key) 55 if val == "" || val == "-" || val[0] == ',' { 56 continue 57 } 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. 64 continue 65 } 66 if i := strings.Index(val, ","); i >= 0 { 67 if key == "xml" { 68 // Use a separate namespace for XML attributes. 69 for _, opt := range strings.Split(val[i:], ",") { 70 if opt == "attr" { 71 key += " attribute" // Key is part of the error message. 72 break 73 } 74 } 75 } 76 val = val[:i] 77 } 78 if *seen == nil { 79 *seen = map[[2]string]token.Pos{} 80 } 81 if pos, ok := (*seen)[[2]string{key, val}]; ok { 82 var name string 83 if len(field.Names) > 0 { 84 name = field.Names[0].Name 85 } else { 86 name = field.Type.(*ast.Ident).Name 87 } 88 f.Badf(field.Pos(), "struct field %s repeats %s tag %q also at %s", name, key, val, f.loc(pos)) 89 } else { 90 (*seen)[[2]string{key, val}] = field.Pos() 91 } 92 } 93 94 // Check for use of json or xml tags with unexported fields. 95 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 { 99 return 100 } 101 102 if field.Names[0].IsExported() { 103 return 104 } 105 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) 109 return 110 } 111 } 112 } 113 114 var ( 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") 120 ) 121 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. 127 128 n := 0 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". 133 return errTagSpace 134 } 135 // Skip leading space. 136 i := 0 137 for i < len(tag) && tag[i] == ' ' { 138 i++ 139 } 140 tag = tag[i:] 141 if tag == "" { 142 break 143 } 144 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. 149 i = 0 150 for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f { 151 i++ 152 } 153 if i == 0 { 154 return errTagKeySyntax 155 } 156 if i+1 >= len(tag) || tag[i] != ':' { 157 return errTagSyntax 158 } 159 if tag[i+1] != '"' { 160 return errTagValueSyntax 161 } 162 key := tag[:i] 163 tag = tag[i+1:] 164 165 // Scan quoted string to find value. 166 i = 1 167 for i < len(tag) && tag[i] != '"' { 168 if tag[i] == '\\' { 169 i++ 170 } 171 i++ 172 } 173 if i >= len(tag) { 174 return errTagValueSyntax 175 } 176 qvalue := tag[:i+1] 177 tag = tag[i+1:] 178 179 value, err := strconv.Unquote(qvalue) 180 if err != nil { 181 return errTagValueSyntax 182 } 183 184 if !checkTagSpaces[key] { 185 continue 186 } 187 188 switch key { 189 case "xml": 190 // If the first or last character in the XML tag is a space, it is 191 // suspicious. 192 if strings.Trim(value, " ") != value { 193 return errTagValueSpace 194 } 195 196 // If there are multiple spaces, they are suspicious. 197 if strings.Count(value, " ") > 1 { 198 return errTagValueSpace 199 } 200 201 // If there is no comma, skip the rest of the checks. 202 comma := strings.IndexRune(value, ',') 203 if comma < 0 { 204 continue 205 } 206 207 // If the character before a comma is a space, this is suspicious. 208 if comma > 0 && value[comma-1] == ' ' { 209 return errTagValueSpace 210 } 211 value = value[comma+1:] 212 case "json": 213 // JSON allows using spaces in the name, so skip it. 214 comma := strings.IndexRune(value, ',') 215 if comma < 0 { 216 continue 217 } 218 value = value[comma+1:] 219 } 220 221 if strings.IndexByte(value, ' ') >= 0 { 222 return errTagValueSpace 223 } 224 } 225 return nil 226 }