| // 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() |
| } |
| } |