...
Run Format

Source file src/cmd/vet/structtag.go

Documentation: cmd/vet

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

View as plain text