Skip to content

Commit 732f823

Browse files
committed
gopls/internal/analysis/modernize: b.N -> b.Loop()
This CL adds a modernizer for go1.24's testing.B.Loop. It replaces for/range loops over b.N by b.Loop() and deletes any preceding calls to b.{Start,Stop,Reset}Timer. Updates golang/go#70815 Change-Id: I504c9fed20beaec3c085c64f9ce42d3f6ad435d8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/638337 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 20cd540 commit 732f823

File tree

10 files changed

+382
-16
lines changed

10 files changed

+382
-16
lines changed
+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package modernize
6+
7+
import (
8+
"fmt"
9+
"go/ast"
10+
"go/token"
11+
"go/types"
12+
13+
"golang.org/x/tools/go/analysis"
14+
"golang.org/x/tools/go/analysis/passes/inspect"
15+
"golang.org/x/tools/go/ast/inspector"
16+
"golang.org/x/tools/go/types/typeutil"
17+
"golang.org/x/tools/internal/astutil/cursor"
18+
"golang.org/x/tools/internal/typesinternal"
19+
"golang.org/x/tools/internal/versions"
20+
)
21+
22+
// bloop updates benchmarks that use "for range b.N", replacing it
23+
// with go1.24's b.Loop() and eliminating any preceding
24+
// b.{Start,Stop,Reset}Timer calls.
25+
//
26+
// Variants:
27+
//
28+
// for i := 0; i < b.N; i++ {} => for b.Loop() {}
29+
// for range b.N {}
30+
func bloop(pass *analysis.Pass) {
31+
if !_imports(pass.Pkg, "testing") {
32+
return
33+
}
34+
35+
info := pass.TypesInfo
36+
37+
// edits computes the text edits for a matched for/range loop
38+
// at the specified cursor. b is the *testing.B value, and
39+
// (start, end) is the portion using b.N to delete.
40+
edits := func(cur cursor.Cursor, b ast.Expr, start, end token.Pos) (edits []analysis.TextEdit) {
41+
// Within the same function, delete all calls to
42+
// b.{Start,Stop,Timer} that precede the loop.
43+
filter := []ast.Node{(*ast.ExprStmt)(nil), (*ast.FuncLit)(nil)}
44+
fn, _ := enclosingFunc(cur)
45+
fn.Inspect(filter, func(cur cursor.Cursor, push bool) (descend bool) {
46+
if push {
47+
node := cur.Node()
48+
if is[*ast.FuncLit](node) {
49+
return false // don't descend into FuncLits (e.g. sub-benchmarks)
50+
}
51+
stmt := node.(*ast.ExprStmt)
52+
if stmt.Pos() > start {
53+
return false // not preceding: stop
54+
}
55+
if call, ok := stmt.X.(*ast.CallExpr); ok {
56+
fn := typeutil.StaticCallee(info, call)
57+
if fn != nil &&
58+
(isMethod(fn, "testing", "B", "StopTimer") ||
59+
isMethod(fn, "testing", "B", "StartTimer") ||
60+
isMethod(fn, "testing", "B", "ResetTimer")) {
61+
62+
// Delete call statement.
63+
// TODO(adonovan): delete following newline, or
64+
// up to start of next stmt? (May delete a comment.)
65+
edits = append(edits, analysis.TextEdit{
66+
Pos: stmt.Pos(),
67+
End: stmt.End(),
68+
})
69+
}
70+
}
71+
}
72+
return true
73+
})
74+
75+
// Replace ...b.N... with b.Loop().
76+
return append(edits, analysis.TextEdit{
77+
Pos: start,
78+
End: end,
79+
NewText: fmt.Appendf(nil, "%s.Loop()", formatNode(pass.Fset, b)),
80+
})
81+
}
82+
83+
// Find all for/range statements.
84+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
85+
filter := []ast.Node{
86+
(*ast.File)(nil),
87+
(*ast.ForStmt)(nil),
88+
(*ast.RangeStmt)(nil),
89+
}
90+
cursor.Root(inspect).Inspect(filter, func(cur cursor.Cursor, push bool) (descend bool) {
91+
if push {
92+
switch n := cur.Node().(type) {
93+
case *ast.File:
94+
if versions.Before(info.FileVersions[n], "go1.24") {
95+
return false
96+
}
97+
98+
case *ast.ForStmt:
99+
// for _; i < b.N; _ {}
100+
if cmp, ok := n.Cond.(*ast.BinaryExpr); ok && cmp.Op == token.LSS {
101+
if sel, ok := cmp.Y.(*ast.SelectorExpr); ok &&
102+
sel.Sel.Name == "N" &&
103+
isPtrToNamed(info.TypeOf(sel.X), "testing", "B") {
104+
105+
delStart, delEnd := n.Cond.Pos(), n.Cond.End()
106+
107+
// Eliminate variable i if no longer needed:
108+
// for i := 0; i < b.N; i++ {
109+
// ...no references to i...
110+
// }
111+
body, _ := cur.LastChild()
112+
if assign, ok := n.Init.(*ast.AssignStmt); ok &&
113+
assign.Tok == token.DEFINE &&
114+
len(assign.Rhs) == 1 &&
115+
isZeroLiteral(assign.Rhs[0]) &&
116+
is[*ast.IncDecStmt](n.Post) &&
117+
n.Post.(*ast.IncDecStmt).Tok == token.INC &&
118+
equalSyntax(n.Post.(*ast.IncDecStmt).X, assign.Lhs[0]) &&
119+
!uses(info, body, info.Defs[assign.Lhs[0].(*ast.Ident)]) {
120+
121+
delStart, delEnd = n.Init.Pos(), n.Post.End()
122+
}
123+
124+
pass.Report(analysis.Diagnostic{
125+
// Highlight "i < b.N".
126+
Pos: n.Cond.Pos(),
127+
End: n.Cond.End(),
128+
Category: "bloop",
129+
Message: "b.N can be modernized using b.Loop()",
130+
SuggestedFixes: []analysis.SuggestedFix{{
131+
Message: "Replace b.N with b.Loop()",
132+
TextEdits: edits(cur, sel.X, delStart, delEnd),
133+
}},
134+
})
135+
}
136+
}
137+
138+
case *ast.RangeStmt:
139+
// for range b.N {} -> for b.Loop() {}
140+
//
141+
// TODO(adonovan): handle "for i := range b.N".
142+
if sel, ok := n.X.(*ast.SelectorExpr); ok &&
143+
n.Key == nil &&
144+
n.Value == nil &&
145+
sel.Sel.Name == "N" &&
146+
isPtrToNamed(info.TypeOf(sel.X), "testing", "B") {
147+
148+
pass.Report(analysis.Diagnostic{
149+
// Highlight "range b.N".
150+
Pos: n.Range,
151+
End: n.X.End(),
152+
Category: "bloop",
153+
Message: "b.N can be modernized using b.Loop()",
154+
SuggestedFixes: []analysis.SuggestedFix{{
155+
Message: "Replace b.N with b.Loop()",
156+
TextEdits: edits(cur, sel.X, n.Range, n.X.End()),
157+
}},
158+
})
159+
}
160+
}
161+
}
162+
return true
163+
})
164+
}
165+
166+
// isPtrToNamed reports whether t is type "*pkgpath.Name".
167+
func isPtrToNamed(t types.Type, pkgpath, name string) bool {
168+
if ptr, ok := t.(*types.Pointer); ok {
169+
named, ok := ptr.Elem().(*types.Named)
170+
return ok &&
171+
named.Obj().Name() == name &&
172+
named.Obj().Pkg().Path() == pkgpath
173+
}
174+
return false
175+
}
176+
177+
// uses reports whether the subtree cur contains a use of obj.
178+
func uses(info *types.Info, cur cursor.Cursor, obj types.Object) bool {
179+
for curId := range cur.Preorder((*ast.Ident)(nil)) {
180+
if info.Uses[curId.Node().(*ast.Ident)] == obj {
181+
return true
182+
}
183+
}
184+
return false
185+
}
186+
187+
// isMethod reports whether fn is pkgpath.(T).Name.
188+
func isMethod(fn *types.Func, pkgpath, T, name string) bool {
189+
if recv := fn.Signature().Recv(); recv != nil {
190+
_, recvName := typesinternal.ReceiverNamed(recv)
191+
return recvName != nil &&
192+
isPackageLevel(recvName.Obj(), pkgpath, T) &&
193+
fn.Name() == name
194+
}
195+
return false
196+
}
197+
198+
// enclosingFunc returns the cursor for the innermost Func{Decl,Lit}
199+
// that encloses (or is) c, if any.
200+
//
201+
// TODO(adonovan): consider adding:
202+
//
203+
// func (Cursor) AnyEnclosing(filter ...ast.Node) (Cursor bool)
204+
// func (Cursor) Enclosing[N ast.Node]() (Cursor, bool)
205+
//
206+
// See comments at [cursor.Cursor.Stack].
207+
func enclosingFunc(c cursor.Cursor) (cursor.Cursor, bool) {
208+
for {
209+
switch c.Node().(type) {
210+
case *ast.FuncLit, *ast.FuncDecl:
211+
return c, true
212+
case nil:
213+
return cursor.Cursor{}, false
214+
}
215+
c = c.Parent()
216+
}
217+
}

