blob: 18be946281e7553c925392d3262a87b5c7400f70 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/typesinternal"
)
// bloop updates benchmarks that use "for range b.N", replacing it
// with go1.24's b.Loop() and eliminating any preceding
// b.{Start,Stop,Reset}Timer calls.
//
// Variants:
//
// for i := 0; i < b.N; i++ {} => for b.Loop() {}
// for range b.N {}
func bloop(pass *analysis.Pass) {
if !_imports(pass.Pkg, "testing") {
return
}
info := pass.TypesInfo
// edits computes the text edits for a matched for/range loop
// at the specified cursor. b is the *testing.B value, and
// (start, end) is the portion using b.N to delete.
edits := func(cur cursor.Cursor, b ast.Expr, start, end token.Pos) (edits []analysis.TextEdit) {
// Within the same function, delete all calls to
// b.{Start,Stop,Timer} that precede the loop.
filter := []ast.Node{(*ast.ExprStmt)(nil), (*ast.FuncLit)(nil)}
fn, _ := enclosingFunc(cur)
fn.Inspect(filter, func(cur cursor.Cursor, push bool) (descend bool) {
if push {
node := cur.Node()
if is[*ast.FuncLit](node) {
return false // don't descend into FuncLits (e.g. sub-benchmarks)
}
stmt := node.(*ast.ExprStmt)
if stmt.Pos() > start {
return false // not preceding: stop
}
if call, ok := stmt.X.(*ast.CallExpr); ok {
fn := typeutil.StaticCallee(info, call)
if fn != nil &&
(isMethod(fn, "testing", "B", "StopTimer") ||
isMethod(fn, "testing", "B", "StartTimer") ||
isMethod(fn, "testing", "B", "ResetTimer")) {
// Delete call statement.
// TODO(adonovan): delete following newline, or
// up to start of next stmt? (May delete a comment.)
edits = append(edits, analysis.TextEdit{
Pos: stmt.Pos(),
End: stmt.End(),
})
}
}
}
return true
})
// Replace ...b.N... with b.Loop().
return append(edits, analysis.TextEdit{
Pos: start,
End: end,
NewText: fmt.Appendf(nil, "%s.Loop()", formatNode(pass.Fset, b)),
})
}
// Find all for/range statements.
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
loops := []ast.Node{
(*ast.ForStmt)(nil),
(*ast.RangeStmt)(nil),
}
for curFile := range filesUsing(inspect, info, "go1.24") {
for curLoop := range curFile.Preorder(loops...) {
switch n := curLoop.Node().(type) {
case *ast.ForStmt:
// for _; i < b.N; _ {}
if cmp, ok := n.Cond.(*ast.BinaryExpr); ok && cmp.Op == token.LSS {
if sel, ok := cmp.Y.(*ast.SelectorExpr); ok &&
sel.Sel.Name == "N" &&
isPtrToNamed(info.TypeOf(sel.X), "testing", "B") {
delStart, delEnd := n.Cond.Pos(), n.Cond.End()
// Eliminate variable i if no longer needed:
// for i := 0; i < b.N; i++ {
// ...no references to i...
// }
body, _ := curLoop.LastChild()
if assign, ok := n.Init.(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE &&
len(assign.Rhs) == 1 &&
isZeroLiteral(assign.Rhs[0]) &&
is[*ast.IncDecStmt](n.Post) &&
n.Post.(*ast.IncDecStmt).Tok == token.INC &&
equalSyntax(n.Post.(*ast.IncDecStmt).X, assign.Lhs[0]) &&
!uses(info, body, info.Defs[assign.Lhs[0].(*ast.Ident)]) {
delStart, delEnd = n.Init.Pos(), n.Post.End()
}
pass.Report(analysis.Diagnostic{
// Highlight "i < b.N".
Pos: n.Cond.Pos(),
End: n.Cond.End(),
Category: "bloop",
Message: "b.N can be modernized using b.Loop()",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace b.N with b.Loop()",
TextEdits: edits(curLoop, sel.X, delStart, delEnd),
}},
})
}
}
case *ast.RangeStmt:
// for range b.N {} -> for b.Loop() {}
//
// TODO(adonovan): handle "for i := range b.N".
if sel, ok := n.X.(*ast.SelectorExpr); ok &&
n.Key == nil &&
n.Value == nil &&
sel.Sel.Name == "N" &&
isPtrToNamed(info.TypeOf(sel.X), "testing", "B") {
pass.Report(analysis.Diagnostic{
// Highlight "range b.N".
Pos: n.Range,
End: n.X.End(),
Category: "bloop",
Message: "b.N can be modernized using b.Loop()",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace b.N with b.Loop()",
TextEdits: edits(curLoop, sel.X, n.Range, n.X.End()),
}},
})
}
}
}
}
}
// isPtrToNamed reports whether t is type "*pkgpath.Name".
func isPtrToNamed(t types.Type, pkgpath, name string) bool {
if ptr, ok := t.(*types.Pointer); ok {
named, ok := ptr.Elem().(*types.Named)
return ok &&
named.Obj().Name() == name &&
named.Obj().Pkg().Path() == pkgpath
}
return false
}
// uses reports whether the subtree cur contains a use of obj.
func uses(info *types.Info, cur cursor.Cursor, obj types.Object) bool {
for curId := range cur.Preorder((*ast.Ident)(nil)) {
if info.Uses[curId.Node().(*ast.Ident)] == obj {
return true
}
}
return false
}
// isMethod reports whether fn is pkgpath.(T).Name.
func isMethod(fn *types.Func, pkgpath, T, name string) bool {
if recv := fn.Signature().Recv(); recv != nil {
_, recvName := typesinternal.ReceiverNamed(recv)
return recvName != nil &&
isPackageLevel(recvName.Obj(), pkgpath, T) &&
fn.Name() == name
}
return false
}
// enclosingFunc returns the cursor for the innermost Func{Decl,Lit}
// that encloses (or is) c, if any.
//
// TODO(adonovan): consider adding:
//
// func (Cursor) AnyEnclosing(filter ...ast.Node) (Cursor bool)
// func (Cursor) Enclosing[N ast.Node]() (Cursor, bool)
//
// See comments at [cursor.Cursor.Stack].
func enclosingFunc(c cursor.Cursor) (cursor.Cursor, bool) {
for {
switch c.Node().(type) {
case *ast.FuncLit, *ast.FuncDecl:
return c, true
case nil:
return cursor.Cursor{}, false
}
c = c.Parent()
}
}