1
2
3
4
5 package modernize
6
7 import (
8 "fmt"
9 "go/ast"
10 "go/constant"
11 "go/token"
12 "go/types"
13 "iter"
14 "strconv"
15
16 "golang.org/x/tools/go/analysis"
17 "golang.org/x/tools/go/analysis/passes/inspect"
18 "golang.org/x/tools/go/ast/edge"
19 "golang.org/x/tools/go/ast/inspector"
20 "golang.org/x/tools/go/types/typeutil"
21 "golang.org/x/tools/internal/analysis/analyzerutil"
22 typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
23 "golang.org/x/tools/internal/astutil"
24 "golang.org/x/tools/internal/goplsexport"
25 "golang.org/x/tools/internal/refactor"
26 "golang.org/x/tools/internal/typesinternal"
27 "golang.org/x/tools/internal/typesinternal/typeindex"
28 "golang.org/x/tools/internal/versions"
29 )
30
31 var stringscutAnalyzer = &analysis.Analyzer{
32 Name: "stringscut",
33 Doc: analyzerutil.MustExtractDoc(doc, "stringscut"),
34 Requires: []*analysis.Analyzer{
35 inspect.Analyzer,
36 typeindexanalyzer.Analyzer,
37 },
38 Run: stringscut,
39 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscut",
40 }
41
42 func init() {
43
44 goplsexport.StringsCutModernizer = stringscutAnalyzer
45 }
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116 func stringscut(pass *analysis.Pass) (any, error) {
117 var (
118 index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
119 info = pass.TypesInfo
120
121 stringsIndex = index.Object("strings", "Index")
122 stringsIndexByte = index.Object("strings", "IndexByte")
123 bytesIndex = index.Object("bytes", "Index")
124 bytesIndexByte = index.Object("bytes", "IndexByte")
125 )
126
127 for _, obj := range []types.Object{
128 stringsIndex,
129 stringsIndexByte,
130 bytesIndex,
131 bytesIndexByte,
132 } {
133
134 nextcall:
135 for curCall := range index.Calls(obj) {
136
137 if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) {
138 continue
139 }
140 indexCall := curCall.Node().(*ast.CallExpr)
141 obj := typeutil.Callee(info, indexCall)
142 if obj == nil {
143 continue
144 }
145
146 var iIdent *ast.Ident
147 switch ek, idx := curCall.ParentEdge(); ek {
148 case edge.ValueSpec_Values:
149
150 curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx)
151 iIdent = curName.Node().(*ast.Ident)
152 case edge.AssignStmt_Rhs:
153
154
155 curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
156 iIdent, _ = curLhs.Node().(*ast.Ident)
157 }
158
159 if iIdent == nil {
160 continue
161 }
162
163
164 iObj := info.ObjectOf(iIdent)
165 if iObj == nil {
166 continue
167 }
168
169 var (
170 s = indexCall.Args[0]
171 substr = indexCall.Args[1]
172 )
173
174
175
176 if !indexArgValid(info, index, s, indexCall.Pos()) ||
177 !indexArgValid(info, index, substr, indexCall.Pos()) {
178 continue nextcall
179 }
180
181
182
183
184
185
186 negative, nonnegative, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr)
187
188
189
190 if negative == nil && nonnegative == nil && beforeSlice == nil && afterSlice == nil {
191 continue
192 }
193
194
195 isContains := (len(negative) > 0 || len(nonnegative) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0
196
197 scope := iObj.Parent()
198 var (
199
200 okVarName = refactor.FreshName(scope, iIdent.Pos(), "ok")
201 beforeVarName = refactor.FreshName(scope, iIdent.Pos(), "before")
202 afterVarName = refactor.FreshName(scope, iIdent.Pos(), "after")
203 foundVarName = refactor.FreshName(scope, iIdent.Pos(), "found")
204 )
205
206
207
208 if len(negative) == 0 && len(nonnegative) == 0 {
209 okVarName = "_"
210 }
211 if len(beforeSlice) == 0 {
212 beforeVarName = "_"
213 }
214 if len(afterSlice) == 0 {
215 afterVarName = "_"
216 }
217
218 var edits []analysis.TextEdit
219 replace := func(exprs []ast.Expr, new string) {
220 for _, expr := range exprs {
221 edits = append(edits, analysis.TextEdit{
222 Pos: expr.Pos(),
223 End: expr.End(),
224 NewText: []byte(new),
225 })
226 }
227 }
228
229
230 indexCallId := typesinternal.UsedIdent(info, indexCall.Fun)
231 replacedFunc := "Cut"
232 if isContains {
233 replacedFunc = "Contains"
234 replace(negative, "!"+foundVarName)
235 replace(nonnegative, foundVarName)
236
237
238
239
240
241
242 edits = append(edits, analysis.TextEdit{
243 Pos: iIdent.Pos(),
244 End: iIdent.End(),
245 NewText: []byte(foundVarName),
246 }, analysis.TextEdit{
247 Pos: indexCallId.Pos(),
248 End: indexCallId.End(),
249 NewText: []byte("Contains"),
250 })
251 } else {
252 replace(negative, "!"+okVarName)
253 replace(nonnegative, okVarName)
254 replace(beforeSlice, beforeVarName)
255 replace(afterSlice, afterVarName)
256
257
258
259
260
261
262 edits = append(edits, analysis.TextEdit{
263 Pos: iIdent.Pos(),
264 End: iIdent.End(),
265 NewText: fmt.Appendf(nil, "%s, %s, %s", beforeVarName, afterVarName, okVarName),
266 }, analysis.TextEdit{
267 Pos: indexCallId.Pos(),
268 End: indexCallId.End(),
269 NewText: []byte("Cut"),
270 })
271 }
272
273
274
275 if obj.Name() == "IndexByte" {
276 switch obj.Pkg().Name() {
277 case "strings":
278 searchByteVal := info.Types[substr].Value
279 if searchByteVal == nil {
280
281
282 edits = append(edits, []analysis.TextEdit{
283 {
284 Pos: substr.Pos(),
285 NewText: []byte("string("),
286 },
287 {
288 Pos: substr.End(),
289 NewText: []byte(")"),
290 },
291 }...)
292 } else {
293
294 val, _ := constant.Int64Val(searchByteVal)
295
296 edits = append(edits, analysis.TextEdit{
297 Pos: substr.Pos(),
298 End: substr.End(),
299 NewText: strconv.AppendQuote(nil, string(byte(val))),
300 })
301 }
302 case "bytes":
303
304 edits = append(edits, []analysis.TextEdit{
305 {
306 Pos: substr.Pos(),
307 NewText: []byte("[]byte{"),
308 },
309 {
310 Pos: substr.End(),
311 NewText: []byte("}"),
312 },
313 }...)
314 }
315 }
316 pass.Report(analysis.Diagnostic{
317 Pos: indexCall.Fun.Pos(),
318 End: indexCall.Fun.End(),
319 Message: fmt.Sprintf("%s.%s can be simplified using %s.%s",
320 obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc),
321 Category: "stringscut",
322 SuggestedFixes: []analysis.SuggestedFix{{
323 Message: fmt.Sprintf("Simplify %s.%s call using %s.%s", obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc),
324 TextEdits: edits,
325 }},
326 })
327 }
328 }
329
330 return nil, nil
331 }
332
333
334
335
336
337
338
339
340 func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afterPos token.Pos) bool {
341 tv := info.Types[expr]
342 if tv.Value != nil {
343 return true
344 }
345 switch expr := expr.(type) {
346 case *ast.CallExpr:
347 return types.Identical(tv.Type, byteSliceType) &&
348 info.Types[expr.Fun].IsType() &&
349 indexArgValid(info, index, expr.Args[0], afterPos)
350 case *ast.Ident:
351 sObj := info.Uses[expr]
352 sUses := index.Uses(sObj)
353 return !hasModifyingUses(info, sUses, afterPos)
354 default:
355
356
357
358
359
360
361
362
363
364 return false
365 }
366 }
367
368
369
370
371
372
373
374
375
376
377 func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr) (negative, nonnegative, beforeSlice, afterSlice []ast.Expr) {
378 use := func(cur inspector.Cursor) bool {
379 ek, _ := cur.ParentEdge()
380 n := cur.Parent().Node()
381 switch ek {
382 case edge.BinaryExpr_X, edge.BinaryExpr_Y:
383 check := n.(*ast.BinaryExpr)
384 switch checkIdxComparison(info, check) {
385 case -1:
386 negative = append(negative, check)
387 return true
388 case 1:
389 nonnegative = append(nonnegative, check)
390 return true
391 }
392
393
394
395
396
397 if slice, ok := cur.Parent().Parent().Node().(*ast.SliceExpr); ok &&
398 sameObject(info, s, slice.X) &&
399 slice.Max == nil {
400 if isBeforeSlice(info, ek, slice) {
401 beforeSlice = append(beforeSlice, slice)
402 return true
403 } else if isAfterSlice(info, ek, slice, substr) {
404 afterSlice = append(afterSlice, slice)
405 return true
406 }
407 }
408 case edge.SliceExpr_Low, edge.SliceExpr_High:
409 slice := n.(*ast.SliceExpr)
410
411
412 if sameObject(info, s, slice.X) && slice.Max == nil {
413 if isBeforeSlice(info, ek, slice) {
414 beforeSlice = append(beforeSlice, slice)
415 return true
416 } else if isAfterSlice(info, ek, slice, substr) {
417 afterSlice = append(afterSlice, slice)
418 return true
419 }
420 }
421 }
422 return false
423 }
424
425 for curIdent := range uses {
426 if !use(curIdent) {
427 return nil, nil, nil, nil
428 }
429 }
430 return negative, nonnegative, beforeSlice, afterSlice
431 }
432
433
434
435
436 func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool {
437 for curUse := range uses {
438 ek, _ := curUse.ParentEdge()
439 if ek == edge.AssignStmt_Lhs {
440 if curUse.Node().Pos() <= afterPos {
441 continue
442 }
443 assign := curUse.Parent().Node().(*ast.AssignStmt)
444 if sameObject(info, assign.Lhs[0], curUse.Node().(*ast.Ident)) {
445
446 return true
447 }
448 } else if ek == edge.UnaryExpr_X &&
449 curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
450
451
452
453
454 return true
455 }
456 }
457 return false
458 }
459
460
461
462
463
464
465
466
467
468
469 func checkIdxComparison(info *types.Info, check *ast.BinaryExpr) int {
470
471 x, op, y := check.X, check.Op, check.Y
472 if info.Types[x].Value != nil {
473 x, op, y = y, flip(op), x
474 }
475
476 yIsInt := func(k int64) bool {
477 return isIntLiteral(info, y, k)
478 }
479
480 if op == token.LSS && yIsInt(0) ||
481 op == token.EQL && yIsInt(-1) ||
482 op == token.LEQ && yIsInt(-1) {
483 return -1
484 }
485
486 if op == token.GEQ && yIsInt(0) ||
487 op == token.NEQ && yIsInt(-1) ||
488 op == token.GTR && yIsInt(-1) {
489 return +1
490 }
491
492 return 0
493 }
494
495
496
497 func flip(op token.Token) token.Token {
498 switch op {
499 case token.EQL:
500 return token.EQL
501 case token.GEQ:
502 return token.LEQ
503 case token.GTR:
504 return token.LSS
505 case token.LEQ:
506 return token.GEQ
507 case token.LSS:
508 return token.GTR
509 }
510 return op
511 }
512
513
514 func isBeforeSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr) bool {
515 return ek == edge.SliceExpr_High && (slice.Low == nil || isZeroIntConst(info, slice.Low))
516 }
517
518
519
520 func isAfterSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr, substr ast.Expr) bool {
521 lowExpr, ok := slice.Low.(*ast.BinaryExpr)
522 if !ok || slice.High != nil {
523 return false
524 }
525
526 isLenCall := func(expr ast.Expr) bool {
527 call, ok := expr.(*ast.CallExpr)
528 if !ok || len(call.Args) != 1 {
529 return false
530 }
531 return sameObject(info, substr, call.Args[0]) && typeutil.Callee(info, call) == builtinLen
532 }
533
534
535 if is[*ast.CallExpr](substr) {
536 call := substr.(*ast.CallExpr)
537 tv := info.Types[call.Fun]
538 if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
539
540 substr = call.Args[0]
541 }
542 }
543 substrLen := -1
544 substrVal := info.Types[substr].Value
545 if substrVal != nil {
546 switch substrVal.Kind() {
547 case constant.String:
548 substrLen = len(constant.StringVal(substrVal))
549 case constant.Int:
550
551
552 substrLen = 1
553 }
554 }
555
556 switch ek {
557 case edge.BinaryExpr_X:
558 kVal := info.Types[lowExpr.Y].Value
559 if kVal == nil {
560
561 return lowExpr.Op == token.ADD && isLenCall(lowExpr.Y)
562 } else {
563
564 kInt, ok := constant.Int64Val(kVal)
565 return ok && substrLen == int(kInt)
566 }
567 case edge.BinaryExpr_Y:
568 kVal := info.Types[lowExpr.X].Value
569 if kVal == nil {
570
571 return lowExpr.Op == token.ADD && isLenCall(lowExpr.X)
572 } else {
573
574 kInt, ok := constant.Int64Val(kVal)
575 return ok && substrLen == int(kInt)
576 }
577 }
578 return false
579 }
580
581
582 func sameObject(info *types.Info, expr1, expr2 ast.Expr) bool {
583 if ident1, ok := expr1.(*ast.Ident); ok {
584 if ident2, ok := expr2.(*ast.Ident); ok {
585 uses1, ok1 := info.Uses[ident1]
586 uses2, ok2 := info.Uses[ident2]
587 return ok1 && ok2 && uses1 == uses2
588 }
589 }
590 return false
591 }
592
View as plain text