...

Source file src/golang.org/x/tools/go/analysis/passes/structtag/structtag.go

Documentation: golang.org/x/tools/go/analysis/passes/structtag

     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  // Package structtag defines an Analyzer that checks struct field tags
     6  // are well formed.
     7  package structtag
     8  
     9  import (
    10  	"errors"
    11  	"go/ast"
    12  	"go/token"
    13  	"go/types"
    14  	"path/filepath"
    15  	"reflect"
    16  	"strconv"
    17  	"strings"
    18  
    19  	"golang.org/x/tools/go/analysis"
    20  	"golang.org/x/tools/go/analysis/passes/inspect"
    21  	"golang.org/x/tools/go/ast/inspector"
    22  )
    23  
    24  const Doc = `check that struct field tags conform to reflect.StructTag.Get
    25  
    26  Also report certain struct tags (json, xml) used with unexported fields.`
    27  
    28  var Analyzer = &analysis.Analyzer{
    29  	Name:             "structtag",
    30  	Doc:              Doc,
    31  	Requires:         []*analysis.Analyzer{inspect.Analyzer},
    32  	RunDespiteErrors: true,
    33  	Run:              run,
    34  }
    35  
    36  func run(pass *analysis.Pass) (interface{}, error) {
    37  	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    38  
    39  	nodeFilter := []ast.Node{
    40  		(*ast.StructType)(nil),
    41  	}
    42  	inspect.Preorder(nodeFilter, func(n ast.Node) {
    43  		styp, ok := pass.TypesInfo.Types[n.(*ast.StructType)].Type.(*types.Struct)
    44  		// Type information may be incomplete.
    45  		if !ok {
    46  			return
    47  		}
    48  		var seen namesSeen
    49  		for i := 0; i < styp.NumFields(); i++ {
    50  			field := styp.Field(i)
    51  			tag := styp.Tag(i)
    52  			checkCanonicalFieldTag(pass, field, tag, &seen)
    53  		}
    54  	})
    55  	return nil, nil
    56  }
    57  
    58  // namesSeen keeps track of encoding tags by their key, name, and nested level
    59  // from the initial struct. The level is taken into account because equal
    60  // encoding key names only conflict when at the same level; otherwise, the lower
    61  // level shadows the higher level.
    62  type namesSeen map[uniqueName]token.Pos
    63  
    64  type uniqueName struct {
    65  	key   string // "xml" or "json"
    66  	name  string // the encoding name
    67  	level int    // anonymous struct nesting level
    68  }
    69  
    70  func (s *namesSeen) Get(key, name string, level int) (token.Pos, bool) {
    71  	if *s == nil {
    72  		*s = make(map[uniqueName]token.Pos)
    73  	}
    74  	pos, ok := (*s)[uniqueName{key, name, level}]
    75  	return pos, ok
    76  }
    77  
    78  func (s *namesSeen) Set(key, name string, level int, pos token.Pos) {
    79  	if *s == nil {
    80  		*s = make(map[uniqueName]token.Pos)
    81  	}
    82  	(*s)[uniqueName{key, name, level}] = pos
    83  }
    84  
    85  var checkTagDups = []string{"json", "xml"}
    86  var checkTagSpaces = map[string]bool{"json": true, "xml": true, "asn1": true}
    87  
    88  // checkCanonicalFieldTag checks a single struct field tag.
    89  func checkCanonicalFieldTag(pass *analysis.Pass, field *types.Var, tag string, seen *namesSeen) {
    90  	switch pass.Pkg.Path() {
    91  	case "encoding/json", "encoding/xml":
    92  		// These packages know how to use their own APIs.
    93  		// Sometimes they are testing what happens to incorrect programs.
    94  		return
    95  	}
    96  
    97  	for _, key := range checkTagDups {
    98  		checkTagDuplicates(pass, tag, key, field, field, seen, 1)
    99  	}
   100  
   101  	if err := validateStructTag(tag); err != nil {
   102  		pass.Reportf(field.Pos(), "struct field tag %#q not compatible with reflect.StructTag.Get: %s", tag, err)
   103  	}
   104  
   105  	// Check for use of json or xml tags with unexported fields.
   106  
   107  	// Embedded struct. Nothing to do for now, but that
   108  	// may change, depending on what happens with issue 7363.
   109  	// TODO(adonovan): investigate, now that that issue is fixed.
   110  	if field.Anonymous() {
   111  		return
   112  	}
   113  
   114  	if field.Exported() {
   115  		return
   116  	}
   117  
   118  	for _, enc := range [...]string{"json", "xml"} {
   119  		switch reflect.StructTag(tag).Get(enc) {
   120  		// Ignore warning if the field not exported and the tag is marked as
   121  		// ignored.
   122  		case "", "-":
   123  		default:
   124  			pass.Reportf(field.Pos(), "struct field %s has %s tag but is not exported", field.Name(), enc)
   125  			return
   126  		}
   127  	}
   128  }
   129  
   130  // checkTagDuplicates checks a single struct field tag to see if any tags are
   131  // duplicated. nearest is the field that's closest to the field being checked,
   132  // while still being part of the top-level struct type.
   133  func checkTagDuplicates(pass *analysis.Pass, tag, key string, nearest, field *types.Var, seen *namesSeen, level int) {
   134  	val := reflect.StructTag(tag).Get(key)
   135  	if val == "-" {
   136  		// Ignored, even if the field is anonymous.
   137  		return
   138  	}
   139  	if val == "" || val[0] == ',' {
   140  		if !field.Anonymous() {
   141  			// Ignored if the field isn't anonymous.
   142  			return
   143  		}
   144  		typ, ok := field.Type().Underlying().(*types.Struct)
   145  		if !ok {
   146  			return
   147  		}
   148  		for i := 0; i < typ.NumFields(); i++ {
   149  			field := typ.Field(i)
   150  			if !field.Exported() {
   151  				continue
   152  			}
   153  			tag := typ.Tag(i)
   154  			checkTagDuplicates(pass, tag, key, nearest, field, seen, level+1)
   155  		}
   156  		return
   157  	}
   158  	if key == "xml" && field.Name() == "XMLName" {
   159  		// XMLName defines the XML element name of the struct being
   160  		// checked. That name cannot collide with element or attribute
   161  		// names defined on other fields of the struct. Vet does not have a
   162  		// check for untagged fields of type struct defining their own name
   163  		// by containing a field named XMLName; see issue 18256.
   164  		return
   165  	}
   166  	if i := strings.Index(val, ","); i >= 0 {
   167  		if key == "xml" {
   168  			// Use a separate namespace for XML attributes.
   169  			for _, opt := range strings.Split(val[i:], ",") {
   170  				if opt == "attr" {
   171  					key += " attribute" // Key is part of the error message.
   172  					break
   173  				}
   174  			}
   175  		}
   176  		val = val[:i]
   177  	}
   178  	if pos, ok := seen.Get(key, val, level); ok {
   179  		alsoPos := pass.Fset.Position(pos)
   180  		alsoPos.Column = 0
   181  
   182  		// Make the "also at" position relative to the current position,
   183  		// to ensure that all warnings are unambiguous and correct. For
   184  		// example, via anonymous struct fields, it's possible for the
   185  		// two fields to be in different packages and directories.
   186  		thisPos := pass.Fset.Position(field.Pos())
   187  		rel, err := filepath.Rel(filepath.Dir(thisPos.Filename), alsoPos.Filename)
   188  		if err != nil {
   189  			// Possibly because the paths are relative; leave the
   190  			// filename alone.
   191  		} else {
   192  			alsoPos.Filename = rel
   193  		}
   194  
   195  		pass.Reportf(nearest.Pos(), "struct field %s repeats %s tag %q also at %s", field.Name(), key, val, alsoPos)
   196  	} else {
   197  		seen.Set(key, val, level, field.Pos())
   198  	}
   199  }
   200  
   201  var (
   202  	errTagSyntax      = errors.New("bad syntax for struct tag pair")
   203  	errTagKeySyntax   = errors.New("bad syntax for struct tag key")
   204  	errTagValueSyntax = errors.New("bad syntax for struct tag value")
   205  	errTagValueSpace  = errors.New("suspicious space in struct tag value")
   206  	errTagSpace       = errors.New("key:\"value\" pairs not separated by spaces")
   207  )
   208  
   209  // validateStructTag parses the struct tag and returns an error if it is not
   210  // in the canonical format, which is a space-separated list of key:"value"
   211  // settings. The value may contain spaces.
   212  func validateStructTag(tag string) error {
   213  	// This code is based on the StructTag.Get code in package reflect.
   214  
   215  	n := 0
   216  	for ; tag != ""; n++ {
   217  		if n > 0 && tag != "" && tag[0] != ' ' {
   218  			// More restrictive than reflect, but catches likely mistakes
   219  			// like `x:"foo",y:"bar"`, which parses as `x:"foo" ,y:"bar"` with second key ",y".
   220  			return errTagSpace
   221  		}
   222  		// Skip leading space.
   223  		i := 0
   224  		for i < len(tag) && tag[i] == ' ' {
   225  			i++
   226  		}
   227  		tag = tag[i:]
   228  		if tag == "" {
   229  			break
   230  		}
   231  
   232  		// Scan to colon. A space, a quote or a control character is a syntax error.
   233  		// Strictly speaking, control chars include the range [0x7f, 0x9f], not just
   234  		// [0x00, 0x1f], but in practice, we ignore the multi-byte control characters
   235  		// as it is simpler to inspect the tag's bytes than the tag's runes.
   236  		i = 0
   237  		for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
   238  			i++
   239  		}
   240  		if i == 0 {
   241  			return errTagKeySyntax
   242  		}
   243  		if i+1 >= len(tag) || tag[i] != ':' {
   244  			return errTagSyntax
   245  		}
   246  		if tag[i+1] != '"' {
   247  			return errTagValueSyntax
   248  		}
   249  		key := tag[:i]
   250  		tag = tag[i+1:]
   251  
   252  		// Scan quoted string to find value.
   253  		i = 1
   254  		for i < len(tag) && tag[i] != '"' {
   255  			if tag[i] == '\\' {
   256  				i++
   257  			}
   258  			i++
   259  		}
   260  		if i >= len(tag) {
   261  			return errTagValueSyntax
   262  		}
   263  		qvalue := tag[:i+1]
   264  		tag = tag[i+1:]
   265  
   266  		value, err := strconv.Unquote(qvalue)
   267  		if err != nil {
   268  			return errTagValueSyntax
   269  		}
   270  
   271  		if !checkTagSpaces[key] {
   272  			continue
   273  		}
   274  
   275  		switch key {
   276  		case "xml":
   277  			// If the first or last character in the XML tag is a space, it is
   278  			// suspicious.
   279  			if strings.Trim(value, " ") != value {
   280  				return errTagValueSpace
   281  			}
   282  
   283  			// If there are multiple spaces, they are suspicious.
   284  			if strings.Count(value, " ") > 1 {
   285  				return errTagValueSpace
   286  			}
   287  
   288  			// If there is no comma, skip the rest of the checks.
   289  			comma := strings.IndexRune(value, ',')
   290  			if comma < 0 {
   291  				continue
   292  			}
   293  
   294  			// If the character before a comma is a space, this is suspicious.
   295  			if comma > 0 && value[comma-1] == ' ' {
   296  				return errTagValueSpace
   297  			}
   298  			value = value[comma+1:]
   299  		case "json":
   300  			// JSON allows using spaces in the name, so skip it.
   301  			comma := strings.IndexRune(value, ',')
   302  			if comma < 0 {
   303  				continue
   304  			}
   305  			value = value[comma+1:]
   306  		}
   307  
   308  		if strings.IndexByte(value, ' ') >= 0 {
   309  			return errTagValueSpace
   310  		}
   311  	}
   312  	return nil
   313  }
   314  

View as plain text