blob: 3f98adc4ca1d251073818d5a1b4c4ef1333cced3 [file] [log] [blame]
// 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 (
"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/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/typesinternal/typeindex"
)
// stringsseq offers a fix to replace a call to strings.Split with
// SplitSeq or strings.Fields with FieldsSeq
// when it is the operand of a range loop, either directly:
//
// for _, line := range strings.Split() {...}
//
// or indirectly, if the variable's sole use is the range statement:
//
// lines := strings.Split()
// for _, line := range lines {...}
//
// Variants:
// - bytes.SplitSeq
// - bytes.FieldsSeq
func stringsseq(pass *analysis.Pass) {
skipGenerated(pass)
var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
stringsSplit = index.Object("strings", "Split")
stringsFields = index.Object("strings", "Fields")
bytesSplit = index.Object("bytes", "Split")
bytesFields = index.Object("bytes", "Fields")
)
if !index.Used(stringsSplit, stringsFields, bytesSplit, bytesFields) {
return
}
for curFile := range filesUsing(inspect, info, "go1.24") {
for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
rng := curRange.Node().(*ast.RangeStmt)
// Reject "for i, line := ..." since SplitSeq is not an iter.Seq2.
// (We require that i is blank.)
if id, ok := rng.Key.(*ast.Ident); ok && id.Name != "_" {
continue
}
// Find the call operand of the range statement,
// whether direct or indirect.
call, ok := rng.X.(*ast.CallExpr)
if !ok {
if id, ok := rng.X.(*ast.Ident); ok {
if v, ok := info.Uses[id].(*types.Var); ok {
if ek, idx := curRange.ParentEdge(); ek == edge.BlockStmt_List && idx > 0 {
curPrev, _ := curRange.PrevSibling()
if assign, ok := curPrev.Node().(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE &&
len(assign.Lhs) == 1 &&
len(assign.Rhs) == 1 &&
info.Defs[assign.Lhs[0].(*ast.Ident)] == v &&
soleUseIs(index, v, id) {
// Have:
// lines := ...
// for _, line := range lines {...}
// and no other uses of lines.
call, _ = assign.Rhs[0].(*ast.CallExpr)
}
}
}
}
}
if call != nil {
var edits []analysis.TextEdit
if rng.Key != nil {
// Delete (blank) RangeStmt.Key:
// for _, line := -> for line :=
// for _, _ := -> for
// for _ := -> for
end := rng.Range
if rng.Value != nil {
end = rng.Value.Pos()
}
edits = append(edits, analysis.TextEdit{
Pos: rng.Key.Pos(),
End: end,
})
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
switch obj := typeutil.Callee(info, call); obj {
case stringsSplit, stringsFields, bytesSplit, bytesFields:
oldFnName := obj.Name()
seqFnName := fmt.Sprintf("%sSeq", oldFnName)
pass.Report(analysis.Diagnostic{
Pos: sel.Pos(),
End: sel.End(),
Category: "stringsseq",
Message: fmt.Sprintf("Ranging over %s is more efficient", seqFnName),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace %s with %s", oldFnName, seqFnName),
TextEdits: append(edits, analysis.TextEdit{
Pos: sel.Sel.Pos(),
End: sel.Sel.End(),
NewText: []byte(seqFnName)}),
}},
})
}
}
}
}
}