...

Source file src/golang.org/x/tools/go/analysis/passes/stringintconv/string.go

Documentation: golang.org/x/tools/go/analysis/passes/stringintconv

     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  

View as plain text