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  }