125 lines
3.4 KiB
Go
125 lines
3.4 KiB
Go
package sa6001
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/types"
|
|
|
|
"honnef.co/go/tools/analysis/lint"
|
|
"honnef.co/go/tools/analysis/report"
|
|
"honnef.co/go/tools/go/ir"
|
|
"honnef.co/go/tools/go/types/typeutil"
|
|
"honnef.co/go/tools/internal/passes/buildir"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
)
|
|
|
|
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
|
|
Analyzer: &analysis.Analyzer{
|
|
Name: "SA6001",
|
|
Run: run,
|
|
Requires: []*analysis.Analyzer{buildir.Analyzer},
|
|
},
|
|
Doc: &lint.RawDocumentation{
|
|
Title: `Missing an optimization opportunity when indexing maps by byte slices`,
|
|
|
|
Text: `Map keys must be comparable, which precludes the use of byte slices.
|
|
This usually leads to using string keys and converting byte slices to
|
|
strings.
|
|
|
|
Normally, a conversion of a byte slice to a string needs to copy the data and
|
|
causes allocations. The compiler, however, recognizes \'m[string(b)]\' and
|
|
uses the data of \'b\' directly, without copying it, because it knows that
|
|
the data can't change during the map lookup. This leads to the
|
|
counter-intuitive situation that
|
|
|
|
k := string(b)
|
|
println(m[k])
|
|
println(m[k])
|
|
|
|
will be less efficient than
|
|
|
|
println(m[string(b)])
|
|
println(m[string(b)])
|
|
|
|
because the first version needs to copy and allocate, while the second
|
|
one does not.
|
|
|
|
For some history on this optimization, check out commit
|
|
f5f5a8b6209f84961687d993b93ea0d397f5d5bf in the Go repository.`,
|
|
Since: "2017.1",
|
|
Severity: lint.SeverityWarning,
|
|
MergeIf: lint.MergeIfAny,
|
|
},
|
|
})
|
|
|
|
var Analyzer = SCAnalyzer.Analyzer
|
|
|
|
func run(pass *analysis.Pass) (any, error) {
|
|
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
|
|
for _, b := range fn.Blocks {
|
|
insLoop:
|
|
for _, ins := range b.Instrs {
|
|
var fromType types.Type
|
|
var toType types.Type
|
|
|
|
// find []byte -> string conversions
|
|
switch ins := ins.(type) {
|
|
case *ir.Convert:
|
|
fromType = ins.X.Type()
|
|
toType = ins.Type()
|
|
case *ir.MultiConvert:
|
|
fromType = ins.X.Type()
|
|
toType = ins.Type()
|
|
default:
|
|
continue
|
|
}
|
|
if toType != types.Universe.Lookup("string").Type() {
|
|
continue
|
|
}
|
|
tset := typeutil.NewTypeSet(fromType)
|
|
// If at least one of the types is []byte, then it's more efficient to inline the conversion
|
|
if !tset.Any(func(term *types.Term) bool {
|
|
s, ok := term.Type().Underlying().(*types.Slice)
|
|
return ok && s.Elem().Underlying() == types.Universe.Lookup("byte").Type()
|
|
}) {
|
|
continue
|
|
}
|
|
refs := ins.Referrers()
|
|
// need at least two (DebugRef) references: the
|
|
// conversion and the *ast.Ident
|
|
if refs == nil || len(*refs) < 2 {
|
|
continue
|
|
}
|
|
ident := false
|
|
// skip first reference, that's the conversion itself
|
|
for _, ref := range (*refs)[1:] {
|
|
switch ref := ref.(type) {
|
|
case *ir.DebugRef:
|
|
if _, ok := ref.Expr.(*ast.Ident); !ok {
|
|
// the string seems to be used somewhere
|
|
// unexpected; the default branch should
|
|
// catch this already, but be safe
|
|
continue insLoop
|
|
} else {
|
|
ident = true
|
|
}
|
|
case *ir.MapLookup:
|
|
default:
|
|
// the string is used somewhere else than a
|
|
// map lookup
|
|
continue insLoop
|
|
}
|
|
}
|
|
|
|
// the result of the conversion wasn't assigned to an
|
|
// identifier
|
|
if !ident {
|
|
continue
|
|
}
|
|
report.Report(pass, ins, "m[string(key)] would be more efficient than k := string(key); m[k]")
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|