Skip to content

Commit 7d216a3

Browse files
committed
lint: add indexAlloc checker
Performance check. Suggests to replace strings.Index with bytes.Index where it can make code more efficient. See golang/go#25864
1 parent 4952c93 commit 7d216a3

File tree

9 files changed

+130
-11
lines changed

9 files changed

+130
-11
lines changed

docs/overview.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ This page describes checks supported by [go-critic](https://github.com/go-critic
180180
<td><a href="#importShadow-ref">importShadow</a></td>
181181
<td>Detects when imported package names shadowed in assignments</td>
182182
</tr>
183+
<tr>
184+
<td><a href="#indexAlloc-ref">indexAlloc</a></td>
185+
<td>Detects strings.Index calls that may cause unwanted allocs</td>
186+
</tr>
183187
<tr>
184188
<td><a href="#indexOnlyLoop-ref">indexOnlyLoop</a></td>
185189
<td>Detects for loops that can benefit from rewrite to range loop</td>
@@ -910,6 +914,24 @@ func myFunc(filename string) {
910914

911915

912916

917+
<a name="indexAlloc-ref"></a>
918+
## indexAlloc
919+
Detects strings.Index calls that may cause unwanted allocs.
920+
921+
922+
923+
**Before:**
924+
```go
925+
strings.Index(string(x), y)
926+
```
927+
928+
**After:**
929+
```go
930+
bytes.Index(x, []byte(y))
931+
```
932+
933+
934+
913935
<a name="indexOnlyLoop-ref"></a>
914936
## indexOnlyLoop
915937
Detects for loops that can benefit from rewrite to range loop.

lint/assignOp_checker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func (c *assignOpChecker) VisitStmt(stmt ast.Stmt) {
2828
assign.Tok == token.ASSIGN &&
2929
len(assign.Lhs) == 1 &&
3030
len(assign.Rhs) == 1 &&
31-
isSafeExpr(assign.Lhs[0])
31+
isSafeExpr(c.ctx.typesInfo, assign.Lhs[0])
3232
if !cond {
3333
return
3434
}

lint/boolExprSimplify_checker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (c *boolExprSimplifyChecker) combineChecks(cur *astutil.Cursor) bool {
113113
if !astequal.Expr(lhs.X, rhs.X) || !astequal.Expr(lhs.Y, rhs.Y) {
114114
return false
115115
}
116-
if !isSafeExpr(lhs.X) || !isSafeExpr(lhs.Y) {
116+
if !isSafeExpr(c.ctx.typesInfo, lhs.X) || !isSafeExpr(c.ctx.typesInfo, lhs.Y) {
117117
return false
118118
}
119119

lint/dupSubExpr_checker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (c *dupSubExprChecker) checkBinaryExpr(expr *ast.BinaryExpr) {
8080
if c.resultIsFloat(expr.X) && c.floatOpsSet[expr.Op] {
8181
return
8282
}
83-
if isSafeExpr(expr) && c.opSet[expr.Op] && astequal.Expr(expr.X, expr.Y) {
83+
if isSafeExpr(c.ctx.typesInfo, expr) && c.opSet[expr.Op] && astequal.Expr(expr.X, expr.Y) {
8484
c.warn(expr)
8585
}
8686
}

lint/indexAlloc_checker.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package lint
2+
3+
import (
4+
"go/ast"
5+
6+
"github.com/go-critic/go-critic/lint/internal/lintutil"
7+
)
8+
9+
func init() {
10+
addChecker(&indexAllocChecker{}, attrExperimental, attrPerformance)
11+
}
12+
13+
type indexAllocChecker struct {
14+
checkerBase
15+
}
16+
17+
func (c *indexAllocChecker) InitDocumentation(d *Documentation) {
18+
d.Summary = "Detects strings.Index calls that may cause unwanted allocs"
19+
d.Before = `strings.Index(string(x), y)`
20+
d.After = `bytes.Index(x, []byte(y))`
21+
}
22+
23+
func (c *indexAllocChecker) VisitExpr(e ast.Expr) {
24+
call := lintutil.AsCallExpr(e)
25+
if qualifiedName(call.Fun) != "strings.Index" {
26+
return
27+
}
28+
stringConv := lintutil.AsCallExpr(call.Args[0])
29+
if qualifiedName(stringConv.Fun) != "string" {
30+
return
31+
}
32+
x := stringConv.Args[0]
33+
y := call.Args[1]
34+
if !isSafeExpr(c.ctx.typesInfo, x) || !isSafeExpr(c.ctx.typesInfo, y) {
35+
return
36+
}
37+
c.warn(e, x, y)
38+
}
39+
40+
func (c *indexAllocChecker) warn(cause ast.Node, x, y ast.Expr) {
41+
c.ctx.Warn(cause, "consider replacing %s with bytes.Index(%s, []byte(%s))",
42+
cause, x, y)
43+
}

lint/nilValReturn_checker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (c *nilValReturnChecker) VisitStmt(stmt ast.Stmt) {
4444
expr, ok := ifStmt.Cond.(*ast.BinaryExpr)
4545
cond := ok &&
4646
expr.Op == token.EQL &&
47-
isSafeExpr(expr.X) &&
47+
isSafeExpr(c.ctx.typesInfo, expr.X) &&
4848
qualifiedName(expr.Y) == "nil" &&
4949
astequal.Expr(expr.X, ret.Results[0])
5050
if cond {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package checker_tests
2+
3+
import "bytes"
4+
5+
func fixedCode(x []byte, y string) {
6+
_ = bytes.Index(x, []byte(y))
7+
_ = bytes.Index([]byte("12"), []byte(y))
8+
_ = bytes.Index([]byte{'1', '2'}, []byte(y))
9+
_ = bytes.Index(x, []byte("a"+y))
10+
}
11+
12+
func getBytes() []byte { return nil }
13+
func getString() string { return "" }
14+
15+
func unsafeArgs(x []byte, y string) {
16+
_ = bytes.Index(getBytes(), []byte(y))
17+
_ = bytes.Index(getBytes(), []byte("a"+y))
18+
19+
_ = bytes.Index(x, []byte(getString()))
20+
_ = bytes.Index([]byte("12"), []byte(getString()))
21+
_ = bytes.Index([]byte{'1', '2'}, []byte(getString()))
22+
_ = bytes.Index(x, []byte("a"+getString()))
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package checker_tests
2+
3+
import "strings"
4+
5+
func positiveTests(x []byte, y string) {
6+
/// consider replacing strings.Index(string(x), y) with bytes.Index(x, []byte(y))
7+
_ = strings.Index(string(x), y)
8+
9+
/// consider replacing strings.Index(string([]byte("12")), y) with bytes.Index([]byte("12"), []byte(y))
10+
_ = strings.Index(string([]byte("12")), y)
11+
12+
/// consider replacing strings.Index(string([]byte{'1', '2'}), y) with bytes.Index([]byte{'1', '2'}, []byte(y))
13+
_ = strings.Index(string([]byte{'1', '2'}), y)
14+
15+
/// consider replacing strings.Index(string(x), "a"+y) with bytes.Index(x, []byte("a" + y))
16+
_ = strings.Index(string(x), "a"+y)
17+
}

lint/util.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"go/types"
77
"strings"
88

9+
"github.com/go-critic/go-critic/lint/internal/lintutil"
910
"golang.org/x/tools/go/ast/astutil"
1011
)
1112

@@ -101,7 +102,10 @@ func typeIsPointer(typ types.Type) bool {
101102
// no significant side-effects. As opposed to strictly safe expressions,
102103
// soft safe expressions permit some forms of side-effects, like
103104
// panic possibility during indexing or nil pointer dereference.
104-
func isSafeExpr(expr ast.Expr) bool {
105+
//
106+
// Uses types info to determine type conversion expressions that
107+
// are the only permitted kinds of call expressions.
108+
func isSafeExpr(info *types.Info, expr ast.Expr) bool {
105109
// This list switch is not comprehensive and uses
106110
// whitelist to be on the conservative side.
107111
// Can be extended as needed.
@@ -111,19 +115,29 @@ func isSafeExpr(expr ast.Expr) bool {
111115
// may cause panics.
112116
switch expr := expr.(type) {
113117
case *ast.StarExpr:
114-
return isSafeExpr(expr.X)
118+
return isSafeExpr(info, expr.X)
115119
case *ast.BinaryExpr:
116-
return isSafeExpr(expr.X) && isSafeExpr(expr.Y)
120+
return isSafeExpr(info, expr.X) && isSafeExpr(info, expr.Y)
117121
case *ast.UnaryExpr:
118-
return expr.Op != token.ARROW && isSafeExpr(expr.X)
122+
return expr.Op != token.ARROW && isSafeExpr(info, expr.X)
119123
case *ast.BasicLit, *ast.Ident:
120124
return true
121125
case *ast.IndexExpr:
122-
return isSafeExpr(expr.X) && isSafeExpr(expr.Index)
126+
return isSafeExpr(info, expr.X) && isSafeExpr(info, expr.Index)
123127
case *ast.SelectorExpr:
124-
return isSafeExpr(expr.X)
128+
return isSafeExpr(info, expr.X)
125129
case *ast.ParenExpr:
126-
return isSafeExpr(expr.X)
130+
return isSafeExpr(info, expr.X)
131+
case *ast.CompositeLit:
132+
for _, x := range expr.Elts {
133+
if !isSafeExpr(info, x) {
134+
return false
135+
}
136+
}
137+
return true
138+
case *ast.CallExpr:
139+
return lintutil.IsTypeExpr(info, expr.Fun)
140+
127141
default:
128142
return false
129143
}

0 commit comments

Comments
 (0)