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 def, ok := index.Def(v)
91 if !ok {
92 continue
93 }
94
95
96
97
98 file := astutil.EnclosingFile(def)
99 if file == lastEditFile && v.Pos() < lastEditEnd {
100 continue
101 }
102
103 ek, _ := def.ParentEdge()
104 if ek == edge.AssignStmt_Lhs &&
105 len(def.Parent().Node().(*ast.AssignStmt).Lhs) == 1 {
106
107
108
109 assign := def.Parent().Node().(*ast.AssignStmt)
110
111
112
113 switch def.Parent().Parent().Node().(type) {
114 case *ast.BlockStmt, *ast.CaseClause, *ast.CommClause:
115
116
117 default:
118 continue
119 }
120
121
122 prefix, importEdits := refactor.AddImport(
123 pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
124 edits = append(edits, importEdits...)
125
126 if isEmptyString(pass.TypesInfo, assign.Rhs[0]) {
127
128
129
130 edits = append(edits, analysis.TextEdit{
131 Pos: assign.Pos(),
132 End: assign.End(),
133 NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder", v.Name(), prefix),
134 })
135
136 } else {
137
138
139
140 edits = append(edits, []analysis.TextEdit{
141 {
142 Pos: assign.Pos(),
143 End: assign.Rhs[0].Pos(),
144 NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder; %[1]s.WriteString(", v.Name(), prefix),
145 },
146 {
147 Pos: assign.End(),
148 End: assign.End(),
149 NewText: []byte(")"),
150 },
151 }...)
152
153 }
154
155 } else if ek == edge.ValueSpec_Names &&
156 len(def.Parent().Node().(*ast.ValueSpec).Names) == 1 {
157
158
159
160
161 prefix, importEdits := refactor.AddImport(
162 pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
163 edits = append(edits, importEdits...)
164
165 spec := def.Parent().Node().(*ast.ValueSpec)
166 decl := def.Parent().Parent().Node().(*ast.GenDecl)
167
168 init := spec.Names[0].End()
169 if spec.Type != nil {
170 init = spec.Type.End()
171 }
172
173
174
175
176 edits = append(edits, analysis.TextEdit{
177 Pos: spec.Names[0].End(),
178 End: init,
179 NewText: fmt.Appendf(nil, " %sBuilder", prefix),
180 })
181
182 if len(spec.Values) > 0 && !isEmptyString(pass.TypesInfo, spec.Values[0]) {
183
184
185
186 edits = append(edits, []analysis.TextEdit{
187 {
188 Pos: init,
189 End: spec.Values[0].Pos(),
190 NewText: fmt.Appendf(nil, "; %s.WriteString(", v.Name()),
191 },
192 {
193 Pos: decl.End(),
194 End: decl.End(),
195 NewText: []byte(")"),
196 },
197 }...)
198 } else {
199
200 edits = append(edits, analysis.TextEdit{
201 Pos: init,
202 End: spec.End(),
203 })
204 }
205
206 } else {
207 continue
208 }
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236 var (
237 numLoopAssigns int
238 loopAssign *ast.AssignStmt
239 seenRvalueUse bool
240 )
241 for curUse := range index.Uses(v) {
242
243 ek, _ := curUse.ParentEdge()
244 for ek == edge.ParenExpr_X {
245 curUse = curUse.Parent()
246 ek, _ = curUse.ParentEdge()
247 }
248
249
250 if seenRvalueUse {
251 continue nextcand
252 }
253
254
255
256 intervening := func(types ...ast.Node) bool {
257 for cur := range curUse.Enclosing(types...) {
258 if v.Pos() <= cur.Node().Pos() {
259 return true
260 }
261 }
262 return false
263 }
264
265 if ek == edge.AssignStmt_Lhs {
266 assign := curUse.Parent().Node().(*ast.AssignStmt)
267 if assign.Tok != token.ADD_ASSIGN {
268 continue nextcand
269 }
270
271
272
273
274
275 if intervening((*ast.ForStmt)(nil), (*ast.RangeStmt)(nil)) {
276 numLoopAssigns++
277 if loopAssign == nil {
278 loopAssign = assign
279 }
280 }
281
282
283
284
285 edits = append(edits, []analysis.TextEdit{
286
287 {
288 Pos: assign.TokPos,
289 End: assign.Rhs[0].Pos(),
290 NewText: []byte(".WriteString("),
291 },
292
293 {
294 Pos: assign.End(),
295 End: assign.End(),
296 NewText: []byte(")"),
297 },
298 }...)
299
300 } else if ek == edge.UnaryExpr_X &&
301 curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
302
303 continue nextcand
304
305 } else {
306
307
308
309
310 seenRvalueUse = true
311
312 edits = append(edits, analysis.TextEdit{
313
314 Pos: curUse.Node().End(),
315 End: curUse.Node().End(),
316 NewText: []byte(".String()"),
317 })
318 }
319 }
320 if !seenRvalueUse {
321 continue nextcand
322 }
323 if numLoopAssigns == 0 {
324 continue nextcand
325 }
326
327 lastEditFile = file
328 lastEditEnd = edits[len(edits)-1].End
329
330 pass.Report(analysis.Diagnostic{
331 Pos: loopAssign.Pos(),
332 End: loopAssign.End(),
333 Message: "using string += string in a loop is inefficient",
334 SuggestedFixes: []analysis.SuggestedFix{{
335 Message: "Replace string += string with strings.Builder",
336 TextEdits: edits,
337 }},
338 })
339 }
340
341 return nil, nil
342 }
343
344
345 func isEmptyString(info *types.Info, e ast.Expr) bool {
346 tv, ok := info.Types[e]
347 return ok && tv.Value != nil && constant.StringVal(tv.Value) == ""
348 }
349
View as plain text