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