1
2
3
4
5
6
7 package stringintconv
8
9 import (
10 "fmt"
11 "go/ast"
12 "go/types"
13 "strings"
14
15 "golang.org/x/tools/go/analysis"
16 "golang.org/x/tools/go/analysis/passes/inspect"
17 "golang.org/x/tools/go/ast/inspector"
18 "golang.org/x/tools/internal/typeparams"
19 )
20
21 const Doc = `check for string(int) conversions
22
23 This checker flags conversions of the form string(x) where x is an integer
24 (but not byte or rune) type. Such conversions are discouraged because they
25 return the UTF-8 representation of the Unicode code point x, and not a decimal
26 string representation of x as one might expect. Furthermore, if x denotes an
27 invalid code point, the conversion cannot be statically rejected.
28
29 For conversions that intend on using the code point, consider replacing them
30 with string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the
31 string representation of the value in the desired base.
32 `
33
34 var Analyzer = &analysis.Analyzer{
35 Name: "stringintconv",
36 Doc: Doc,
37 Requires: []*analysis.Analyzer{inspect.Analyzer},
38 Run: run,
39 }
40
41
42
43
44
45 func describe(typ, inType types.Type, inName string) string {
46 name := inName
47 if typ != inType {
48 name = typeName(typ)
49 }
50 if name == "" {
51 return ""
52 }
53
54 var parentheticals []string
55 if underName := typeName(typ.Underlying()); underName != "" && underName != name {
56 parentheticals = append(parentheticals, underName)
57 }
58
59 if typ != inType && inName != "" && inName != name {
60 parentheticals = append(parentheticals, "in "+inName)
61 }
62
63 if len(parentheticals) > 0 {
64 name += " (" + strings.Join(parentheticals, ", ") + ")"
65 }
66
67 return name
68 }
69
70 func typeName(typ types.Type) string {
71 if v, _ := typ.(interface{ Name() string }); v != nil {
72 return v.Name()
73 }
74 if v, _ := typ.(interface{ Obj() *types.TypeName }); v != nil {
75 return v.Obj().Name()
76 }
77 return ""
78 }
79
80 func run(pass *analysis.Pass) (interface{}, error) {
81 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
82 nodeFilter := []ast.Node{
83 (*ast.CallExpr)(nil),
84 }
85 inspect.Preorder(nodeFilter, func(n ast.Node) {
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 (did you mean fmt.Sprint(x)?)", source, target),
178 }
179
180 if convertibleToRune {
181 diag.SuggestedFixes = []analysis.SuggestedFix{
182 {
183 Message: "Did you mean to convert a rune to a string?",
184 TextEdits: []analysis.TextEdit{
185 {
186 Pos: arg.Pos(),
187 End: arg.Pos(),
188 NewText: []byte("rune("),
189 },
190 {
191 Pos: arg.End(),
192 End: arg.End(),
193 NewText: []byte(")"),
194 },
195 },
196 },
197 }
198 }
199 pass.Report(diag)
200 })
201 return nil, nil
202 }
203
204 func structuralTypes(t types.Type) ([]types.Type, error) {
205 var structuralTypes []types.Type
206 switch t := t.(type) {
207 case *typeparams.TypeParam:
208 terms, err := typeparams.StructuralTerms(t)
209 if err != nil {
210 return nil, err
211 }
212 for _, term := range terms {
213 structuralTypes = append(structuralTypes, term.Type())
214 }
215 default:
216 structuralTypes = append(structuralTypes, t)
217 }
218 return structuralTypes, nil
219 }
220
View as plain text