129 lines
3.6 KiB
Go
129 lines
3.6 KiB
Go
|
|
package qf1012
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"go/ast"
|
||
|
|
"go/token"
|
||
|
|
"go/types"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"honnef.co/go/tools/analysis/code"
|
||
|
|
"honnef.co/go/tools/analysis/edit"
|
||
|
|
"honnef.co/go/tools/analysis/lint"
|
||
|
|
"honnef.co/go/tools/analysis/report"
|
||
|
|
"honnef.co/go/tools/knowledge"
|
||
|
|
"honnef.co/go/tools/pattern"
|
||
|
|
|
||
|
|
"golang.org/x/tools/go/analysis"
|
||
|
|
)
|
||
|
|
|
||
|
|
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
|
||
|
|
Analyzer: &analysis.Analyzer{
|
||
|
|
Name: "QF1012",
|
||
|
|
Run: run,
|
||
|
|
Requires: code.RequiredAnalyzers,
|
||
|
|
},
|
||
|
|
Doc: &lint.RawDocumentation{
|
||
|
|
Title: `Use \'fmt.Fprintf(x, ...)\' instead of \'x.Write(fmt.Sprintf(...))\'`,
|
||
|
|
Since: "2022.1",
|
||
|
|
Severity: lint.SeverityHint,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
var Analyzer = SCAnalyzer.Analyzer
|
||
|
|
|
||
|
|
var (
|
||
|
|
checkWriteBytesSprintfQ = pattern.MustParse(`
|
||
|
|
(CallExpr
|
||
|
|
(SelectorExpr recv (Ident "Write"))
|
||
|
|
(CallExpr (ArrayType nil (Ident "byte"))
|
||
|
|
(CallExpr
|
||
|
|
fn@(Or
|
||
|
|
(Symbol "fmt.Sprint")
|
||
|
|
(Symbol "fmt.Sprintf")
|
||
|
|
(Symbol "fmt.Sprintln"))
|
||
|
|
args)
|
||
|
|
))`)
|
||
|
|
|
||
|
|
checkWriteStringSprintfQ = pattern.MustParse(`
|
||
|
|
(CallExpr
|
||
|
|
(SelectorExpr recv (Ident "WriteString"))
|
||
|
|
(CallExpr
|
||
|
|
fn@(Or
|
||
|
|
(Symbol "fmt.Sprint")
|
||
|
|
(Symbol "fmt.Sprintf")
|
||
|
|
(Symbol "fmt.Sprintln"))
|
||
|
|
args))`)
|
||
|
|
)
|
||
|
|
|
||
|
|
func run(pass *analysis.Pass) (any, error) {
|
||
|
|
fn := func(node ast.Node) {
|
||
|
|
getRecv := func(m *pattern.Matcher) (ast.Expr, types.Type) {
|
||
|
|
recv := m.State["recv"].(ast.Expr)
|
||
|
|
recvT := pass.TypesInfo.TypeOf(recv)
|
||
|
|
|
||
|
|
// Use *N, not N, for the interface check if N
|
||
|
|
// is a named non-interface type, since the pointer
|
||
|
|
// has a larger method set (https://staticcheck.dev/issues/1097).
|
||
|
|
// We assume the receiver expression is addressable
|
||
|
|
// since otherwise the code wouldn't compile.
|
||
|
|
if _, ok := types.Unalias(recvT).(*types.Named); ok && !types.IsInterface(recvT) {
|
||
|
|
recvT = types.NewPointer(recvT)
|
||
|
|
recv = &ast.UnaryExpr{Op: token.AND, X: recv}
|
||
|
|
|
||
|
|
}
|
||
|
|
return recv, recvT
|
||
|
|
}
|
||
|
|
|
||
|
|
if m, ok := code.Match(pass, checkWriteBytesSprintfQ, node); ok {
|
||
|
|
recv, recvT := getRecv(m)
|
||
|
|
if !types.Implements(recvT, knowledge.Interfaces["io.Writer"]) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
name := m.State["fn"].(*types.Func).Name()
|
||
|
|
newName := "F" + strings.TrimPrefix(name, "S")
|
||
|
|
msg := fmt.Sprintf("Use fmt.%s(...) instead of Write([]byte(fmt.%s(...)))", newName, name)
|
||
|
|
|
||
|
|
args := m.State["args"].([]ast.Expr)
|
||
|
|
fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.CallExpr{
|
||
|
|
Fun: &ast.SelectorExpr{
|
||
|
|
X: ast.NewIdent("fmt"),
|
||
|
|
Sel: ast.NewIdent(newName),
|
||
|
|
},
|
||
|
|
Args: append([]ast.Expr{recv}, args...),
|
||
|
|
}))
|
||
|
|
report.Report(pass, node, msg, report.Fixes(fix))
|
||
|
|
} else if m, ok := code.Match(pass, checkWriteStringSprintfQ, node); ok {
|
||
|
|
recv, recvT := getRecv(m)
|
||
|
|
if !types.Implements(recvT, knowledge.Interfaces["io.StringWriter"]) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
// The type needs to implement both StringWriter and Writer.
|
||
|
|
// If it doesn't implement Writer, then we cannot pass it to fmt.Fprint.
|
||
|
|
if !types.Implements(recvT, knowledge.Interfaces["io.Writer"]) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
name := m.State["fn"].(*types.Func).Name()
|
||
|
|
newName := "F" + strings.TrimPrefix(name, "S")
|
||
|
|
msg := fmt.Sprintf("Use fmt.%s(...) instead of WriteString(fmt.%s(...))", newName, name)
|
||
|
|
|
||
|
|
args := m.State["args"].([]ast.Expr)
|
||
|
|
fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.CallExpr{
|
||
|
|
Fun: &ast.SelectorExpr{
|
||
|
|
X: ast.NewIdent("fmt"),
|
||
|
|
Sel: ast.NewIdent(newName),
|
||
|
|
},
|
||
|
|
Args: append([]ast.Expr{recv}, args...),
|
||
|
|
}))
|
||
|
|
report.Report(pass, node, msg, report.Fixes(fix))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !code.CouldMatchAny(pass, checkWriteBytesSprintfQ, checkWriteStringSprintfQ) {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
code.Preorder(pass, fn, (*ast.CallExpr)(nil))
|
||
|
|
return nil, nil
|
||
|
|
}
|