src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }