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/aliases"
19 "golang.org/x/tools/internal/analysisinternal"
20 "golang.org/x/tools/internal/typeparams"
21 )
22
23
24 var doc string
25
26 var Analyzer = &analysis.Analyzer{
27 Name: "stringintconv",
28 Doc: analysisutil.MustExtractDoc(doc, "stringintconv"),
29 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stringintconv",
30 Requires: []*analysis.Analyzer{inspect.Analyzer},
31 Run: run,
32 }
33
34
35
36
37
38 func describe(typ, inType types.Type, inName string) string {
39 name := inName
40 if typ != inType {
41 name = typeName(typ)
42 }
43 if name == "" {
44 return ""
45 }
46
47 var parentheticals []string
48 if underName := typeName(typ.Underlying()); underName != "" && underName != name {
49 parentheticals = append(parentheticals, underName)
50 }
51
52 if typ != inType && inName != "" && inName != name {
53 parentheticals = append(parentheticals, "in "+inName)
54 }
55
56 if len(parentheticals) > 0 {
57 name += " (" + strings.Join(parentheticals, ", ") + ")"
58 }
59
60 return name
61 }
62
63 func typeName(t types.Type) string {
64 type hasTypeName interface{ Obj() *types.TypeName }
65 switch t := t.(type) {
66 case *types.Basic:
67 return t.Name()
68 case hasTypeName:
69 return t.Obj().Name()
70 }
71 return ""
72 }
73
74 func run(pass *analysis.Pass) (interface{}, error) {
75 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
76 nodeFilter := []ast.Node{
77 (*ast.File)(nil),
78 (*ast.CallExpr)(nil),
79 }
80 var file *ast.File
81 inspect.Preorder(nodeFilter, func(n ast.Node) {
82 if n, ok := n.(*ast.File); ok {
83 file = n
84 return
85 }
86 call := n.(*ast.CallExpr)
87
88 if len(call.Args) != 1 {
89 return
90 }
91 arg := call.Args[0]
92
93
94 var tname *types.TypeName
95 switch fun := call.Fun.(type) {
96 case *ast.Ident:
97 tname, _ = pass.TypesInfo.Uses[fun].(*types.TypeName)
98 case *ast.SelectorExpr:
99 tname, _ = pass.TypesInfo.Uses[fun.Sel].(*types.TypeName)
100 }
101 if tname == nil {
102 return
103 }
104
105
106
107
108
109
110
111
112 T := tname.Type()
113 ttypes, err := structuralTypes(T)
114 if err != nil {
115 return
116 }
117
118 var T0 types.Type
119
120 for _, tt := range ttypes {
121 u, _ := tt.Underlying().(*types.Basic)
122 if u != nil && u.Kind() == types.String {
123 T0 = tt
124 break
125 }
126 }
127
128 if T0 == nil {
129
130 return
131 }
132
133
134
135 V := pass.TypesInfo.TypeOf(arg)
136 vtypes, err := structuralTypes(V)
137 if err != nil {
138 return
139 }
140
141 var V0 types.Type
142
143 for _, vt := range vtypes {
144 u, _ := vt.Underlying().(*types.Basic)
145 if u != nil && u.Info()&types.IsInteger != 0 {
146 switch u.Kind() {
147 case types.Byte, types.Rune, types.UntypedRune:
148 continue
149 }
150 V0 = vt
151 break
152 }
153 }
154
155 if V0 == nil {
156
157 return
158 }
159
160 convertibleToRune := true
161 for _, t := range vtypes {
162 if !types.ConvertibleTo(t, types.Typ[types.Rune]) {
163 convertibleToRune = false
164 break
165 }
166 }
167
168 target := describe(T0, T, tname.Name())
169 source := describe(V0, V, typeName(V))
170
171 if target == "" || source == "" {
172 return
173 }
174
175 diag := analysis.Diagnostic{
176 Pos: n.Pos(),
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 })
184 }
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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
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
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
232 if convertibleToRune {
233 addFix("Convert a single rune to a string", []analysis.TextEdit{
234 {
235 Pos: arg.Pos(),
236 End: arg.Pos(),
237 NewText: []byte("rune("),
238 },
239 {
240 Pos: arg.End(),
241 End: arg.End(),
242 NewText: []byte(")"),
243 },
244 })
245 }
246 pass.Report(diag)
247 })
248 return nil, nil
249 }
250
251 func structuralTypes(t types.Type) ([]types.Type, error) {
252 var structuralTypes []types.Type
253 if tp, ok := aliases.Unalias(t).(*types.TypeParam); ok {
254 terms, err := typeparams.StructuralTerms(tp)
255 if err != nil {
256 return nil, err
257 }
258 for _, term := range terms {
259 structuralTypes = append(structuralTypes, term.Type())
260 }
261 } else {
262 structuralTypes = append(structuralTypes, t)
263 }
264 return structuralTypes, nil
265 }
266
View as plain text