[インデックス 15398] ファイルの概要
このコミットは、Go言語の静的解析ツールである cmd/vet
に、printf
形式の関数呼び出しにおける引数の型チェック機能を追加するものです。これにより、フォーマット文字列と引数の型が一致しない場合に vet
が警告を発するようになり、実行時エラーや予期せぬ動作を防ぐことができます。具体的には、fmt.Printf
などの関数で %d
に文字列を渡すといった誤った使用法を検出できるようになります。
コミット
commit 72b6daa3ba7a5842c07724cb49f41f178e1af778
Author: Rob Pike <r@golang.org>
Date: Sat Feb 23 15:08:36 2013 -0800
cmd/vet: check argument types in printf formats
Fixes #4404.
R=gri, rsc
CC=golang-dev
https://golang.org/cl/7378061
---
src/cmd/vet/Makefile | 2 +-\n src/cmd/vet/atomic.go | 2 +-\n src/cmd/vet/main.go | 7 +-\n src/cmd/vet/print.go | 266 +++++++++++++++++++++++++++++++++-----------\n src/cmd/vet/print_unsafe.go | 19 ++++\n 5 files changed, 231 insertions(+), 65 deletions(-)
diff --git a/src/cmd/vet/Makefile b/src/cmd/vet/Makefile
index 2cdf96261f..c0e3169989 100644
--- a/src/cmd/vet/Makefile
+++ b/src/cmd/vet/Makefile
@@ -3,6 +3,6 @@
# license that can be found in the LICENSE file.\n \n test testshort:\n-\tgo build\n+\tgo build -tags unsafe\n \t../../../test/errchk ./vet -printfuncs=\'Warn:1,Warnf:1\' *.go\n \ndiff --git a/src/cmd/vet/atomic.go b/src/cmd/vet/atomic.go
index 9c7ae7dbfc..0abc6f5241 100644
--- a/src/cmd/vet/atomic.go
+++ b/src/cmd/vet/atomic.go
@@ -10,7 +10,7 @@ import (\n \t\"sync/atomic\"\n )\n \n-// checkAtomicAssignment walks the assignment statement checking for comomon\n+// checkAtomicAssignment walks the assignment statement checking for common\n // mistaken usage of atomic package, such as: x = atomic.AddUint64(&x, 1)\n func (f *File) checkAtomicAssignment(n *ast.AssignStmt) {\n \tif !vet(\"atomic\") {\ndiff --git a/src/cmd/vet/main.go b/src/cmd/vet/main.go
index 0fe26f8725..a00b299ad4 100644
--- a/src/cmd/vet/main.go
+++ b/cmd/vet/main.go
@@ -160,7 +160,8 @@ func doPackageDir(directory string) {\n }\n \n type Package struct {\n-\ttypes map[ast.Expr]types.Type\n+\ttypes map[ast.Expr]types.Type\n+\tvalues map[ast.Expr]interface{}\n }\n \n // doPackage analyzes the single package constructed from the named files.\n@@ -188,8 +189,12 @@ func doPackage(names []string) {\n \t}\n \tpkg := new(Package)\n \tpkg.types = make(map[ast.Expr]types.Type)\n+\tpkg.values = make(map[ast.Expr]interface{})\n \texprFn := func(x ast.Expr, typ types.Type, val interface{}) {\n \t\tpkg.types[x] = typ\n+\t\tif val != nil {\n+\t\t\tpkg.values[x] = val\n+\t\t}\n \t}\n \tcontext := types.Context{\n \t\tExpr: exprFn,\ndiff --git a/src/cmd/vet/print.go b/src/cmd/vet/print.go
index 007bb3f0f4..b164a9b588 100644
--- a/src/cmd/vet/print.go
+++ b/src/cmd/vet/print.go
@@ -77,8 +77,11 @@ func (f *File) literal(value ast.Expr) *ast.BasicLit {\n \t\t\tx, errX := strconv.Unquote(litX.Value)\n \t\t\ty, errY := strconv.Unquote(litY.Value)\n \t\t\tif errX == nil && errY == nil {\n-\t\t\t\tlit.Value = strconv.Quote(x + y)\n-\t\t\t\treturn &lit\n+\t\t\t\treturn &ast.BasicLit{\n+\t\t\t\t\tValuePos: lit.ValuePos,\n+\t\t\t\t\tKind: lit.Kind,\n+\t\t\t\t\tValue: strconv.Quote(x + y),\n+\t\t\t\t}\n \t\t\t}\n \t\t}\n \tcase *ast.Ident:\n@@ -104,13 +107,12 @@ func (f *File) literal(value ast.Expr) *ast.BasicLit {\n }\n \n // checkPrintf checks a call to a formatted print routine such as Printf.\n-// The skip argument records how many arguments to ignore; that is,\n-// call.Args[skip] is (well, should be) the format argument.\n-func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {\n-\tif len(call.Args) <= skip {\n+// call.Args[formatIndex] is (well, should be) the format argument.\n+func (f *File) checkPrintf(call *ast.CallExpr, name string, formatIndex int) {\n+\tif formatIndex >= len(call.Args) {\n \t\treturn\n \t}\n-\tlit := f.literal(call.Args[skip])\n+\tlit := f.literal(call.Args[formatIndex])\n \tif lit == nil {\n \t\tif *verbose {\n \t\t\tf.Warn(call.Pos(), \"can\'t check non-literal format in call to\", name)\n@@ -122,60 +124,69 @@ func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {\n \t}\n \tformat, err := strconv.Unquote(lit.Value)\n \tif err != nil {\n+\t\t// Shouldn\'t happen if parser returned no errors, but be safe.\n \t\tf.Badf(call.Pos(), \"invalid quoted string literal\")\n \t}\n+\tfirstArg := formatIndex + 1 // Arguments are immediately after format string.\n \tif !strings.Contains(format, \"%\") {\n-\t\tif len(call.Args) > skip+1 {\n+\t\tif len(call.Args) > firstArg {\n \t\t\tf.Badf(call.Pos(), \"no formatting directive in %s call\", name)\n \t\t}\n \t\treturn\n \t}\n \t// Hard part: check formats against args.\n-\t// Trivial but useful test: count.\n-\tnumArgs := 0\n+\targNum := firstArg\n \tfor i, w := 0, 0; i < len(format); i += w {\n \t\tw = 1\n \t\tif format[i] == \'%\' {\n-\t\t\tnbytes, nargs := f.parsePrintfVerb(call, format[i:])\n+\t\t\tverb, flags, nbytes, nargs := f.parsePrintfVerb(call, format[i:])\n \t\t\tw = nbytes\n-\t\t\tnumArgs += nargs\n+\t\t\tif verb == \'%\' { // \"%%\" does nothing interesting.\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\t// If we\'ve run out of args, print after loop will pick that up.\n+\t\t\tif argNum+nargs <= len(call.Args) {\n+\t\t\t\tf.checkPrintfArg(call, verb, flags, argNum, nargs)\n+\t\t\t}\n+\t\t\targNum += nargs\n \t\t}\n \t}\n-\texpect := len(call.Args) - (skip + 1)\n-\t// Don\'t be too strict on dotdotdot.\n-\tif call.Ellipsis.IsValid() && numArgs >= expect {\n+\t// TODO: Dotdotdot is hard.\n+\tif call.Ellipsis.IsValid() && argNum != len(call.Args) {\n \t\treturn\n \t}\n-\tif numArgs != expect {\n-\t\tf.Badf(call.Pos(), \"wrong number of args in %s call: %d needed but %d args\", name, numArgs, expect)\n+\tif argNum != len(call.Args) {\n+\t\texpect := argNum - firstArg\n+\t\tnumArgs := len(call.Args) - firstArg\n+\t\tf.Badf(call.Pos(), \"wrong number of args for format in %s call: %d needed but %d args\", name, expect, numArgs)\n \t}\n }\n \n-// parsePrintfVerb returns the number of bytes and number of arguments\n-// consumed by the Printf directive that begins s, including its percent sign\n-// and verb.\n-func (f *File) parsePrintfVerb(call *ast.CallExpr, s string) (nbytes, nargs int) {\n+// parsePrintfVerb returns the verb that begins the format string, along with its flags,\n+// the number of bytes to advance the format to step past the verb, and number of\n+// arguments it consumes.\n+func (f *File) parsePrintfVerb(call *ast.CallExpr, format string) (verb rune, flags []byte, nbytes, nargs int) {\n \t// There\'s guaranteed a percent sign.\n-\tflags := make([]byte, 0, 5)\n+\tflags = make([]byte, 0, 5)\n \tnbytes = 1\n-\tend := len(s)\n+\tend := len(format)\n \t// There may be flags.\n FlagLoop:\n \tfor nbytes < end {\n-\t\tswitch s[nbytes] {\n+\t\tswitch format[nbytes] {\n \t\tcase \'#\', \'0\', \'+\', \'-\', \' \':\n-\t\t\tflags = append(flags, s[nbytes])\n+\t\t\tflags = append(flags, format[nbytes])\n \t\t\tnbytes++\n \t\tdefault:\n \t\t\tbreak FlagLoop\n \t\t}\n \t}\n \tgetNum := func() {\n-\t\tif nbytes < end && s[nbytes] == \'*\' {\n+\t\tif nbytes < end && format[nbytes] == \'*\' {\n \t\t\tnbytes++\n \t\t\tnargs++\n \t\t} else {\n-\t\t\tfor nbytes < end && \'0\' <= s[nbytes] && s[nbytes] <= \'9\' {\n+\t\t\tfor nbytes < end && \'0\' <= format[nbytes] && format[nbytes] <= \'9\' {\n \t\t\t\tnbytes++\n \t\t\t}\n \t\t}\n@@ -183,24 +194,38 @@ FlagLoop:\n \t// There may be a width.\n \tgetNum()\n \t// If there\'s a period, there may be a precision.\n-\tif nbytes < end && s[nbytes] == \'.\' {\n+\tif nbytes < end && format[nbytes] == \'.\' {\n \t\tflags = append(flags, \'.\') // Treat precision as a flag.\n \t\tnbytes++\n \t\tgetNum()\n \t}\n \t// Now a verb.\n-\tc, w := utf8.DecodeRuneInString(s[nbytes:])\n+\tc, w := utf8.DecodeRuneInString(format[nbytes:])\n \tnbytes += w\n+\tverb = c\n \tif c != \'%\' {\n \t\tnargs++\n-\t\tf.checkPrintfVerb(call, c, flags)\n \t}\n \treturn\n }\n \n+// printfArgType encodes the types of expressions a printf verb accepts. It is a bitmask.\n+type printfArgType int\n+\n+const (\n+\targBool printfArgType = 1 << iota\n+\targInt\n+\targRune\n+\targString\n+\targFloat\n+\targPointer\n+\tanyType printfArgType = ^0\n+)\n+\n type printVerb struct {\n \tverb rune\n \tflags string // known flags are all ASCII\n+\ttyp printfArgType\n }\n \n // Common flag sets for printf verbs.\n@@ -219,36 +244,52 @@ var printVerbs = []printVerb{\n \t// \'+\' is required sign for numbers, Go format for %v.\n \t// \'#\' is alternate format for several verbs.\n \t// \' \' is spacer for numbers\n-\t{\'b\', numFlag},\n-\t{\'c\', \"-\"},\n-\t{\'d\', numFlag},\n-\t{\'e\', numFlag},\n-\t{\'E\', numFlag},\n-\t{\'f\', numFlag},\n-\t{\'F\', numFlag},\n-\t{\'g\', numFlag},\n-\t{\'G\', numFlag},\n-\t{\'o\', sharpNumFlag},\n-\t{\'p\', \"-#\"},\n-\t{\'q\', \" -+.0#\"},\n-\t{\'s\', \" -+.0\"},\n-\t{\'t\', \"-\"},\n-\t{\'T\', \"-\"},\n-\t{\'U\', \"-#\"},\n-\t{\'v\', allFlags},\n-\t{\'x\', sharpNumFlag},\n-\t{\'X\', sharpNumFlag},\n+\t{\'b\', numFlag, argInt},\n+\t{\'c\', \"-\", argRune | argInt},\n+\t{\'d\', numFlag, argInt},\n+\t{\'e\', numFlag, argFloat},\n+\t{\'E\', numFlag, argFloat},\n+\t{\'f\', numFlag, argFloat},\n+\t{\'F\', numFlag, argFloat},\n+\t{\'g\', numFlag, argFloat},\n+\t{\'G\', numFlag, argFloat},\n+\t{\'o\', sharpNumFlag, argInt},\n+\t{\'p\', \"-#\", argPointer},\n+\t{\'q\', \" -+.0#\", argRune | argInt | argString},\n+\t{\'s\', \" -+.0\", argString},\n+\t{\'t\', \"-\", argBool},\n+\t{\'T\', \"-\", anyType},\n+\t{\'U\', \"-#\", argRune | argInt},\n+\t{\'v\', allFlags, anyType},\n+\t{\'x\', sharpNumFlag, argRune | argInt | argString},\n+\t{\'X\', sharpNumFlag, argRune | argInt | argString},\n }\n \n const printfVerbs = \"bcdeEfFgGopqstTvxUX\"\n \n-func (f *File) checkPrintfVerb(call *ast.CallExpr, verb rune, flags []byte) {\n+func (f *File) checkPrintfArg(call *ast.CallExpr, verb rune, flags []byte, argNum, nargs int) {\n \t// Linear scan is fast enough for a small list.\n \tfor _, v := range printVerbs {\n \t\tif v.verb == verb {\n \t\t\tfor _, flag := range flags {\n \t\t\t\tif !strings.ContainsRune(v.flags, rune(flag)) {\n \t\t\t\t\tf.Badf(call.Pos(), \"unrecognized printf flag for verb %q: %q\", verb, flag)\n+\t\t\t\t\treturn\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\t// Verb is good. If nargs>1, we have something like %.*s and all but the final\n+\t\t\t// arg must be integer.\n+\t\t\tfor i := 0; i < nargs-1; i++ {\n+\t\t\t\tif !f.matchArgType(argInt, call.Args[argNum+i]) {\n+\t\t\t\t\tf.Badf(call.Pos(), \"arg for * in printf format not of type int\")\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tfor _, v := range printVerbs {\n+\t\t\t\tif v.verb == verb {\n+\t\t\t\t\tif !f.matchArgType(v.typ, call.Args[argNum+nargs-1]) {\n+\t\t\t\t\t\tf.Badf(call.Pos(), \"arg for printf verb %%%c of wrong type\", verb)\n+\t\t\t\t\t}\n+\t\t\t\t\tbreak\n \t\t\t\t}\n \t\t\t}\n \t\t\treturn\n@@ -257,15 +298,65 @@ func (f *File) checkPrintfVerb(call *ast.CallExpr, verb rune, flags []byte) {\n \tf.Badf(call.Pos(), \"unrecognized printf verb %q\", verb)\n }\n \n+func (f *File) matchArgType(t printfArgType, arg ast.Expr) bool {\n+\tif f.pkg == nil {\n+\t\treturn true // Don\'t know; assume OK.\n+\t}\n+\t// TODO: for now, we can only test builtin types and untyped constants.\n+\ttyp := f.pkg.types[arg]\n+\tif typ == nil {\n+\t\treturn true\n+\t}\n+\tbasic, ok := typ.(*types.Basic)\n+\tif !ok {\n+\t\treturn true\n+\t}\n+\tswitch basic.Kind {\n+\tcase types.Bool:\n+\t\treturn t&argBool != 0\n+\tcase types.Int, types.Int8, types.Int16, types.Int32, types.Int64:\n+\t\tfallthrough\n+\tcase types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64, types.Uintptr:\n+\t\treturn t&argInt != 0\n+\tcase types.Float32, types.Float64, types.Complex64, types.Complex128:\n+\t\treturn t&argFloat != 0\n+\tcase types.String:\n+\t\treturn t&argString != 0\n+\tcase types.UnsafePointer:\n+\t\treturn t&argPointer != 0\n+\tcase types.UntypedBool:\n+\t\treturn t&argBool != 0\n+\tcase types.UntypedComplex:\n+\t\treturn t&argFloat != 0\n+\tcase types.UntypedFloat:\n+\t\t// If it\'s integral, we can use an int format.\n+\t\tswitch f.pkg.values[arg].(type) {\n+\t\tcase int, int8, int16, int32, int64:\n+\t\t\treturn t&(argInt|argFloat) != 0\n+\t\tcase uint, uint8, uint16, uint32, uint64:\n+\t\t\treturn t&(argInt|argFloat) != 0\n+\t\t}\n+\t\treturn t&argFloat != 0\n+\tcase types.UntypedInt:\n+\t\treturn t&(argInt|argFloat) != 0 // You might say Printf(\"%g\", 1234)\n+\tcase types.UntypedRune:\n+\t\treturn t&(argInt|argRune) != 0\n+\tcase types.UntypedString:\n+\t\treturn t&argString != 0\n+\tcase types.UntypedNil:\n+\t\treturn t&argPointer != 0 // TODO?\n+\t}\n+\treturn false\n+}\n+\n // checkPrint checks a call to an unformatted print routine such as Println.\n-// The skip argument records how many arguments to ignore; that is,\n-// call.Args[skip] is the first argument to be printed.\n-func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {\n+// call.Args[firstArg] is the first argument to be printed.\n+func (f *File) checkPrint(call *ast.CallExpr, name string, firstArg int) {\n \tisLn := strings.HasSuffix(name, \"ln\")\n \tisF := strings.HasPrefix(name, \"F\")\n \targs := call.Args\n \t// check for Println(os.Stderr, ...)\n-\tif skip == 0 && !isF && len(args) > 0 {\n+\tif firstArg == 0 && !isF && len(args) > 0 {\n \t\tif sel, ok := args[0].(*ast.SelectorExpr); ok {\n \t\t\tif x, ok := sel.X.(*ast.Ident); ok {\n \t\t\t\tif x.Name == \"os\" && strings.HasPrefix(sel.Sel.Name, \"Std\") {\n@@ -274,7 +365,7 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {\n \t\t\t}\n \t\t}\n \t}\n-\tif len(args) <= skip {\n+\tif len(args) <= firstArg {\n \t\t// If we have a call to a method called Error that satisfies the Error interface,\n \t\t// then it\'s ok. Otherwise it\'s something like (*T).Error from the testing package\n \t\t// and we need to check it.\n@@ -284,13 +375,13 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {\n \t\t// If it\'s an Error call now, it\'s probably for printing errors.\n \t\tif !isLn {\n \t\t\t// Check the signature to be sure: there are niladic functions called \"error\".\n-\t\t\tif f.pkg == nil || skip != 0 || f.numArgsInSignature(call) != skip {\n+\t\t\tif f.pkg == nil || firstArg != 0 || f.numArgsInSignature(call) != firstArg {\n \t\t\t\tf.Badf(call.Pos(), \"no args in %s call\", name)\n \t\t\t}\n \t\t}\n \t\treturn\n \t}\n-\targ := args[skip]\n+\targ := args[firstArg]\n \tif lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING {\n \t\tif strings.Contains(lit.Value, \"%\") {\n \t\t\tf.Badf(call.Pos(), \"possible formatting directive in %s call\", name)\n@@ -399,15 +490,66 @@ func (errorTest5) error() { // niladic; don\'t complain if no args (was bug)\n // This function never executes, but it serves as a simple test for the program.\n // Test with make test.\n func BadFunctionUsedInTests() {\n+\tvar b bool\n+\tvar i int\n+\tvar r rune\n+\tvar s string\n+\tvar x float64\n+\tvar p *int\n+\t// Some good format/argtypes\n+\tfmt.Printf(\"\")\n+\tfmt.Printf(\"%b %b\", 3, i)\n+\tfmt.Printf(\"%c %c %c %c\", 3, i, \'x\', r)\n+\tfmt.Printf(\"%d %d\", 3, i)\n+\tfmt.Printf(\"%e %e %e\", 3, 3e9, x)\n+\tfmt.Printf(\"%E %E %E\", 3, 3e9, x)\n+\tfmt.Printf(\"%f %f %f\", 3, 3e9, x)\n+\tfmt.Printf(\"%F %F %F\", 3, 3e9, x)\n+\tfmt.Printf(\"%g %g %g\", 3, 3e9, x)\n+\tfmt.Printf(\"%G %G %G\", 3, 3e9, x)\n+\tfmt.Printf(\"%o %o\", 3, i)\n+\tfmt.Printf(\"%p %p\", p, nil)\n+\tfmt.Printf(\"%q %q %q %q\", 3, i, \'x\', r)\n+\tfmt.Printf(\"%s %s\", \"hi\", s)\n+\tfmt.Printf(\"%t %t\", true, b)\n+\tfmt.Printf(\"%T %T\", 3, i)\n+\tfmt.Printf(\"%U %U\", 3, i)\n+\tfmt.Printf(\"%v %v\", 3, i)\n+\tfmt.Printf(\"%x %x %x %x\", 3, i, \"hi\", s)\n+\tfmt.Printf(\"%X %X %X %X\", 3, i, \"hi\", s)\n+\tfmt.Printf(\"%.*s %d %g\", 3, \"hi\", 23, 2.3)\n+\t// Some bad format/argTypes\n+\tfmt.Printf(\"%b\", 2.3) // ERROR \"arg for printf verb %b of wrong type\"\n+\tfmt.Printf(\"%c\", 2.3) // ERROR \"arg for printf verb %c of wrong type\"\n+\tfmt.Printf(\"%d\", 2.3) // ERROR \"arg for printf verb %d of wrong type\"\n+\tfmt.Printf(\"%e\", \"hi\") // ERROR \"arg for printf verb %e of wrong type\"\n+\tfmt.Printf(\"%E\", true) // ERROR \"arg for printf verb %E of wrong type\"\n+\tfmt.Printf(\"%f\", \"hi\") // ERROR \"arg for printf verb %f of wrong type\"\n+\tfmt.Printf(\"%F\", \'x\') // ERROR \"arg for printf verb %F of wrong type\"\n+\tfmt.Printf(\"%g\", \"hi\") // ERROR \"arg for printf verb %g of wrong type\"\n+\tfmt.Printf(\"%G\", i) // ERROR \"arg for printf verb %G of wrong type\"\n+\tfmt.Printf(\"%o\", x) // ERROR \"arg for printf verb %o of wrong type\"\n+\tfmt.Printf(\"%p\", 23) // ERROR \"arg for printf verb %p of wrong type\"\n+\tfmt.Printf(\"%q\", x) // ERROR \"arg for printf verb %q of wrong type\"\n+\tfmt.Printf(\"%s\", b) // ERROR \"arg for printf verb %s of wrong type\"\n+\tfmt.Printf(\"%t\", 23) // ERROR \"arg for printf verb %t of wrong type\"\n+\tfmt.Printf(\"%U\", x) // ERROR \"arg for printf verb %U of wrong type\"\n+\tfmt.Printf(\"%x\", nil) // ERROR \"arg for printf verb %x of wrong type\"\n+\tfmt.Printf(\"%X\", 2.3) // ERROR \"arg for printf verb %X of wrong type\"\n+\tfmt.Printf(\"%.*s %d %g\", 3, \"hi\", 23, \'x\') // ERROR \"arg for printf verb %g of wrong type\"\n+\t// TODO\n \tfmt.Println() // not an error\n \tfmt.Println(\"%s\", \"hi\") // ERROR \"possible formatting directive in Println call\"\n-\tfmt.Printf(\"%s\", \"hi\", 3) // ERROR \"wrong number of args in Printf call\"\n-\tfmt.Printf(\"%\"+(\"s\"), \"hi\", 3) // ERROR \"wrong number of args in Printf call\"\n+\tfmt.Printf(\"%s\", \"hi\", 3) // ERROR \"wrong number of args for format in Printf call\"\n+\tfmt.Printf(\"%\"+(\"s\"), \"hi\", 3) // ERROR \"wrong number of args for format in Printf call\"\n \tfmt.Printf(\"%s%%%d\", \"hi\", 3) // correct\n \tfmt.Printf(\"%08s\", \"woo\") // correct\n \tfmt.Printf(\"% 8s\", \"woo\") // correct\n \tfmt.Printf(\"%.*d\", 3, 3) // correct\n-\tfmt.Printf(\"%.*d\", 3, 3, 3) // ERROR \"wrong number of args in Printf call\"\n+\tfmt.Printf(\"%.*d\", 3, 3, 3) // ERROR \"wrong number of args for format in Printf call\"\n+\tfmt.Printf(\"%.*d\", \"hi\", 3) // ERROR \"arg for \\* in printf format not of type int\"\n+\tfmt.Printf(\"%.*d\", i, 3) // correct\n+\tfmt.Printf(\"%.*d\", s, 3) // ERROR \"arg for \\* in printf format not of type int\"\n \tfmt.Printf(\"%q %q\", multi()...) // ok\n \tfmt.Printf(\"%#q\", `blah`) // ok\n \tprintf(\"now is the time\", \"buddy\") // ERROR \"no formatting directive\"\n@@ -415,10 +557,10 @@ func BadFunctionUsedInTests() {\n \tPrintf(\"hi\") // ok\n \tconst format = \"%s %s\\n\"\n \tPrintf(format, \"hi\", \"there\")\n-\tPrintf(format, \"hi\") // ERROR \"wrong number of args in Printf call\"\n+\tPrintf(format, \"hi\") // ERROR \"wrong number of args for format in Printf call\"\n \tf := new(File)\n \tf.Warn(0, \"%s\", \"hello\", 3) // ERROR \"possible formatting directive in Warn call\"\n-\tf.Warnf(0, \"%s\", \"hello\", 3) // ERROR \"wrong number of args in Warnf call\"\n+\tf.Warnf(0, \"%s\", \"hello\", 3) // ERROR \"wrong number of args for format in Warnf call\"\n \tf.Warnf(0, \"%r\", \"hello\") // ERROR \"unrecognized printf verb\"\n \tf.Warnf(0, \"%#s\", \"hello\") // ERROR \"unrecognized printf flag\"\n \t// Something that satisfies the error interface.\ndiff --git a/src/cmd/vet/print_unsafe.go b/src/cmd/vet/print_unsafe.go
new file mode 100644
index 0000000000..1446b927dc
--- /dev/null
+++ b/src/cmd/vet/print_unsafe.go
@@ -0,0 +1,19 @@\n+// Copyright 2013 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// +build unsafe\n+\n+// This file contains a special test for the printf-checker that tests unsafe.Pointer.\n+\n+package main\n+\n+import (\n+\t\"fmt\"\n+\t\"unsafe\" // just for test case printing unsafe.Pointer\n+)\n+\n+func UnsafePointerPrintfTest() {\n+\tvar up *unsafe.Pointer\n+\tfmt.Printf(\"%p\", up)\n+}\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/72b6daa3ba7a5842c07724cb49f41f178e1af778](https://github.com/golang/go/commit/72b6daa3ba7a5842c07724cb49f41f178e1af778)
## 元コミット内容
`cmd/vet`: `printf` 形式の関数呼び出しにおける引数の型をチェックする。
Issue #4404 を修正。
## 変更の背景
Go言語の `fmt` パッケージには、C言語の `printf` に似た書式指定文字列を用いた出力関数群(`Printf`, `Sprintf`, `Errorf` など)があります。これらの関数は、書式指定文字列(例: `%d`, `%s`, `%f`)とそれに続く引数の型が一致していることを前提としています。しかし、プログラマが誤って異なる型の引数を渡してしまうと、コンパイル時にはエラーにならないものの、実行時にパニック(プログラムの異常終了)を引き起こしたり、予期せぬ出力になったりする可能性がありました。
例えば、整数を期待する `%d` に文字列を渡した場合、Goコンパイラはこれを構文エラーとはみなしません。しかし、実行時には `fmt` パッケージが型変換に失敗し、プログラムがクラッシュする原因となります。
`go vet` は、このようなコンパイラでは検出できないが、実行時に問題を引き起こす可能性のあるコードパターンを静的に解析し、警告を発するためのツールです。このコミット以前の `go vet` は、`printf` 形式の関数呼び出しにおいて、引数の数と書式指定子の数が一致するかどうかはチェックしていましたが、引数の「型」が書式指定子と一致するかどうかまではチェックしていませんでした。
Issue #4404 は、まさにこの「引数の型チェックの欠如」を指摘しており、このコミットはその問題を解決するために導入されました。これにより、開発者はより早期に潜在的なバグを発見し、堅牢なコードを書くことができるようになります。
## 前提知識の解説
### `go vet`
`go vet` は、Go言語に標準で付属する静的解析ツールです。コンパイラが検出できないような、しかし潜在的に問題を引き起こす可能性のあるコードの慣用的な誤用や疑わしい構造を検出することを目的としています。`printf` 形式の書式チェックの他にも、以下のような様々なチェックを行います。
* **`printf` 形式の書式チェック**: フォーマット文字列と引数の不一致(引数の数、型の不一致、不正な書式指定子など)。
* **`atomic` パッケージの誤用**: `sync/atomic` パッケージの関数が正しく使用されているか。
* **構造体タグの誤り**: `json` や `xml` などの構造体タグの構文エラー。
* **到達不能なコード**: 常に実行されないコードパス。
* **ロックの誤用**: `sync.Mutex` などのロックが正しく取得・解放されているか。
* **シャドーイング**: 変数が意図せずシャドーイングされている可能性。
`go vet` は、`go build` や `go test` とは異なり、コードのコンパイルや実行は行いません。ソースコードを解析し、問題のあるパターンを報告するだけです。これにより、開発者はコードレビューやテストの前に、一般的な間違いを自動的に検出できます。
### `printf` 形式の書式指定子と型
Go言語の `fmt` パッケージにおける `printf` 形式の関数は、C言語と同様に書式指定子(verb)を使用して引数の出力形式を制御します。主要な書式指定子とその期待される型は以下の通りです。
* **一般**:
* `%v`: 値をデフォルトの形式で出力。任意の型を受け入れる。
* `%T`: 値の型を出力。任意の型を受け入れる。
* **真偽値**:
* `%t`: `true` または `false`。`bool` 型を期待。
* **整数**:
* `%b`: 2進数。整数型を期待。
* `%d`: 10進数。整数型を期待。
* `%o`: 8進数。整数型を期待。
* `%x`, `%X`: 16進数。整数型を期待。
* `%c`: Unicode文字。`rune` または整数型を期待。
* **浮動小数点数**:
* `%e`, `%E`: 科学表記。浮動小数点数型を期待。
* `%f`, `%F`: 小数点表記。浮動小数点数型を期待。
* `%g`, `%G`: 状況に応じて `%e` または `%f` を選択。浮動小数点数型を期待。
* **文字列**:
* `%s`: 文字列。`string` 型を期待。
* `%q`: クォートされた文字列。`string` 型を期待。
* **ポインタ**:
* `%p`: ポインタのアドレス。ポインタ型を期待。
これらの書式指定子には、幅、精度、フラグ(例: `#`, `0`, `+`, `-`, ` `)などのオプションを組み合わせることができます。例えば、`%.*s` は、可変長の精度を持つ文字列を出力する書式指定子で、アスタリスク (`*`) の部分には整数型の引数が、`s` の部分には文字列型の引数が必要です。
### Go言語の型システムと静的解析
Go言語は静的型付け言語であり、コンパイル時に厳密な型チェックが行われます。しかし、`interface{}` 型(Goにおける任意の型)やリフレクションを使用する場面では、コンパイル時の型チェックだけでは不十分な場合があります。`fmt.Printf` のような可変引数関数は、内部的に `interface{}` を利用して様々な型の引数を受け取ります。
`go vet` は、Goのコンパイラが生成する抽象構文木(AST)や型情報(`go/types` パッケージ)を利用して、プログラムの構造と型情報を詳細に分析します。このコミットでは、`go/types` パッケージから取得できる型情報と、`printf` の書式指定子が期待する型を照合することで、より高度な型チェックを実現しています。
## 技術的詳細
このコミットの主要な変更点は、`cmd/vet` が `printf` 形式の関数呼び出しにおいて、フォーマット文字列の各書式指定子に対応する引数の「型」が正しいかどうかを検証する機能を追加したことです。
### `Package` 構造体への `values` フィールドの追加
`src/cmd/vet/main.go` において、`Package` 構造体に `values` フィールドが追加されました。
```go
type Package struct {
types map[ast.Expr]types.Type
values map[ast.Expr]interface{}
}
types
フィールドは以前から存在し、ASTの各式 (ast.Expr
) に対応する型 (types.Type
) を格納していました。今回追加された values
フィールドは、コンパイル時に定数として評価できる式(リテラルなど)の「値」を格納するために使用されます。これにより、vet
は単に型だけでなく、具体的な値(例えば、123
や "hello"
)も考慮に入れたチェックが可能になります。特に、printf
の書式指定子の中には、%.*s
のようにアスタリスク (*
) を用いて幅や精度を動的に指定するものがあり、このアスタリスクに対応する引数は整数型の定数である必要があります。values
フィールドはこのようなケースで役立ちます。
checkPrintf
関数の変更
src/cmd/vet/print.go
の checkPrintf
関数は、printf
形式の関数呼び出しを解析する主要な関数です。この関数は大幅に改修されました。
- 引数インデックスの変更:
skip
引数がformatIndex
に変更され、フォーマット文字列がcall.Args
の何番目の引数であるかをより明確に示しています。 - 引数の数チェックの改善: フォーマット文字列中の書式指定子によって期待される引数の数 (
argNum
) と、実際に渡された引数の数 (len(call.Args) - firstArg
) を比較し、不一致があれば警告を発します。以前はnumArgs
という変数で単純に書式指定子の数を数えていましたが、より厳密なチェックが行われるようになりました。 parsePrintfVerb
の戻り値の拡張:parsePrintfVerb
関数は、書式指定子(verb)とそのフラグ、消費するバイト数、消費する引数の数に加えて、verb rune
とflags []byte
を返すようになりました。これにより、書式指定子の詳細な情報を取得できるようになりました。checkPrintfArg
の導入: 各書式指定子に対応する引数の型チェックを行うために、新たにcheckPrintfArg
関数が導入されました。
printfArgType
と printVerb
構造体の導入
printf
の書式指定子とそれらが期待する引数の型をマッピングするために、新しい型と構造体が定義されました。
-
printfArgType
:int
型のビットマスクとして定義され、printf
の書式指定子が受け入れる引数の型を表します。type printfArgType int const ( argBool printfArgType = 1 << iota argInt argRune argString argFloat argPointer anyType printfArgType = ^0 )
これにより、例えば
%c
がrune
またはint
を受け入れるといった、複数の型を許容する書式指定子を表現できます。 -
printVerb
: 各書式指定子(verb)とその許容されるフラグ、そして期待される引数の型 (typ printfArgType
) を格納する構造体です。type printVerb struct { verb rune flags string // known flags are all ASCII typ printfArgType }
printVerbs
というグローバル変数に、全ての標準的なprintf
書式指定子とその対応するprintVerb
エントリが定義されています。
checkPrintfArg
関数の実装
checkPrintfArg
関数は、特定の書式指定子 (verb
) とそのフラグ (flags
)、そして対応する引数 (call.Args[argNum]
) を受け取り、型チェックを実行します。
- フラグのチェック: まず、書式指定子に指定されたフラグがその書式指定子で許容されているかをチェックします。
- 可変幅/精度の引数チェック:
%.*s
のようにアスタリスク (*
) を含む書式指定子の場合、アスタリスクに対応する引数(通常は最初の引数)がint
型であることをmatchArgType
を使って確認します。 - 主要な引数の型チェック: 最後に、書式指定子に対応する主要な引数(通常は最後の引数)の型が、
printVerbs
で定義されたtyp
と一致するかをmatchArgType
を使って確認します。
matchArgType
関数の実装
matchArgType
関数は、printfArgType
と実際の引数 (ast.Expr
) を受け取り、引数の型が printfArgType
で許容される型と一致するかどうかを判定します。
この関数は、f.pkg.types[arg]
を使用して引数の型情報を取得します。go/types
パッケージの types.Basic
型を利用して、Goの組み込み型(bool
, int
, float
, string
, unsafe.Pointer
など)と printfArgType
のビットマスクを比較します。
特に注目すべきは、types.Untyped*
のような型なし定数に対する処理です。例えば、型なしの浮動小数点数リテラル (types.UntypedFloat
) が整数として表現できる場合(例: 3.0
)、argInt
と argFloat
の両方を許容するようにしています。これにより、fmt.Printf("%d", 3.0)
のようなコードでも、値が整数であれば警告が出ないように柔軟に対応しています。
print_unsafe.go
の追加
src/cmd/vet/print_unsafe.go
という新しいファイルが追加されました。このファイルは // +build unsafe
ディレクティブを持つため、unsafe
ビルドタグが指定された場合にのみコンパイルされます。このファイルには、unsafe.Pointer
型の引数に対する %p
書式指定子のテストケースが含まれています。これは、unsafe.Pointer
が特殊な型であるため、その型チェックが正しく機能するかを確認するためのものです。
コアとなるコードの変更箇所
src/cmd/vet/Makefile
:go build
コマンドに-tags unsafe
が追加され、print_unsafe.go
がビルドに含まれるようになりました。src/cmd/vet/main.go
:Package
構造体にvalues
フィールドが追加され、doPackage
関数内でその値が設定されるようになりました。src/cmd/vet/print.go
:checkPrintf
関数のシグネチャと内部ロジックが大幅に変更され、引数の型チェックが導入されました。parsePrintfVerb
関数の戻り値が拡張され、書式指定子の詳細な情報が返されるようになりました。printfArgType
型とprintVerb
構造体が新しく定義されました。printVerbs
グローバル変数に、各書式指定子に対応する期待される引数の型情報が追加されました。checkPrintfVerb
関数がcheckPrintfArg
にリネームされ、引数の型チェックロジックが実装されました。matchArgType
関数が新しく追加され、実際の引数の型と期待されるprintfArgType
を比較するロジックが実装されました。BadFunctionUsedInTests
関数に、型チェックのテストケース(正しい使用例と誤った使用例)が多数追加されました。
src/cmd/vet/print_unsafe.go
:unsafe.Pointer
のprintf
フォーマットチェックをテストするための新しいファイルが追加されました。
コアとなるコードの解説
src/cmd/vet/main.go
の変更
type Package struct {
types map[ast.Expr]types.Type
values map[ast.Expr]interface{} // 追加
}
// doPackage 関数内
// ...
pkg.types = make(map[ast.Expr]types.Type)
pkg.values = make(map[ast.Expr]interface{}) // 追加
exprFn := func(x ast.Expr, typ types.Type, val interface{}) {
pkg.types[x] = typ
if val != nil { // val が nil でない場合のみ values に格納
pkg.values[x] = val
}
}
context := types.Context{
Expr: exprFn,
// ...
Package
構造体に values
マップが追加されたことで、vet
はASTノードに対応する型情報だけでなく、そのノードが定数として評価される場合の具体的な値も取得できるようになりました。これは、printf
の書式指定子で %.*s
のようにアスタリスク (*
) を使用して幅や精度を動的に指定する場合に、そのアスタリスクに対応する引数が整数型の定数であるかをチェックするために利用されます。
src/cmd/vet/print.go
の変更
checkPrintf
関数の変更点
// checkPrintf checks a call to a formatted print routine such as Printf.
// call.Args[formatIndex] is (well, should be) the format argument.
func (f *File) checkPrintf(call *ast.CallExpr, name string, formatIndex int) {
// ... (フォーマット文字列の取得と解析) ...
firstArg := formatIndex + 1 // Arguments are immediately after format string.
// ... (フォーマット文字列に '%' が含まれない場合の引数数チェック) ...
argNum := firstArg // 現在処理している引数のインデックス
for i, w := 0, 0; i < len(format); i += w {
w = 1
if format[i] == '%' {
verb, flags, nbytes, nargs := f.parsePrintfVerb(call, format[i:])
w = nbytes
if verb == '%' { // "%%" は特別な処理をしない
continue
}
// 引数が残っている場合のみ型チェックを実行
if argNum+nargs <= len(call.Args) {
f.checkPrintfArg(call, verb, flags, argNum, nargs)
}
argNum += nargs // 消費した引数の数だけインデックスを進める
}
}
// ... (引数の総数チェック) ...
}
checkPrintf
は、フォーマット文字列を走査し、各書式指定子 (%
で始まる部分) を parsePrintfVerb
で解析します。そして、解析結果(書式指定子、フラグ、消費する引数の数など)を基に、checkPrintfArg
を呼び出して実際の引数の型チェックを行います。argNum
は、現在処理している printf
引数のインデックスを追跡するために使用されます。
printfArgType
と printVerb
type printfArgType int
const (
argBool printfArgType = 1 << iota
argInt
argRune
argString
argFloat
argPointer
anyType printfArgType = ^0
)
type printVerb struct {
verb rune
flags string // known flags are all ASCII
typ printfArgType // この書式指定子が期待する引数の型
}
var printVerbs = []printVerb{
{'b', numFlag, argInt},
{'c', "-", argRune | argInt}, // 'c' は rune または int を受け入れる
{'d', numFlag, argInt},
// ... 他の書式指定子 ...
{'v', allFlags, anyType}, // 'v' は任意の型を受け入れる
// ...
}
printfArgType
はビットマスクとして定義されており、複数の型を許容する書式指定子(例: %c
は rune
または int
)を簡潔に表現できます。printVerbs
配列は、各書式指定子 (verb
) が許容するフラグ (flags
) と、期待される引数の型 (typ
) をマッピングしています。このテーブルが、型チェックの基準となります。
checkPrintfArg
関数の実装
func (f *File) checkPrintfArg(call *ast.CallExpr, verb rune, flags []byte, argNum, nargs int) {
for _, v := range printVerbs {
if v.verb == verb {
// フラグのチェック
for _, flag := range flags {
if !strings.ContainsRune(v.flags, rune(flag)) {
f.Badf(call.Pos(), "unrecognized printf flag for verb %q: %q", verb, flag)
return
}
}
// 可変幅/精度の引数(例: %.*s の '*' に対応する引数)の型チェック
for i := 0; i < nargs-1; i++ {
if !f.matchArgType(argInt, call.Args[argNum+i]) {
f.Badf(call.Pos(), "arg for * in printf format not of type int")
}
}
// 主要な引数の型チェック
if !f.matchArgType(v.typ, call.Args[argNum+nargs-1]) {
f.Badf(call.Pos(), "arg for printf verb %%%c of wrong type", verb)
}
return
}
}
f.Badf(call.Pos(), "unrecognized printf verb %q", verb)
}
この関数は、printVerbs
テーブルを参照して、与えられた書式指定子 (verb
) の情報 (v
) を取得します。
- まず、書式指定子に付与されたフラグが有効であるかをチェックします。
- 次に、
%.*s
のようにアスタリスク (*
) を含む書式指定子の場合、アスタリスクに対応する引数(nargs-1
個の引数)がint
型であることをmatchArgType
を使って確認します。 - 最後に、書式指定子に対応する主要な引数(最後の引数)の型が、
printVerbs
で定義された期待される型 (v.typ
) と一致するかをmatchArgType
を使って確認します。
matchArgType
関数の実装
func (f *File) matchArgType(t printfArgType, arg ast.Expr) bool {
if f.pkg == nil {
return true // 型情報がない場合はチェックをスキップ
}
typ := f.pkg.types[arg] // 引数の型情報を取得
if typ == nil {
return true // 型情報が取得できない場合はチェックをスキップ
}
basic, ok := typ.(*types.Basic)
if !ok {
return true // 基本型でない場合はチェックをスキップ (TODO: より詳細なチェックが必要な可能性)
}
switch basic.Kind {
case types.Bool:
return t&argBool != 0
case types.Int, types.Int8, types.Int16, types.Int32, types.Int64:
fallthrough
case types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64, types.Uintptr:
return t&argInt != 0
case types.Float32, types.Float64, types.Complex64, types.Complex128:
return t&argFloat != 0
case types.String:
return t&argString != 0
case types.UnsafePointer:
return t&argPointer != 0
case types.UntypedBool:
return t&argBool != 0
case types.UntypedComplex:
return t&argFloat != 0
case types.UntypedFloat:
// 型なし浮動小数点数が整数として評価できる場合
switch f.pkg.values[arg].(type) {
case int, int8, int16, int32, int64:
return t&(argInt|argFloat) != 0
case uint, uint8, uint16, uint32, uint64:
return t&(argInt|argFloat) != 0
}
return t&argFloat != 0
case types.UntypedInt:
return t&(argInt|argFloat) != 0 // Printf("%g", 1234) のようなケース
case types.UntypedRune:
return t&(argInt|argRune) != 0
case types.UntypedString:
return t&argString != 0
case types.UntypedNil:
return t&argPointer != 0 // TODO?
}
return false
}
matchArgType
は、go/types
パッケージから取得した引数の実際の型 (typ
) と、printfArgType
で表現された期待される型 (t
) を比較します。特に、型なし定数(types.UntypedBool
, types.UntypedInt
, types.UntypedFloat
, types.UntypedRune
, types.UntypedString
, types.UntypedNil
)の扱いが重要です。これらの型は、文脈によって異なる具体的な型に変換される可能性があるため、vet
はより柔軟なチェックを行います。例えば、型なしの整数リテラルは、%d
(int) と %f
(float) の両方で許容される可能性があります。
関連リンク
- Go Issue #4404: cmd/vet: check argument types in printf formats
- Go CL 7378061: cmd/vet: check argument types in printf formats
参考にした情報源リンク
go vet
の公式ドキュメント: go.dev/doc/go-vetfmt
パッケージのドキュメント: pkg.go.dev/fmtgo vet
のprintf
形式チェックに関する記事: yourbasic.org/golang/go-vet-printf-format-checking/- Go言語の静的解析ツール
go vet
の概要: dev.to/fatih/go-vet-a-static-analysis-tool-for-go-312f