gopls/internal/analysis/modernize/modernize.go

+16
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func run(pass *analysis.Pass) (any, error) {
5757
sortslice(pass)
5858
efaceany(pass)
5959
appendclipped(pass)
60+
bloop(pass)
6061

6162
// TODO(adonovan):
6263
// - more modernizers here; see #70815.
@@ -106,3 +107,18 @@ func formatExprs(fset *token.FileSet, exprs []ast.Expr) string {
106107
}
107108
return buf.String()
108109
}
110+
111+
// isZeroLiteral reports whether e is the literal 0.
112+
func isZeroLiteral(e ast.Expr) bool {
113+
lit, ok := e.(*ast.BasicLit)
114+
return ok && lit.Kind == token.INT && lit.Value == "0"
115+
}
116+
117+
// isPackageLevel reports whether obj is the package-level symbol pkg.Name.
118+
func isPackageLevel(obj types.Object, pkgpath, name string) bool {
119+
pkg := obj.Pkg()
120+
return pkg != nil &&
121+
obj.Parent() == pkg.Scope() &&
122+
obj.Pkg().Path() == pkgpath &&
123+
obj.Name() == name
124+
}

gopls/internal/analysis/modernize/modernize_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ func Test(t *testing.T) {
1616
"minmax",
1717
"sortslice",
1818
"efaceany",
19-
"appendclipped")
19+
"appendclipped",
20+
"bloop")
2021
}

