src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

sa5008.go (3422B)


      1 package sa5008
      2 
      3 import (
      4 	"fmt"
      5 	"go/ast"
      6 	"go/types"
      7 	"strings"
      8 
      9 	"honnef.co/go/tools/analysis/code"
     10 	"honnef.co/go/tools/analysis/lint"
     11 	"honnef.co/go/tools/analysis/report"
     12 	"honnef.co/go/tools/staticcheck/fakereflect"
     13 	"honnef.co/go/tools/staticcheck/fakexml"
     14 
     15 	"golang.org/x/tools/go/analysis"
     16 	"golang.org/x/tools/go/analysis/passes/inspect"
     17 )
     18 
     19 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
     20 	Analyzer: &analysis.Analyzer{
     21 		Name:     "SA5008",
     22 		Run:      run,
     23 		Requires: []*analysis.Analyzer{inspect.Analyzer},
     24 	},
     25 	Doc: &lint.RawDocumentation{
     26 		Title:    `Invalid struct tag`,
     27 		Since:    "2019.2",
     28 		Severity: lint.SeverityWarning,
     29 		MergeIf:  lint.MergeIfAny,
     30 	},
     31 })
     32 
     33 var Analyzer = SCAnalyzer.Analyzer
     34 
     35 func run(pass *analysis.Pass) (any, error) {
     36 	importsGoFlags := false
     37 
     38 	// we use the AST instead of (*types.Package).Imports to work
     39 	// around vendored packages in GOPATH mode. A vendored package's
     40 	// path will include the vendoring subtree as a prefix.
     41 	for _, f := range pass.Files {
     42 		for _, imp := range f.Imports {
     43 			v := imp.Path.Value
     44 			if v[1:len(v)-1] == "github.com/jessevdk/go-flags" {
     45 				importsGoFlags = true
     46 				break
     47 			}
     48 		}
     49 	}
     50 
     51 	fn := func(node ast.Node) {
     52 		structNode := node.(*ast.StructType)
     53 		T := pass.TypesInfo.Types[structNode].Type.(*types.Struct)
     54 		rt := fakereflect.TypeAndCanAddr{
     55 			Type: T,
     56 		}
     57 		for i, field := range structNode.Fields.List {
     58 			if field.Tag == nil {
     59 				continue
     60 			}
     61 			tags, err := parseStructTag(field.Tag.Value[1 : len(field.Tag.Value)-1])
     62 			if err != nil {
     63 				report.Report(pass, field.Tag, fmt.Sprintf("unparseable struct tag: %s", err))
     64 				continue
     65 			}
     66 			for k, v := range tags {
     67 				if len(v) > 1 {
     68 					isGoFlagsTag := importsGoFlags &&
     69 						(k == "choice" || k == "optional-value" || k == "default")
     70 					if !isGoFlagsTag {
     71 						report.Report(pass, field.Tag, fmt.Sprintf("duplicate struct tag %q", k))
     72 					}
     73 				}
     74 
     75 				switch k {
     76 				case "json":
     77 					checkJSONTag(pass, field, v[0])
     78 				case "xml":
     79 					if _, err := fakexml.StructFieldInfo(rt.Field(i)); err != nil {
     80 						report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: %s", err))
     81 					}
     82 					checkXMLTag(pass, field, v[0])
     83 				}
     84 			}
     85 		}
     86 	}
     87 	code.Preorder(pass, fn, (*ast.StructType)(nil))
     88 	return nil, nil
     89 }
     90 
     91 func checkJSONTag(pass *analysis.Pass, field *ast.Field, tag string) {
     92 	if pass.Pkg.Path() == "encoding/json" ||
     93 		pass.Pkg.Path() == "encoding/json_test" ||
     94 		pass.Pkg.Path() == "encoding/json/v2" ||
     95 		pass.Pkg.Path() == "encoding/json/v2_test" {
     96 		// don't flag malformed JSON tags in the encoding/json
     97 		// package; it knows what it is doing, and it is testing
     98 		// itself.
     99 		return
    100 	}
    101 	//lint:ignore SA9003 TODO(dh): should we flag empty tags?
    102 	if len(tag) == 0 {
    103 	}
    104 
    105 	validateJSONTag(pass, field, tag)
    106 }
    107 
    108 func checkXMLTag(pass *analysis.Pass, field *ast.Field, tag string) {
    109 	//lint:ignore SA9003 TODO(dh): should we flag empty tags?
    110 	if len(tag) == 0 {
    111 	}
    112 	fields := strings.Split(tag, ",")
    113 	counts := map[string]int{}
    114 	for _, s := range fields[1:] {
    115 		switch s {
    116 		case "attr", "chardata", "cdata", "innerxml", "comment":
    117 			counts[s]++
    118 		case "omitempty", "any":
    119 			counts[s]++
    120 		case "":
    121 		default:
    122 			report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: unknown option %q", s))
    123 		}
    124 	}
    125 	for k, v := range counts {
    126 		if v > 1 {
    127 			report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: duplicate option %q", k))
    128 		}
    129 	}
    130 }