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  }