sa1006.go (3136B)
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/pattern" 13 14 "golang.org/x/tools/go/analysis" 15 ) 16 17 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 18 Analyzer: &analysis.Analyzer{ 19 Name: "SA1006", 20 Run: run, 21 Requires: code.RequiredAnalyzers, 22 }, 23 Doc: &lint.RawDocumentation{ 24 Title: `\'Printf\' with dynamic first argument and no further arguments`, 25 Text: `Using \'fmt.Printf\' with a dynamic first argument can lead to unexpected 26 output. The first argument is a format string, where certain character 27 combinations have special meaning. If, for example, a user were to 28 enter a string such as 29 30 Interest rate: 5% 31 32 and you printed it with 33 34 fmt.Printf(s) 35 36 it would lead to the following output: 37 38 Interest rate: 5%!(NOVERB). 39 40 Similarly, forming the first parameter via string concatenation with 41 user input should be avoided for the same reason. When printing user 42 input, either use a variant of \'fmt.Print\', or use the \'%s\' Printf verb 43 and pass the string as an argument.`, 44 Since: "2017.1", 45 Severity: lint.SeverityWarning, 46 MergeIf: lint.MergeIfAny, 47 }, 48 }) 49 50 var Analyzer = SCAnalyzer.Analyzer 51 52 var query1 = pattern.MustParse(` 53 (CallExpr 54 (Symbol 55 name@(Or 56 "fmt.Errorf" 57 "fmt.Printf" 58 "fmt.Sprintf" 59 "log.Fatalf" 60 "log.Panicf" 61 "log.Printf" 62 "(*log.Logger).Printf" 63 "(*testing.common).Logf" 64 "(*testing.common).Errorf" 65 "(*testing.common).Fatalf" 66 "(*testing.common).Skipf" 67 "(testing.TB).Logf" 68 "(testing.TB).Errorf" 69 "(testing.TB).Fatalf" 70 "(testing.TB).Skipf")) 71 format:[]) 72 `) 73 74 var query2 = pattern.MustParse(`(CallExpr (Symbol "fmt.Fprintf") _:format:[])`) 75 76 func run(pass *analysis.Pass) (any, error) { 77 for node, m := range code.Matches(pass, query1, query2) { 78 call := node.(*ast.CallExpr) 79 name, ok := m.State["name"].(string) 80 if !ok { 81 name = "fmt.Fprintf" 82 } 83 84 arg := m.State["format"].(ast.Expr) 85 switch arg.(type) { 86 case *ast.CallExpr, *ast.Ident: 87 default: 88 continue 89 } 90 91 if _, ok := pass.TypesInfo.TypeOf(arg).(*types.Tuple); ok { 92 // the called function returns multiple values and got 93 // splatted into the call. for all we know, it is 94 // returning good arguments. 95 continue 96 } 97 98 var alt string 99 if name == "fmt.Errorf" { 100 // The alternative to fmt.Errorf isn't fmt.Error but errors.New 101 alt = "errors.New" 102 } else { 103 // This can be either a function call like log.Printf or a method call with an 104 // arbitrarily complex selector, such as foo.bar[0].Printf. In either case, 105 // all we have to do is remove the final 'f' from the existing call.Fun 106 // expression. 107 alt = report.Render(pass, call.Fun) 108 alt = alt[:len(alt)-1] 109 } 110 report.Report(pass, call, 111 "printf-style function with dynamic format string and no further arguments should use print-style function instead", 112 report.Fixes(edit.Fix(fmt.Sprintf("Use %s instead of %s", alt, name), edit.ReplaceWithString(call.Fun, alt)))) 113 } 114 return nil, nil 115 }