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 }