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 for curBodyStmt := range curBody.Children() {
237 if curBodyStmt != curLastStmt {
238 for range curBodyStmt.Preorder((*ast.BranchStmt)(nil), (*ast.ReturnStmt)(nil)) {
239 return
240 }
241 }
242 }
243
244 switch lastStmt := curLastStmt.Node().(type) {
245 case *ast.ReturnStmt:
246
247
248
249
250
251 if curNext, ok := curRange.NextSibling(); ok {
252 nextStmt := curNext.Node().(ast.Stmt)
253 tval := isReturnTrueOrFalse(info, lastStmt)
254 fval := isReturnTrueOrFalse(info, nextStmt)
255 if len(body.List) == 1 && tval*fval < 0 {
256
257
258 report([]analysis.TextEdit{
259
260 {
261 Pos: rng.Pos(),
262 End: nextStmt.Pos(),
263 },
264
265 {
266 Pos: nextStmt.Pos(),
267 End: nextStmt.End(),
268 NewText: fmt.Appendf(nil, "return %s%s",
269 cond(tval > 0, "", "!"),
270 contains),
271 },
272 })
273 return
274 }
275 }
276
277
278
279 report([]analysis.TextEdit{
280
281 {
282 Pos: rng.Pos(),
283 End: ifStmt.Body.Pos(),
284 NewText: fmt.Appendf(nil, "if %s ", contains),
285 },
286
287 {
288 Pos: ifStmt.Body.End(),
289 End: rng.End(),
290 },
291 })
292 return
293
294 case *ast.BranchStmt:
295 if lastStmt.Tok == token.BREAK && lastStmt.Label == nil {
296
297
298 var prevStmt ast.Stmt
299 if curPrev, ok := curRange.PrevSibling(); ok {
300
301
302
303
304
305
306
307
308 prevStmt, _ = curPrev.Node().(ast.Stmt)
309 }
310
311
312
313
314 if assign, ok := body.List[0].(*ast.AssignStmt); ok &&
315 len(body.List) == 2 &&
316 assign.Tok == token.ASSIGN &&
317 len(assign.Lhs) == 1 &&
318 len(assign.Rhs) == 1 {
319
320
321 if prevAssign, ok := prevStmt.(*ast.AssignStmt); ok &&
322 len(prevAssign.Lhs) == 1 &&
323 len(prevAssign.Rhs) == 1 &&
324 astutil.EqualSyntax(prevAssign.Lhs[0], assign.Lhs[0]) &&
325 isTrueOrFalse(info, assign.Rhs[0]) ==
326 -isTrueOrFalse(info, prevAssign.Rhs[0]) {
327
328
329
330
331
332
333
334
335
336
337 neg := cond(isTrueOrFalse(info, assign.Rhs[0]) < 0, "!", "")
338 report([]analysis.TextEdit{
339
340 {
341 Pos: prevAssign.Rhs[0].Pos(),
342 End: prevAssign.Rhs[0].End(),
343 NewText: []byte(neg + contains),
344 },
345
346 {
347 Pos: prevAssign.Rhs[0].End(),
348 End: rng.End(),
349 },
350 })
351 return
352 }
353 }
354
355
356
357
358 report([]analysis.TextEdit{
359
360 {
361 Pos: rng.Pos(),
362 End: ifStmt.Body.Pos(),
363 NewText: fmt.Appendf(nil, "if %s ", contains),
364 },
365
366 {
367 Pos: func() token.Pos {
368 if len(body.List) > 1 {
369 beforeBreak, _ := curLastStmt.PrevSibling()
370 return beforeBreak.Node().End()
371 }
372 return lastStmt.Pos()
373 }(),
374 End: lastStmt.End(),
375 },
376
377 {
378 Pos: ifStmt.Body.End(),
379 End: rng.End(),
380 },
381 })
382 return
383 }
384 }
385 }
386
387 for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
388 file := curFile.Node().(*ast.File)
389
390 for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
391 rng := curRange.Node().(*ast.RangeStmt)
392
393 if is[*ast.Ident](rng.Key) &&
394 rng.Tok == token.DEFINE &&
395 len(rng.Body.List) == 1 &&
396 is[*types.Slice](typeparams.CoreType(info.TypeOf(rng.X))) {
397
398
399
400
401
402 if ifStmt, ok := rng.Body.List[0].(*ast.IfStmt); ok &&
403 ifStmt.Init == nil && ifStmt.Else == nil {
404
405
406 check(file, curRange)
407 }
408 }
409 }
410 }
411 return nil, nil
412 }
413
414
415
416
417 func isReturnTrueOrFalse(info *types.Info, stmt ast.Stmt) int {
418 if ret, ok := stmt.(*ast.ReturnStmt); ok && len(ret.Results) == 1 {
419 return isTrueOrFalse(info, ret.Results[0])
420 }
421 return 0
422 }
423
424
425 func isTrueOrFalse(info *types.Info, expr ast.Expr) int {
426 if id, ok := expr.(*ast.Ident); ok {
427 switch info.Uses[id] {
428 case builtinTrue:
429 return +1
430 case builtinFalse:
431 return -1
432 }
433 }
434 return 0
435 }
436
View as plain text