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