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