go/analysis/passes/printf: report non-constant format, no args
Calls such as fmt.Printf(s), where s is non-constant and there
are no arguments to format, are invariably a mistake. This
CL causes the printf checker to report them, and to offer
a suggested fix of fmt.Printf("%s", s).
Also:
- tests
- fixes to existing violations in x/tools (3 bugs, 2 merely bad form).
- an ignore-tagged main file for the printf checker.
Fixes golang/go#60529
diff --git a/cmd/goyacc/yacc.go b/cmd/goyacc/yacc.go
index 5a8ede0..bc63954 100644
--- a/cmd/goyacc/yacc.go
+++ b/cmd/goyacc/yacc.go
@@ -606,7 +606,7 @@
}
j = chfind(2, tokname)
if j >= NTBASE {
- lerrorf(ruleline, "nonterminal "+nontrst[j-NTBASE].name+" illegal after %%prec")
+ lerrorf(ruleline, "nonterminal %s illegal after %%prec", nontrst[j-NTBASE].name)
}
levprd[nprod] = toklev[j]
t = gettok()
@@ -1565,7 +1565,7 @@
}
if pempty[i] != OK {
fatfl = 0
- errorf("nonterminal " + nontrst[i].name + " never derives any token string")
+ errorf("nonterminal %s never derives any token string", nontrst[i].name)
}
}
diff --git a/go/analysis/passes/printf/main.go b/go/analysis/passes/printf/main.go
new file mode 100644
index 0000000..2a0fb7a
--- /dev/null
+++ b/go/analysis/passes/printf/main.go
@@ -0,0 +1,20 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build ignore
+
+// The printf command applies the printf checker to the specified
+// packages of Go source code.
+//
+// Run with:
+//
+// $ go run ./go/analysis/passes/printf/main.go -- packages...
+package main
+
+import (
+ "golang.org/x/tools/go/analysis/passes/printf"
+ "golang.org/x/tools/go/analysis/singlechecker"
+)
+
+func main() { singlechecker.Main(printf.Analyzer) }
diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go
index 3235019..6ec0d72 100644
--- a/go/analysis/passes/printf/printf.go
+++ b/go/analysis/passes/printf/printf.go
@@ -382,54 +382,26 @@
// if the call's signature cannot be determined.
//
// If it cannot find any format string parameter, it returns ("", -1).
-func formatString(pass *analysis.Pass, call *ast.CallExpr) (format string, idx int) {
+func formatStringIndex(pass *analysis.Pass, call *ast.CallExpr) int {
typ := pass.TypesInfo.Types[call.Fun].Type
- if typ != nil {
- if sig, ok := typ.(*types.Signature); ok {
- if !sig.Variadic() {
- // Skip checking non-variadic functions.
- return "", -1
- }
- idx := sig.Params().Len() - 2
- if idx < 0 {
- // Skip checking variadic functions without
- // fixed arguments.
- return "", -1
- }
- s, ok := stringConstantArg(pass, call, idx)
- if !ok {
- // The last argument before variadic args isn't a string.
- return "", -1
- }
- return s, idx
- }
+ if typ == nil {
+ return -1 // missing type
}
-
- // Cannot determine call's signature. Fall back to scanning for the first
- // string constant in the call.
- for idx := range call.Args {
- if s, ok := stringConstantArg(pass, call, idx); ok {
- return s, idx
- }
- if pass.TypesInfo.Types[call.Args[idx]].Type == types.Typ[types.String] {
- // Skip checking a call with a non-constant format
- // string argument, since its contents are unavailable
- // for validation.
- return "", -1
- }
+ sig, ok := typ.(*types.Signature)
+ if !ok {
+ return -1 // ill-typed
}
- return "", -1
-}
-
-// stringConstantArg returns call's string constant argument at the index idx.
-//
-// ("", false) is returned if call's argument at the index idx isn't a string
-// constant.
-func stringConstantArg(pass *analysis.Pass, call *ast.CallExpr, idx int) (string, bool) {
- if idx >= len(call.Args) {
- return "", false
+ if !sig.Variadic() {
+ // Skip checking non-variadic functions.
+ return -1
}
- return stringConstantExpr(pass, call.Args[idx])
+ idx := sig.Params().Len() - 2
+ if idx < 0 {
+ // Skip checking variadic functions without
+ // fixed arguments.
+ return -1
+ }
+ return idx
}
// stringConstantExpr returns expression's string constant value.
@@ -536,10 +508,34 @@
// checkPrintf checks a call to a formatted print routine such as Printf.
func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, fn *types.Func) {
- format, idx := formatString(pass, call)
- if idx < 0 {
- if false {
- pass.Reportf(call.Lparen, "can't check non-constant format in call to %s", fn.FullName())
+ idx := formatStringIndex(pass, call)
+ if idx < 0 || idx >= len(call.Args) {
+ return
+ }
+ formatArg := call.Args[idx]
+ format, ok := stringConstantExpr(pass, formatArg)
+ if !ok {
+ // Format string argument is non-constant.
+
+ // It is a common mistake to call fmt.Printf(msg) with a
+ // non-constant format string and no arguments:
+ // if msg contains "%", misformatting occurs.
+ // Report the problem and suggest a fix: fmt.Printf("%s", msg).
+ if idx == len(call.Args)-1 && pass.TypesInfo.Types[formatArg].Value == nil {
+ pass.Report(analysis.Diagnostic{
+ Pos: formatArg.Pos(),
+ End: formatArg.End(),
+ Message: fmt.Sprintf("non-constant format string in call to %s",
+ fn.FullName()),
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: "Insert \"%s\" format string",
+ TextEdits: []analysis.TextEdit{{
+ Pos: formatArg.Pos(),
+ End: formatArg.Pos(),
+ NewText: []byte(`"%s", `),
+ }},
+ }},
+ })
}
return
}
diff --git a/go/analysis/passes/printf/printf_test.go b/go/analysis/passes/printf/printf_test.go
index 853d861..3506fec 100644
--- a/go/analysis/passes/printf/printf_test.go
+++ b/go/analysis/passes/printf/printf_test.go
@@ -16,4 +16,5 @@
printf.Analyzer.Flags.Set("funcs", "Warn,Warnf")
analysistest.Run(t, testdata, printf.Analyzer, "a", "b", "nofmt", "typeparams")
+ analysistest.RunWithSuggestedFixes(t, testdata, printf.Analyzer, "fix")
}
diff --git a/go/analysis/passes/printf/testdata/src/fix/fix.go b/go/analysis/passes/printf/testdata/src/fix/fix.go
new file mode 100644
index 0000000..9f9057a
--- /dev/null
+++ b/go/analysis/passes/printf/testdata/src/fix/fix.go
@@ -0,0 +1,20 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains tests of the printf checker's suggested fixes
+
+package fix
+
+import (
+ "fmt"
+ "log"
+ "os"
+)
+
+func nonConstantFormat(s string) { // #60529
+ fmt.Printf(s) // want `non-constant format string in call to fmt.Printf`
+ fmt.Printf(s, "arg")
+ fmt.Fprintf(os.Stderr, s) // want `non-constant format string in call to fmt.Fprintf`
+ log.Printf(s) // want `non-constant format string in call to log.Printf`
+}
diff --git a/go/analysis/passes/printf/testdata/src/fix/fix.go.golden b/go/analysis/passes/printf/testdata/src/fix/fix.go.golden
new file mode 100644
index 0000000..db972b0
--- /dev/null
+++ b/go/analysis/passes/printf/testdata/src/fix/fix.go.golden
@@ -0,0 +1,20 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains tests of the printf checker's suggested fixes
+
+package fix
+
+import (
+ "fmt"
+ "log"
+ "os"
+)
+
+func nonConstantFormat(s string) { // #60529
+ fmt.Printf("%s", s) // want `non-constant format string in call to fmt.Printf`
+ fmt.Printf(s, "arg")
+ fmt.Fprintf(os.Stderr, "%s", s) // want `non-constant format string in call to fmt.Fprintf`
+ log.Printf("%s", s) // want `non-constant format string in call to log.Printf`
+}
diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go
index f5e760c..5b45982 100644
--- a/go/analysis/passes/tests/tests.go
+++ b/go/analysis/passes/tests/tests.go
@@ -6,7 +6,6 @@
import (
_ "embed"
- "fmt"
"go/ast"
"go/token"
"go/types"
@@ -184,13 +183,13 @@
i := mismatched[0]
expr := call.Args[i]
t := pass.TypesInfo.Types[expr].Type
- pass.ReportRangef(expr, fmt.Sprintf("mismatched type in call to (*testing.F).Add: %v, fuzz target expects %v", t, params.At(i+1).Type()))
+ pass.ReportRangef(expr, "mismatched type in call to (*testing.F).Add: %v, fuzz target expects %v", t, params.At(i+1).Type())
} else if len(mismatched) > 1 {
var gotArgs, wantArgs []types.Type
for i := 0; i < len(call.Args); i++ {
gotArgs, wantArgs = append(gotArgs, pass.TypesInfo.Types[call.Args[i]].Type), append(wantArgs, params.At(i+1).Type())
}
- pass.ReportRangef(call, fmt.Sprintf("mismatched types in call to (*testing.F).Add: %v, fuzz target expects %v", gotArgs, wantArgs))
+ pass.ReportRangef(call, "mismatched types in call to (*testing.F).Add: %v, fuzz target expects %v", gotArgs, wantArgs)
}
}
return true
@@ -244,7 +243,7 @@
}
}
}
- pass.ReportRangef(exprRange, "fuzzing arguments can only have the following types: "+formatAcceptedFuzzType())
+ pass.ReportRangef(exprRange, "fuzzing arguments can only have the following types: %s", formatAcceptedFuzzType())
ok = false
}
}
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
lerrorf(ruleline, "nonterminal %s illegal after %%prec", nontrst[j-NTBASE].name)Does this count as a false positive? The previous code was sound so long as name doesn't contain "%", which it very likely does not in this case.
I wonder whether it needs a variant to catch ("..." + s + "...") that suggests using ("...%s...", s).
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Russ: Tim's on vacation; would you mind looking at this CL before the freeze? Thanks.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
// Cannot determine call's signature. Fall back to scanning for the first(This block was deleted as it is unreachable in well-typed code, which the printf analyzer requires.)
lerrorf(ruleline, "nonterminal %s illegal after %%prec", nontrst[j-NTBASE].name)Does this count as a false positive? The previous code was sound so long as name doesn't contain "%", which it very likely does not in this case.
I wonder whether it needs a variant to catch ("..." + s + "...") that suggests using ("...%s...", s).
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
lerrorf(ruleline, "nonterminal %s illegal after %%prec", nontrst[j-NTBASE].name)Alan DonovanDoes this count as a false positive? The previous code was sound so long as name doesn't contain "%", which it very likely does not in this case.
I wonder whether it needs a variant to catch ("..." + s + "...") that suggests using ("...%s...", s).
(Russ opined in the proposal committee that this is fine.)
Acknowledged
// formatString returns the format string argument and its index within
// the given printf-like call expression.
//
// The last parameter before variadic arguments is assumed to be
// a format string.
//
// The first string literal or string constant is assumed to be a format string
// if the call's signature cannot be determined.
//
// If it cannot find any format string parameter, it returns ("", -1).This comment is stale, and needs to be updated for the new signature of this function.
if idx == len(call.Args)-1 && pass.TypesInfo.Types[formatArg].Value == nil {We already know that the argument is non constant, so shouldn't need to check Value == nil again.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
// formatString returns the format string argument and its index within
// the given printf-like call expression.
//
// The last parameter before variadic arguments is assumed to be
// a format string.
//
// The first string literal or string constant is assumed to be a format string
// if the call's signature cannot be determined.
//
// If it cannot find any format string parameter, it returns ("", -1).This comment is stale, and needs to be updated for the new signature of this function.
Oops, I missed that. Rewrote the whole to:
// formatStringIndex returns the index of the format string (the last
// non-variadic parameter) within the given printf-like call
// expression, or -1 if unknown.
// Cannot determine call's signature. Fall back to scanning for the first(This block was deleted as it is unreachable in well-typed code, which the printf analyzer requires.)
Acknowledged
if idx == len(call.Args)-1 && pass.TypesInfo.Types[formatArg].Value == nil {We already know that the argument is non constant, so shouldn't need to check Value == nil again.
Nice. Done.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Hold | +1 |
Hold for go1.24, otherwise revendoring x/tools in GOROOT will bring this into 1.23 during the freeze.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
DO NOT SUMBIT until CL 587855 (to clean up std) landsDon't forget to add a go1.24 release note when we eventually vendor this into GOROOT.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Alan DonovanPlease wait for Go 1.24 at this point.
For once, I'm a step ahead of you! ;-)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
idx := sig.Params().Len() - 2I think a comment should explain "-2" here. Took me a minute.
formatArg := call.Args[idx]Is it worth bothering adjusted idx for method expressions? My guess is no, but I thought I'd ask.
Message: "Insert \"%s\" format string",nit: Good place to use back ticks `
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Hold | +1 |
Don't forget to add a go1.24 release note when we eventually vendor this into GOROOT.
Will do.
I think a comment should explain "-2" here. Took me a minute.
The doc comment explains that it "returns the index of the format string (the last
non-variadic parameter)". Not sure we need to repeat that here.
formatArg := call.Args[idx]Is it worth bothering adjusted idx for method expressions? My guess is no, but I thought I'd ask.
We're analyzing a CallExpr that statically calls a function symbol, so there's no method expression here. I'm not sure what you're suggesting.
nit: Good place to use back ticks `
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
formatArg := call.Args[idx]Alan DonovanIs it worth bothering adjusted idx for method expressions? My guess is no, but I thought I'd ask.
We're analyzing a CallExpr that statically calls a function symbol, so there's no method expression here. I'm not sure what you're suggesting.
Acknowledged
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Removed Hold+1 by Russ Cox <r...@golang.org>
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
go/analysis/passes/printf: report non-constant format, no args
Calls such as fmt.Printf(s), where s is non-constant and there
are no arguments to format, are invariably a mistake. This
CL causes the printf checker to report them, and to offer
a suggested fix of fmt.Printf("%s", s).
Also:
- tests
- docs
- fixes to existing violations in x/tools (3 bugs, 2 merely bad form).
- an ignore-tagged main file for the printf checker.
Fixes golang/go#60529
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Russ, this is awaiting your unhold.
I just realized that I can remove your Hold, and have done so.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |