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 }