Skip to content

Commit 7042bab

Browse files
xieyuschengopherbot
authored andcommitted
gopls/internal/analysis/modernize: modernizer to suggest using strings.CutPrefix
This CL defines a modernizer to suggest users using strings.CutPrefix rather than a combination of strings.HasPrefix and strings.TrimPrefix; or strings.TrimPrefix first with a further comparison in an if statement. Updates golang/go#71369 Change-Id: Id373bbf34292231f3fbfa41d7ffcf23505682beb Reviewed-on: https://go-review.googlesource.com/c/tools/+/655777 Reviewed-by: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Auto-Submit: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 3d22fef commit 7042bab

13 files changed

+562
-10
lines changed

Diff for: gopls/doc/analyzers.md

+3
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,9 @@ Categories of modernize diagnostic:
550550
- stringseq: replace Split in "for range strings.Split(...)" by go1.24's
551551
more efficient SplitSeq, or Fields with FieldSeq.
552552

553+
- stringscutprefix: replace some uses of HasPrefix followed by TrimPrefix with CutPrefix,
554+
added to the strings package in go1.20.
555+
553556
Default: on.
554557

555558
Package documentation: [modernize](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize)

Diff for: gopls/internal/analysis/modernize/doc.go

+3
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,7 @@
8282
//
8383
// - stringseq: replace Split in "for range strings.Split(...)" by go1.24's
8484
// more efficient SplitSeq, or Fields with FieldSeq.
85+
//
86+
// - stringscutprefix: replace some uses of HasPrefix followed by TrimPrefix with CutPrefix,
87+
// added to the strings package in go1.20.
8588
package modernize

