Skip to content

Commit a5df6ad

Browse files
adonovangopherbot
authored andcommitted
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 Change-Id: Ic7cc4f3e7487bc1549948fa03b5b0b27959948e5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/585795 Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Tim King <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent c03e5c2 commit a5df6ad

File tree

8 files changed

+125
-64
lines changed

8 files changed

+125
-64
lines changed

cmd/goyacc/yacc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ outer:
606606
}
607607
j = chfind(2, tokname)
608608
if j >= NTBASE {
609-
lerrorf(ruleline, "nonterminal "+nontrst[j-NTBASE].name+" illegal after %%prec")
609+
lerrorf(ruleline, "nonterminal %s illegal after %%prec", nontrst[j-NTBASE].name)
610610
}
611611
levprd[nprod] = toklev[j]
612612
t = gettok()
@@ -1565,7 +1565,7 @@ more:
15651565
}
15661566
if pempty[i] != OK {
15671567
fatfl = 0
1568-
errorf("nonterminal " + nontrst[i].name + " never derives any token string")
1568+
errorf("nonterminal %s never derives any token string", nontrst[i].name)
15691569
}
15701570
}
15711571

go/analysis/passes/printf/doc.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@
4545
//
4646
// log.Print("%d", 123) // log.Print call has possible formatting directive %d
4747
//
48+
// Conversely, it also reports calls to Printf-like functions with a
49+
// non-constant format string and no other arguments:
50+
//
51+
// fmt.Printf(message) // non-constant format string in call to fmt.Printf
52+
//
53+
// Such calls may have been intended for the function's Print-like
54+
// counterpart: if the value of message happens to contain "%",
55+
// misformatting will occur. In this case, the checker additionally
56+
// suggests a fix to turn the call into:
57+
//
58+
// fmt.Printf("%s", message)
59+
//
4860
// # Inferred printf wrappers
4961
//
5062
// Functions that delegate their arguments to fmt.Printf are

go/analysis/passes/printf/main.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build ignore
6+
7+
// The printf command applies the printf checker to the specified
8+
// packages of Go source code.
9+
//
10+
// Run with:
11+
//
12+
// $ go run ./go/analysis/passes/printf/main.go -- packages...
13+
package main
14+
15+
import (
16+
"golang.org/x/tools/go/analysis/passes/printf"
17+
"golang.org/x/tools/go/analysis/singlechecker"
18+
)
19+
20+
func main() { singlechecker.Main(printf.Analyzer) }

go/analysis/passes/printf/printf.go

Lines changed: 47 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -372,64 +372,29 @@ var isPrint = stringSet{
372372
"(testing.TB).Skipf": true,
373373
}
374374

