Skip to content

Commit b71b35e

Browse files
cuishuanggopherbot
authored andcommitted
gopls/internal/analysis/modernize: stringscutprefix: handle CutSuffix too
For golang/go#75205 Change-Id: I81f5bd4a4f38ce642e4cb860429c5ba350710427 Reviewed-on: https://go-review.googlesource.com/c/tools/+/701575 Commit-Queue: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Reviewed-by: Michael Knyszek <[email protected]>
1 parent 3e53a25 commit b71b35e

File tree

12 files changed

+274
-48
lines changed

12 files changed

+274
-48
lines changed

gopls/doc/analyzers.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4510,6 +4510,31 @@ checks for a prefix with `strings.HasPrefix` and then removes it with
45104510
to `strings.CutPrefix`, introduced in Go 1.20. The analyzer also handles
45114511
the equivalent functions in the `bytes` package.
45124512

4513+
For example, this input:
4514+
4515+
if strings.HasPrefix(s, prefix) {
4516+
use(strings.TrimPrefix(s, prefix))
4517+
}
4518+
4519+
is fixed to:
4520+
4521+
if after, ok := strings.CutPrefix(s, prefix); ok {
4522+
use(after)
4523+
}
4524+
4525+
The analyzer also offers fixes to use CutSuffix in a similar way.
4526+
This input:
4527+
4528+
if strings.HasSuffix(s, suffix) {
4529+
use(strings.TrimSuffix(s, suffix))
4530+
}
4531+
4532+
is fixed to:
4533+
4534+
if before, ok := strings.CutSuffix(s, suffix); ok {
4535+
use(before)
4536+
}
4537+
45134538
Default: on.
45144539

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

gopls/internal/analysis/modernize/doc.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,31 @@ checks for a prefix with `strings.HasPrefix` and then removes it with
227227
to `strings.CutPrefix`, introduced in Go 1.20. The analyzer also handles
228228
the equivalent functions in the `bytes` package.
229229
230+
For example, this input:
231+
232+
if strings.HasPrefix(s, prefix) {
233+
use(strings.TrimPrefix(s, prefix))
234+
}
235+
236+
is fixed to:
237+
238+
if after, ok := strings.CutPrefix(s, prefix); ok {
239+
use(after)
240+
}
241+
242+
The analyzer also offers fixes to use CutSuffix in a similar way.
243+
This input:
244+
245+
if strings.HasSuffix(s, suffix) {
246+
use(strings.TrimSuffix(s, suffix))
247+
}
248+
249+
is fixed to:
250+
251+
if before, ok := strings.CutSuffix(s, suffix); ok {
252+
use(before)
253+
}
254+
230255
# Analyzer stringsseq
231256
232257
stringsseq: replace ranging over Split/Fields with SplitSeq/FieldsSeq

gopls/internal/analysis/modernize/stringscutprefix.go

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"go/ast"
1010
"go/token"
11+
"strings"
1112

1213
"golang.org/x/tools/go/analysis"
1314
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -32,7 +33,7 @@ var StringsCutPrefixAnalyzer = &analysis.Analyzer{
3233
}
3334

