Skip to content

Commit 4419f4f

Browse files
committed
go/analysis/passes/stringintconv: offer fix of fmt.Sprint(x)
Previously, the offered fix was string(x) -> string(rune(x)), which is not the most commonly desired change. Now, the analyzer offers both fixes, with informative descriptions. The Sprint fix must take care to preserve the type when it's not exactly string. Also, it may require adding an import of "fmt", using the new AddImport refactoring helper. The "did you mean?" hint in the diagnostic (added by CL 235797 for golang/go#39151) has been removed, since the descriptions of the offered fixes capture the same information, and there was a recent editorial decision made against such hints. Also: - split the tests into tests of diagnostics and tests of fixes, since the latter now need to use the clumsy golden-file-is-a-txtar mechanism. Reduce the regexp patterns to just the minimum. - clarify RunWithSuggestedFixes' explanation of txtar mechanism. - clarify SuggestedFix.Message and provide an example of the proper form. Googlers: see b/346560254 Change-Id: I493cf675a4c71d4ee40632fc9ad47a8071eafa76 Reviewed-on: https://go-review.googlesource.com/c/tools/+/592155 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Lasse Folger <[email protected]> Reviewed-by: Tim King <[email protected]>
1 parent a69d9a2 commit 4419f4f

File tree

12 files changed

+141
-121
lines changed

12 files changed

+141
-121
lines changed

