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