sa5008.go raw
1 package sa5008
2
3 import (
4 "fmt"
5 "go/ast"
6 "go/types"
7 "sort"
8 "strings"
9 "unicode"
10
11 "honnef.co/go/tools/analysis/code"
12 "honnef.co/go/tools/analysis/lint"
13 "honnef.co/go/tools/analysis/report"
14 "honnef.co/go/tools/go/types/typeutil"
15 "honnef.co/go/tools/staticcheck/fakereflect"
16 "honnef.co/go/tools/staticcheck/fakexml"
17
18 "golang.org/x/tools/go/analysis"
19 "golang.org/x/tools/go/analysis/passes/inspect"
20 )
21
22 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
23 Analyzer: &analysis.Analyzer{
24 Name: "SA5008",
25 Run: run,
26 Requires: []*analysis.Analyzer{inspect.Analyzer},
27 },
28 Doc: &lint.RawDocumentation{
29 Title: `Invalid struct tag`,
30 Since: "2019.2",
31 Severity: lint.SeverityWarning,
32 MergeIf: lint.MergeIfAny,
33 },
34 })
35
36 var Analyzer = SCAnalyzer.Analyzer
37
38 func run(pass *analysis.Pass) (interface{}, error) {
39 importsGoFlags := false
40
41 // we use the AST instead of (*types.Package).Imports to work
42 // around vendored packages in GOPATH mode. A vendored package's
43 // path will include the vendoring subtree as a prefix.
44 for _, f := range pass.Files {
45 for _, imp := range f.Imports {
46 v := imp.Path.Value
47 if v[1:len(v)-1] == "github.com/jessevdk/go-flags" {
48 importsGoFlags = true
49 break
50 }
51 }
52 }
53
54 fn := func(node ast.Node) {
55 structNode := node.(*ast.StructType)
56 T := pass.TypesInfo.Types[structNode].Type.(*types.Struct)
57 rt := fakereflect.TypeAndCanAddr{
58 Type: T,
59 }
60 for i, field := range structNode.Fields.List {
61 if field.Tag == nil {
62 continue
63 }
64 tags, err := parseStructTag(field.Tag.Value[1 : len(field.Tag.Value)-1])
65 if err != nil {
66 report.Report(pass, field.Tag, fmt.Sprintf("unparseable struct tag: %s", err))
67 continue
68 }
69 for k, v := range tags {
70 if len(v) > 1 {
71 isGoFlagsTag := importsGoFlags &&
72 (k == "choice" || k == "optional-value" || k == "default")
73 if !isGoFlagsTag {
74 report.Report(pass, field.Tag, fmt.Sprintf("duplicate struct tag %q", k))
75 }
76 }
77
78 switch k {
79 case "json":
80 checkJSONTag(pass, field, v[0])
81 case "xml":
82 if _, err := fakexml.StructFieldInfo(rt.Field(i)); err != nil {
83 report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: %s", err))
84 }
85 checkXMLTag(pass, field, v[0])
86 }
87 }
88 }
89 }
90 code.Preorder(pass, fn, (*ast.StructType)(nil))
91 return nil, nil
92 }
93
94 func checkJSONTag(pass *analysis.Pass, field *ast.Field, tag string) {
95 if pass.Pkg.Path() == "encoding/json" || pass.Pkg.Path() == "encoding/json_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 if i := strings.Index(tag, ",format:"); i >= 0 {
105 tag = tag[:i]
106 }
107 fields := strings.Split(tag, ",")
108 for _, r := range fields[0] {
109 if !unicode.IsLetter(r) && !unicode.IsDigit(r) && !strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", r) {
110 report.Report(pass, field.Tag, fmt.Sprintf("invalid JSON field name %q", fields[0]))
111 }
112 }
113 options := make(map[string]int)
114 for _, s := range fields[1:] {
115 switch s {
116 case "":
117 // allow stuff like "-,"
118 case "string":
119 // only for string, floating point, integer and bool
120 options[s]++
121 tset := typeutil.NewTypeSet(pass.TypesInfo.TypeOf(field.Type))
122 if len(tset.Terms) == 0 {
123 // TODO(dh): improve message, call out the use of type parameters
124 report.Report(pass, field.Tag, "the JSON string option only applies to fields of type string, floating point, integer or bool, or pointers to those")
125 continue
126 }
127 for _, term := range tset.Terms {
128 T := typeutil.Dereference(term.Type().Underlying())
129 for _, term2 := range typeutil.NewTypeSet(T).Terms {
130 basic, ok := term2.Type().Underlying().(*types.Basic)
131 if !ok || (basic.Info()&(types.IsBoolean|types.IsInteger|types.IsFloat|types.IsString)) == 0 {
132 // TODO(dh): improve message, show how we arrived at the type
133 report.Report(pass, field.Tag, "the JSON string option only applies to fields of type string, floating point, integer or bool, or pointers to those")
134 }
135 }
136 }
137 case "omitzero", "omitempty", "nocase", "inline", "unknown":
138 options[s]++
139 default:
140 report.Report(pass, field.Tag, fmt.Sprintf("unknown JSON option %q", s))
141 }
142 }
143 var duplicates []string
144 for option, n := range options {
145 if n > 1 {
146 duplicates = append(duplicates, option)
147 }
148 }
149 if len(duplicates) > 0 {
150 sort.Strings(duplicates)
151 for _, option := range duplicates {
152 report.Report(pass, field.Tag, fmt.Sprintf("duplicate JSON option %q", option))
153 }
154 }
155 }
156
157 func checkXMLTag(pass *analysis.Pass, field *ast.Field, tag string) {
158 //lint:ignore SA9003 TODO(dh): should we flag empty tags?
159 if len(tag) == 0 {
160 }
161 fields := strings.Split(tag, ",")
162 counts := map[string]int{}
163 for _, s := range fields[1:] {
164 switch s {
165 case "attr", "chardata", "cdata", "innerxml", "comment":
166 counts[s]++
167 case "omitempty", "any":
168 counts[s]++
169 case "":
170 default:
171 report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: unknown option %q", s))
172 }
173 }
174 for k, v := range counts {
175 if v > 1 {
176 report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: duplicate option %q", k))
177 }
178 }
179 }
180