go structtag 源码

2022-07-15 浏览 (861)

golang structtag 代码

文件路径:/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/structtag/structtag.go

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

相关信息

go 源码目录

相关文章

go go_android_exec 源码

go argposition_test 源码

go badsym_test 源码

go errors_test 源码

go ptr_test 源码

go err1 源码

go err2 源码

go err4 源码

go issue11097a 源码

go issue11097b 源码

^