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