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

     1  package sa1006
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/types"
     7  
     8  	"github.com/amarpal/go-tools/analysis/code"
     9  	"github.com/amarpal/go-tools/analysis/edit"
    10  	"github.com/amarpal/go-tools/analysis/lint"
    11  	"github.com/amarpal/go-tools/analysis/report"
    12  	"github.com/amarpal/go-tools/knowledge"
    13  
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/go/analysis/passes/inspect"
    16  )
    17  
    18  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    19  	Analyzer: &analysis.Analyzer{
    20  		Name:     "SA1006",
    21  		Run:      run,
    22  		Requires: []*analysis.Analyzer{inspect.Analyzer},
    23  	},
    24  	Doc: &lint.Documentation{
    25  		Title: `\'Printf\' with dynamic first argument and no further arguments`,
    26  		Text: `Using \'fmt.Printf\' with a dynamic first argument can lead to unexpected
    27  output. The first argument is a format string, where certain character
    28  combinations have special meaning. If, for example, a user were to
    29  enter a string such as
    30  
    31      Interest rate: 5%
    32  
    33  and you printed it with
    34  
    35      fmt.Printf(s)
    36  
    37  it would lead to the following output:
    38  
    39      Interest rate: 5%!(NOVERB).
    40  
    41  Similarly, forming the first parameter via string concatenation with
    42  user input should be avoided for the same reason. When printing user
    43  input, either use a variant of \'fmt.Print\', or use the \'%s\' Printf verb
    44  and pass the string as an argument.`,
    45  		Since:    "2017.1",
    46  		Severity: lint.SeverityWarning,
    47  		MergeIf:  lint.MergeIfAny,
    48  	},
    49  })
    50  
    51  var Analyzer = SCAnalyzer.Analyzer
    52  
    53  func run(pass *analysis.Pass) (interface{}, error) {
    54  	fn := func(node ast.Node) {
    55  		call := node.(*ast.CallExpr)
    56  		name := code.CallName(pass, call)
    57  		var arg int
    58  
    59  		switch name {
    60  		case "fmt.Printf", "fmt.Sprintf", "log.Printf":
    61  			arg = knowledge.Arg("fmt.Printf.format")
    62  		case "fmt.Fprintf":
    63  			arg = knowledge.Arg("fmt.Fprintf.format")
    64  		default:
    65  			return
    66  		}
    67  		if len(call.Args) != arg+1 {
    68  			return
    69  		}
    70  		switch call.Args[arg].(type) {
    71  		case *ast.CallExpr, *ast.Ident:
    72  		default:
    73  			return
    74  		}
    75  
    76  		if _, ok := pass.TypesInfo.TypeOf(call.Args[arg]).(*types.Tuple); ok {
    77  			// the called function returns multiple values and got
    78  			// splatted into the call. for all we know, it is
    79  			// returning good arguments.
    80  			return
    81  		}
    82  
    83  		alt := name[:len(name)-1]
    84  		report.Report(pass, call,
    85  			"printf-style function with dynamic format string and no further arguments should use print-style function instead",
    86  			report.Fixes(edit.Fix(fmt.Sprintf("use %s instead of %s", alt, name), edit.ReplaceWithString(call.Fun, alt))))
    87  	}
    88  	code.Preorder(pass, fn, (*ast.CallExpr)(nil))
    89  	return nil, nil
    90  }