3435
// stringscutprefix offers a fix to replace an if statement which
35-
// calls to the 2 patterns below with strings.CutPrefix.
36+
// calls to the 2 patterns below with strings.CutPrefix or strings.CutSuffix.
3637
//
3738
// Patterns:
3839
//
@@ -44,11 +45,13 @@ var StringsCutPrefixAnalyzer = &analysis.Analyzer{
4445
// =>
4546
// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
4647
//
48+
// Similar patterns apply for CutSuffix.
49+
//
4750
// The use must occur within the first statement of the block, and the offered fix
48-
// only replaces the first occurrence of strings.TrimPrefix.
51+
// only replaces the first occurrence of strings.TrimPrefix/TrimSuffix.
4952
//
5053
// Variants:
51-
// - bytes.HasPrefix usage as pattern 1.
54+
// - bytes.HasPrefix/HasSuffix usage as pattern 1.
5255
func stringscutprefix(pass *analysis.Pass) (any, error) {
5356
skipGenerated(pass)
5457

@@ -59,13 +62,13 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
5962

6063
stringsTrimPrefix = index.Object("strings", "TrimPrefix")
6164
bytesTrimPrefix = index.Object("bytes", "TrimPrefix")
65+
stringsTrimSuffix = index.Object("strings", "TrimSuffix")
66+
bytesTrimSuffix = index.Object("bytes", "TrimSuffix")
6267
)
63-
if !index.Used(stringsTrimPrefix, bytesTrimPrefix) {
68+
if !index.Used(stringsTrimPrefix, bytesTrimPrefix, stringsTrimSuffix, bytesTrimSuffix) {
6469
return nil, nil
6570
}
6671

67-
const fixedMessage = "Replace HasPrefix/TrimPrefix with CutPrefix"
68-
6972
for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.20") {
7073
for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
7174
ifStmt := curIfStmt.Node().(*ast.IfStmt)
@@ -74,24 +77,43 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
7477
if call, ok := ifStmt.Cond.(*ast.CallExpr); ok && ifStmt.Init == nil && len(ifStmt.Body.List) > 0 {
7578

7679
obj := typeutil.Callee(info, call)
77-
if !analysisinternal.IsFunctionNamed(obj, "strings", "HasPrefix") &&
78-
!analysisinternal.IsFunctionNamed(obj, "bytes", "HasPrefix") {
80+
if !analysisinternal.IsFunctionNamed(obj, "strings", "HasPrefix", "HasSuffix") &&
81+
!analysisinternal.IsFunctionNamed(obj, "bytes", "HasPrefix", "HasSuffix") {
7982
continue
8083
}
84+
isPrefix := strings.HasSuffix(obj.Name(), "Prefix")
8185

8286
// Replace the first occurrence of strings.TrimPrefix(s, pre) in the first statement only,
83-
// but not later statements in case s or pre are modified by intervening logic.
87+
// but not later statements in case s or pre are modified by intervening logic (ditto Suffix).
8488
firstStmt := curIfStmt.Child(ifStmt.Body).Child(ifStmt.Body.List[0])
8589
for curCall := range firstStmt.Preorder((*ast.CallExpr)(nil)) {
8690
call1 := curCall.Node().(*ast.CallExpr)
8791
obj1 := typeutil.Callee(info, call1)
8892
// bytesTrimPrefix or stringsTrimPrefix might be nil if the file doesn't import it,
89-
// so we need to ensure the obj1 is not nil otherwise the call1 is not TrimPrefix and cause a panic.
93+
// so we need to ensure the obj1 is not nil otherwise the call1 is not TrimPrefix and cause a panic (ditto Suffix).
9094
if obj1 == nil ||
91-
obj1 != stringsTrimPrefix && obj1 != bytesTrimPrefix {
95+
obj1 != stringsTrimPrefix && obj1 != bytesTrimPrefix &&
96+
obj1 != stringsTrimSuffix && obj1 != bytesTrimSuffix {
97+
continue
98+
}
99+
100+
isPrefix1 := strings.HasSuffix(obj1.Name(), "Prefix")
101+
var cutFuncName, varName, message, fixMessage string
102+
if isPrefix && isPrefix1 {
103+
cutFuncName = "CutPrefix"
104+
varName = "after"
105+
message = "HasPrefix + TrimPrefix can be simplified to CutPrefix"
106+
fixMessage = "Replace HasPrefix/TrimPrefix with CutPrefix"
107+
} else if !isPrefix && !isPrefix1 {
108+
cutFuncName = "CutSuffix"
109+
varName = "before"
110+
message = "HasSuffix + TrimSuffix can be simplified to CutSuffix"
111+
fixMessage = "Replace HasSuffix/TrimSuffix with CutSuffix"
112+
} else {
92113
continue
93114
}
94-
// Have: if strings.HasPrefix(s0, pre0) { ...strings.TrimPrefix(s, pre)... }
115+
116+
// Have: if strings.HasPrefix(s0, pre0) { ...strings.TrimPrefix(s, pre)... } (ditto Suffix)
95117
var (
96118
s0 = call.Args[0]
97119
pre0 = call.Args[1]
@@ -100,28 +122,29 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
100122
)
101123

102124
// check whether the obj1 uses the exact the same argument with strings.HasPrefix
103-
// shadow variables won't be valid because we only access the first statement.
125+
// shadow variables won't be valid because we only access the first statement (ditto Suffix).
104126
if equalSyntax(s0, s) && equalSyntax(pre0, pre) {
105-
after := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "after")
127+
after := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), varName)
106128
_, prefix, importEdits := analysisinternal.AddImport(
107129
info,
108130
curFile.Node().(*ast.File),
109131
obj1.Pkg().Name(),
110132
obj1.Pkg().Path(),
111-
"CutPrefix",
133+
cutFuncName,
112134
call.Pos(),
113135
)
114136
okVarName := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
115137
pass.Report(analysis.Diagnostic{
116-
// highlight at HasPrefix call.
138+
// highlight at HasPrefix call (ditto Suffix).
117139
Pos: call.Pos(),
118140
End: call.End(),
119-
Message: "HasPrefix + TrimPrefix can be simplified to CutPrefix",
141+
Message: message,
120142
SuggestedFixes: []analysis.SuggestedFix{{
121-
Message: fixedMessage,
143+
Message: fixMessage,
122144
// if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre)) }
123145
// ------------ ----------------- ----- --------------------------
124146
// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
147+
// (ditto Suffix)
125148
TextEdits: append(importEdits, []analysis.TextEdit{
126149
{
127150
Pos: call.Fun.Pos(),
@@ -131,7 +154,7 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
131154
{
132155
Pos: call.Fun.Pos(),
133156
End: call.Fun.End(),
134-
NewText: fmt.Appendf(nil, "%sCutPrefix", prefix),
157+
NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName),
135158
},
136159
{
137160
Pos: call.End(),
@@ -160,13 +183,29 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
160183
if call, ok := assign.Rhs[0].(*ast.CallExpr); ok && assign.Tok == token.DEFINE {
161184
lhs := assign.Lhs[0]
162185
obj := typeutil.Callee(info, call)
163-
if obj == stringsTrimPrefix &&
164-
(equalSyntax(lhs, bin.X) && equalSyntax(call.Args[0], bin.Y) ||
165-
(equalSyntax(lhs, bin.Y) && equalSyntax(call.Args[0], bin.X))) {
186+
187+
if obj != stringsTrimPrefix && obj != bytesTrimPrefix && obj != stringsTrimSuffix && obj != bytesTrimSuffix {
188+
continue
189+
}
190+
191+
isPrefix1 := strings.HasSuffix(obj.Name(), "Prefix")
192+
var cutFuncName, message, fixMessage string
193+
if isPrefix1 {
194+
cutFuncName = "CutPrefix"
195+
message = "TrimPrefix can be simplified to CutPrefix"
196+
fixMessage = "Replace TrimPrefix with CutPrefix"
197+
} else {
198+
cutFuncName = "CutSuffix"
199+
message = "TrimSuffix can be simplified to CutSuffix"
200+
fixMessage = "Replace TrimSuffix with CutSuffix"
201+
}
202+
203+
if equalSyntax(lhs, bin.X) && equalSyntax(call.Args[0], bin.Y) ||
204+
(equalSyntax(lhs, bin.Y) && equalSyntax(call.Args[0], bin.X)) {
166205
okVarName := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
167206
// Have one of:
168-
// if rest := TrimPrefix(s, prefix); rest != s {
169-
// if rest := TrimPrefix(s, prefix); s != rest {
207+
// if rest := TrimPrefix(s, prefix); rest != s { (ditto Suffix)
208+
// if rest := TrimPrefix(s, prefix); s != rest { (ditto Suffix)
170209

171210
// We use AddImport not to add an import (since it exists already)
172211
// but to compute the correct prefix in the dot-import case.
@@ -175,20 +214,21 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
175214
curFile.Node().(*ast.File),
176215
obj.Pkg().Name(),
177216
obj.Pkg().Path(),
178-
"CutPrefix",
217+
cutFuncName,
179218
call.Pos(),
180219
)
181220

182221
pass.Report(analysis.Diagnostic{
183222
// highlight from the init and the condition end.
184223
Pos: ifStmt.Init.Pos(),
185224
End: ifStmt.Cond.End(),
186-
Message: "TrimPrefix can be simplified to CutPrefix",
225+
Message: message,
187226
SuggestedFixes: []analysis.SuggestedFix{{
188-
Message: fixedMessage,
227+
Message: fixMessage,
189228
// if x := strings.TrimPrefix(s, pre); x != s ...
190229
// ---- ---------- ------
191230
// if x, ok := strings.CutPrefix (s, pre); ok ...
231+
// (ditto Suffix)
192232
TextEdits: append(importEdits, []analysis.TextEdit{
193233
{
194234
Pos: assign.Lhs[0].End(),
@@ -198,7 +238,7 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
198238
{
199239
Pos: call.Fun.Pos(),
200240
End: call.Fun.End(),
201-
NewText: fmt.Appendf(nil, "%sCutPrefix", prefix),
241+
NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName),
202242
},
203243
{
204244
Pos: ifStmt.Cond.Pos(),

gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,13 @@ func _() {
1313
a := bytes.TrimPrefix([]byte(""), []byte(""))
1414
_ = a
1515
}
16+
17+
if bytes.HasSuffix(bss, bssuf) { // want "HasSuffix \\+ TrimSuffix can be simplified to CutSuffix"
18+
a := bytes.TrimSuffix(bss, bssuf)
19+
_ = a
20+
}
21+
if bytes.HasSuffix([]byte(""), []byte("")) { // want "HasSuffix \\+ TrimSuffix can be simplified to CutSuffix"
22+
a := bytes.TrimSuffix([]byte(""), []byte(""))
23+
_ = a
24+
}
1625
}

gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix.go.golden

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,13 @@ func _() {
1313
a := after
1414
_ = a
1515
}
16+
17+
if before, ok := bytes.CutSuffix(bss, bssuf); ok { // want "HasSuffix \\+ TrimSuffix can be simplified to CutSuffix"
18+
a := before
19+
_ = a
20+
}
21+
if before, ok := bytes.CutSuffix([]byte(""), []byte("")); ok { // want "HasSuffix \\+ TrimSuffix can be simplified to CutSuffix"
22+
a := before
23+
_ = a
24+
}
1625
}

gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix_dot.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import (
44
. "bytes"
55
)
66

7-
var bss, bspre []byte
7+
var bss, bspre, bssuf []byte
88

99
// test supported cases of pattern 1
1010
func _() {
1111
if HasPrefix(bss, bspre) { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
1212
a := TrimPrefix(bss, bspre)
1313
_ = a
1414
}
15+
16+
if HasSuffix(bss, bssuf) { // want "HasSuffix \\+ TrimSuffix can be simplified to CutSuffix"
17+
b := TrimSuffix(bss, bssuf)
18+
_ = b
19+
}
1520
}

gopls/internal/analysis/modernize/testdata/src/stringscutprefix/bytescutprefix/bytescutprefix_dot.go.golden

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import (
44
. "bytes"
55
)
66

7-
var bss, bspre []byte
7+
var bss, bspre, bssuf []byte
88

99
// test supported cases of pattern 1
1010
func _() {
1111
if after, ok := CutPrefix(bss, bspre); ok { // want "HasPrefix \\+ TrimPrefix can be simplified to CutPrefix"
1212
a := after
1313
_ = a
1414
}
15-
}
15+
16+
if before, ok := CutSuffix(bss, bssuf); ok { // want "HasSuffix \\+ TrimSuffix can be simplified to CutSuffix"
17+
b := before
18+
_ = b
19+
}
20+
}

0 commit comments

Comments
 (0)