Home | History | Annotate | Download | only in 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