s1038.go raw
1 package s1038
2
3 import (
4 "fmt"
5 "go/ast"
6 "go/types"
7
8 "honnef.co/go/tools/analysis/code"
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/pattern"
14
15 "golang.org/x/tools/go/analysis"
16 "golang.org/x/tools/go/analysis/passes/inspect"
17 )
18
19 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
20 Analyzer: &analysis.Analyzer{
21 Name: "S1038",
22 Run: run,
23 Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
24 },
25 Doc: &lint.RawDocumentation{
26 Title: "Unnecessarily complex way of printing formatted string",
27 Text: `Instead of using \'fmt.Print(fmt.Sprintf(...))\', one can use \'fmt.Printf(...)\'.`,
28 Since: "2020.1",
29 MergeIf: lint.MergeIfAny,
30 },
31 })
32
33 var Analyzer = SCAnalyzer.Analyzer
34
35 var (
36 checkPrintSprintQ = pattern.MustParse(`
37 (Or
38 (CallExpr
39 fn@(Or
40 (Symbol "fmt.Print")
41 (Symbol "fmt.Sprint")
42 (Symbol "fmt.Println")
43 (Symbol "fmt.Sprintln"))
44 [(CallExpr (Symbol "fmt.Sprintf") f:_)])
45 (CallExpr
46 fn@(Or
47 (Symbol "fmt.Fprint")
48 (Symbol "fmt.Fprintln"))
49 [_ (CallExpr (Symbol "fmt.Sprintf") f:_)]))`)
50
51 checkTestingErrorSprintfQ = pattern.MustParse(`
52 (CallExpr
53 sel@(SelectorExpr
54 recv
55 (Ident
56 name@(Or
57 "Error"
58 "Fatal"
59 "Fatalln"
60 "Log"
61 "Panic"
62 "Panicln"
63 "Print"
64 "Println"
65 "Skip")))
66 [(CallExpr (Symbol "fmt.Sprintf") args)])`)
67
68 checkLogSprintfQ = pattern.MustParse(`
69 (CallExpr
70 (Symbol
71 (Or
72 "log.Fatal"
73 "log.Fatalln"
74 "log.Panic"
75 "log.Panicln"
76 "log.Print"
77 "log.Println"))
78 [(CallExpr (Symbol "fmt.Sprintf") args)])`)
79
80 checkSprintfMapping = map[string]struct {
81 recv string
82 alternative string
83 }{
84 "(*testing.common).Error": {"(*testing.common)", "Errorf"},
85 "(testing.TB).Error": {"(testing.TB)", "Errorf"},
86 "(*testing.common).Fatal": {"(*testing.common)", "Fatalf"},
87 "(testing.TB).Fatal": {"(testing.TB)", "Fatalf"},
88 "(*testing.common).Log": {"(*testing.common)", "Logf"},
89 "(testing.TB).Log": {"(testing.TB)", "Logf"},
90 "(*testing.common).Skip": {"(*testing.common)", "Skipf"},
91 "(testing.TB).Skip": {"(testing.TB)", "Skipf"},
92 "(*log.Logger).Fatal": {"(*log.Logger)", "Fatalf"},
93 "(*log.Logger).Fatalln": {"(*log.Logger)", "Fatalf"},
94 "(*log.Logger).Panic": {"(*log.Logger)", "Panicf"},
95 "(*log.Logger).Panicln": {"(*log.Logger)", "Panicf"},
96 "(*log.Logger).Print": {"(*log.Logger)", "Printf"},
97 "(*log.Logger).Println": {"(*log.Logger)", "Printf"},
98 "log.Fatal": {"", "log.Fatalf"},
99 "log.Fatalln": {"", "log.Fatalf"},
100 "log.Panic": {"", "log.Panicf"},
101 "log.Panicln": {"", "log.Panicf"},
102 "log.Print": {"", "log.Printf"},
103 "log.Println": {"", "log.Printf"},
104 }
105 )
106
107 func run(pass *analysis.Pass) (interface{}, error) {
108 fmtPrintf := func(node ast.Node) {
109 m, ok := code.Match(pass, checkPrintSprintQ, node)
110 if !ok {
111 return
112 }
113
114 name := m.State["fn"].(*types.Func).Name()
115 var msg string
116 switch name {
117 case "Print", "Fprint", "Sprint":
118 newname := name + "f"
119 msg = fmt.Sprintf("should use fmt.%s instead of fmt.%s(fmt.Sprintf(...))", newname, name)
120 case "Println", "Fprintln", "Sprintln":
121 if _, ok := m.State["f"].(*ast.BasicLit); !ok {
122 // This may be an instance of
123 // fmt.Println(fmt.Sprintf(arg, ...)) where arg is an
124 // externally provided format string and the caller
125 // cannot guarantee that the format string ends with a
126 // newline.
127 return
128 }
129 newname := name[:len(name)-2] + "f"
130 msg = fmt.Sprintf("should use fmt.%s instead of fmt.%s(fmt.Sprintf(...)) (but don't forget the newline)", newname, name)
131 }
132 report.Report(pass, node, msg,
133 report.FilterGenerated())
134 }
135
136 methSprintf := func(node ast.Node) {
137 m, ok := code.Match(pass, checkTestingErrorSprintfQ, node)
138 if !ok {
139 return
140 }
141 mapped, ok := checkSprintfMapping[code.CallName(pass, node.(*ast.CallExpr))]
142 if !ok {
143 return
144 }
145
146 // Ensure that Errorf/Fatalf refer to the right method
147 recvTV, ok := pass.TypesInfo.Types[m.State["recv"].(ast.Expr)]
148 if !ok {
149 return
150 }
151 obj, _, _ := types.LookupFieldOrMethod(recvTV.Type, recvTV.Addressable(), nil, mapped.alternative)
152 f, ok := obj.(*types.Func)
153 if !ok {
154 return
155 }
156 if typeutil.FuncName(f) != mapped.recv+"."+mapped.alternative {
157 return
158 }
159
160 alt := &ast.SelectorExpr{
161 X: m.State["recv"].(ast.Expr),
162 Sel: &ast.Ident{Name: mapped.alternative},
163 }
164 report.Report(pass, node, fmt.Sprintf("should use %s(...) instead of %s(fmt.Sprintf(...))", report.Render(pass, alt), report.Render(pass, m.State["sel"].(*ast.SelectorExpr))))
165 }
166
167 pkgSprintf := func(node ast.Node) {
168 _, ok := code.Match(pass, checkLogSprintfQ, node)
169 if !ok {
170 return
171 }
172 callName := code.CallName(pass, node.(*ast.CallExpr))
173 mapped, ok := checkSprintfMapping[callName]
174 if !ok {
175 return
176 }
177 report.Report(pass, node, fmt.Sprintf("should use %s(...) instead of %s(fmt.Sprintf(...))", mapped.alternative, callName))
178 }
179
180 fn := func(node ast.Node) {
181 fmtPrintf(node)
182 // TODO(dh): add suggested fixes
183 methSprintf(node)
184 pkgSprintf(node)
185 }
186 code.Preorder(pass, fn, (*ast.CallExpr)(nil))
187 return nil, nil
188 }
189