8
8
"fmt"
9
9
"go/ast"
10
10
"go/token"
11
+ "strings"
11
12
12
13
"golang.org/x/tools/go/analysis"
13
14
"golang.org/x/tools/go/analysis/passes/inspect"
@@ -32,7 +33,7 @@ var StringsCutPrefixAnalyzer = &analysis.Analyzer{
32
33
}
33
34
34
35
// 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 .
36
37
//
37
38
// Patterns:
38
39
//
@@ -44,11 +45,13 @@ var StringsCutPrefixAnalyzer = &analysis.Analyzer{
44
45
// =>
45
46
// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
46
47
//
48
+ // Similar patterns apply for CutSuffix.
49
+ //
47
50
// 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 .
49
52
//
50
53
// Variants:
51
- // - bytes.HasPrefix usage as pattern 1.
54
+ // - bytes.HasPrefix/HasSuffix usage as pattern 1.
52
55
func stringscutprefix (pass * analysis.Pass ) (any , error ) {
53
56
skipGenerated (pass )
54
57
@@ -59,13 +62,13 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
59
62
60
63
stringsTrimPrefix = index .Object ("strings" , "TrimPrefix" )
61
64
bytesTrimPrefix = index .Object ("bytes" , "TrimPrefix" )
65
+ stringsTrimSuffix = index .Object ("strings" , "TrimSuffix" )
66
+ bytesTrimSuffix = index .Object ("bytes" , "TrimSuffix" )
62
67
)
63
- if ! index .Used (stringsTrimPrefix , bytesTrimPrefix ) {
68
+ if ! index .Used (stringsTrimPrefix , bytesTrimPrefix , stringsTrimSuffix , bytesTrimSuffix ) {
64
69
return nil , nil
65
70
}
66
71
67
- const fixedMessage = "Replace HasPrefix/TrimPrefix with CutPrefix"
68
-
69
72
for curFile := range filesUsing (inspect , pass .TypesInfo , "go1.20" ) {
70
73
for curIfStmt := range curFile .Preorder ((* ast .IfStmt )(nil )) {
71
74
ifStmt := curIfStmt .Node ().(* ast.IfStmt )
@@ -74,24 +77,43 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
74
77
if call , ok := ifStmt .Cond .(* ast.CallExpr ); ok && ifStmt .Init == nil && len (ifStmt .Body .List ) > 0 {
75
78
76
79
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" ) {
79
82
continue
80
83
}
84
+ isPrefix := strings .HasSuffix (obj .Name (), "Prefix" )
81
85
82
86
// 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) .
84
88
firstStmt := curIfStmt .Child (ifStmt .Body ).Child (ifStmt .Body .List [0 ])
85
89
for curCall := range firstStmt .Preorder ((* ast .CallExpr )(nil )) {
86
90
call1 := curCall .Node ().(* ast.CallExpr )
87
91
obj1 := typeutil .Callee (info , call1 )
88
92
// 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) .
90
94
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 {
92
113
continue
93
114
}
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)
95
117
var (
96
118
s0 = call .Args [0 ]
97
119
pre0 = call .Args [1 ]
@@ -100,28 +122,29 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
100
122
)
101
123
102
124
// 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) .
104
126
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 )
106
128
_ , prefix , importEdits := analysisinternal .AddImport (
107
129
info ,
108
130
curFile .Node ().(* ast.File ),
109
131
obj1 .Pkg ().Name (),
110
132
obj1 .Pkg ().Path (),
111
- "CutPrefix" ,
133
+ cutFuncName ,
112
134
call .Pos (),
113
135
)
114
136
okVarName := analysisinternal .FreshName (info .Scopes [ifStmt ], ifStmt .Pos (), "ok" )
115
137
pass .Report (analysis.Diagnostic {
116
- // highlight at HasPrefix call.
138
+ // highlight at HasPrefix call (ditto Suffix) .
117
139
Pos : call .Pos (),
118
140
End : call .End (),
119
- Message : "HasPrefix + TrimPrefix can be simplified to CutPrefix" ,
141
+ Message : message ,
120
142
SuggestedFixes : []analysis.SuggestedFix {{
121
- Message : fixedMessage ,
143
+ Message : fixMessage ,
122
144
// if strings.HasPrefix(s, pre) { use(strings.TrimPrefix(s, pre)) }
123
145
// ------------ ----------------- ----- --------------------------
124
146
// if after, ok := strings.CutPrefix(s, pre); ok { use(after) }
147
+ // (ditto Suffix)
125
148
TextEdits : append (importEdits , []analysis.TextEdit {
126
149
{
127
150
Pos : call .Fun .Pos (),
@@ -131,7 +154,7 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
131
154
{
132
155
Pos : call .Fun .Pos (),
133
156
End : call .Fun .End (),
134
- NewText : fmt .Appendf (nil , "%sCutPrefix " , prefix ),
157
+ NewText : fmt .Appendf (nil , "%s%s " , prefix , cutFuncName ),
135
158
},
136
159
{
137
160
Pos : call .End (),
@@ -160,13 +183,29 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
160
183
if call , ok := assign .Rhs [0 ].(* ast.CallExpr ); ok && assign .Tok == token .DEFINE {
161
184
lhs := assign .Lhs [0 ]
162
185
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 )) {
166
205
okVarName := analysisinternal .FreshName (info .Scopes [ifStmt ], ifStmt .Pos (), "ok" )
167
206
// 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)
170
209
171
210
// We use AddImport not to add an import (since it exists already)
172
211
// but to compute the correct prefix in the dot-import case.
@@ -175,20 +214,21 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
175
214
curFile .Node ().(* ast.File ),
176
215
obj .Pkg ().Name (),
177
216
obj .Pkg ().Path (),
178
- "CutPrefix" ,
217
+ cutFuncName ,
179
218
call .Pos (),
180
219
)
181
220
182
221
pass .Report (analysis.Diagnostic {
183
222
// highlight from the init and the condition end.
184
223
Pos : ifStmt .Init .Pos (),
185
224
End : ifStmt .Cond .End (),
186
- Message : "TrimPrefix can be simplified to CutPrefix" ,
225
+ Message : message ,
187
226
SuggestedFixes : []analysis.SuggestedFix {{
188
- Message : fixedMessage ,
227
+ Message : fixMessage ,
189
228
// if x := strings.TrimPrefix(s, pre); x != s ...
190
229
// ---- ---------- ------
191
230
// if x, ok := strings.CutPrefix (s, pre); ok ...
231
+ // (ditto Suffix)
192
232
TextEdits : append (importEdits , []analysis.TextEdit {
193
233
{
194
234
Pos : assign .Lhs [0 ].End (),
@@ -198,7 +238,7 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
198
238
{
199
239
Pos : call .Fun .Pos (),
200
240
End : call .Fun .End (),
201
- NewText : fmt .Appendf (nil , "%sCutPrefix " , prefix ),
241
+ NewText : fmt .Appendf (nil , "%s%s " , prefix , cutFuncName ),
202
242
},
203
243
{
204
244
Pos : ifStmt .Cond .Pos (),
0 commit comments