blob: 6f88ab77ed904113530bf9cbade9481f81bb4b02 [file]
// Copyright 2025 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 (
"go/ast"
"go/token"
"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/gopls/internal/util/bug"
"golang.org/x/tools/internal/analysisinternal"
)
// forvar offers to fix unnecessary copying of a for variable
//
// for _, x := range foo {
// x := x // offer to remove this superfluous assignment
// }
//
// Prerequisites:
// First statement in a range loop has to be <ident> := <ident>
// where the two idents are the same,
// and the ident is defined (:=) as a variable in the for statement.
// (Note that this 'fix' does not work for three clause loops
// because the Go specification says "The variable used by each subsequent iteration
// is declared implicitly before executing the post statement and initialized to the
// value of the previous iteration's variable at that moment.")
func forvar(pass *analysis.Pass) {
info := pass.TypesInfo
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
for curFile := range filesUsing(inspect, info, "go1.22") {
for curLoop := range curFile.Preorder((*ast.RangeStmt)(nil)) {
// in a range loop. Is the first statement var := var?
// if so, is var one of the range vars, and is it defined
// in the for statement?
// If so, decide how much to delete.
loop := curLoop.Node().(*ast.RangeStmt)
if loop.Tok != token.DEFINE {
continue
}
v, stmt := loopVarRedecl(loop.Body)
if v == nil {
continue // index is not redeclared
}
if (loop.Key == nil || !equalSyntax(loop.Key, v)) &&
(loop.Value == nil || !equalSyntax(loop.Value, v)) {
continue
}
astFile := curFile.Node().(*ast.File)
edits := analysisinternal.DeleteStmt(pass.Fset, astFile, stmt, bug.Reportf)
if len(edits) == 0 {
bug.Reportf("forvar failed to delete statement")
continue
}
remove := edits[0]
diag := analysis.Diagnostic{
Pos: remove.Pos,
End: remove.End,
Category: "forvar",
Message: "copying variable is unneeded",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Remove unneeded redeclaration",
TextEdits: []analysis.TextEdit{remove},
}},
}
pass.Report(diag)
}
}
}
// if the first statement is var := var, return var and the stmt
func loopVarRedecl(body *ast.BlockStmt) (*ast.Ident, *ast.AssignStmt) {
if len(body.List) < 1 {
return nil, nil
}
stmt, ok := body.List[0].(*ast.AssignStmt)
if !ok || !isSimpleAssign(stmt) || stmt.Tok != token.DEFINE {
return nil, nil
}
if _, ok := stmt.Lhs[0].(*ast.Ident); !ok {
return nil, nil
}
if _, ok := stmt.Rhs[0].(*ast.Ident); !ok {
return nil, nil
}
if stmt.Lhs[0].(*ast.Ident).Name == stmt.Rhs[0].(*ast.Ident).Name {
return stmt.Lhs[0].(*ast.Ident), stmt
}
return nil, nil
}