1
2
3
4
5 package modernize
6
7
8
9 import (
10 "fmt"
11 "go/ast"
12 "go/token"
13 "go/types"
14
15 "golang.org/x/tools/go/analysis"
16 "golang.org/x/tools/go/analysis/passes/inspect"
17 "golang.org/x/tools/go/ast/inspector"
18 "golang.org/x/tools/internal/analysis/analyzerutil"
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/versions"
24 )
25
26 var MapsLoopAnalyzer = &analysis.Analyzer{
27 Name: "mapsloop",
28 Doc: analyzerutil.MustExtractDoc(doc, "mapsloop"),
29 Requires: []*analysis.Analyzer{inspect.Analyzer},
30 Run: mapsloop,
31 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop",
32 }
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 func mapsloop(pass *analysis.Pass) (any, error) {
55
56
57 if within(pass, "maps", "bytes", "runtime") {
58 return nil, nil
59 }
60
61 info := pass.TypesInfo
62
63
64
65 check := func(file *ast.File, curRange inspector.Cursor, assign *ast.AssignStmt, m, x ast.Expr) {
66
67
68 tx := types.Unalias(info.TypeOf(x))
69 var xmap bool
70 switch typeparams.CoreType(tx).(type) {
71 case *types.Map:
72 xmap = true
73
74 case *types.Signature:
75 k, v, ok := assignableToIterSeq2(tx)
76 if !ok {
77 return
78 }
79 xmap = false
80
81
82
83
84 tx = types.NewMap(k, v)
85
86 default:
87 return
88 }
89
90
91
92
93
94
95 var mrhs ast.Expr
96 var mAssign token.Token
97 if curPrev, ok := curRange.PrevSibling(); ok {
98 if assign, ok := curPrev.Node().(*ast.AssignStmt); ok &&
99 len(assign.Lhs) == 1 &&
100 len(assign.Rhs) == 1 &&
101 astutil.EqualSyntax(assign.Lhs[0], m) {
102 mAssign = assign.Tok
103
104 var newMap bool
105 rhs := assign.Rhs[0]
106 switch rhs := ast.Unparen(rhs).(type) {
107 case *ast.CallExpr:
108 if id, ok := ast.Unparen(rhs.Fun).(*ast.Ident); ok &&
109 info.Uses[id] == builtinMake {
110
111 newMap = true
112 }
113 case *ast.CompositeLit:
114 if len(rhs.Elts) == 0 {
115
116 newMap = true
117 }
118 }
119
120
121 if newMap {
122 trhs := info.TypeOf(rhs)
123
124
125
126
127
128 if assign.Tok == token.DEFINE {
129
130
131 if types.Identical(tx, trhs) {
132 mrhs = rhs
133 }
134 } else {
135
136
137 if types.AssignableTo(tx, trhs) {
138 mrhs = rhs
139 }
140 }
141
142
143
144
145
146
147
148 if xmap {
149 mrhs = nil
150 }
151 }
152 }
153 }
154
155
156 var funcName string
157 if mrhs != nil {
158 funcName = cond(xmap, "Clone", "Collect")
159 } else {
160 funcName = cond(xmap, "Copy", "Insert")
161 }
162
163
164 rng := curRange.Node()
165 prefix, importEdits := refactor.AddImport(info, file, "maps", "maps", funcName, rng.Pos())
166 var (
167 newText []byte
168 start, end token.Pos
169 )
170 if mrhs != nil {
171
172
173
174
175
176
177
178
179
180 curPrev, _ := curRange.PrevSibling()
181 start, end = curPrev.Node().Pos(), rng.End()
182 newText = fmt.Appendf(nil, "%s%s %s %s%s(%s)",
183 allComments(file, start, end),
184 astutil.Format(pass.Fset, m),
185 mAssign.String(),
186 prefix,
187 funcName,
188 astutil.Format(pass.Fset, x))
189 } else {
190
191
192
193
194
195
196
197
198 start, end = rng.Pos(), rng.End()
199 newText = fmt.Appendf(nil, "%s%s%s(%s, %s)",
200 allComments(file, start, end),
201 prefix,
202 funcName,
203 astutil.Format(pass.Fset, m),
204 astutil.Format(pass.Fset, x))
205 }
206 pass.Report(analysis.Diagnostic{
207 Pos: assign.Lhs[0].Pos(),
208 End: assign.Lhs[0].End(),
209 Message: "Replace m[k]=v loop with maps." + funcName,
210 SuggestedFixes: []analysis.SuggestedFix{{
211 Message: "Replace m[k]=v loop with maps." + funcName,
212 TextEdits: append(importEdits, []analysis.TextEdit{{
213 Pos: start,
214 End: end,
215 NewText: newText,
216 }}...),
217 }},
218 })
219
220 }
221
222
223 for curFile := range filesUsingGoVersion(pass, versions.Go1_23) {
224 file := curFile.Node().(*ast.File)
225
226 for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
227 rng := curRange.Node().(*ast.RangeStmt)
228
229 if rng.Tok == token.DEFINE &&
230 rng.Key != nil &&
231 rng.Value != nil &&
232 isAssignBlock(rng.Body) {
233
234
235 assign := rng.Body.List[0].(*ast.AssignStmt)
236
237
238 usesKV := func(e ast.Expr) bool {
239 k := info.Defs[rng.Key.(*ast.Ident)]
240 v := info.Defs[rng.Value.(*ast.Ident)]
241 for n := range ast.Preorder(e) {
242 if id, ok := n.(*ast.Ident); ok {
243 obj := info.Uses[id]
244 if obj != nil &&
245 (obj == k || obj == v) {
246 return true
247 }
248 }
249 }
250 return false
251 }
252
253 if index, ok := assign.Lhs[0].(*ast.IndexExpr); ok &&
254 len(assign.Lhs) == 1 &&
255 astutil.EqualSyntax(rng.Key, index.Index) &&
256 astutil.EqualSyntax(rng.Value, assign.Rhs[0]) &&
257 !usesKV(index.X) {
258 if tmap, ok := typeparams.CoreType(info.TypeOf(index.X)).(*types.Map); ok &&
259 types.Identical(info.TypeOf(index), info.TypeOf(rng.Value)) &&
260 types.Identical(tmap.Key(), info.TypeOf(rng.Key)) {
261
262
263
264
265 check(file, curRange, assign, index.X, rng.X)
266 }
267 }
268 }
269 }
270 }
271 return nil, nil
272 }
273
274
275
276 func assignableToIterSeq2(t types.Type) (k, v types.Type, ok bool) {
277
278 if is[*types.Named](t) {
279 if !typesinternal.IsTypeNamed(t, "iter", "Seq2") {
280 return
281 }
282 t = t.Underlying()
283 }
284
285 if t, ok := t.(*types.Signature); ok {
286
287 if t.Params().Len() == 1 && t.Results().Len() == 0 {
288 if yield, ok := t.Params().At(0).Type().(*types.Signature); ok {
289 if yield.Params().Len() == 2 &&
290 yield.Results().Len() == 1 &&
291 types.Identical(yield.Results().At(0).Type(), builtinBool.Type()) {
292 return yield.Params().At(0).Type(), yield.Params().At(1).Type(), true
293 }
294 }
295 }
296 }
297 return
298 }
299
View as plain text