sa1006.go raw

   1  package sa1006
   2  
   3  import (
   4  	"fmt"
   5  	"go/ast"
   6  	"go/types"
   7  
   8  	"honnef.co/go/tools/analysis/code"
   9  	"honnef.co/go/tools/analysis/edit"
  10  	"honnef.co/go/tools/analysis/lint"
  11  	"honnef.co/go/tools/analysis/report"
  12  	"honnef.co/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.RawDocumentation{
  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.Errorf", "fmt.Printf", "fmt.Sprintf",
  61  			"log.Fatalf", "log.Panicf", "log.Printf", "(*log.Logger).Printf",
  62  			"(*testing.common).Logf", "(*testing.common).Errorf",
  63  			"(*testing.common).Fatalf", "(*testing.common).Skipf",
  64  			"(testing.TB).Logf", "(testing.TB).Errorf",
  65  			"(testing.TB).Fatalf", "(testing.TB).Skipf":
  66  			arg = knowledge.Arg("fmt.Printf.format")
  67  		case "fmt.Fprintf":
  68  			arg = knowledge.Arg("fmt.Fprintf.format")
  69  		default:
  70  			return
  71  		}
  72  		if len(call.Args) != arg+1 {
  73  			// This filters out calls of method expressions like (*log.Logger).Printf(nil, s)
  74  			return
  75  		}
  76  		switch call.Args[arg].(type) {
  77  		case *ast.CallExpr, *ast.Ident:
  78  		default:
  79  			return
  80  		}
  81  
  82  		if _, ok := pass.TypesInfo.TypeOf(call.Args[arg]).(*types.Tuple); ok {
  83  			// the called function returns multiple values and got
  84  			// splatted into the call. for all we know, it is
  85  			// returning good arguments.
  86  			return
  87  		}
  88  
  89  		var alt string
  90  		if name == "fmt.Errorf" {
  91  			// The alternative to fmt.Errorf isn't fmt.Error but errors.New
  92  			alt = "errors.New"
  93  		} else {
  94  			// This can be either a function call like log.Printf or a method call with an
  95  			// arbitrarily complex selector, such as foo.bar[0].Printf. In either case,
  96  			// all we have to do is remove the final 'f' from the existing call.Fun
  97  			// expression.
  98  			alt = report.Render(pass, call.Fun)
  99  			alt = alt[:len(alt)-1]
 100  		}
 101  		report.Report(pass, call,
 102  			"printf-style function with dynamic format string and no further arguments should use print-style function instead",
 103  			report.Fixes(edit.Fix(fmt.Sprintf("use %s instead of %s", alt, name), edit.ReplaceWithString(call.Fun, alt))))
 104  	}
 105  	code.Preorder(pass, fn, (*ast.CallExpr)(nil))
 106  	return nil, nil
 107  }
 108