s1025.go raw

   1  package s1025
   2  
   3  import (
   4  	"go/ast"
   5  	"go/types"
   6  
   7  	"honnef.co/go/tools/analysis/code"
   8  	"honnef.co/go/tools/analysis/edit"
   9  	"honnef.co/go/tools/analysis/facts/generated"
  10  	"honnef.co/go/tools/analysis/lint"
  11  	"honnef.co/go/tools/analysis/report"
  12  	"honnef.co/go/tools/go/types/typeutil"
  13  	"honnef.co/go/tools/internal/passes/buildir"
  14  	"honnef.co/go/tools/knowledge"
  15  	"honnef.co/go/tools/pattern"
  16  
  17  	"golang.org/x/exp/typeparams"
  18  	"golang.org/x/tools/go/analysis"
  19  	"golang.org/x/tools/go/analysis/passes/inspect"
  20  )
  21  
  22  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
  23  	Analyzer: &analysis.Analyzer{
  24  		Name:     "S1025",
  25  		Run:      run,
  26  		Requires: []*analysis.Analyzer{buildir.Analyzer, inspect.Analyzer, generated.Analyzer},
  27  	},
  28  	Doc: &lint.RawDocumentation{
  29  		Title: `Don't use \'fmt.Sprintf("%s", x)\' unnecessarily`,
  30  		Text: `In many instances, there are easier and more efficient ways of getting
  31  a value's string representation. Whenever a value's underlying type is
  32  a string already, or the type has a String method, they should be used
  33  directly.
  34  
  35  Given the following shared definitions
  36  
  37      type T1 string
  38      type T2 int
  39  
  40      func (T2) String() string { return "Hello, world" }
  41  
  42      var x string
  43      var y T1
  44      var z T2
  45  
  46  we can simplify
  47  
  48      fmt.Sprintf("%s", x)
  49      fmt.Sprintf("%s", y)
  50      fmt.Sprintf("%s", z)
  51  
  52  to
  53  
  54      x
  55      string(y)
  56      z.String()
  57  `,
  58  		Since:   "2017.1",
  59  		MergeIf: lint.MergeIfAll,
  60  	},
  61  })
  62  
  63  var Analyzer = SCAnalyzer.Analyzer
  64  
  65  var checkRedundantSprintfQ = pattern.MustParse(`(CallExpr (Symbol "fmt.Sprintf") [format arg])`)
  66  
  67  func run(pass *analysis.Pass) (interface{}, error) {
  68  	fn := func(node ast.Node) {
  69  		m, ok := code.Match(pass, checkRedundantSprintfQ, node)
  70  		if !ok {
  71  			return
  72  		}
  73  
  74  		format := m.State["format"].(ast.Expr)
  75  		arg := m.State["arg"].(ast.Expr)
  76  		// TODO(dh): should we really support named constants here?
  77  		// shouldn't we only look for string literals? to avoid false
  78  		// positives via build tags?
  79  		if s, ok := code.ExprToString(pass, format); !ok || s != "%s" {
  80  			return
  81  		}
  82  		typ := pass.TypesInfo.TypeOf(arg)
  83  		if typeparams.IsTypeParam(typ) {
  84  			return
  85  		}
  86  		irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
  87  
  88  		if typeutil.IsTypeWithName(typ, "reflect.Value") {
  89  			// printing with %s produces output different from using
  90  			// the String method
  91  			return
  92  		}
  93  
  94  		if isFormatter(typ, &irpkg.Prog.MethodSets) {
  95  			// the type may choose to handle %s in arbitrary ways
  96  			return
  97  		}
  98  
  99  		if types.Implements(typ, knowledge.Interfaces["fmt.Stringer"]) {
 100  			replacement := &ast.CallExpr{
 101  				Fun: &ast.SelectorExpr{
 102  					X:   arg,
 103  					Sel: &ast.Ident{Name: "String"},
 104  				},
 105  			}
 106  			report.Report(pass, node, "should use String() instead of fmt.Sprintf",
 107  				report.Fixes(edit.Fix("replace with call to String method", edit.ReplaceWithNode(pass.Fset, node, replacement))))
 108  		} else if types.Unalias(typ) == types.Universe.Lookup("string").Type() {
 109  			report.Report(pass, node, "the argument is already a string, there's no need to use fmt.Sprintf",
 110  				report.FilterGenerated(),
 111  				report.Fixes(edit.Fix("remove unnecessary call to fmt.Sprintf", edit.ReplaceWithNode(pass.Fset, node, arg))))
 112  		} else if typ.Underlying() == types.Universe.Lookup("string").Type() {
 113  			replacement := &ast.CallExpr{
 114  				Fun:  &ast.Ident{Name: "string"},
 115  				Args: []ast.Expr{arg},
 116  			}
 117  			report.Report(pass, node, "the argument's underlying type is a string, should use a simple conversion instead of fmt.Sprintf",
 118  				report.FilterGenerated(),
 119  				report.Fixes(edit.Fix("replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement))))
 120  		} else if code.IsOfStringConvertibleByteSlice(pass, arg) {
 121  			replacement := &ast.CallExpr{
 122  				Fun:  &ast.Ident{Name: "string"},
 123  				Args: []ast.Expr{arg},
 124  			}
 125  			report.Report(pass, node, "the argument's underlying type is a slice of bytes, should use a simple conversion instead of fmt.Sprintf",
 126  				report.FilterGenerated(),
 127  				report.Fixes(edit.Fix("replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement))))
 128  		}
 129  
 130  	}
 131  	code.Preorder(pass, fn, (*ast.CallExpr)(nil))
 132  	return nil, nil
 133  }
 134  
 135  func isFormatter(T types.Type, msCache *typeutil.MethodSetCache) bool {
 136  	// TODO(dh): this function also exists in staticcheck/lint.go – deduplicate.
 137  
 138  	ms := msCache.MethodSet(T)
 139  	sel := ms.Lookup(nil, "Format")
 140  	if sel == nil {
 141  		return false
 142  	}
 143  	fn, ok := sel.Obj().(*types.Func)
 144  	if !ok {
 145  		// should be unreachable
 146  		return false
 147  	}
 148  	sig := fn.Type().(*types.Signature)
 149  	if sig.Params().Len() != 2 {
 150  		return false
 151  	}
 152  	// TODO(dh): check the types of the arguments for more
 153  	// precision
 154  	if sig.Results().Len() != 0 {
 155  		return false
 156  	}
 157  	return true
 158  }
 159