github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa6001/sa6001.go (about)

     1  package sa6001
     2  
     3  import (
     4  	"go/ast"
     5  	"go/types"
     6  
     7  	"github.com/amarpal/go-tools/analysis/lint"
     8  	"github.com/amarpal/go-tools/analysis/report"
     9  	"github.com/amarpal/go-tools/go/ir"
    10  	"github.com/amarpal/go-tools/go/types/typeutil"
    11  	"github.com/amarpal/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  				// find []byte -> string conversions
    63  				conv, ok := ins.(*ir.Convert)
    64  				if !ok || conv.Type() != types.Universe.Lookup("string").Type() {
    65  					continue
    66  				}
    67  				tset := typeutil.NewTypeSet(conv.X.Type())
    68  				// If at least one of the types is []byte, then it's more efficient to inline the conversion
    69  				if !tset.Any(func(term *types.Term) bool {
    70  					s, ok := term.Type().Underlying().(*types.Slice)
    71  					return ok && s.Elem().Underlying() == types.Universe.Lookup("byte").Type()
    72  				}) {
    73  					continue
    74  				}
    75  				refs := conv.Referrers()
    76  				// need at least two (DebugRef) references: the
    77  				// conversion and the *ast.Ident
    78  				if refs == nil || len(*refs) < 2 {
    79  					continue
    80  				}
    81  				ident := false
    82  				// skip first reference, that's the conversion itself
    83  				for _, ref := range (*refs)[1:] {
    84  					switch ref := ref.(type) {
    85  					case *ir.DebugRef:
    86  						if _, ok := ref.Expr.(*ast.Ident); !ok {
    87  							// the string seems to be used somewhere
    88  							// unexpected; the default branch should
    89  							// catch this already, but be safe
    90  							continue insLoop
    91  						} else {
    92  							ident = true
    93  						}
    94  					case *ir.MapLookup:
    95  					default:
    96  						// the string is used somewhere else than a
    97  						// map lookup
    98  						continue insLoop
    99  					}
   100  				}
   101  
   102  				// the result of the conversion wasn't assigned to an
   103  				// identifier
   104  				if !ident {
   105  					continue
   106  				}
   107  				report.Report(pass, conv, "m[string(key)] would be more efficient than k := string(key); m[k]")
   108  			}
   109  		}
   110  	}
   111  	return nil, nil
   112  }