github.com/hikaru7719/go@v0.0.0-20181025140707-c8b2ac68906a/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 "go/types" 14 "reflect" 15 "strconv" 16 "strings" 17 ) 18 19 func init() { 20 register("structtags", 21 "check that struct field tags have canonical format and apply to exported fields as needed", 22 checkStructFieldTags, 23 structType) 24 } 25 26 // checkStructFieldTags checks all the field tags of a struct, including checking for duplicates. 27 func checkStructFieldTags(f *File, node ast.Node) { 28 astType := node.(*ast.StructType) 29 typ := f.pkg.types[astType].Type.(*types.Struct) 30 var seen map[[2]string]token.Pos 31 for i := 0; i < typ.NumFields(); i++ { 32 field := typ.Field(i) 33 tag := typ.Tag(i) 34 checkCanonicalFieldTag(f, astType, field, tag, &seen) 35 } 36 } 37 38 var checkTagDups = []string{"json", "xml"} 39 var checkTagSpaces = map[string]bool{"json": true, "xml": true, "asn1": true} 40 41 // checkCanonicalFieldTag checks a single struct field tag. 42 // top is the top-level struct type that is currently being checked. 43 func checkCanonicalFieldTag(f *File, top *ast.StructType, field *types.Var, tag string, seen *map[[2]string]token.Pos) { 44 for _, key := range checkTagDups { 45 checkTagDuplicates(f, tag, key, field, field, seen) 46 } 47 48 if err := validateStructTag(tag); err != nil { 49 f.Badf(field.Pos(), "struct field tag %#q not compatible with reflect.StructTag.Get: %s", tag, err) 50 } 51 52 // Check for use of json or xml tags with unexported fields. 53 54 // Embedded struct. Nothing to do for now, but that 55 // may change, depending on what happens with issue 7363. 56 if field.Anonymous() { 57 return 58 } 59 60 if field.Exported() { 61 return 62 } 63 64 for _, enc := range [...]string{"json", "xml"} { 65 if reflect.StructTag(tag).Get(enc) != "" { 66 f.Badf(field.Pos(), "struct field %s has %s tag but is not exported", field.Name(), enc) 67 return 68 } 69 } 70 } 71 72 // checkTagDuplicates checks a single struct field tag to see if any tags are 73 // duplicated. nearest is the field that's closest to the field being checked, 74 // while still being part of the top-level struct type. 75 func checkTagDuplicates(f *File, tag, key string, nearest, field *types.Var, seen *map[[2]string]token.Pos) { 76 val := reflect.StructTag(tag).Get(key) 77 if val == "-" { 78 // Ignored, even if the field is anonymous. 79 return 80 } 81 if val == "" || val[0] == ',' { 82 if field.Anonymous() { 83 typ, ok := field.Type().Underlying().(*types.Struct) 84 if !ok { 85 return 86 } 87 for i := 0; i < typ.NumFields(); i++ { 88 field := typ.Field(i) 89 if !field.Exported() { 90 continue 91 } 92 tag := typ.Tag(i) 93 checkTagDuplicates(f, tag, key, nearest, field, seen) 94 } 95 } 96 // Ignored if the field isn't anonymous. 97 return 98 } 99 if key == "xml" && field.Name() == "XMLName" { 100 // XMLName defines the XML element name of the struct being 101 // checked. That name cannot collide with element or attribute 102 // names defined on other fields of the struct. Vet does not have a 103 // check for untagged fields of type struct defining their own name 104 // by containing a field named XMLName; see issue 18256. 105 return 106 } 107 if i := strings.Index(val, ","); i >= 0 { 108 if key == "xml" { 109 // Use a separate namespace for XML attributes. 110 for _, opt := range strings.Split(val[i:], ",") { 111 if opt == "attr" { 112 key += " attribute" // Key is part of the error message. 113 break 114 } 115 } 116 } 117 val = val[:i] 118 } 119 if *seen == nil { 120 *seen = map[[2]string]token.Pos{} 121 } 122 if pos, ok := (*seen)[[2]string{key, val}]; ok { 123 f.Badf(nearest.Pos(), "struct field %s repeats %s tag %q also at %s", field.Name(), key, val, f.loc(pos)) 124 } else { 125 (*seen)[[2]string{key, val}] = field.Pos() 126 } 127 } 128 129 var ( 130 errTagSyntax = errors.New("bad syntax for struct tag pair") 131 errTagKeySyntax = errors.New("bad syntax for struct tag key") 132 errTagValueSyntax = errors.New("bad syntax for struct tag value") 133 errTagValueSpace = errors.New("suspicious space in struct tag value") 134 errTagSpace = errors.New("key:\"value\" pairs not separated by spaces") 135 ) 136 137 // validateStructTag parses the struct tag and returns an error if it is not 138 // in the canonical format, which is a space-separated list of key:"value" 139 // settings. The value may contain spaces. 140 func validateStructTag(tag string) error { 141 // This code is based on the StructTag.Get code in package reflect. 142 143 n := 0 144 for ; tag != ""; n++ { 145 if n > 0 && tag != "" && tag[0] != ' ' { 146 // More restrictive than reflect, but catches likely mistakes 147 // like `x:"foo",y:"bar"`, which parses as `x:"foo" ,y:"bar"` with second key ",y". 148 return errTagSpace 149 } 150 // Skip leading space. 151 i := 0 152 for i < len(tag) && tag[i] == ' ' { 153 i++ 154 } 155 tag = tag[i:] 156 if tag == "" { 157 break 158 } 159 160 // Scan to colon. A space, a quote or a control character is a syntax error. 161 // Strictly speaking, control chars include the range [0x7f, 0x9f], not just 162 // [0x00, 0x1f], but in practice, we ignore the multi-byte control characters 163 // as it is simpler to inspect the tag's bytes than the tag's runes. 164 i = 0 165 for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f { 166 i++ 167 } 168 if i == 0 { 169 return errTagKeySyntax 170 } 171 if i+1 >= len(tag) || tag[i] != ':' { 172 return errTagSyntax 173 } 174 if tag[i+1] != '"' { 175 return errTagValueSyntax 176 } 177 key := tag[:i] 178 tag = tag[i+1:] 179 180 // Scan quoted string to find value. 181 i = 1 182 for i < len(tag) && tag[i] != '"' { 183 if tag[i] == '\\' { 184 i++ 185 } 186 i++ 187 } 188 if i >= len(tag) { 189 return errTagValueSyntax 190 } 191 qvalue := tag[:i+1] 192 tag = tag[i+1:] 193 194 value, err := strconv.Unquote(qvalue) 195 if err != nil { 196 return errTagValueSyntax 197 } 198 199 if !checkTagSpaces[key] { 200 continue 201 } 202 203 switch key { 204 case "xml": 205 // If the first or last character in the XML tag is a space, it is 206 // suspicious. 207 if strings.Trim(value, " ") != value { 208 return errTagValueSpace 209 } 210 211 // If there are multiple spaces, they are suspicious. 212 if strings.Count(value, " ") > 1 { 213 return errTagValueSpace 214 } 215 216 // If there is no comma, skip the rest of the checks. 217 comma := strings.IndexRune(value, ',') 218 if comma < 0 { 219 continue 220 } 221 222 // If the character before a comma is a space, this is suspicious. 223 if comma > 0 && value[comma-1] == ' ' { 224 return errTagValueSpace 225 } 226 value = value[comma+1:] 227 case "json": 228 // JSON allows using spaces in the name, so skip it. 229 comma := strings.IndexRune(value, ',') 230 if comma < 0 { 231 continue 232 } 233 value = value[comma+1:] 234 } 235 236 if strings.IndexByte(value, ' ') >= 0 { 237 return errTagValueSpace 238 } 239 } 240 return nil 241 }