package s1025 import ( "go/ast" "go/types" "honnef.co/go/tools/analysis/code" "honnef.co/go/tools/analysis/edit" "honnef.co/go/tools/analysis/facts/generated" "honnef.co/go/tools/analysis/lint" "honnef.co/go/tools/analysis/report" "honnef.co/go/tools/go/types/typeutil" "honnef.co/go/tools/internal/passes/buildir" "honnef.co/go/tools/knowledge" "honnef.co/go/tools/pattern" "golang.org/x/exp/typeparams" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" ) var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ Analyzer: &analysis.Analyzer{ Name: "S1025", Run: run, Requires: []*analysis.Analyzer{buildir.Analyzer, inspect.Analyzer, generated.Analyzer}, }, Doc: &lint.RawDocumentation{ Title: `Don't use \'fmt.Sprintf("%s", x)\' unnecessarily`, Text: `In many instances, there are easier and more efficient ways of getting a value's string representation. Whenever a value's underlying type is a string already, or the type has a String method, they should be used directly. Given the following shared definitions type T1 string type T2 int func (T2) String() string { return "Hello, world" } var x string var y T1 var z T2 we can simplify fmt.Sprintf("%s", x) fmt.Sprintf("%s", y) fmt.Sprintf("%s", z) to x string(y) z.String() `, Since: "2017.1", MergeIf: lint.MergeIfAll, }, }) var Analyzer = SCAnalyzer.Analyzer var checkRedundantSprintfQ = pattern.MustParse(`(CallExpr (Symbol "fmt.Sprintf") [format arg])`) func run(pass *analysis.Pass) (interface{}, error) { fn := func(node ast.Node) { m, ok := code.Match(pass, checkRedundantSprintfQ, node) if !ok { return } format := m.State["format"].(ast.Expr) arg := m.State["arg"].(ast.Expr) // TODO(dh): should we really support named constants here? // shouldn't we only look for string literals? to avoid false // positives via build tags? if s, ok := code.ExprToString(pass, format); !ok || s != "%s" { return } typ := pass.TypesInfo.TypeOf(arg) if typeparams.IsTypeParam(typ) { return } irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg if typeutil.IsTypeWithName(typ, "reflect.Value") { // printing with %s produces output different from using // the String method return } if isFormatter(typ, &irpkg.Prog.MethodSets) { // the type may choose to handle %s in arbitrary ways return } if types.Implements(typ, knowledge.Interfaces["fmt.Stringer"]) { replacement := &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: arg, Sel: &ast.Ident{Name: "String"}, }, } report.Report(pass, node, "should use String() instead of fmt.Sprintf", report.Fixes(edit.Fix("replace with call to String method", edit.ReplaceWithNode(pass.Fset, node, replacement)))) } else if types.Unalias(typ) == types.Universe.Lookup("string").Type() { report.Report(pass, node, "the argument is already a string, there's no need to use fmt.Sprintf", report.FilterGenerated(), report.Fixes(edit.Fix("remove unnecessary call to fmt.Sprintf", edit.ReplaceWithNode(pass.Fset, node, arg)))) } else if typ.Underlying() == types.Universe.Lookup("string").Type() { replacement := &ast.CallExpr{ Fun: &ast.Ident{Name: "string"}, Args: []ast.Expr{arg}, } report.Report(pass, node, "the argument's underlying type is a string, should use a simple conversion instead of fmt.Sprintf", report.FilterGenerated(), report.Fixes(edit.Fix("replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement)))) } else if code.IsOfStringConvertibleByteSlice(pass, arg) { replacement := &ast.CallExpr{ Fun: &ast.Ident{Name: "string"}, Args: []ast.Expr{arg}, } report.Report(pass, node, "the argument's underlying type is a slice of bytes, should use a simple conversion instead of fmt.Sprintf", report.FilterGenerated(), report.Fixes(edit.Fix("replace with conversion to string", edit.ReplaceWithNode(pass.Fset, node, replacement)))) } } code.Preorder(pass, fn, (*ast.CallExpr)(nil)) return nil, nil } func isFormatter(T types.Type, msCache *typeutil.MethodSetCache) bool { // TODO(dh): this function also exists in staticcheck/lint.go – deduplicate. ms := msCache.MethodSet(T) sel := ms.Lookup(nil, "Format") if sel == nil { return false } fn, ok := sel.Obj().(*types.Func) if !ok { // should be unreachable return false } sig := fn.Type().(*types.Signature) if sig.Params().Len() != 2 { return false } // TODO(dh): check the types of the arguments for more // precision if sig.Results().Len() != 0 { return false } return true }