1 | // Copyright 2020 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | // Package stringintconv defines an Analyzer that flags type conversions |
6 | // from integers to strings. |
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 | // describe returns a string describing the type typ contained within the type |
42 | // set of inType. If non-empty, inName is used as the name of inType (this is |
43 | // necessary so that we can use alias type names that may not be reachable from |
44 | // inType itself). |
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 | // Retrieve target type name. |
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 | // In the conversion T(v) of a value v of type V to a target type T, we |
106 | // look for types T0 in the type set of T and V0 in the type set of V, such |
107 | // that V0->T0 is a problematic conversion. If T and V are not type |
108 | // parameters, this amounts to just checking if V->T is a problematic |
109 | // conversion. |
110 | |
111 | // First, find a type T0 in T that has an underlying type of string. |
112 | T := tname.Type() |
113 | ttypes, err := structuralTypes(T) |
114 | if err != nil { |
115 | return // invalid type |
116 | } |
117 | |
118 | var T0 types.Type // string type in the type set of T |
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 | // No target types have an underlying type of string. |
130 | return |
131 | } |
132 | |
133 | // Next, find a type V0 in V that has an underlying integral type that is |
134 | // not byte or rune. |
135 | V := pass.TypesInfo.TypeOf(arg) |
136 | vtypes, err := structuralTypes(V) |
137 | if err != nil { |
138 | return // invalid type |
139 | } |
140 | |
141 | var V0 types.Type // integral type in the type set of V |
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 | // No source types are non-byte or rune integer types. |
157 | return |
158 | } |
159 | |
160 | convertibleToRune := true // if true, we can suggest a fix |
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 // something went wrong |
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 |
Members