--- /dev/null
+// 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"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
+)
+
+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,
+}
+
+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)
+
+ // 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
+ }
+ target := tname.Name()
+
+ // Check that target type T in T(v) has an underlying type of string.
+ T, _ := tname.Type().Underlying().(*types.Basic)
+ if T == nil || T.Kind() != types.String {
+ return
+ }
+ if s := T.Name(); target != s {
+ target += " (" + s + ")"
+ }
+
+ // Check that type V of v has an underlying integral type that is not byte or rune.
+ if len(call.Args) != 1 {
+ return
+ }
+ v := call.Args[0]
+ vtyp := pass.TypesInfo.TypeOf(v)
+ V, _ := vtyp.Underlying().(*types.Basic)
+ if V == nil || V.Info()&types.IsInteger == 0 {
+ return
+ }
+ switch V.Kind() {
+ case types.Byte, types.Rune, types.UntypedRune:
+ return
+ }
+
+ // Retrieve source type name.
+ source := typeName(vtyp)
+ if source == "" {
+ return
+ }
+ if s := V.Name(); source != s {
+ source += " (" + s + ")"
+ }
+ 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),
+ SuggestedFixes: []analysis.SuggestedFix{
+ {
+ Message: "Did you mean to convert a rune to a string?",
+ TextEdits: []analysis.TextEdit{
+ {
+ Pos: v.Pos(),
+ End: v.Pos(),
+ NewText: []byte("rune("),
+ },
+ {
+ Pos: v.End(),
+ End: v.End(),
+ NewText: []byte(")"),
+ },
+ },
+ },
+ },
+ }
+ pass.Report(diag)
+ })
+ return nil, nil
+}