375-
// formatString returns the format string argument and its index within
376-
// the given printf-like call expression.
377-
//
378-
// The last parameter before variadic arguments is assumed to be
379-
// a format string.
380-
//
381-
// The first string literal or string constant is assumed to be a format string
382-
// if the call's signature cannot be determined.
383-
//
384-
// If it cannot find any format string parameter, it returns ("", -1).
385-
func formatString(pass *analysis.Pass, call *ast.CallExpr) (format string, idx int) {
375+
// formatStringIndex returns the index of the format string (the last
376+
// non-variadic parameter) within the given printf-like call
377+
// expression, or -1 if unknown.
378+
func formatStringIndex(pass *analysis.Pass, call *ast.CallExpr) int {
386379
typ := pass.TypesInfo.Types[call.Fun].Type
387-
if typ != nil {
388-
if sig, ok := typ.(*types.Signature); ok {
389-
if !sig.Variadic() {
390-
// Skip checking non-variadic functions.
391-
return "", -1
392-
}
393-
idx := sig.Params().Len() - 2
394-
if idx < 0 {
395-
// Skip checking variadic functions without
396-
// fixed arguments.
397-
return "", -1
398-
}
399-
s, ok := stringConstantArg(pass, call, idx)
400-
if !ok {
401-
// The last argument before variadic args isn't a string.
402-
return "", -1
403-
}
404-
return s, idx
405-
}
380+
if typ == nil {
381+
return -1 // missing type
406382
}
407-
408-
// Cannot determine call's signature. Fall back to scanning for the first
409-
// string constant in the call.
410-
for idx := range call.Args {
411-
if s, ok := stringConstantArg(pass, call, idx); ok {
412-
return s, idx
413-
}
414-
if pass.TypesInfo.Types[call.Args[idx]].Type == types.Typ[types.String] {
415-
// Skip checking a call with a non-constant format
416-
// string argument, since its contents are unavailable
417-
// for validation.
418-
return "", -1
419-
}
383+
sig, ok := typ.(*types.Signature)
384+
if !ok {
385+
return -1 // ill-typed
420386
}
421-
return "", -1
422-
}
423-
424-
// stringConstantArg returns call's string constant argument at the index idx.
425-
//
426-
// ("", false) is returned if call's argument at the index idx isn't a string
427-
// constant.
428-
func stringConstantArg(pass *analysis.Pass, call *ast.CallExpr, idx int) (string, bool) {
429-
if idx >= len(call.Args) {
430-
return "", false
387+
if !sig.Variadic() {
388+
// Skip checking non-variadic functions.
389+
return -1
431390
}
432-
return stringConstantExpr(pass, call.Args[idx])
391+
idx := sig.Params().Len() - 2
392+
if idx < 0 {
393+
// Skip checking variadic functions without
394+
// fixed arguments.
395+
return -1
396+
}
397+
return idx
433398
}
434399

435400
// stringConstantExpr returns expression's string constant value.
@@ -536,10 +501,34 @@ type formatState struct {
536501

537502
// checkPrintf checks a call to a formatted print routine such as Printf.
538503
func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, fn *types.Func) {
539-
format, idx := formatString(pass, call)
540-
if idx < 0 {
541-
if false {
542-
pass.Reportf(call.Lparen, "can't check non-constant format in call to %s", fn.FullName())
504+
idx := formatStringIndex(pass, call)
505+
if idx < 0 || idx >= len(call.Args) {
506+
return
507+
}
508+
formatArg := call.Args[idx]
509+
format, ok := stringConstantExpr(pass, formatArg)
510+
if !ok {
511+
// Format string argument is non-constant.
512+
513+
// It is a common mistake to call fmt.Printf(msg) with a
514+
// non-constant format string and no arguments:
515+
// if msg contains "%", misformatting occurs.
516+
// Report the problem and suggest a fix: fmt.Printf("%s", msg).
517+
if idx == len(call.Args)-1 {
518+
pass.Report(analysis.Diagnostic{
519+
Pos: formatArg.Pos(),
520+
End: formatArg.End(),
521+
Message: fmt.Sprintf("non-constant format string in call to %s",
522+
fn.FullName()),
523+
SuggestedFixes: []analysis.SuggestedFix{{
524+
Message: `Insert "%s" format string`,
525+
TextEdits: []analysis.TextEdit{{
526+
Pos: formatArg.Pos(),
527+
End: formatArg.Pos(),
528+
NewText: []byte(`"%s", `),
529+
}},
530+
}},
531+
})
543532
}
544533
return
545534
}

go/analysis/passes/printf/printf_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ func Test(t *testing.T) {
1616
printf.Analyzer.Flags.Set("funcs", "Warn,Warnf")
1717

1818
analysistest.Run(t, testdata, printf.Analyzer, "a", "b", "nofmt", "typeparams")
19+
analysistest.RunWithSuggestedFixes(t, testdata, printf.Analyzer, "fix")
1920
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file contains tests of the printf checker's suggested fixes.
6+
7+
package fix
8+
9+
import (
10+
"fmt"
11+
"log"
12+
"os"
13+
)
14+
15+
func nonConstantFormat(s string) { // #60529
16+
fmt.Printf(s) // want `non-constant format string in call to fmt.Printf`
17+
fmt.Printf(s, "arg")
18+
fmt.Fprintf(os.Stderr, s) // want `non-constant format string in call to fmt.Fprintf`
19+
log.Printf(s) // want `non-constant format string in call to log.Printf`
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file contains tests of the printf checker's suggested fixes.
6+
7+
package fix
8+
9+
import (
10+
"fmt"
11+
"log"
12+
"os"
13+
)
14+
15+
func nonConstantFormat(s string) { // #60529
16+
fmt.Printf("%s", s) // want `non-constant format string in call to fmt.Printf`
17+
fmt.Printf(s, "arg")
18+
fmt.Fprintf(os.Stderr, "%s", s) // want `non-constant format string in call to fmt.Fprintf`
19+
log.Printf("%s", s) // want `non-constant format string in call to log.Printf`
20+
}

go/analysis/passes/tests/tests.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package tests
66

77
import (
88
_ "embed"
9-
"fmt"
109
"go/ast"
1110
"go/token"
1211
"go/types"
@@ -184,13 +183,13 @@ func checkAddCalls(pass *analysis.Pass, fn *ast.FuncDecl, params *types.Tuple) {
184183
i := mismatched[0]
185184
expr := call.Args[i]
186185
t := pass.TypesInfo.Types[expr].Type
187-
pass.ReportRangef(expr, fmt.Sprintf("mismatched type in call to (*testing.F).Add: %v, fuzz target expects %v", t, params.At(i+1).Type()))
186+
pass.ReportRangef(expr, "mismatched type in call to (*testing.F).Add: %v, fuzz target expects %v", t, params.At(i+1).Type())
188187
} else if len(mismatched) > 1 {
189188
var gotArgs, wantArgs []types.Type
190189
for i := 0; i < len(call.Args); i++ {
191190
gotArgs, wantArgs = append(gotArgs, pass.TypesInfo.Types[call.Args[i]].Type), append(wantArgs, params.At(i+1).Type())
192191
}
193-
pass.ReportRangef(call, fmt.Sprintf("mismatched types in call to (*testing.F).Add: %v, fuzz target expects %v", gotArgs, wantArgs))
192+
pass.ReportRangef(call, "mismatched types in call to (*testing.F).Add: %v, fuzz target expects %v", gotArgs, wantArgs)
194193
}
195194
}
196195
return true
@@ -244,7 +243,7 @@ func validateFuzzArgs(pass *analysis.Pass, params *types.Tuple, expr ast.Expr) b
244243
}
245244
}
246245
}
247-
pass.ReportRangef(exprRange, "fuzzing arguments can only have the following types: "+formatAcceptedFuzzType())
246+
pass.ReportRangef(exprRange, "fuzzing arguments can only have the following types: %s", formatAcceptedFuzzType())
248247
ok = false
249248
}
250249
}

0 commit comments

Comments
 (0)