Skip to content

Commit 7e46809

Browse files
noamcohen97adonovan
authored andcommitted
go/analysis/passes/modernize: stringsbuilder: allow multiple rvalue uses
The stringsbuilder analyzer previously rejected any candidate variable that had more than one rvalue use of the finished string. Multiple rvalue uses are valid and each gets .String() appended. Fixes golang/go#77659 Change-Id: Ice14b56d639809c789ffee10744244d4eb27fd2f Reviewed-on: https://go-review.googlesource.com/c/tools/+/746540 Commit-Queue: Alan Donovan <adonovan@google.com> Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Madeline Kalil <mkalil@google.com> Auto-Submit: Alan Donovan <adonovan@google.com>
1 parent 55840e9 commit 7e46809

File tree

6 files changed

+64
-16
lines changed

6 files changed

+64
-16
lines changed

go/analysis/passes/modernize/doc.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,14 +460,15 @@ is replaced by:
460460
461461
This avoids quadratic memory allocation and improves performance.
462462
463-
The analyzer requires that all references to s except the final one
463+
The analyzer requires that all references to s before the final uses
464464
are += operations. To avoid warning about trivial cases, at least one
465465
must appear within a loop. The variable s must be a local
466466
variable, not a global or parameter.
467467
468-
The sole use of the finished string must be the last reference to the
469-
variable s. (It may appear within an intervening loop or function literal,
470-
since even s.String() is called repeatedly, it does not allocate memory.)
468+
All uses of the finished string must come after the last += operation.
469+
Each such use will be replaced by a call to strings.Builder's String method.
470+
(These may appear within an intervening loop or function literal, since even
471+
if s.String() is called repeatedly, it does not allocate memory.)
471472
472473
Often the addend is a call to fmt.Sprintf, as in this example:
473474

go/analysis/passes/modernize/stringsbuilder.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ nextcand:
254254
// var s string
255255
// for ... { s += expr }
256256
//
257-
// - The final use of s must be as an rvalue (e.g. use(s), not &s).
258-
// This will become s.String().
257+
// - All uses of s after the last += must be rvalue uses (e.g. use(s), not &s).
258+
// Each of these will become s.String().
259259
//
260260
// Perhaps surprisingly, it is fine for there to be an
261261
// intervening loop or lambda w.r.t. the declaration of s:
@@ -270,7 +270,7 @@ nextcand:
270270
var (
271271
numLoopAssigns int // number of += assignments within a loop
272272
loopAssign *ast.AssignStmt // first += assignment within a loop
273-
seenRvalueUse bool // => we've seen the sole final use of s as an rvalue
273+
seenRvalueUse bool // => we've seen at least one rvalue use of s
274274
)
275275
for curUse := range index.Uses(v) {
276276
// Strip enclosing parens around Ident.
@@ -280,11 +280,6 @@ nextcand:
280280
ek = curUse.ParentEdgeKind()
281281
}
282282

283-
// The rvalueUse must be the lexically last use.
284-
if seenRvalueUse {
285-
continue nextcand
286-
}
287-
288283
// intervening reports whether cur has an ancestor of
289284
// one of the given types that is within the scope of v.
290285
intervening := func(types ...ast.Node) bool {
@@ -297,6 +292,11 @@ nextcand:
297292
}
298293

299294
if ek == edge.AssignStmt_Lhs {
295+
// After an rvalue use, no more assignments are allowed.
296+
if seenRvalueUse {
297+
continue nextcand
298+
}
299+
300300
assign := curUse.Parent().Node().(*ast.AssignStmt)
301301
if assign.Tok != token.ADD_ASSIGN {
302302
continue nextcand

go/analysis/passes/modernize/testdata/src/stringsbuilder/stringsbuilder.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,26 @@ func nopeStringIsNotLastValueSpecInVarDecl() {
153153
}
154154
println(str, after)
155155
}
156+
157+
// Regression test for go.dev/issue/77659.
158+
// Multiple rvalue uses of the accumulated string should all be converted.
159+
func _() {
160+
acc := "s1"
161+
for _, s := range []string{"foo", "bar"} {
162+
acc += s // want "using string \\+= string in a loop is inefficient"
163+
}
164+
_ = acc
165+
_ = acc + "footer2"
166+
}
167+
168+
// Regression test for go.dev/issue/77659 (negative case).
169+
// += after an rvalue use should still be rejected.
170+
func _() {
171+
var s string
172+
for _, x := range []string{"a", "b"} {
173+
s += x
174+
}
175+
print(s)
176+
s += "extra" // nope: += after rvalue use
177+
print(s)
178+
}

go/analysis/passes/modernize/testdata/src/stringsbuilder/stringsbuilder.go.golden

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,27 @@ func nopeStringIsNotLastValueSpecInVarDecl() {
158158
}
159159
println(str, after)
160160
}
161+
162+
// Regression test for go.dev/issue/77659.
163+
// Multiple rvalue uses of the accumulated string should all be converted.
164+
func _() {
165+
var acc strings.Builder
166+
acc.WriteString("s1")
167+
for _, s := range []string{"foo", "bar"} {
168+
acc.WriteString(s) // want "using string \\+= string in a loop is inefficient"
169+
}
170+
_ = acc.String()
171+
_ = acc.String() + "footer2"
172+
}
173+
174+
// Regression test for go.dev/issue/77659 (negative case).
175+
// += after an rvalue use should still be rejected.
176+
func _() {
177+
var s string
178+
for _, x := range []string{"a", "b"} {
179+
s += x
180+
}
181+
print(s)
182+
s += "extra" // nope: += after rvalue use
183+
print(s)
184+
}

