Skip to content

Commit

Permalink
gopls/internal/analysis/modernize: strings.Split -> SplitSeq
Browse files Browse the repository at this point in the history
This CL defines a modernizer for calls to strings.Split and
bytes.Split, that offers a fix to instead use go1.24's SplitSeq,
which avoids allocating an array. The fix is offered only
if Split is used as the operand of a range statement, either
directly, or indirectly via a variable whose sole use is the
range statement.

+ tests

Change-Id: I7c6c128d21ccf7f8b3c7745538177d2d162f62de
Reviewed-on: https://go-review.googlesource.com/c/tools/+/647438
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
  • Loading branch information
adonovan authored and gopherbot committed Feb 7, 2025
1 parent a1eb5fd commit 5f9967d
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 2 deletions.
2 changes: 2 additions & 0 deletions gopls/doc/analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ existing code by using more modern features of Go, such as:
added in go1.21
- replacing a 3-clause for i := 0; i < n; i++ {} loop by
for i := range n {}, added in go1.22;
- replacing Split in "for range strings.Split(...)" by go1.24's
more efficient SplitSeq;

Default: on.

Expand Down
2 changes: 2 additions & 0 deletions gopls/internal/analysis/modernize/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@
// added in go1.21
// - replacing a 3-clause for i := 0; i < n; i++ {} loop by
// for i := range n {}, added in go1.22;
// - replacing Split in "for range strings.Split(...)" by go1.24's
// more efficient SplitSeq;
package modernize
1 change: 1 addition & 0 deletions gopls/internal/analysis/modernize/modernize.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func run(pass *analysis.Pass) (any, error) {
rangeint(pass)
slicescontains(pass)
slicesdelete(pass)
splitseq(pass)
sortslice(pass)
testingContext(pass)

Expand Down
1 change: 1 addition & 0 deletions gopls/internal/analysis/modernize/modernize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func Test(t *testing.T) {
"rangeint",
"slicescontains",
"slicesdelete",
"splitseq",
"sortslice",
"testingcontext",
)
Expand Down
112 changes: 112 additions & 0 deletions gopls/internal/analysis/modernize/splitseq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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"
"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/analysisinternal"
"golang.org/x/tools/internal/astutil/edge"
)

// splitseq offers a fix to replace a call to strings.Split with
// SplitSeq 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
func splitseq(pass *analysis.Pass) {
if !analysisinternal.Imports(pass.Pkg, "strings") &&
!analysisinternal.Imports(pass.Pkg, "bytes") {
return
}
info := pass.TypesInfo
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
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.Edge(); 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 &&
soleUse(info, 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,
})
}

if sel, ok := call.Fun.(*ast.SelectorExpr); ok &&
(analysisinternal.IsFunctionNamed(typeutil.Callee(info, call), "strings", "Split") ||
analysisinternal.IsFunctionNamed(typeutil.Callee(info, call), "bytes", "Split")) {
pass.Report(analysis.Diagnostic{
Pos: sel.Pos(),
End: sel.End(),
Category: "splitseq",
Message: "Ranging over SplitSeq is more efficient",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace Split with SplitSeq",
TextEdits: append(edits, analysis.TextEdit{
// Split -> SplitSeq
Pos: sel.Sel.Pos(),
End: sel.Sel.End(),
NewText: []byte("SplitSeq")}),
}},
})
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//go:build go1.24

package splitseq

import (
"bytes"
"strings"
)

func _() {
for _, line := range strings.Split("", "") { // want "Ranging over SplitSeq is more efficient"
println(line)
}
for i, line := range strings.Split("", "") { // nope: uses index var
println(i, line)
}
for i, _ := range strings.Split("", "") { // nope: uses index var
println(i)
}
for i := range strings.Split("", "") { // nope: uses index var
println(i)
}
for _ = range strings.Split("", "") { // want "Ranging over SplitSeq is more efficient"
}
for range strings.Split("", "") { // want "Ranging over SplitSeq is more efficient"
}
for range bytes.Split(nil, nil) { // want "Ranging over SplitSeq is more efficient"
}
{
lines := strings.Split("", "") // want "Ranging over SplitSeq is more efficient"
for _, line := range lines {
println(line)
}
}
{
lines := strings.Split("", "") // nope: lines is used not just by range
for _, line := range lines {
println(line)
}
println(lines)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//go:build go1.24

package splitseq

import (
"bytes"
"strings"
)

func _() {
for line := range strings.SplitSeq("", "") { // want "Ranging over SplitSeq is more efficient"
println(line)
}
for i, line := range strings.Split("", "") { // nope: uses index var
println(i, line)
}
for i, _ := range strings.Split("", "") { // nope: uses index var
println(i)
}
for i := range strings.Split("", "") { // nope: uses index var
println(i)
}
for range strings.SplitSeq("", "") { // want "Ranging over SplitSeq is more efficient"
}
for range strings.SplitSeq("", "") { // want "Ranging over SplitSeq is more efficient"
}
for range bytes.SplitSeq(nil, nil) { // want "Ranging over SplitSeq is more efficient"
}
{
lines := strings.SplitSeq("", "") // want "Ranging over SplitSeq is more efficient"
for line := range lines {
println(line)
}
}
{
lines := strings.Split("", "") // nope: lines is used not just by range
for _, line := range lines {
println(line)
}
println(lines)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package splitseq
4 changes: 2 additions & 2 deletions gopls/internal/doc/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@
},
{
"Name": "\"modernize\"",
"Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;",
"Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;",
"Default": "true"
},
{
Expand Down Expand Up @@ -1189,7 +1189,7 @@
},
{
"Name": "modernize",
"Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;",
"Doc": "simplify code by using modern constructs\n\nThis analyzer reports opportunities for simplifying and clarifying\nexisting code by using more modern features of Go, such as:\n\n - replacing an if/else conditional assignment by a call to the\n built-in min or max functions added in go1.21;\n - replacing sort.Slice(x, func(i, j int) bool) { return s[i] \u003c s[j] }\n by a call to slices.Sort(s), added in go1.21;\n - replacing interface{} by the 'any' type added in go1.18;\n - replacing append([]T(nil), s...) by slices.Clone(s) or\n slices.Concat(s), added in go1.21;\n - replacing a loop around an m[k]=v map update by a call\n to one of the Collect, Copy, Clone, or Insert functions\n from the maps package, added in go1.21;\n - replacing []byte(fmt.Sprintf...) by fmt.Appendf(nil, ...),\n added in go1.19;\n - replacing uses of context.WithCancel in tests with t.Context, added in\n go1.24;\n - replacing omitempty by omitzero on structs, added in go1.24;\n - replacing append(s[:i], s[i+1]...) by slices.Delete(s, i, i+1),\n added in go1.21\n - replacing a 3-clause for i := 0; i \u003c n; i++ {} loop by\n for i := range n {}, added in go1.22;\n - replacing Split in \"for range strings.Split(...)\" by go1.24's\n more efficient SplitSeq;",
"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize",
"Default": true
},
Expand Down

0 comments on commit 5f9967d

Please sign in to comment.