qf1012.go raw
1 package qf1012
2
3 import (
4 "fmt"
5 "go/ast"
6 "go/types"
7 "strings"
8
9 "honnef.co/go/tools/analysis/code"
10 "honnef.co/go/tools/analysis/edit"
11 "honnef.co/go/tools/analysis/lint"
12 "honnef.co/go/tools/analysis/report"
13 "honnef.co/go/tools/knowledge"
14 "honnef.co/go/tools/pattern"
15
16 "golang.org/x/tools/go/analysis"
17 "golang.org/x/tools/go/analysis/passes/inspect"
18 )
19
20 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
21 Analyzer: &analysis.Analyzer{
22 Name: "QF1012",
23 Run: run,
24 Requires: []*analysis.Analyzer{inspect.Analyzer},
25 },
26 Doc: &lint.RawDocumentation{
27 Title: `Use \'fmt.Fprintf(x, ...)\' instead of \'x.Write(fmt.Sprintf(...))\'`,
28 Since: "2022.1",
29 Severity: lint.SeverityHint,
30 },
31 })
32
33 var Analyzer = SCAnalyzer.Analyzer
34
35 var (
36 checkWriteBytesSprintfQ = pattern.MustParse(`
37 (CallExpr
38 (SelectorExpr recv (Ident "Write"))
39 (CallExpr (ArrayType nil (Ident "byte"))
40 (CallExpr
41 fn@(Or
42 (Symbol "fmt.Sprint")
43 (Symbol "fmt.Sprintf")
44 (Symbol "fmt.Sprintln"))
45 args)
46 ))`)
47
48 checkWriteStringSprintfQ = pattern.MustParse(`
49 (CallExpr
50 (SelectorExpr recv (Ident "WriteString"))
51 (CallExpr
52 fn@(Or
53 (Symbol "fmt.Sprint")
54 (Symbol "fmt.Sprintf")
55 (Symbol "fmt.Sprintln"))
56 args))`)
57 )
58
59 func run(pass *analysis.Pass) (interface{}, error) {
60 fn := func(node ast.Node) {
61 if m, ok := code.Match(pass, checkWriteBytesSprintfQ, node); ok {
62 recv := m.State["recv"].(ast.Expr)
63 recvT := pass.TypesInfo.TypeOf(recv)
64 if !types.Implements(recvT, knowledge.Interfaces["io.Writer"]) {
65 return
66 }
67
68 name := m.State["fn"].(*types.Func).Name()
69 newName := "F" + strings.TrimPrefix(name, "S")
70 msg := fmt.Sprintf("Use fmt.%s(...) instead of Write([]byte(fmt.%s(...)))", newName, name)
71
72 args := m.State["args"].([]ast.Expr)
73 fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.CallExpr{
74 Fun: &ast.SelectorExpr{
75 X: ast.NewIdent("fmt"),
76 Sel: ast.NewIdent(newName),
77 },
78 Args: append([]ast.Expr{recv}, args...),
79 }))
80 report.Report(pass, node, msg, report.Fixes(fix))
81 } else if m, ok := code.Match(pass, checkWriteStringSprintfQ, node); ok {
82 recv := m.State["recv"].(ast.Expr)
83 recvT := pass.TypesInfo.TypeOf(recv)
84 if !types.Implements(recvT, knowledge.Interfaces["io.StringWriter"]) {
85 return
86 }
87 // The type needs to implement both StringWriter and Writer.
88 // If it doesn't implement Writer, then we cannot pass it to fmt.Fprint.
89 if !types.Implements(recvT, knowledge.Interfaces["io.Writer"]) {
90 return
91 }
92
93 name := m.State["fn"].(*types.Func).Name()
94 newName := "F" + strings.TrimPrefix(name, "S")
95 msg := fmt.Sprintf("Use fmt.%s(...) instead of WriteString(fmt.%s(...))", newName, name)
96
97 args := m.State["args"].([]ast.Expr)
98 fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.CallExpr{
99 Fun: &ast.SelectorExpr{
100 X: ast.NewIdent("fmt"),
101 Sel: ast.NewIdent(newName),
102 },
103 Args: append([]ast.Expr{recv}, args...),
104 }))
105 report.Report(pass, node, msg, report.Fixes(fix))
106 }
107 }
108 code.Preorder(pass, fn, (*ast.CallExpr)(nil))
109 return nil, nil
110 }
111