gopls/internal/analysis/modernize/slices.go

-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package modernize
99
import (
1010
"fmt"
1111
"go/ast"
12-
"go/token"
1312
"go/types"
1413
"slices"
1514

@@ -197,10 +196,6 @@ func isClippedSlice(info *types.Info, e ast.Expr) (clipped, empty bool) {
197196
switch e := e.(type) {
198197
case *ast.SliceExpr:
199198
// x[:0:0], x[:len(x):len(x)], x[:k:k], x[:0]
200-
isZeroLiteral := func(e ast.Expr) bool {
201-
lit, ok := e.(*ast.BasicLit)
202-
return ok && lit.Kind == token.INT && lit.Value == "0"
203-
}
204199
clipped = e.Slice3 && e.High != nil && e.Max != nil && equalSyntax(e.High, e.Max) // x[:k:k]
205200
empty = e.High != nil && isZeroLiteral(e.High) // x[:0:*]
206201
return

gopls/internal/analysis/modernize/sortslice.go

+1-7
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,7 @@ func isQualifiedIdent(info *types.Info, e ast.Expr, pkgpath, name string) bool {
116116
return false
117117
}
118118
obj, ok := info.Uses[id]
119-
return ok && isPackageLevel(obj) && obj.Pkg().Path() == pkgpath && id.Name == name
120-
}
121-
122-
// isPackageLevel reports whether obj is a package-level func/var/const/type.
123-
func isPackageLevel(obj types.Object) bool {
124-
pkg := obj.Pkg()
125-
return pkg != nil && obj.Parent() == pkg.Scope()
119+
return ok && isPackageLevel(obj, pkgpath, name)
126120
}
127121

128122
// enclosingFile returns the file enclosing pos.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package bloop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//go:build go1.24
2+
3+
package bloop
4+
5+
import "testing"
6+
7+
func BenchmarkA(b *testing.B) {
8+
println("slow")
9+
b.ResetTimer()
10+
11+
for range b.N { // want "b.N can be modernized using b.Loop.."
12+
}
13+
}
14+
15+
func BenchmarkB(b *testing.B) {
16+
// setup
17+
{
18+
b.StopTimer()
19+
println("slow")
20+
b.StartTimer()
21+
}
22+
23+
for i := range b.N { // Nope. Should we change this to "for i := 0; b.Loop(); i++"?
24+
print(i)
25+
}
26+
27+
b.StopTimer()
28+
println("slow")
29+
}
30+
31+
func BenchmarkC(b *testing.B) {
32+
// setup
33+
{
34+
b.StopTimer()
35+
println("slow")
36+
b.StartTimer()
37+
}
38+
39+
for i := 0; i < b.N; i++ { // want "b.N can be modernized using b.Loop.."
40+
println("no uses of i")
41+
}
42+
43+
b.StopTimer()
44+
println("slow")
45+
}
46+
47+
func BenchmarkD(b *testing.B) {
48+
for i := 0; i < b.N; i++ { // want "b.N can be modernized using b.Loop.."
49+
println(i)
50+
}
51+
}
52+
53+
func BenchmarkE(b *testing.B) {
54+
b.Run("sub", func(b *testing.B) {
55+
b.StopTimer() // not deleted
56+
println("slow")
57+
b.StartTimer() // not deleted
58+
59+
// ...
60+
})
61+
b.ResetTimer()
62+
63+
for i := 0; i < b.N; i++ { // want "b.N can be modernized using b.Loop.."
64+
println("no uses of i")
65+
}
66+
67+
b.StopTimer()
68+
println("slow")
69+
}

0 commit comments

Comments
 (0)