github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/quickfix/qf1012/qf1012.go (about)

     1  package qf1012
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/types"
     7  	"strings"
     8  
     9  	"github.com/amarpal/go-tools/analysis/code"
    10  	"github.com/amarpal/go-tools/analysis/edit"
    11  	"github.com/amarpal/go-tools/analysis/lint"
    12  	"github.com/amarpal/go-tools/analysis/report"
    13  	"github.com/amarpal/go-tools/knowledge"
    14  	"github.com/amarpal/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.Documentation{
    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  }