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

     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
     6  
     7  import (
     8  	_ "embed"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/types"
    12  	"strings"
    13  
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/go/analysis/passes/inspect"
    16  	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
    17  	"golang.org/x/tools/go/ast/inspector"
    18  	"golang.org/x/tools/internal/aliases"
    19  	"golang.org/x/tools/internal/analysisinternal"
    20  	"golang.org/x/tools/internal/typeparams"
    21  )
    22  
    23  //go:embed doc.go
    24  var doc string
    25  
    26  var Analyzer = &analysis.Analyzer{
    27  	Name:     "stringintconv",
    28  	Doc:      analysisutil.MustExtractDoc(doc, "stringintconv"),
    29  	URL:      "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stringintconv",
    30  	Requires: []*analysis.Analyzer{inspect.Analyzer},
    31  	Run:      run,
    32  }
    33  
    34  // describe returns a string describing the type typ contained within the type
    35  // set of inType. If non-empty, inName is used as the name of inType (this is
    36  // necessary so that we can use alias type names that may not be reachable from
    37  // inType itself).
    38  func describe(typ, inType types.Type, inName string) string {
    39  	name := inName
    40  	if typ != inType {
    41  		name = typeName(typ)
    42  	}
    43  	if name == "" {
    44  		return ""
    45  	}
    46  
    47  	var parentheticals []string
    48  	if underName := typeName(typ.Underlying()); underName != "" && underName != name {
    49  		parentheticals = append(parentheticals, underName)
    50  	}
    51  
    52  	if typ != inType && inName != "" && inName != name {
    53  		parentheticals = append(parentheticals, "in "+inName)
    54  	}
    55  
    56  	if len(parentheticals) > 0 {
    57  		name += " (" + strings.Join(parentheticals, ", ") + ")"
    58  	}
    59  
    60  	return name
    61  }
    62  
    63  func typeName(t types.Type) string {
    64  	type hasTypeName interface{ Obj() *types.TypeName } // Alias, Named, TypeParam
    65  	switch t := t.(type) {
    66  	case *types.Basic:
    67  		return t.Name()
    68  	case hasTypeName:
    69  		return t.Obj().Name()
    70  	}
    71  	return ""
    72  }
    73  
    74  func run(pass *analysis.Pass) (interface{}, error) {
    75  	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    76  	nodeFilter := []ast.Node{
    77  		(*ast.File)(nil),
    78  		(*ast.CallExpr)(nil),
    79  	}
    80  	var file *ast.File
    81  	inspect.Preorder(nodeFilter, func(n ast.Node) {
    82  		if n, ok := n.(*ast.File); ok {
    83  			file = n
    84  			return
    85  		}
    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", source, target),
   178  		}
   179  		addFix := func(message string, edits []analysis.TextEdit) {
   180  			diag.SuggestedFixes = append(diag.SuggestedFixes, analysis.SuggestedFix{
   181  				Message:   message,
   182  				TextEdits: edits,
   183  			})
   184  		}
   185  
   186  		// Fix 1: use fmt.Sprint(x)
   187  		//
   188  		// Prefer fmt.Sprint over strconv.Itoa, FormatInt,
   189  		// or FormatUint, as it works for any type.
   190  		// Add an import of "fmt" as needed.
   191  		//
   192  		// Unless the type is exactly string, we must retain the conversion.
   193  		//
   194  		// Do not offer this fix if type parameters are involved,
   195  		// as there are too many combinations and subtleties.
   196  		// Consider x = rune | int16 | []byte: in all cases,
   197  		// string(x) is legal, but the appropriate diagnostic
   198  		// and fix differs. Similarly, don't offer the fix if
   199  		// the type has methods, as some {String,GoString,Format}
   200  		// may change the behavior of fmt.Sprint.
   201  		if len(ttypes) == 1 && len(vtypes) == 1 && types.NewMethodSet(V0).Len() == 0 {
   202  			fmtName, importEdit := analysisinternal.AddImport(pass.TypesInfo, file, arg.Pos(), "fmt", "fmt")
   203  			if types.Identical(T0, types.Typ[types.String]) {
   204  				// string(x) -> fmt.Sprint(x)
   205  				addFix("Format the number as a decimal", []analysis.TextEdit{
   206  					importEdit,
   207  					{
   208  						Pos:     call.Fun.Pos(),
   209  						End:     call.Fun.End(),
   210  						NewText: []byte(fmtName + ".Sprint"),
   211  					},
   212  				})
   213  			} else {
   214  				// mystring(x) -> mystring(fmt.Sprint(x))
   215  				addFix("Format the number as a decimal", []analysis.TextEdit{
   216  					importEdit,
   217  					{
   218  						Pos:     call.Lparen + 1,
   219  						End:     call.Lparen + 1,
   220  						NewText: []byte(fmtName + ".Sprint("),
   221  					},
   222  					{
   223  						Pos:     call.Rparen,
   224  						End:     call.Rparen,
   225  						NewText: []byte(")"),
   226  					},
   227  				})
   228  			}
   229  		}
   230  
   231  		// Fix 2: use string(rune(x))
   232  		if convertibleToRune {
   233  			addFix("Convert a single rune to a string", []analysis.TextEdit{
   234  				{
   235  					Pos:     arg.Pos(),
   236  					End:     arg.Pos(),
   237  					NewText: []byte("rune("),
   238  				},
   239  				{
   240  					Pos:     arg.End(),
   241  					End:     arg.End(),
   242  					NewText: []byte(")"),
   243  				},
   244  			})
   245  		}
   246  		pass.Report(diag)
   247  	})
   248  	return nil, nil
   249  }
   250  
   251  func structuralTypes(t types.Type) ([]types.Type, error) {
   252  	var structuralTypes []types.Type
   253  	if tp, ok := aliases.Unalias(t).(*types.TypeParam); ok {
   254  		terms, err := typeparams.StructuralTerms(tp)
   255  		if err != nil {
   256  			return nil, err
   257  		}
   258  		for _, term := range terms {
   259  			structuralTypes = append(structuralTypes, term.Type())
   260  		}
   261  	} else {
   262  		structuralTypes = append(structuralTypes, t)
   263  	}
   264  	return structuralTypes, nil
   265  }
   266  

View as plain text