--- /dev/null
+// 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
+}