116 lines
3.1 KiB
Go
116 lines
3.1 KiB
Go
package sa1006
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/types"
|
|
|
|
"honnef.co/go/tools/analysis/code"
|
|
"honnef.co/go/tools/analysis/edit"
|
|
"honnef.co/go/tools/analysis/lint"
|
|
"honnef.co/go/tools/analysis/report"
|
|
"honnef.co/go/tools/pattern"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
)
|
|
|
|
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
|
|
Analyzer: &analysis.Analyzer{
|
|
Name: "SA1006",
|
|
Run: run,
|
|
Requires: code.RequiredAnalyzers,
|
|
},
|
|
Doc: &lint.RawDocumentation{
|
|
Title: `\'Printf\' with dynamic first argument and no further arguments`,
|
|
Text: `Using \'fmt.Printf\' with a dynamic first argument can lead to unexpected
|
|
output. The first argument is a format string, where certain character
|
|
combinations have special meaning. If, for example, a user were to
|
|
enter a string such as
|
|
|
|
Interest rate: 5%
|
|
|
|
and you printed it with
|
|
|
|
fmt.Printf(s)
|
|
|
|
it would lead to the following output:
|
|
|
|
Interest rate: 5%!(NOVERB).
|
|
|
|
Similarly, forming the first parameter via string concatenation with
|
|
user input should be avoided for the same reason. When printing user
|
|
input, either use a variant of \'fmt.Print\', or use the \'%s\' Printf verb
|
|
and pass the string as an argument.`,
|
|
Since: "2017.1",
|
|
Severity: lint.SeverityWarning,
|
|
MergeIf: lint.MergeIfAny,
|
|
},
|
|
})
|
|
|
|
var Analyzer = SCAnalyzer.Analyzer
|
|
|
|
var query1 = pattern.MustParse(`
|
|
(CallExpr
|
|
(Symbol
|
|
name@(Or
|
|
"fmt.Errorf"
|
|
"fmt.Printf"
|
|
"fmt.Sprintf"
|
|
"log.Fatalf"
|
|
"log.Panicf"
|
|
"log.Printf"
|
|
"(*log.Logger).Printf"
|
|
"(*testing.common).Logf"
|
|
"(*testing.common).Errorf"
|
|
"(*testing.common).Fatalf"
|
|
"(*testing.common).Skipf"
|
|
"(testing.TB).Logf"
|
|
"(testing.TB).Errorf"
|
|
"(testing.TB).Fatalf"
|
|
"(testing.TB).Skipf"))
|
|
format:[])
|
|
`)
|
|
|
|
var query2 = pattern.MustParse(`(CallExpr (Symbol "fmt.Fprintf") _:format:[])`)
|
|
|
|
func run(pass *analysis.Pass) (any, error) {
|
|
for node, m := range code.Matches(pass, query1, query2) {
|
|
call := node.(*ast.CallExpr)
|
|
name, ok := m.State["name"].(string)
|
|
if !ok {
|
|
name = "fmt.Fprintf"
|
|
}
|
|
|
|
arg := m.State["format"].(ast.Expr)
|
|
switch arg.(type) {
|
|
case *ast.CallExpr, *ast.Ident:
|
|
default:
|
|
continue
|
|
}
|
|
|
|
if _, ok := pass.TypesInfo.TypeOf(arg).(*types.Tuple); ok {
|
|
// the called function returns multiple values and got
|
|
// splatted into the call. for all we know, it is
|
|
// returning good arguments.
|
|
continue
|
|
}
|
|
|
|
var alt string
|
|
if name == "fmt.Errorf" {
|
|
// The alternative to fmt.Errorf isn't fmt.Error but errors.New
|
|
alt = "errors.New"
|
|
} else {
|
|
// This can be either a function call like log.Printf or a method call with an
|
|
// arbitrarily complex selector, such as foo.bar[0].Printf. In either case,
|
|
// all we have to do is remove the final 'f' from the existing call.Fun
|
|
// expression.
|
|
alt = report.Render(pass, call.Fun)
|
|
alt = alt[:len(alt)-1]
|
|
}
|
|
report.Report(pass, call,
|
|
"printf-style function with dynamic format string and no further arguments should use print-style function instead",
|
|
report.Fixes(edit.Fix(fmt.Sprintf("Use %s instead of %s", alt, name), edit.ReplaceWithString(call.Fun, alt))))
|
|
}
|
|
return nil, nil
|
|
}
|