gopls/doc/analyzers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4037,9 +4037,9 @@ is replaced by:
40374037

40384038
This avoids quadratic memory allocation and improves performance.
40394039

4040-
The analyzer requires that all references to s except the final one are += operations. To avoid warning about trivial cases, at least one must appear within a loop. The variable s must be a local variable, not a global or parameter.
4040+
The analyzer requires that all references to s before the final uses are += operations. To avoid warning about trivial cases, at least one must appear within a loop. The variable s must be a local variable, not a global or parameter.
40414041

4042-
The sole use of the finished string must be the last reference to the variable s. (It may appear within an intervening loop or function literal, since even s.String() is called repeatedly, it does not allocate memory.)
4042+
All uses of the finished string must come after the last += operation. Each such use will be replaced by a call to strings.Builder's String method. (These may appear within an intervening loop or function literal, since even if s.String() is called repeatedly, it does not allocate memory.)
40434043

40444044
Often the addend is a call to fmt.Sprintf, as in this example:
40454045

gopls/internal/doc/api.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,7 +1702,7 @@
17021702
},
17031703
{
17041704
"Name": "\"stringsbuilder\"",
1705-
"Doc": "replace += with strings.Builder\n\nThis analyzer replaces repeated string += string concatenation\noperations with calls to Go 1.10's strings.Builder.\n\nFor example:\n\n\tvar s = \"[\"\n\tfor x := range seq {\n\t\ts += x\n\t\ts += \".\"\n\t}\n\ts += \"]\"\n\tuse(s)\n\nis replaced by:\n\n\tvar s strings.Builder\n\ts.WriteString(\"[\")\n\tfor x := range seq {\n\t\ts.WriteString(x)\n\t\ts.WriteString(\".\")\n\t}\n\ts.WriteString(\"]\")\n\tuse(s.String())\n\nThis avoids quadratic memory allocation and improves performance.\n\nThe analyzer requires that all references to s except the final one\nare += operations. To avoid warning about trivial cases, at least one\nmust appear within a loop. The variable s must be a local\nvariable, not a global or parameter.\n\nThe sole use of the finished string must be the last reference to the\nvariable s. (It may appear within an intervening loop or function literal,\nsince even s.String() is called repeatedly, it does not allocate memory.)\n\nOften the addend is a call to fmt.Sprintf, as in this example:\n\n\tvar s string\n\tfor x := range seq {\n\t\ts += fmt.Sprintf(\"%v\", x)\n\t}\n\nwhich, once the suggested fix is applied, becomes:\n\n\tvar s strings.Builder\n\tfor x := range seq {\n\t\ts.WriteString(fmt.Sprintf(\"%v\", x))\n\t}\n\nThe WriteString call can be further simplified to the more efficient\nfmt.Fprintf(\u0026s, \"%v\", x), avoiding the allocation of an intermediary.\nHowever, stringsbuilder does not perform this simplification;\nit requires staticcheck analyzer QF1012. (See https://go.dev/issue/76918.)",
1705+
"Doc": "replace += with strings.Builder\n\nThis analyzer replaces repeated string += string concatenation\noperations with calls to Go 1.10's strings.Builder.\n\nFor example:\n\n\tvar s = \"[\"\n\tfor x := range seq {\n\t\ts += x\n\t\ts += \".\"\n\t}\n\ts += \"]\"\n\tuse(s)\n\nis replaced by:\n\n\tvar s strings.Builder\n\ts.WriteString(\"[\")\n\tfor x := range seq {\n\t\ts.WriteString(x)\n\t\ts.WriteString(\".\")\n\t}\n\ts.WriteString(\"]\")\n\tuse(s.String())\n\nThis avoids quadratic memory allocation and improves performance.\n\nThe analyzer requires that all references to s before the final uses\nare += operations. To avoid warning about trivial cases, at least one\nmust appear within a loop. The variable s must be a local\nvariable, not a global or parameter.\n\nAll uses of the finished string must come after the last += operation.\nEach such use will be replaced by a call to strings.Builder's String method.\n(These may appear within an intervening loop or function literal, since even\nif s.String() is called repeatedly, it does not allocate memory.)\n\nOften the addend is a call to fmt.Sprintf, as in this example:\n\n\tvar s string\n\tfor x := range seq {\n\t\ts += fmt.Sprintf(\"%v\", x)\n\t}\n\nwhich, once the suggested fix is applied, becomes:\n\n\tvar s strings.Builder\n\tfor x := range seq {\n\t\ts.WriteString(fmt.Sprintf(\"%v\", x))\n\t}\n\nThe WriteString call can be further simplified to the more efficient\nfmt.Fprintf(\u0026s, \"%v\", x), avoiding the allocation of an intermediary.\nHowever, stringsbuilder does not perform this simplification;\nit requires staticcheck analyzer QF1012. (See https://go.dev/issue/76918.)",
17061706
"Default": "true",
17071707
"Status": ""
17081708
},
@@ -3625,7 +3625,7 @@
36253625
},
36263626
{
36273627
"Name": "stringsbuilder",
3628-
"Doc": "replace += with strings.Builder\n\nThis analyzer replaces repeated string += string concatenation\noperations with calls to Go 1.10's strings.Builder.\n\nFor example:\n\n\tvar s = \"[\"\n\tfor x := range seq {\n\t\ts += x\n\t\ts += \".\"\n\t}\n\ts += \"]\"\n\tuse(s)\n\nis replaced by:\n\n\tvar s strings.Builder\n\ts.WriteString(\"[\")\n\tfor x := range seq {\n\t\ts.WriteString(x)\n\t\ts.WriteString(\".\")\n\t}\n\ts.WriteString(\"]\")\n\tuse(s.String())\n\nThis avoids quadratic memory allocation and improves performance.\n\nThe analyzer requires that all references to s except the final one\nare += operations. To avoid warning about trivial cases, at least one\nmust appear within a loop. The variable s must be a local\nvariable, not a global or parameter.\n\nThe sole use of the finished string must be the last reference to the\nvariable s. (It may appear within an intervening loop or function literal,\nsince even s.String() is called repeatedly, it does not allocate memory.)\n\nOften the addend is a call to fmt.Sprintf, as in this example:\n\n\tvar s string\n\tfor x := range seq {\n\t\ts += fmt.Sprintf(\"%v\", x)\n\t}\n\nwhich, once the suggested fix is applied, becomes:\n\n\tvar s strings.Builder\n\tfor x := range seq {\n\t\ts.WriteString(fmt.Sprintf(\"%v\", x))\n\t}\n\nThe WriteString call can be further simplified to the more efficient\nfmt.Fprintf(\u0026s, \"%v\", x), avoiding the allocation of an intermediary.\nHowever, stringsbuilder does not perform this simplification;\nit requires staticcheck analyzer QF1012. (See https://go.dev/issue/76918.)",
3628+
"Doc": "replace += with strings.Builder\n\nThis analyzer replaces repeated string += string concatenation\noperations with calls to Go 1.10's strings.Builder.\n\nFor example:\n\n\tvar s = \"[\"\n\tfor x := range seq {\n\t\ts += x\n\t\ts += \".\"\n\t}\n\ts += \"]\"\n\tuse(s)\n\nis replaced by:\n\n\tvar s strings.Builder\n\ts.WriteString(\"[\")\n\tfor x := range seq {\n\t\ts.WriteString(x)\n\t\ts.WriteString(\".\")\n\t}\n\ts.WriteString(\"]\")\n\tuse(s.String())\n\nThis avoids quadratic memory allocation and improves performance.\n\nThe analyzer requires that all references to s before the final uses\nare += operations. To avoid warning about trivial cases, at least one\nmust appear within a loop. The variable s must be a local\nvariable, not a global or parameter.\n\nAll uses of the finished string must come after the last += operation.\nEach such use will be replaced by a call to strings.Builder's String method.\n(These may appear within an intervening loop or function literal, since even\nif s.String() is called repeatedly, it does not allocate memory.)\n\nOften the addend is a call to fmt.Sprintf, as in this example:\n\n\tvar s string\n\tfor x := range seq {\n\t\ts += fmt.Sprintf(\"%v\", x)\n\t}\n\nwhich, once the suggested fix is applied, becomes:\n\n\tvar s strings.Builder\n\tfor x := range seq {\n\t\ts.WriteString(fmt.Sprintf(\"%v\", x))\n\t}\n\nThe WriteString call can be further simplified to the more efficient\nfmt.Fprintf(\u0026s, \"%v\", x), avoiding the allocation of an intermediary.\nHowever, stringsbuilder does not perform this simplification;\nit requires staticcheck analyzer QF1012. (See https://go.dev/issue/76918.)",
36293629
"URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringbuilder",
36303630
"Default": true
36313631
},

0 commit comments

Comments
 (0)