src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

s1025.go (4570B)


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