1
2
3
4
5 package stringintconv
6
7 import (
8 _ "embed"
9 "fmt"
10 "go/ast"
11 "go/types"
12 "strings"
13
14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/go/analysis/passes/inspect"
16 "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
17 "golang.org/x/tools/go/ast/inspector"
18 "golang.org/x/tools/internal/analysisinternal"
19 "golang.org/x/tools/internal/typeparams"
20 )
21
22
23 var doc string
24
25 var Analyzer = &analysis.Analyzer{
26 Name: "stringintconv",
27 Doc: analysisutil.MustExtractDoc(doc, "stringintconv"),
28 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stringintconv",
29 Requires: []*analysis.Analyzer{inspect.Analyzer},
30 Run: run,
31 }
32
33
34
35
36
37 func describe(typ, inType types.Type, inName string) string {
38 name := inName
39 if typ != inType {
40 name = typeName(typ)
41 }
42 if name == "" {
43 return ""
44 }
45
46 var parentheticals []string
47 if underName := typeName(typ.Underlying()); underName != "" && underName != name {
48 parentheticals = append(parentheticals, underName)
49 }
50
51 if typ != inType && inName != "" && inName != name {
52 parentheticals = append(parentheticals, "in "+inName)
53 }
54
55 if len(parentheticals) > 0 {
56 name += " (" + strings.Join(parentheticals, ", ") + ")"
57 }
58
59 return name
60 }
61
62 func typeName(t types.Type) string {
63 type hasTypeName interface{ Obj() *types.TypeName }
64 switch t := t.(type) {
65 case *types.Basic:
66 return t.Name()
67 case hasTypeName:
68 return t.Obj().Name()
69 }
70 return ""
71 }
72
73 func run(pass *analysis.Pass) (interface{}, error) {
74 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
75 nodeFilter := []ast.Node{
76 (*ast.File)(nil),
77 (*ast.CallExpr)(nil),
78 }
79 var file *ast.File
80 inspect.Preorder(nodeFilter, func(n ast.Node) {
81 if n, ok := n.(*ast.File); ok {
82 file = n
83 return
84 }
85 call := n.(*ast.CallExpr)
86
87 if len(call.Args) != 1 {
88 return
89 }
90 arg := call.Args[0]
91
92
93 var tname *types.TypeName
94 switch fun := call.Fun.(type) {
95 case *ast.Ident:
96 tname, _ = pass.TypesInfo.Uses[fun].(*types.TypeName)
97 case *ast.SelectorExpr:
98 tname, _ = pass.TypesInfo.Uses[fun.Sel].(*types.TypeName)
99 }
100 if tname == nil {
101 return
102 }
103
104
105
106
107
108
109
110
111 T := tname.Type()
112 ttypes, err := structuralTypes(T)
113 if err != nil {
114 return
115 }
116
117 var T0 types.Type
118
119 for _, tt := range ttypes {
120 u, _ := tt.Underlying().(*types.Basic)
121 if u != nil && u.Kind() == types.String {
122 T0 = tt
123 break
124 }
125 }
126
127 if T0 == nil {
128
129 return
130 }
131
132
133
134 V := pass.TypesInfo.TypeOf(arg)
135 vtypes, err := structuralTypes(V)
136 if err != nil {
137 return
138 }
139
140 var V0 types.Type
141
142 for _, vt := range vtypes {
143 u, _ := vt.Underlying().(*types.Basic)
144 if u != nil && u.Info()&types.IsInteger != 0 {
145 switch u.Kind() {
146 case types.Byte, types.Rune, types.UntypedRune:
147 continue
148 }
149 V0 = vt
150 break
151 }
152 }
153
154 if V0 == nil {
155
156 return
157 }
158
159 convertibleToRune := true
160 for _, t := range vtypes {
161 if !types.ConvertibleTo(t, types.Typ[types.Rune]) {
162 convertibleToRune = false
163 break
164 }
165 }
166
167 target := describe(T0, T, tname.Name())
168 source := describe(V0, V, typeName(V))
169
170 if target == "" || source == "" {
171 return
172 }
173
174 diag := analysis.Diagnostic{
175 Pos: n.Pos(),
176 Message: fmt.Sprintf("conversion from %s to %s yields a string of one rune, not a string of digits", source, target),
177 }
178 addFix := func(message string, edits []analysis.TextEdit) {
179 diag.SuggestedFixes = append(diag.SuggestedFixes, analysis.SuggestedFix{
180 Message: message,
181 TextEdits: edits,
182 })
183 }
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200 if len(ttypes) == 1 && len(vtypes) == 1 && types.NewMethodSet(V0).Len() == 0 {
201 fmtName, importEdits := analysisinternal.AddImport(pass.TypesInfo, file, arg.Pos(), "fmt", "fmt")
202 if types.Identical(T0, types.Typ[types.String]) {
203
204 addFix("Format the number as a decimal", append(importEdits,
205 analysis.TextEdit{
206 Pos: call.Fun.Pos(),
207 End: call.Fun.End(),
208 NewText: []byte(fmtName + ".Sprint"),
209 }),
210 )
211 } else {
212
213 addFix("Format the number as a decimal", append(importEdits,
214 analysis.TextEdit{
215 Pos: call.Lparen + 1,
216 End: call.Lparen + 1,
217 NewText: []byte(fmtName + ".Sprint("),
218 },
219 analysis.TextEdit{
220 Pos: call.Rparen,
221 End: call.Rparen,
222 NewText: []byte(")"),
223 }),
224 )
225 }
226 }
227
228
229 if convertibleToRune {
230 addFix("Convert a single rune to a string", []analysis.TextEdit{
231 {
232 Pos: arg.Pos(),
233 End: arg.Pos(),
234 NewText: []byte("rune("),
235 },
236 {
237 Pos: arg.End(),
238 End: arg.End(),
239 NewText: []byte(")"),
240 },
241 })
242 }
243 pass.Report(diag)
244 })
245 return nil, nil
246 }
247
248 func structuralTypes(t types.Type) ([]types.Type, error) {
249 var structuralTypes []types.Type
250 if tp, ok := types.Unalias(t).(*types.TypeParam); ok {
251 terms, err := typeparams.StructuralTerms(tp)
252 if err != nil {
253 return nil, err
254 }
255 for _, term := range terms {
256 structuralTypes = append(structuralTypes, term.Type())
257 }
258 } else {
259 structuralTypes = append(structuralTypes, t)
260 }
261 return structuralTypes, nil
262 }
263
View as plain text