go/analysis/analysistest/analysistest.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ type Testing interface {
114114
// into nonconflicting parts.
115115
//
116116
// Conflicts of the second kind can be avoided by giving the
117-
// alternative fixes different names (SuggestedFix.Message) and using
118-
// a multi-section .txtar file with a named section for each
119-
// alternative fix.
117+
// alternative fixes different names (SuggestedFix.Message) and
118+
// defining the .golden file as a multi-section txtar file with a
119+
// named section for each alternative fix, as shown above.
120120
//
121121
// Analyzers that compute fixes from a textual diff of the
122122
// before/after file contents (instead of directly from syntax tree

go/analysis/diagnostic.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ type RelatedInformation struct {
5858
//
5959
// The TextEdits must not overlap, nor contain edits for other packages.
6060
type SuggestedFix struct {
61-
// A description for this suggested fix to be shown to a user deciding
62-
// whether to accept it.
61+
// A verb phrase describing the fix, to be shown to
62+
// a user trying to decide whether to accept it.
63+
//
64+
// Example: "Remove the surplus argument"
6365
Message string
6466
TextEdits []TextEdit
6567
}

go/analysis/passes/stringintconv/string.go

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
1717
"golang.org/x/tools/go/ast/inspector"
1818
"golang.org/x/tools/internal/aliases"
19+
"golang.org/x/tools/internal/analysisinternal"
1920
"golang.org/x/tools/internal/typeparams"
2021
)
2122

@@ -73,9 +74,15 @@ func typeName(t types.Type) string {
7374
func run(pass *analysis.Pass) (interface{}, error) {
7475
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
7576
nodeFilter := []ast.Node{
77+
(*ast.File)(nil),
7678
(*ast.CallExpr)(nil),
7779
}
80+
var file *ast.File
7881
inspect.Preorder(nodeFilter, func(n ast.Node) {
82+
if n, ok := n.(*ast.File); ok {
83+
file = n
84+
return
85+
}
7986
call := n.(*ast.CallExpr)
8087

8188
if len(call.Args) != 1 {
@@ -167,27 +174,74 @@ func run(pass *analysis.Pass) (interface{}, error) {
167174

168175
diag := analysis.Diagnostic{
169176
Pos: n.Pos(),
170-
Message: fmt.Sprintf("conversion from %s to %s yields a string of one rune, not a string of digits (did you mean fmt.Sprint(x)?)", source, target),
177+
Message: fmt.Sprintf("conversion from %s to %s yields a string of one rune, not a string of digits", source, target),
178+
}
179+
addFix := func(message string, edits []analysis.TextEdit) {
180+
diag.SuggestedFixes = append(diag.SuggestedFixes, analysis.SuggestedFix{
181+
Message: message,
182+
TextEdits: edits,
183+
})
171184
}
172185

186+
// Fix 1: use fmt.Sprint(x)
187+
//
188+
// Prefer fmt.Sprint over strconv.Itoa, FormatInt,
189+
// or FormatUint, as it works for any type.
190+
// Add an import of "fmt" as needed.
191+
//
192+
// Unless the type is exactly string, we must retain the conversion.
193+
//
194+
// Do not offer this fix if type parameters are involved,
195+
// as there are too many combinations and subtleties.
196+
// Consider x = rune | int16 | []byte: in all cases,
197+
// string(x) is legal, but the appropriate diagnostic
198+
// and fix differs. Similarly, don't offer the fix if
199+
// the type has methods, as some {String,GoString,Format}
200+
// may change the behavior of fmt.Sprint.
201+
if len(ttypes) == 1 && len(vtypes) == 1 && types.NewMethodSet(V0).Len() == 0 {
202+
fmtName, importEdit := analysisinternal.AddImport(pass.TypesInfo, file, arg.Pos(), "fmt", "fmt")
203+
if types.Identical(T0, types.Typ[types.String]) {
204+
// string(x) -> fmt.Sprint(x)
205+
addFix("Format the number as a decimal", []analysis.TextEdit{
206+
importEdit,
207+
{
208+
Pos: call.Fun.Pos(),
209+
End: call.Fun.End(),
210+
NewText: []byte(fmtName + ".Sprint"),
211+
},
212+
})
213+
} else {
214+
// mystring(x) -> mystring(fmt.Sprint(x))
215+
addFix("Format the number as a decimal", []analysis.TextEdit{
216+
importEdit,
217+
{
218+
Pos: call.Lparen + 1,
219+
End: call.Lparen + 1,
220+
NewText: []byte(fmtName + ".Sprint("),
221+
},
222+
{
223+
Pos: call.Rparen,
224+
End: call.Rparen,
225+
NewText: []byte(")"),
226+
},
227+
})
228+
}
229+
}
230+
231+
// Fix 2: use string(rune(x))
173232
if convertibleToRune {
174-
diag.SuggestedFixes = []analysis.SuggestedFix{
233+
addFix("Convert a single rune to a string", []analysis.TextEdit{
175234
{
176-
Message: "Did you mean to convert a rune to a string?",
177-
TextEdits: []analysis.TextEdit{
178-
{
179-
Pos: arg.Pos(),
180-
End: arg.Pos(),
181-
NewText: []byte("rune("),
182-
},
183-
{
184-
Pos: arg.End(),
185-
End: arg.End(),
186-
NewText: []byte(")"),
187-
},
188-
},
235+
Pos: arg.Pos(),
236+
End: arg.Pos(),
237+
NewText: []byte("rune("),
189238
},
190-
}
239+
{
240+
Pos: arg.End(),
241+
End: arg.End(),
242+
NewText: []byte(")"),
243+
},
244+
})
191245
}
192246
pass.Report(diag)
193247
})

go/analysis/passes/stringintconv/string_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ import (
1313

1414
func Test(t *testing.T) {
1515
testdata := analysistest.TestData()
16-
analysistest.RunWithSuggestedFixes(t, testdata, stringintconv.Analyzer, "a", "typeparams")
16+
analysistest.Run(t, testdata, stringintconv.Analyzer, "a", "typeparams")
17+
analysistest.RunWithSuggestedFixes(t, testdata, stringintconv.Analyzer, "fix")
1718
}

go/analysis/passes/stringintconv/testdata/src/a/a.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ func StringTest() {
2525
o struct{ x int }
2626
)
2727
const p = 0
28-
_ = string(i) // want `^conversion from int to string yields a string of one rune, not a string of digits \(did you mean fmt\.Sprint\(x\)\?\)$`
28+
// First time only, assert the complete message:
29+
_ = string(i) // want `^conversion from int to string yields a string of one rune, not a string of digits$`
2930
_ = string(j)
3031
_ = string(k)
31-
_ = string(p) // want `^conversion from untyped int to string yields a string of one rune, not a string of digits \(did you mean fmt\.Sprint\(x\)\?\)$`
32-
_ = A(l) // want `^conversion from C \(int\) to A \(string\) yields a string of one rune, not a string of digits \(did you mean fmt\.Sprint\(x\)\?\)$`
33-
_ = B(m) // want `^conversion from (uintptr|D \(uintptr\)) to B \(string\) yields a string of one rune, not a string of digits \(did you mean fmt\.Sprint\(x\)\?\)$`
34-
_ = string(n[1]) // want `^conversion from int to string yields a string of one rune, not a string of digits \(did you mean fmt\.Sprint\(x\)\?\)$`
35-
_ = string(o.x) // want `^conversion from int to string yields a string of one rune, not a string of digits \(did you mean fmt\.Sprint\(x\)\?\)$`
32+
_ = string(p) // want `...from untyped int to string...`
33+
_ = A(l) // want `...from C \(int\) to A \(string\)...`
34+
_ = B(m) // want `...from (uintptr|D \(uintptr\)) to B \(string\)...`
35+
_ = string(n[1]) // want `...from int to string...`
36+
_ = string(o.x) // want `...from int to string...`
3637
}

go/analysis/passes/stringintconv/testdata/src/a/a.go.golden

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package fix
2+
3+
func _(x uint64) {
4+
println(string(x)) // want `conversion from uint64 to string yields...`
5+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Format the number as a decimal --
2+
package fix
3+
4+
import "fmt"
5+
6+
func _(x uint64) {
7+
println(fmt.Sprint(x)) // want `conversion from uint64 to string yields...`
8+
}
9+
10+
-- Convert a single rune to a string --
11+
package fix
12+
13+
func _(x uint64) {
14+
println(string(rune(x))) // want `conversion from uint64 to string yields...`
15+
}
16+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package fix
2+
3+
type mystring string
4+
5+
func _(x int16) mystring {
6+
return mystring(x) // want `conversion from int16 to mystring \(string\)...`
7+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- Format the number as a decimal --
2+
package fix
3+
4+
import "fmt"
5+
6+
type mystring string
7+
8+
func _(x int16) mystring {
9+
return mystring(fmt.Sprint(x)) // want `conversion from int16 to mystring \(string\)...`
10+
}
11+
12+
-- Convert a single rune to a string --
13+
package fix
14+
15+
type mystring string
16+
17+
func _(x int16) mystring {
18+
return mystring(rune(x)) // want `conversion from int16 to mystring \(string\)...`
19+
}

go/analysis/passes/stringintconv/testdata/src/typeparams/typeparams.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ func _[AllString ~string, MaybeString ~string | ~int, NotString ~int | byte, Nam
2222
)
2323
const p = 0
2424

25-
_ = MaybeString(i) // want `conversion from int to string .in MaybeString. yields a string of one rune, not a string of digits .did you mean fmt\.Sprint.x.\?.`
25+
_ = MaybeString(i) // want `conversion from int to string .in MaybeString. yields a string of one rune, not a string of digits`
2626
_ = MaybeString(r)
2727
_ = MaybeString(b)
28-
_ = MaybeString(I) // want `conversion from Int .int. to string .in MaybeString. yields a string of one rune, not a string of digits .did you mean fmt\.Sprint.x.\?.`
29-
_ = MaybeString(U) // want `conversion from uintptr to string .in MaybeString. yields a string of one rune, not a string of digits .did you mean fmt\.Sprint.x.\?.`
28+
_ = MaybeString(I) // want `conversion from Int .int. to string .in MaybeString. yields a string of one rune, not a string of digits`
29+
_ = MaybeString(U) // want `conversion from uintptr to string .in MaybeString. yields a string of one rune, not a string of digits`
3030
// Type parameters are never constant types, so arguments are always
3131
// converted to their default type (int versus untyped int, in this case)
32-
_ = MaybeString(p) // want `conversion from int to string .in MaybeString. yields a string of one rune, not a string of digits .did you mean fmt\.Sprint.x.\?.`
32+
_ = MaybeString(p) // want `conversion from int to string .in MaybeString. yields a string of one rune, not a string of digits`
3333
// ...even if the type parameter is only strings.
34-
_ = AllString(p) // want `conversion from int to string .in AllString. yields a string of one rune, not a string of digits .did you mean fmt\.Sprint.x.\?.`
34+
_ = AllString(p) // want `conversion from int to string .in AllString. yields a string of one rune, not a string of digits`
3535

3636
_ = NotString(i)
3737
_ = NotString(r)
@@ -40,10 +40,10 @@ func _[AllString ~string, MaybeString ~string | ~int, NotString ~int | byte, Nam
4040
_ = NotString(U)
4141
_ = NotString(p)
4242

43-
_ = NamedString(i) // want `conversion from int to String .string, in NamedString. yields a string of one rune, not a string of digits .did you mean fmt\.Sprint.x.\?.`
44-
_ = string(M) // want `conversion from int .in MaybeString. to string yields a string of one rune, not a string of digits .did you mean fmt\.Sprint.x.\?.`
43+
_ = NamedString(i) // want `conversion from int to String .string, in NamedString. yields a string of one rune, not a string of digits`
44+
_ = string(M) // want `conversion from int .in MaybeString. to string yields a string of one rune, not a string of digits`
4545

4646
// Note that M is not convertible to rune.
47-
_ = MaybeString(M) // want `conversion from int .in MaybeString. to string .in MaybeString. yields a string of one rune, not a string of digits .did you mean fmt\.Sprint.x.\?.`
47+
_ = MaybeString(M) // want `conversion from int .in MaybeString. to string .in MaybeString. yields a string of one rune, not a string of digits`
4848
_ = NotString(N) // ok
4949
}

go/analysis/passes/stringintconv/testdata/src/typeparams/typeparams.go.golden

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)