sa6001.go raw

   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.RawDocumentation{
  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  }
 125