Diff for: gopls/internal/analysis/modernize/modernize.go

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func run(pass *analysis.Pass) (any, error) {
8787
rangeint(pass)
8888
slicescontains(pass)
8989
slicesdelete(pass)
90+
stringscutprefix(pass)
9091
stringsseq(pass)
9192
sortslice(pass)
9293
testingContext(pass)

Diff for: gopls/internal/analysis/modernize/modernize_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func Test(t *testing.T) {
2424
"rangeint",
2525
"slicescontains",
2626
"slicesdelete",
27+
"stringscutprefix",
2728
"splitseq",
2829
"fieldsseq",
2930
"sortslice",
+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package modernize
6+
7+
import (
8+
"fmt"
9+
"go/ast"
10+
"go/token"
11+
12+
"golang.org/x/tools/go/analysis"
13+
"golang.org/x/tools/go/analysis/passes/inspect"
14+
"golang.org/x/tools/go/ast/inspector"
15+
"golang.org/x/tools/go/types/typeutil"
16+
"golang.org/x/tools/internal/analysisinternal"
17+
)
18+
19+
// stringscutprefix offers a fix to replace an if statement which
20+
// calls to the 2 patterns below with strings.CutPrefix.
21+
//
22+
// Patterns:
23+
//
24+
// 1. if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre) }
25+
// =>
26+
// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
27+
//
28+
// 2. if after := strings.TrimPrefix(s, pre); after != s { use(after) }
29+
// =>
30+
// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
31+
//
32+
// The use must occur within the first statement of the block, and the offered fix
33+
// only replaces the first occurrence of strings.TrimPrefix.
34+
//
35+
// Variants:
36+
// - bytes.HasPrefix usage as pattern 1.
37+
func stringscutprefix(pass *analysis.Pass) {
38+
if !analysisinternal.Imports(pass.Pkg, "strings") &&
39+
!analysisinternal.Imports(pass.Pkg, "bytes") {
40+
return
41+
}
42+
43+
const (
44+
category = "stringscutprefix"
45+
fixedMessage = "Replace HasPrefix/TrimPrefix with CutPrefix"
46+
)
47+
48+
info := pass.TypesInfo
49+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
50+
for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.20") {
51+
for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
52+
ifStmt := curIfStmt.Node().(*ast.IfStmt)
53+
54+
// pattern1
55+
if call, ok := ifStmt.Cond.(*ast.CallExpr); ok && len(ifStmt.Body.List) > 0 {
56+
obj := typeutil.Callee(info, call)
57+
if !analysisinternal.IsFunctionNamed(obj, "strings", "HasPrefix") &&
58+
!analysisinternal.IsFunctionNamed(obj, "bytes", "HasPrefix") {
59+
continue
60+
}
61+
62+
// Replace the first occurrence of strings.TrimPrefix(s, pre) in the first statement only,
63+
// but not later statements in case s or pre are modified by intervening logic.
64+
firstStmt := curIfStmt.Child(ifStmt.Body).Child(ifStmt.Body.List[0])
65+
for curCall := range firstStmt.Preorder((*ast.CallExpr)(nil)) {
66+
call1 := curCall.Node().(*ast.CallExpr)
67+
obj1 := typeutil.Callee(info, call1)
68+
if !analysisinternal.IsFunctionNamed(obj1, "strings", "TrimPrefix") &&
69+
!analysisinternal.IsFunctionNamed(obj1, "bytes", "TrimPrefix") {
70+
continue
71+
}
72+
73+
// Have: if strings.HasPrefix(s0, pre0) { ...strings.TrimPrefix(s, pre)... }
74+
var (
75+
s0 = call.Args[0]
76+
pre0 = call.Args[1]
77+
s = call1.Args[0]
78+
pre = call1.Args[1]
79+
)
80+
81+
// check whether the obj1 uses the exact the same argument with strings.HasPrefix
82+
// shadow variables won't be valid because we only access the first statement.
83+
if equalSyntax(s0, s) && equalSyntax(pre0, pre) {
84+
after := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "after")
85+
_, prefix, importEdits := analysisinternal.AddImport(
86+
info,
87+
curFile.Node().(*ast.File),
88+
obj1.Pkg().Name(),
89+
obj1.Pkg().Path(),
90+
"CutPrefix",
91+
call.Pos(),
92+
)
93+
okVarName := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
94+
pass.Report(analysis.Diagnostic{
95+
// highlight at HasPrefix call.
96+
Pos: call.Pos(),
97+
End: call.End(),
98+
Category: category,
99+
Message: "HasPrefix + TrimPrefix can be simplified to CutPrefix",
100+
SuggestedFixes: []analysis.SuggestedFix{{
101+
Message: fixedMessage,
102+
// if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre)) }
103+
// ------------ ----------------- ----- --------------------------
104+
// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
105+
TextEdits: append(importEdits, []analysis.TextEdit{
106+
{
107+
Pos: call.Fun.Pos(),
108+
End: call.Fun.Pos(),
109+
NewText: []byte(fmt.Sprintf("%s, %s :=", after, okVarName)),
110+
},
111+
{
112+
Pos: call.Fun.Pos(),
113+
End: call.Fun.End(),
114+
NewText: fmt.Appendf(nil, "%sCutPrefix", prefix),
115+
},
116+
{
117+
Pos: call.End(),
118+
End: call.End(),
119+
NewText: []byte(fmt.Sprintf("; %s ", okVarName)),
120+
},
121+
{
122+
Pos: call1.Pos(),
123+
End: call1.End(),
124+
NewText: []byte(after),
125+
},
126+
}...),
127+
}}},
128+
)
129+
break
130+
}
131+
}
132+
}
133+
134+
// pattern2
135+
if bin, ok := ifStmt.Cond.(*ast.BinaryExpr); ok &&
136+
bin.Op == token.NEQ &&
137+
ifStmt.Init != nil &&
138+
isSimpleAssign(ifStmt.Init) {
139+
assign := ifStmt.Init.(*ast.AssignStmt)
140+
if call, ok := assign.Rhs[0].(*ast.CallExpr); ok && assign.Tok == token.DEFINE {
141+
lhs := assign.Lhs[0]
142+
obj := typeutil.Callee(info, call)
143+
if analysisinternal.IsFunctionNamed(obj, "strings", "TrimPrefix") &&
144+
(equalSyntax(lhs, bin.X) && equalSyntax(call.Args[0], bin.Y) ||
145+
(equalSyntax(lhs, bin.Y) && equalSyntax(call.Args[0], bin.X))) {
146+
okVarName := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
147+
// Have one of:
148+
// if rest := TrimPrefix(s, prefix); rest != s {
149+
// if rest := TrimPrefix(s, prefix); s != rest {
150+
151+
// We use AddImport not to add an import (since it exists already)
152+
// but to compute the correct prefix in the dot-import case.
153+
_, prefix, importEdits := analysisinternal.AddImport(
154+
info,
155+
curFile.Node().(*ast.File),
156+
obj.Pkg().Name(),
157+
obj.Pkg().Path(),
158+
"CutPrefix",
159+
call.Pos(),
160+
)
161+
162+
pass.Report(analysis.Diagnostic{
163+
// highlight from the init and the condition end.
164+
Pos: ifStmt.Init.Pos(),
165+
End: ifStmt.Cond.End(),
166+
Category: category,
167+
Message: "TrimPrefix can be simplified to CutPrefix",
168+
SuggestedFixes: []analysis.SuggestedFix{{
169+
Message: fixedMessage,
170+
// if x := strings.TrimPrefix(s, pre); x != s ...
171+
// ---- ---------- ------
172+
// if x, ok := strings.CutPrefix (s, pre); ok ...
173+
TextEdits: append(importEdits, []analysis.TextEdit{
174+
{
175+
Pos: assign.Lhs[0].End(),
176+
End: assign.Lhs[0].End(),
177+
NewText: fmt.Appendf(nil, ", %s", okVarName),
178+
},
179+
{
180+
Pos: call.Fun.Pos(),
181+
End: call.Fun.End(),
182+
NewText: fmt.Appendf(nil, "%sCutPrefix", prefix),
183+
},
184+
{
185+
Pos: ifStmt.Cond.Pos(),
186+
End: ifStmt.Cond.End(),
187+
NewText: []byte(okVarName),
188+
},
189+
}...),
190+
}},
191+
})
192+
}
193+
}
194+
}
195+
}
196+
}
197+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package stringscutprefix
2+
3+
import (
4+
. "bytes"
5+
)
6+
7+
// test supported cases of pattern 1
8+
func _() {
9+
if HasPrefix(bss, bspre) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
10+
a := TrimPrefix(bss, bspre)
11+
_ = a
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package stringscutprefix
2+
3+
import (
4+
. "bytes"
5+
)
6+
7+
// test supported cases of pattern 1
8+
func _() {
9+
if after, ok := CutPrefix(bss, bspre); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
10+
a := after
11+
_ = a
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package stringscutprefix
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
)
7+
8+
var (
9+
s, pre string
10+
bss, bspre []byte
11+
)
12+
13+
// test supported cases of pattern 1
14+
func _() {
15+
if strings.HasPrefix(s, pre) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
16+
a := strings.TrimPrefix(s, pre)
17+
_ = a
18+
}
19+
if strings.HasPrefix("", "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
20+
a := strings.TrimPrefix("", "")
21+
_ = a
22+
}
23+
if strings.HasPrefix(s, "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
24+
println([]byte(strings.TrimPrefix(s, "")))
25+
}
26+
if strings.HasPrefix(s, "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
27+
a, b := "", strings.TrimPrefix(s, "")
28+
_, _ = a, b
29+
}
30+
if strings.HasPrefix(s, "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
31+
a, b := strings.TrimPrefix(s, ""), strings.TrimPrefix(s, "") // only replace the first occurrence
32+
s = "123"
33+
b = strings.TrimPrefix(s, "") // only replace the first occurrence
34+
_, _ = a, b
35+
}
36+
37+
if bytes.HasPrefix(bss, bspre) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
38+
a := bytes.TrimPrefix(bss, bspre)
39+
_ = a
40+
}
41+
if bytes.HasPrefix([]byte(""), []byte("")) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
42+
a := bytes.TrimPrefix([]byte(""), []byte(""))
43+
_ = a
44+
}
45+
var a, b string
46+
if strings.HasPrefix(s, "") { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
47+
a, b = "", strings.TrimPrefix(s, "")
48+
_, _ = a, b
49+
}
50+
}
51+
52+
// test cases that are not supported by pattern1
53+
func _() {
54+
ok := strings.HasPrefix("", "")
55+
if ok { // noop, currently it doesn't track the result usage of HasPrefix
56+
a := strings.TrimPrefix("", "")
57+
_ = a
58+
}
59+
if strings.HasPrefix(s, pre) {
60+
a := strings.TrimPrefix("", "") // noop, as the argument isn't the same
61+
_ = a
62+
}
63+
if strings.HasPrefix(s, pre) {
64+
var result string
65+
result = strings.TrimPrefix("", "") // noop, as we believe define is more popular.
66+
_ = result
67+
}
68+
if strings.HasPrefix("", "") {
69+
a := strings.TrimPrefix(s, pre) // noop, as the argument isn't the same
70+
_ = a
71+
}
72+
}
73+
74+
var value0 string
75+
76+
// test supported cases of pattern2
77+
func _() {
78+
if after := strings.TrimPrefix(s, pre); after != s { // want "TrimPrefix can be simplified to CutPrefix"
79+
println(after)
80+
}
81+
if after := strings.TrimPrefix(s, pre); s != after { // want "TrimPrefix can be simplified to CutPrefix"
82+
println(after)
83+
}
84+
if after := strings.TrimPrefix(s, pre); s != after { // want "TrimPrefix can be simplified to CutPrefix"
85+
println(strings.TrimPrefix(s, pre)) // noop here
86+
}
87+
if after := strings.TrimPrefix(s, ""); s != after { // want "TrimPrefix can be simplified to CutPrefix"
88+
println(after)
89+
}
90+
var ok bool // define an ok variable to test the fix won't shadow it for its if stmt body
91+
_ = ok
92+
if after := strings.TrimPrefix(s, pre); after != s { // want "TrimPrefix can be simplified to CutPrefix"
93+
println(after)
94+
}
95+
var predefined string
96+
if predefined = strings.TrimPrefix(s, pre); s != predefined { // noop
97+
println(predefined)
98+
}
99+
if predefined = strings.TrimPrefix(s, pre); s != predefined { // noop
100+
println(&predefined)
101+
}
102+
var value string
103+
if value = strings.TrimPrefix(s, pre); s != value { // noop
104+
println(value)
105+
}
106+
lhsMap := make(map[string]string)
107+
if lhsMap[""] = strings.TrimPrefix(s, pre); s != lhsMap[""] { // noop
108+
println(lhsMap[""])
109+
}
110+
arr := make([]string, 0)
111+
if arr[0] = strings.TrimPrefix(s, pre); s != arr[0] { // noop
112+
println(arr[0])
113+
}
114+
type example struct {
115+
field string
116+
}
117+
var e example
118+
if e.field = strings.TrimPrefix(s, pre); s != e.field { // noop
119+
println(e.field)
120+
}
121+
}
122+
123+
// test cases that not supported by pattern2
124+
func _() {
125+
if after := strings.TrimPrefix(s, pre); s != pre { // noop
126+
println(after)
127+
}
128+
if after := strings.TrimPrefix(s, pre); after != pre { // noop
129+
println(after)
130+
}
131+
if strings.TrimPrefix(s, pre) != s {
132+
println(strings.TrimPrefix(s, pre))
133+
}
134+
}

0 commit comments

Comments
 (0)