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 }