Files

116 lines
3.1 KiB
Go
Raw Permalink Normal View History

2026-06-06 01:17:20 +00:00
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
}