Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.gocheckPrintf 関数は、printf 形式の関数呼び出しを解析する主要な関数です。この関数は大幅に改修されました。

  • 引数インデックスの変更: skip 引数が formatIndex に変更され、フォーマット文字列が call.Args の何番目の引数であるかをより明確に示しています。
  • 引数の数チェックの改善: フォーマット文字列中の書式指定子によって期待される引数の数 (argNum) と、実際に渡された引数の数 (len(call.Args) - firstArg) を比較し、不一致があれば警告を発します。以前は numArgs という変数で単純に書式指定子の数を数えていましたが、より厳密なチェックが行われるようになりました。
  • parsePrintfVerb の戻り値の拡張: parsePrintfVerb 関数は、書式指定子(verb)とそのフラグ、消費するバイト数、消費する引数の数に加えて、verb runeflags []byte を返すようになりました。これにより、書式指定子の詳細な情報を取得できるようになりました。
  • checkPrintfArg の導入: 各書式指定子に対応する引数の型チェックを行うために、新たに checkPrintfArg 関数が導入されました。

printfArgTypeprintVerb 構造体の導入

printf の書式指定子とそれらが期待する引数の型をマッピングするために、新しい型と構造体が定義されました。

  • printfArgType: int 型のビットマスクとして定義され、printf の書式指定子が受け入れる引数の型を表します。

    type printfArgType int
    
    const (
        argBool printfArgType = 1 << iota
        argInt
        argRune
        argString
        argFloat
        argPointer
        anyType printfArgType = ^0
    )
    

    これにより、例えば %crune または 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]) を受け取り、型チェックを実行します。

  1. フラグのチェック: まず、書式指定子に指定されたフラグがその書式指定子で許容されているかをチェックします。
  2. 可変幅/精度の引数チェック: %.*s のようにアスタリスク (*) を含む書式指定子の場合、アスタリスクに対応する引数(通常は最初の引数)が int 型であることを matchArgType を使って確認します。
  3. 主要な引数の型チェック: 最後に、書式指定子に対応する主要な引数(通常は最後の引数)の型が、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)、argIntargFloat の両方を許容するようにしています。これにより、fmt.Printf("%d", 3.0) のようなコードでも、値が整数であれば警告が出ないように柔軟に対応しています。

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.Pointerprintf フォーマットチェックをテストするための新しいファイルが追加されました。

コアとなるコードの解説

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 引数のインデックスを追跡するために使用されます。

printfArgTypeprintVerb

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 はビットマスクとして定義されており、複数の型を許容する書式指定子(例: %crune または 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) を取得します。

  1. まず、書式指定子に付与されたフラグが有効であるかをチェックします。
  2. 次に、%.*s のようにアスタリスク (*) を含む書式指定子の場合、アスタリスクに対応する引数(nargs-1 個の引数)が int 型であることを matchArgType を使って確認します。
  3. 最後に、書式指定子に対応する主要な引数(最後の引数)の型が、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) の両方で許容される可能性があります。

関連リンク

参考にした情報源リンク