github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa5008/sa5008.go (about) 1 package sa5008 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/types" 7 "sort" 8 "strings" 9 "unicode" 10 11 "github.com/amarpal/go-tools/analysis/code" 12 "github.com/amarpal/go-tools/analysis/lint" 13 "github.com/amarpal/go-tools/analysis/report" 14 "github.com/amarpal/go-tools/go/types/typeutil" 15 "github.com/amarpal/go-tools/staticcheck/fakereflect" 16 "github.com/amarpal/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.Documentation{ 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 }