honnef.co/go/tools@v0.5.0-0.dev.0.20240520180541-dcae280a5e87/staticcheck/sa6001/sa6001.go (about)

     1  package sa6001
     2  
     3  import (
     4  	"go/ast"
     5  	"go/types"
     6  
     7  	"honnef.co/go/tools/analysis/lint"
     8  	"honnef.co/go/tools/analysis/report"
     9  	"honnef.co/go/tools/go/ir"
    10  	"honnef.co/go/tools/go/types/typeutil"
    11  	"honnef.co/go/tools/internal/passes/buildir"
    12  
    13  	"golang.org/x/tools/go/analysis"
    14  )
    15  
    16  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    17  	Analyzer: &analysis.Analyzer{
    18  		Name:     "SA6001",
    19  		Run:      run,
    20  		Requires: []*analysis.Analyzer{buildir.Analyzer},
    21  	},
    22  	Doc: &lint.Documentation{
    23  		Title: `Missing an optimization opportunity when indexing maps by byte slices`,
    24  
    25  		Text: `Map keys must be comparable, which precludes the use of byte slices.
    26  This usually leads to using string keys and converting byte slices to
    27  strings.
    28  
    29  Normally, a conversion of a byte slice to a string needs to copy the data and
    30  causes allocations. The compiler, however, recognizes \'m[string(b)]\' and
    31  uses the data of \'b\' directly, without copying it, because it knows that
    32  the data can't change during the map lookup. This leads to the
    33  counter-intuitive situation that
    34  
    35      k := string(b)
    36      println(m[k])
    37      println(m[k])
    38  
    39  will be less efficient than
    40  
    41      println(m[string(b)])
    42      println(m[string(b)])
    43  
    44  because the first version needs to copy and allocate, while the second
    45  one does not.
    46  
    47  For some history on this optimization, check out commit
    48  f5f5a8b6209f84961687d993b93ea0d397f5d5bf in the Go repository.`,
    49  		Since:    "2017.1",
    50  		Severity: lint.SeverityWarning,
    51  		MergeIf:  lint.MergeIfAny,
    52  	},
    53  })
    54  
    55  var Analyzer = SCAnalyzer.Analyzer
    56  
    57  func run(pass *analysis.Pass) (interface{}, error) {
    58  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
    59  		for _, b := range fn.Blocks {
    60  		insLoop:
    61  			for _, ins := range b.Instrs {
    62  				var fromType types.Type
    63  				var toType types.Type
    64  
    65  				// find []byte -> string conversions
    66  				switch ins := ins.(type) {
    67  				case *ir.Convert:
    68  					fromType = ins.X.Type()
    69  					toType = ins.Type()
    70  				case *ir.MultiConvert:
    71  					fromType = ins.X.Type()
    72  					toType = ins.Type()
    73  				default:
    74  					continue
    75  				}
    76  				if toType != types.Universe.Lookup("string").Type() {
    77  					continue
    78  				}
    79  				tset := typeutil.NewTypeSet(fromType)
    80  				// If at least one of the types is []byte, then it's more efficient to inline the conversion
    81  				if !tset.Any(func(term *types.Term) bool {
    82  					s, ok := term.Type().Underlying().(*types.Slice)
    83  					return ok && s.Elem().Underlying() == types.Universe.Lookup("byte").Type()
    84  				}) {
    85  					continue
    86  				}
    87  				refs := ins.Referrers()
    88  				// need at least two (DebugRef) references: the
    89  				// conversion and the *ast.Ident
    90  				if refs == nil || len(*refs) < 2 {
    91  					continue
    92  				}
    93  				ident := false
    94  				// skip first reference, that's the conversion itself
    95  				for _, ref := range (*refs)[1:] {
    96  					switch ref := ref.(type) {
    97  					case *ir.DebugRef:
    98  						if _, ok := ref.Expr.(*ast.Ident); !ok {
    99  							// the string seems to be used somewhere
   100  							// unexpected; the default branch should
   101  							// catch this already, but be safe
   102  							continue insLoop
   103  						} else {
   104  							ident = true
   105  						}
   106  					case *ir.MapLookup:
   107  					default:
   108  						// the string is used somewhere else than a
   109  						// map lookup
   110  						continue insLoop
   111  					}
   112  				}
   113  
   114  				// the result of the conversion wasn't assigned to an
   115  				// identifier
   116  				if !ident {
   117  					continue
   118  				}
   119  				report.Report(pass, ins, "m[string(key)] would be more efficient than k := string(key); m[k]")
   120  			}
   121  		}
   122  	}
   123  	return nil, nil
   124  }