1
2
3
4
5 package modernize
6
7 import (
8 "cmp"
9 "fmt"
10 "go/ast"
11 "go/constant"
12 "go/token"
13 "go/types"
14 "maps"
15 "slices"
16
17 "golang.org/x/tools/go/analysis"
18 "golang.org/x/tools/go/analysis/passes/inspect"
19 "golang.org/x/tools/go/ast/edge"
20 "golang.org/x/tools/go/ast/inspector"
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/refactor"
25 "golang.org/x/tools/internal/typesinternal"
26 "golang.org/x/tools/internal/typesinternal/typeindex"
27 )
28
29 var StringsBuilderAnalyzer = &analysis.Analyzer{
30 Name: "stringsbuilder",
31 Doc: analyzerutil.MustExtractDoc(doc, "stringsbuilder"),
32 Requires: []*analysis.Analyzer{
33 inspect.Analyzer,
34 typeindexanalyzer.Analyzer,
35 },
36 Run: stringsbuilder,
37 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringbuilder",
38 }
39
40
41 func stringsbuilder(pass *analysis.Pass) (any, error) {
42
43
44 if within(pass, "strings", "runtime") {
45 return nil, nil
46 }
47
48 var (
49 inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
50 index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
51 )
52
53
54
55 candidates := make(map[*types.Var]bool)
56 for curAssign := range inspect.Root().Preorder((*ast.AssignStmt)(nil)) {
57 assign := curAssign.Node().(*ast.AssignStmt)
58 if assign.Tok == token.ADD_ASSIGN && is[*ast.Ident](assign.Lhs[0]) {
59 if v, ok := pass.TypesInfo.Uses[assign.Lhs[0].(*ast.Ident)].(*types.Var); ok &&
60 !typesinternal.IsPackageLevel(v) &&
61 types.Identical(v.Type(), builtinString.Type()) {
62 candidates[v] = true
63 }
64 }
65 }
66
67 lexicalOrder := func(x, y *types.Var) int { return cmp.Compare(x.Pos(), y.Pos()) }
68
69
70
71 var (
72 lastEditFile *ast.File
73 lastEditEnd token.Pos
74 )
75
76
77 nextcand:
78 for _, v := range slices.SortedFunc(maps.Keys(candidates), lexicalOrder) {
79 var edits []analysis.TextEdit
80
81
82
83
84
85
86
87
88
89
90
91
92 def, ok := index.Def(v)
93 if !ok {
94 continue
95 }
96
97
98
99
100 file := astutil.EnclosingFile(def)
101 if file == lastEditFile && v.Pos() < lastEditEnd {
102 continue
103 }
104
105 ek, _ := def.ParentEdge()
106 if ek == edge.AssignStmt_Lhs &&
107 len(def.Parent().Node().(*ast.AssignStmt).Lhs) == 1 {
108
109
110
111 assign := def.Parent().Node().(*ast.AssignStmt)
112
113
114
115 switch def.Parent().Parent().Node().(type) {
116 case *ast.BlockStmt, *ast.CaseClause, *ast.CommClause:
117
118
119 default:
120 continue
121 }
122
123
124 prefix, importEdits := refactor.AddImport(
125 pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
126 edits = append(edits, importEdits...)
127
128 if isEmptyString(pass.TypesInfo, assign.Rhs[0]) {
129
130
131
132 edits = append(edits, analysis.TextEdit{
133 Pos: assign.Pos(),
134 End: assign.End(),
135 NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder", v.Name(), prefix),
136 })
137
138 } else {
139
140
141
142 edits = append(edits, []analysis.TextEdit{
143 {
144 Pos: assign.Pos(),
145 End: assign.Rhs[0].Pos(),
146 NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder; %[1]s.WriteString(", v.Name(), prefix),
147 },
148 {
149 Pos: assign.End(),
150 End: assign.End(),
151 NewText: []byte(")"),
152 },
153 }...)
154
155 }
156
157 } else if ek == edge.ValueSpec_Names &&
158 len(def.Parent().Node().(*ast.ValueSpec).Names) == 1 &&
159 first(def.Parent().Parent().LastChild()) == def.Parent() {
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174 prefix, importEdits := refactor.AddImport(
175 pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
176 edits = append(edits, importEdits...)
177
178 spec := def.Parent().Node().(*ast.ValueSpec)
179 decl := def.Parent().Parent().Node().(*ast.GenDecl)
180
181 init := spec.Names[0].End()
182 if spec.Type != nil {
183 init = spec.Type.End()
184 }
185
186
187
188
189
190
191 edits = append(edits, analysis.TextEdit{
192 Pos: spec.Names[0].End(),
193 End: init,
194 NewText: fmt.Appendf(nil, " %sBuilder", prefix),
195 })
196
197 if len(spec.Values) > 0 && !isEmptyString(pass.TypesInfo, spec.Values[0]) {
198 if decl.Rparen.IsValid() {
199
200
201
202
203
204 edits = append(edits, []analysis.TextEdit{
205 {
206 Pos: init,
207 End: init,
208 NewText: []byte(")"),
209 },
210 {
211 Pos: spec.Values[0].End(),
212 End: decl.End(),
213 },
214 }...)
215 }
216
217
218
219
220 edits = append(edits, []analysis.TextEdit{
221 {
222 Pos: init,
223 End: spec.Values[0].Pos(),
224 NewText: fmt.Appendf(nil, "; %s.WriteString(", v.Name()),
225 },
226 {
227 Pos: spec.Values[0].End(),
228 End: spec.Values[0].End(),
229 NewText: []byte(")"),
230 },
231 }...)
232 } else {
233
234 edits = append(edits, analysis.TextEdit{
235 Pos: init,
236 End: spec.End(),
237 })
238 }
239
240 } else {
241 continue
242 }
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270 var (
271 numLoopAssigns int
272 loopAssign *ast.AssignStmt
273 seenRvalueUse bool
274 )
275 for curUse := range index.Uses(v) {
276
277 ek, _ := curUse.ParentEdge()
278 for ek == edge.ParenExpr_X {
279 curUse = curUse.Parent()
280 ek, _ = curUse.ParentEdge()
281 }
282
283
284 if seenRvalueUse {
285 continue nextcand
286 }
287
288
289
290 intervening := func(types ...ast.Node) bool {
291 for cur := range curUse.Enclosing(types...) {
292 if v.Pos() <= cur.Node().Pos() {
293 return true
294 }
295 }
296 return false
297 }
298
299 if ek == edge.AssignStmt_Lhs {
300 assign := curUse.Parent().Node().(*ast.AssignStmt)
301 if assign.Tok != token.ADD_ASSIGN {
302 continue nextcand
303 }
304
305
306
307
308
309 if intervening((*ast.ForStmt)(nil), (*ast.RangeStmt)(nil)) {
310 numLoopAssigns++
311 if loopAssign == nil {
312 loopAssign = assign
313 }
314 }
315
316
317
318
319 edits = append(edits, []analysis.TextEdit{
320
321 {
322 Pos: assign.TokPos,
323 End: assign.Rhs[0].Pos(),
324 NewText: []byte(".WriteString("),
325 },
326
327 {
328 Pos: assign.End(),
329 End: assign.End(),
330 NewText: []byte(")"),
331 },
332 }...)
333
334 } else if ek == edge.UnaryExpr_X &&
335 curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
336
337 continue nextcand
338
339 } else {
340
341
342
343
344 seenRvalueUse = true
345
346 edits = append(edits, analysis.TextEdit{
347
348 Pos: curUse.Node().End(),
349 End: curUse.Node().End(),
350 NewText: []byte(".String()"),
351 })
352 }
353 }
354 if !seenRvalueUse {
355 continue nextcand
356 }
357 if numLoopAssigns == 0 {
358 continue nextcand
359 }
360
361 lastEditFile = file
362 lastEditEnd = edits[len(edits)-1].End
363
364 pass.Report(analysis.Diagnostic{
365 Pos: loopAssign.Pos(),
366 End: loopAssign.End(),
367 Message: "using string += string in a loop is inefficient",
368 SuggestedFixes: []analysis.SuggestedFix{{
369 Message: "Replace string += string with strings.Builder",
370 TextEdits: edits,
371 }},
372 })
373 }
374
375 return nil, nil
376 }
377
378
379 func isEmptyString(info *types.Info, e ast.Expr) bool {
380 tv, ok := info.Types[e]
381 return ok && tv.Value != nil && constant.StringVal(tv.Value) == ""
382 }
383
View as plain text