1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
|
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package stringintconv defines an Analyzer that flags type conversions
// from integers to strings.
package stringintconv
import (
"fmt"
"go/ast"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/typeparams"
)
const Doc = `check for string(int) conversions
This checker flags conversions of the form string(x) where x is an integer
(but not byte or rune) type. Such conversions are discouraged because they
return the UTF-8 representation of the Unicode code point x, and not a decimal
string representation of x as one might expect. Furthermore, if x denotes an
invalid code point, the conversion cannot be statically rejected.
For conversions that intend on using the code point, consider replacing them
with string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the
string representation of the value in the desired base.
`
var Analyzer = &analysis.Analyzer{
Name: "stringintconv",
Doc: Doc,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
// describe returns a string describing the type typ contained within the type
// set of inType. If non-empty, inName is used as the name of inType (this is
// necessary so that we can use alias type names that may not be reachable from
// inType itself).
func describe(typ, inType types.Type, inName string) string {
name := inName
if typ != inType {
name = typeName(typ)
}
if name == "" {
return ""
}
var parentheticals []string
if underName := typeName(typ.Underlying()); underName != "" && underName != name {
parentheticals = append(parentheticals, underName)
}
if typ != inType && inName != "" && inName != name {
parentheticals = append(parentheticals, "in "+inName)
}
if len(parentheticals) > 0 {
name += " (" + strings.Join(parentheticals, ", ") + ")"
}
return name
}
func typeName(typ types.Type) string {
if v, _ := typ.(interface{ Name() string }); v != nil {
return v.Name()
}
if v, _ := typ.(interface{ Obj() *types.TypeName }); v != nil {
return v.Obj().Name()
}
return ""
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.CallExpr)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
call := n.(*ast.CallExpr)
if len(call.Args) != 1 {
return
}
arg := call.Args[0]
// Retrieve target type name.
var tname *types.TypeName
switch fun := call.Fun.(type) {
case *ast.Ident:
tname, _ = pass.TypesInfo.Uses[fun].(*types.TypeName)
case *ast.SelectorExpr:
tname, _ = pass.TypesInfo.Uses[fun.Sel].(*types.TypeName)
}
if tname == nil {
return
}
// In the conversion T(v) of a value v of type V to a target type T, we
// look for types T0 in the type set of T and V0 in the type set of V, such
// that V0->T0 is a problematic conversion. If T and V are not type
// parameters, this amounts to just checking if V->T is a problematic
// conversion.
// First, find a type T0 in T that has an underlying type of string.
T := tname.Type()
ttypes, err := structuralTypes(T)
if err != nil {
return // invalid type
}
var T0 types.Type // string type in the type set of T
for _, tt := range ttypes {
u, _ := tt.Underlying().(*types.Basic)
if u != nil && u.Kind() == types.String {
T0 = tt
break
}
}
if T0 == nil {
// No target types have an underlying type of string.
return
}
// Next, find a type V0 in V that has an underlying integral type that is
// not byte or rune.
V := pass.TypesInfo.TypeOf(arg)
vtypes, err := structuralTypes(V)
if err != nil {
return // invalid type
}
var V0 types.Type // integral type in the type set of V
for _, vt := range vtypes {
u, _ := vt.Underlying().(*types.Basic)
if u != nil && u.Info()&types.IsInteger != 0 {
switch u.Kind() {
case types.Byte, types.Rune, types.UntypedRune:
continue
}
V0 = vt
break
}
}
if V0 == nil {
// No source types are non-byte or rune integer types.
return
}
convertibleToRune := true // if true, we can suggest a fix
for _, t := range vtypes {
if !types.ConvertibleTo(t, types.Typ[types.Rune]) {
convertibleToRune = false
break
}
}
target := describe(T0, T, tname.Name())
source := describe(V0, V, typeName(V))
if target == "" || source == "" {
return // something went wrong
}
diag := analysis.Diagnostic{
Pos: n.Pos(),
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),
}
if convertibleToRune {
diag.SuggestedFixes = []analysis.SuggestedFix{
{
Message: "Did you mean to convert a rune to a string?",
TextEdits: []analysis.TextEdit{
{
Pos: arg.Pos(),
End: arg.Pos(),
NewText: []byte("rune("),
},
{
Pos: arg.End(),
End: arg.End(),
NewText: []byte(")"),
},
},
},
}
}
pass.Report(diag)
})
return nil, nil
}
func structuralTypes(t types.Type) ([]types.Type, error) {
var structuralTypes []types.Type
switch t := t.(type) {
case *typeparams.TypeParam:
terms, err := typeparams.StructuralTerms(t)
if err != nil {
return nil, err
}
for _, term := range terms {
structuralTypes = append(structuralTypes, term.Type())
}
default:
structuralTypes = append(structuralTypes, t)
}
return structuralTypes, nil
}
|