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.
5255func 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 (),
0 commit comments