1
2
3
4
5 package modernize
6
7 import (
8 "fmt"
9 "go/ast"
10 "go/token"
11 "go/types"
12
13 "golang.org/x/tools/go/analysis"
14 "golang.org/x/tools/go/analysis/passes/inspect"
15 "golang.org/x/tools/go/ast/inspector"
16 "golang.org/x/tools/go/types/typeutil"
17 "golang.org/x/tools/internal/analysis/analyzerutil"
18 typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
19 "golang.org/x/tools/internal/astutil"
20 "golang.org/x/tools/internal/refactor"
21 "golang.org/x/tools/internal/typeparams"
22 "golang.org/x/tools/internal/typesinternal"
23 "golang.org/x/tools/internal/typesinternal/typeindex"
24 "golang.org/x/tools/internal/versions"
25 )
26
27 var SlicesContainsAnalyzer = &analysis.Analyzer{
28 Name: "slicescontains",
29 Doc: analyzerutil.MustExtractDoc(doc, "slicescontains"),
30 Requires: []*analysis.Analyzer{
31 inspect.Analyzer,
32 typeindexanalyzer.Analyzer,
33 },
34 Run: slicescontains,
35 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicescontains",
36 }
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 func slicescontains(pass *analysis.Pass) (any, error) {
65
66
67 if within(pass, "slices", "runtime") {
68 return nil, nil
69 }
70
71 var (
72 index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
73 info = pass.TypesInfo
74 )
75
76
77
78 check := func(file *ast.File, curRange inspector.Cursor) {
79 rng := curRange.Node().(*ast.RangeStmt)
80 ifStmt := rng.Body.List[0].(*ast.IfStmt)
81
82
83
84 isSliceElem := func(e ast.Expr) bool {
85 if rng.Value != nil && astutil.EqualSyntax(e, rng.Value) {
86 return true
87 }
88 if x, ok := e.(*ast.IndexExpr); ok &&
89 astutil.EqualSyntax(x.X, rng.X) &&
90 astutil.EqualSyntax(x.Index, rng.Key) {
91 return true
92 }
93 return false
94 }
95
96
97
98
99
100 var (
101 funcName string
102 arg2 ast.Expr
103 )
104 switch cond := ifStmt.Cond.(type) {
105 case *ast.BinaryExpr:
106 if cond.Op == token.EQL {
107 var elem ast.Expr
108 if isSliceElem(cond.X) {
109 funcName = "Contains"
110 elem = cond.X
111 arg2 = cond.Y
112 } else if isSliceElem(cond.Y) {
113 funcName = "Contains"
114 elem = cond.Y
115 arg2 = cond.X
116 }
117
118
119 if elem != nil {
120 tElem := info.TypeOf(elem)
121 tNeedle := info.TypeOf(arg2)
122 if !types.Identical(tElem, tNeedle) {
123
124 if !types.AssignableTo(tNeedle, tElem) {
125 return
126 }
127
128
129
130
131 return
132 }
133 }
134 }
135
136 case *ast.CallExpr:
137 if len(cond.Args) == 1 &&
138 isSliceElem(cond.Args[0]) &&
139 typeutil.Callee(info, cond) != nil {
140
141
142 sig, isSignature := info.TypeOf(cond.Fun).(*types.Signature)
143 if isSignature {
144
145 if sig.Variadic() {
146 return
147 }
148
149
150 var (
151 tElem = typeparams.CoreType(info.TypeOf(rng.X)).(*types.Slice).Elem()
152 tParam = sig.Params().At(0).Type()
153 )
154 if !types.Identical(tElem, tParam) {
155 return
156 }
157 }
158
159 funcName = "ContainsFunc"
160 arg2 = cond.Fun
161 }
162 }
163 if funcName == "" {
164 return
165 }
166
167
168 body := ifStmt.Body
169 if len(body.List) == 0 {
170
171 return
172 }
173
174
175 if !typesinternal.NoEffects(info, arg2) {
176 return
177 }
178
179
180 usesRangeVar := func(n ast.Node) bool {
181 cur, ok := curRange.FindNode(n)
182 if !ok {
183 panic(fmt.Sprintf("FindNode(%T) failed", n))
184 }
185 return uses(index, cur, info.Defs[rng.Key.(*ast.Ident)]) ||
186 rng.Value != nil && uses(index, cur, info.Defs[rng.Value.(*ast.Ident)])
187 }
188 if usesRangeVar(body) {
189
190
191
192
193
194
195
196 return
197 }
198 if usesRangeVar(arg2) {
199 return
200 }
201
202
203 prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", funcName, rng.Pos())
204 contains := fmt.Sprintf("%s%s(%s, %s)",
205 prefix,
206 funcName,
207 astutil.Format(pass.Fset, rng.X),
208 astutil.Format(pass.Fset, arg2))
209
210 report := func(edits []analysis.TextEdit) {
211 pass.Report(analysis.Diagnostic{
212 Pos: rng.Pos(),
213 End: rng.End(),
214 Message: fmt.Sprintf("Loop can be simplified using slices.%s", funcName),
215 SuggestedFixes: []analysis.SuggestedFix{{
216 Message: "Replace loop by call to slices." + funcName,
217 TextEdits: append(edits, importEdits...),
218 }},
219 })
220 }
221
222
223
224
225
226
227
228 curBody, _ := curRange.FindNode(body)
229 curLastStmt, _ := curBody.LastChild()
230
231
232
233
234
235
236 numBodyStmts := 0
237 for curBodyStmt := range curBody.Children() {
238 numBodyStmts += 1
239 if curBodyStmt != curLastStmt {
240 for range curBodyStmt.Preorder((*ast.BranchStmt)(nil), (*ast.ReturnStmt)(nil)) {
241 return
242 }
243 }
244 }
245
246 switch lastStmt := curLastStmt.Node().(type) {
247 case *ast.ReturnStmt:
248
249
250
251
252
253 if curNext, ok := curRange.NextSibling(); ok {
254 nextStmt := curNext.Node().(ast.Stmt)
255 tval := isReturnTrueOrFalse(info, lastStmt)
256 fval := isReturnTrueOrFalse(info, nextStmt)
257 if len(body.List) == 1 && tval*fval < 0 {
258
259
260 report([]analysis.TextEdit{
261
262 {
263 Pos: rng.Pos(),
264 End: nextStmt.Pos(),
265 },
266
267 {
268 Pos: nextStmt.Pos(),
269 End: nextStmt.End(),
270 NewText: fmt.Appendf(nil, "return %s%s",
271 cond(tval > 0, "", "!"),
272 contains),
273 },
274 })
275 return
276 }
277 }
278
279
280
281 report([]analysis.TextEdit{
282
283 {
284 Pos: rng.Pos(),
285 End: ifStmt.Body.Pos(),
286 NewText: fmt.Appendf(nil, "if %s ", contains),
287 },
288
289 {
290 Pos: ifStmt.Body.End(),
291 End: rng.End(),
292 },
293 })
294 return
295
296 case *ast.BranchStmt:
297 if lastStmt.Tok == token.BREAK && lastStmt.Label == nil {
298
299 if numBodyStmts == 1 {
300
301
302
303
304
305
306
307 return
308 }
309 var prevStmt ast.Stmt
310 if curPrev, ok := curRange.PrevSibling(); ok {
311
312
313
314
315
316
317
318
319 prevStmt, _ = curPrev.Node().(ast.Stmt)
320 }
321
322
323
324
325 if assign, ok := body.List[0].(*ast.AssignStmt); ok &&
326 len(body.List) == 2 &&
327 assign.Tok == token.ASSIGN &&
328 len(assign.Lhs) == 1 &&
329 len(assign.Rhs) == 1 {
330
331
332 assignBool := isTrueOrFalse(info, assign.Rhs[0])
333 if prevAssign, ok := prevStmt.(*ast.AssignStmt); ok &&
334 len(prevAssign.Lhs) == 1 &&
335 len(prevAssign.Rhs) == 1 &&
336 assignBool != 0 &&
337 astutil.EqualSyntax(prevAssign.Lhs[0], assign.Lhs[0]) &&
338 assignBool == -isTrueOrFalse(info, prevAssign.Rhs[0]) {
339
340
341
342
343
344
345
346
347
348
349 neg := cond(assignBool < 0, "!", "")
350 report([]analysis.TextEdit{
351
352 {
353 Pos: prevAssign.Rhs[0].Pos(),
354 End: prevAssign.Rhs[0].End(),
355 NewText: []byte(neg + contains),
356 },
357
358 {
359 Pos: prevAssign.Rhs[0].End(),
360 End: rng.End(),
361 },
362 })
363 return
364 }
365 }
366
367
368
369
370 report([]analysis.TextEdit{
371
372 {
373 Pos: rng.Pos(),
374 End: ifStmt.Body.Pos(),
375 NewText: fmt.Appendf(nil, "if %s ", contains),
376 },
377
378 {
379 Pos: func() token.Pos {
380 if len(body.List) > 1 {
381 beforeBreak, _ := curLastStmt.PrevSibling()
382 return beforeBreak.Node().End()
383 }
384 return lastStmt.Pos()
385 }(),
386 End: lastStmt.End(),
387 },
388
389 {
390 Pos: ifStmt.Body.End(),
391 End: rng.End(),
392 },
393 })
394 return
395 }
396 }
397 }
398
399 for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
400 file := curFile.Node().(*ast.File)
401
402 for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
403 rng := curRange.Node().(*ast.RangeStmt)
404
405 if is[*ast.Ident](rng.Key) &&
406 rng.Tok == token.DEFINE &&
407 len(rng.Body.List) == 1 &&
408 is[*types.Slice](typeparams.CoreType(info.TypeOf(rng.X))) {
409
410
411
412
413
414 if ifStmt, ok := rng.Body.List[0].(*ast.IfStmt); ok &&
415 ifStmt.Init == nil && ifStmt.Else == nil {
416
417
418 check(file, curRange)
419 }
420 }
421 }
422 }
423 return nil, nil
424 }
425
426
427
428
429 func isReturnTrueOrFalse(info *types.Info, stmt ast.Stmt) int {
430 if ret, ok := stmt.(*ast.ReturnStmt); ok && len(ret.Results) == 1 {
431 return isTrueOrFalse(info, ret.Results[0])
432 }
433 return 0
434 }
435
436
437 func isTrueOrFalse(info *types.Info, expr ast.Expr) int {
438 if id, ok := expr.(*ast.Ident); ok {
439 switch info.Uses[id] {
440 case builtinTrue:
441 return +1
442 case builtinFalse:
443 return -1
444 }
445 }
446 return 0
447 }
448
View as plain text