qf1012.go (3695B)
1 package qf1012 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/token" 7 "go/types" 8 "strings" 9 10 "honnef.co/go/tools/analysis/code" 11 "honnef.co/go/tools/analysis/edit" 12 "honnef.co/go/tools/analysis/lint" 13 "honnef.co/go/tools/analysis/report" 14 "honnef.co/go/tools/knowledge" 15 "honnef.co/go/tools/pattern" 16 17 "golang.org/x/tools/go/analysis" 18 ) 19 20 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 21 Analyzer: &analysis.Analyzer{ 22 Name: "QF1012", 23 Run: run, 24 Requires: code.RequiredAnalyzers, 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) (any, error) { 60 fn := func(node ast.Node) { 61 getRecv := func(m *pattern.Matcher) (ast.Expr, types.Type) { 62 recv := m.State["recv"].(ast.Expr) 63 recvT := pass.TypesInfo.TypeOf(recv) 64 65 // Use *N, not N, for the interface check if N 66 // is a named non-interface type, since the pointer 67 // has a larger method set (https://staticcheck.dev/issues/1097). 68 // We assume the receiver expression is addressable 69 // since otherwise the code wouldn't compile. 70 if _, ok := types.Unalias(recvT).(*types.Named); ok && !types.IsInterface(recvT) { 71 recvT = types.NewPointer(recvT) 72 recv = &ast.UnaryExpr{Op: token.AND, X: recv} 73 74 } 75 return recv, recvT 76 } 77 78 if m, ok := code.Match(pass, checkWriteBytesSprintfQ, node); ok { 79 recv, recvT := getRecv(m) 80 if !types.Implements(recvT, knowledge.Interfaces["io.Writer"]) { 81 return 82 } 83 84 name := m.State["fn"].(*types.Func).Name() 85 newName := "F" + strings.TrimPrefix(name, "S") 86 msg := fmt.Sprintf("Use fmt.%s(...) instead of Write([]byte(fmt.%s(...)))", newName, name) 87 88 args := m.State["args"].([]ast.Expr) 89 fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.CallExpr{ 90 Fun: &ast.SelectorExpr{ 91 X: ast.NewIdent("fmt"), 92 Sel: ast.NewIdent(newName), 93 }, 94 Args: append([]ast.Expr{recv}, args...), 95 })) 96 report.Report(pass, node, msg, report.Fixes(fix)) 97 } else if m, ok := code.Match(pass, checkWriteStringSprintfQ, node); ok { 98 recv, recvT := getRecv(m) 99 if !types.Implements(recvT, knowledge.Interfaces["io.StringWriter"]) { 100 return 101 } 102 // The type needs to implement both StringWriter and Writer. 103 // If it doesn't implement Writer, then we cannot pass it to fmt.Fprint. 104 if !types.Implements(recvT, knowledge.Interfaces["io.Writer"]) { 105 return 106 } 107 108 name := m.State["fn"].(*types.Func).Name() 109 newName := "F" + strings.TrimPrefix(name, "S") 110 msg := fmt.Sprintf("Use fmt.%s(...) instead of WriteString(fmt.%s(...))", newName, name) 111 112 args := m.State["args"].([]ast.Expr) 113 fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.CallExpr{ 114 Fun: &ast.SelectorExpr{ 115 X: ast.NewIdent("fmt"), 116 Sel: ast.NewIdent(newName), 117 }, 118 Args: append([]ast.Expr{recv}, args...), 119 })) 120 report.Report(pass, node, msg, report.Fixes(fix)) 121 } 122 } 123 if !code.CouldMatchAny(pass, checkWriteBytesSprintfQ, checkWriteStringSprintfQ) { 124 return nil, nil 125 } 126 code.Preorder(pass, fn, (*ast.CallExpr)(nil)) 127 return nil, nil 128 }