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