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