summaryrefslogtreecommitdiff
path: root/src/cmd/vet/print.go
diff options
context:
space:
mode:
authorMichael Stapelberg <stapelberg@debian.org>2013-03-04 21:27:36 +0100
committerMichael Stapelberg <michael@stapelberg.de>2013-03-04 21:27:36 +0100
commit04b08da9af0c450d645ab7389d1467308cfc2db8 (patch)
treedb247935fa4f2f94408edc3acd5d0d4f997aa0d8 /src/cmd/vet/print.go
parent917c5fb8ec48e22459d77e3849e6d388f93d3260 (diff)
downloadgolang-upstream/1.1_hg20130304.tar.gz
Imported Upstream version 1.1~hg20130304upstream/1.1_hg20130304
Diffstat (limited to 'src/cmd/vet/print.go')
-rw-r--r--src/cmd/vet/print.go382
1 files changed, 285 insertions, 97 deletions
diff --git a/src/cmd/vet/print.go b/src/cmd/vet/print.go
index ee9a33c70..fb0fb9f9b 100644
--- a/src/cmd/vet/print.go
+++ b/src/cmd/vet/print.go
@@ -8,9 +8,10 @@ package main
import (
"flag"
- "fmt"
"go/ast"
"go/token"
+ "go/types"
+ "strconv"
"strings"
"unicode/utf8"
)
@@ -43,6 +44,9 @@ var printList = map[string]int{
// checkCall triggers the print-specific checks if the call invokes a print function.
func (f *File) checkFmtPrintfCall(call *ast.CallExpr, Name string) {
+ if !vet("printf") {
+ return
+ }
name := strings.ToLower(Name)
if skip, ok := printfList[name]; ok {
f.checkPrintf(call, Name, skip)
@@ -54,73 +58,134 @@ func (f *File) checkFmtPrintfCall(call *ast.CallExpr, Name string) {
}
}
+// literal returns the literal value represented by the expression, or nil if it is not a literal.
+func (f *File) literal(value ast.Expr) *ast.BasicLit {
+ switch v := value.(type) {
+ case *ast.BasicLit:
+ return v
+ case *ast.ParenExpr:
+ return f.literal(v.X)
+ case *ast.BinaryExpr:
+ if v.Op != token.ADD {
+ break
+ }
+ litX := f.literal(v.X)
+ litY := f.literal(v.Y)
+ if litX != nil && litY != nil {
+ lit := *litX
+ x, errX := strconv.Unquote(litX.Value)
+ y, errY := strconv.Unquote(litY.Value)
+ if errX == nil && errY == nil {
+ return &ast.BasicLit{
+ ValuePos: lit.ValuePos,
+ Kind: lit.Kind,
+ Value: strconv.Quote(x + y),
+ }
+ }
+ }
+ case *ast.Ident:
+ // See if it's a constant or initial value (we can't tell the difference).
+ if v.Obj == nil || v.Obj.Decl == nil {
+ return nil
+ }
+ valueSpec, ok := v.Obj.Decl.(*ast.ValueSpec)
+ if ok && len(valueSpec.Names) == len(valueSpec.Values) {
+ // Find the index in the list of names
+ var i int
+ for i = 0; i < len(valueSpec.Names); i++ {
+ if valueSpec.Names[i].Name == v.Name {
+ if lit, ok := valueSpec.Values[i].(*ast.BasicLit); ok {
+ return lit
+ }
+ return nil
+ }
+ }
+ }
+ }
+ return nil
+}
+
// checkPrintf checks a call to a formatted print routine such as Printf.
-// The skip argument records how many arguments to ignore; that is,
-// call.Args[skip] is (well, should be) the format argument.
-func (f *File) checkPrintf(call *ast.CallExpr, name string, skip int) {
- if len(call.Args) <= skip {
+// call.Args[formatIndex] is (well, should be) the format argument.
+func (f *File) checkPrintf(call *ast.CallExpr, name string, formatIndex int) {
+ if formatIndex >= len(call.Args) {
return
}
- // Common case: literal is first argument.
- arg := call.Args[skip]
- lit, ok := arg.(*ast.BasicLit)
- if !ok {
- // Too hard to check.
+ lit := f.literal(call.Args[formatIndex])
+ if lit == nil {
if *verbose {
f.Warn(call.Pos(), "can't check non-literal format in call to", name)
}
return
}
- if lit.Kind == token.STRING {
- if !strings.Contains(lit.Value, "%") {
- if len(call.Args) > skip+1 {
- f.Badf(call.Pos(), "no formatting directive in %s call", name)
- }
- return
+ if lit.Kind != token.STRING {
+ f.Badf(call.Pos(), "literal %v not a string in call to", lit.Value, name)
+ }
+ format, err := strconv.Unquote(lit.Value)
+ if err != nil {
+ // Shouldn't happen if parser returned no errors, but be safe.
+ f.Badf(call.Pos(), "invalid quoted string literal")
+ }
+ firstArg := formatIndex + 1 // Arguments are immediately after format string.
+ if !strings.Contains(format, "%") {
+ if len(call.Args) > firstArg {
+ f.Badf(call.Pos(), "no formatting directive in %s call", name)
}
+ return
}
// Hard part: check formats against args.
- // Trivial but useful test: count.
- numArgs := 0
- for i, w := 0, 0; i < len(lit.Value); i += w {
+ argNum := firstArg
+ for i, w := 0, 0; i < len(format); i += w {
w = 1
- if lit.Value[i] == '%' {
- nbytes, nargs := f.parsePrintfVerb(call, lit.Value[i:])
+ if format[i] == '%' {
+ verb, flags, nbytes, nargs := f.parsePrintfVerb(call, format[i:])
w = nbytes
- numArgs += nargs
+ if verb == '%' { // "%%" does nothing interesting.
+ continue
+ }
+ // If we've run out of args, print after loop will pick that up.
+ if argNum+nargs <= len(call.Args) {
+ f.checkPrintfArg(call, verb, flags, argNum, nargs)
+ }
+ argNum += nargs
}
}
- expect := len(call.Args) - (skip + 1)
- if numArgs != expect {
- f.Badf(call.Pos(), "wrong number of args in %s call: %d needed but %d args", name, numArgs, expect)
+ // TODO: Dotdotdot is hard.
+ if call.Ellipsis.IsValid() && argNum != len(call.Args) {
+ return
+ }
+ if argNum != len(call.Args) {
+ expect := argNum - firstArg
+ numArgs := len(call.Args) - firstArg
+ f.Badf(call.Pos(), "wrong number of args for format in %s call: %d needed but %d args", name, expect, numArgs)
}
}
-// parsePrintfVerb returns the number of bytes and number of arguments
-// consumed by the Printf directive that begins s, including its percent sign
-// and verb.
-func (f *File) parsePrintfVerb(call *ast.CallExpr, s string) (nbytes, nargs int) {
+// parsePrintfVerb returns the verb that begins the format string, along with its flags,
+// the number of bytes to advance the format to step past the verb, and number of
+// arguments it consumes.
+func (f *File) parsePrintfVerb(call *ast.CallExpr, format string) (verb rune, flags []byte, nbytes, nargs int) {
// There's guaranteed a percent sign.
- flags := make([]byte, 0, 5)
+ flags = make([]byte, 0, 5)
nbytes = 1
- end := len(s)
+ end := len(format)
// There may be flags.
FlagLoop:
for nbytes < end {
- switch s[nbytes] {
+ switch format[nbytes] {
case '#', '0', '+', '-', ' ':
- flags = append(flags, s[nbytes])
+ flags = append(flags, format[nbytes])
nbytes++
default:
break FlagLoop
}
}
getNum := func() {
- if nbytes < end && s[nbytes] == '*' {
+ if nbytes < end && format[nbytes] == '*' {
nbytes++
nargs++
} else {
- for nbytes < end && '0' <= s[nbytes] && s[nbytes] <= '9' {
+ for nbytes < end && '0' <= format[nbytes] && format[nbytes] <= '9' {
nbytes++
}
}
@@ -128,24 +193,38 @@ FlagLoop:
// There may be a width.
getNum()
// If there's a period, there may be a precision.
- if nbytes < end && s[nbytes] == '.' {
+ if nbytes < end && format[nbytes] == '.' {
flags = append(flags, '.') // Treat precision as a flag.
nbytes++
getNum()
}
// Now a verb.
- c, w := utf8.DecodeRuneInString(s[nbytes:])
+ c, w := utf8.DecodeRuneInString(format[nbytes:])
nbytes += w
+ verb = c
if c != '%' {
nargs++
- f.checkPrintfVerb(call, c, flags)
}
return
}
+// printfArgType encodes the types of expressions a printf verb accepts. It is a bitmask.
+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
}
// Common flag sets for printf verbs.
@@ -164,36 +243,57 @@ var printVerbs = []printVerb{
// '+' is required sign for numbers, Go format for %v.
// '#' is alternate format for several verbs.
// ' ' is spacer for numbers
- {'b', numFlag},
- {'c', "-"},
- {'d', numFlag},
- {'e', numFlag},
- {'E', numFlag},
- {'f', numFlag},
- {'F', numFlag},
- {'g', numFlag},
- {'G', numFlag},
- {'o', sharpNumFlag},
- {'p', "-#"},
- {'q', "-+#."},
- {'s', "-."},
- {'t', "-"},
- {'T', "-"},
- {'U', "-#"},
- {'v', allFlags},
- {'x', sharpNumFlag},
- {'X', sharpNumFlag},
+ {'b', numFlag, argInt | argFloat},
+ {'c', "-", argRune | argInt},
+ {'d', numFlag, argInt},
+ {'e', numFlag, argFloat},
+ {'E', numFlag, argFloat},
+ {'f', numFlag, argFloat},
+ {'F', numFlag, argFloat},
+ {'g', numFlag, argFloat},
+ {'G', numFlag, argFloat},
+ {'o', sharpNumFlag, argInt},
+ {'p', "-#", argPointer},
+ {'q', " -+.0#", argRune | argInt | argString},
+ {'s', " -+.0", argString},
+ {'t', "-", argBool},
+ {'T', "-", anyType},
+ {'U', "-#", argRune | argInt},
+ {'v', allFlags, anyType},
+ {'x', sharpNumFlag, argRune | argInt | argString},
+ {'X', sharpNumFlag, argRune | argInt | argString},
}
const printfVerbs = "bcdeEfFgGopqstTvxUX"
-func (f *File) checkPrintfVerb(call *ast.CallExpr, verb rune, flags []byte) {
+func (f *File) checkPrintfArg(call *ast.CallExpr, verb rune, flags []byte, argNum, nargs int) {
// Linear scan is fast enough for a small list.
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
+ }
+ }
+ // Verb is good. If nargs>1, we have something like %.*s and all but the final
+ // arg must be integer.
+ for i := 0; i < nargs-1; i++ {
+ if !f.matchArgType(argInt, call.Args[argNum+i]) {
+ f.Badf(call.Pos(), "arg %s for * in printf format not of type int", f.gofmt(call.Args[argNum+i]))
+ }
+ }
+ for _, v := range printVerbs {
+ if v.verb == verb {
+ arg := call.Args[argNum+nargs-1]
+ if !f.matchArgType(v.typ, arg) {
+ typeString := ""
+ if typ := f.pkg.types[arg]; typ != nil {
+ typeString = typ.String()
+ }
+ f.Badf(call.Pos(), "arg %s for printf verb %%%c of wrong type: %s", f.gofmt(arg), verb, typeString)
+ }
+ break
}
}
return
@@ -202,15 +302,67 @@ func (f *File) checkPrintfVerb(call *ast.CallExpr, verb rune, flags []byte) {
f.Badf(call.Pos(), "unrecognized printf verb %q", verb)
}
+func (f *File) matchArgType(t printfArgType, arg ast.Expr) bool {
+ // TODO: for now, we can only test builtin types and untyped constants.
+ typ := f.pkg.types[arg]
+ if typ == nil {
+ return true
+ }
+ basic, ok := typ.(*types.Basic)
+ if !ok {
+ return true
+ }
+ 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|argInt) != 0
+ case types.UntypedBool:
+ return t&argBool != 0
+ case types.UntypedComplex:
+ return t&argFloat != 0
+ case types.UntypedFloat:
+ // If it's integral, we can use an int format.
+ 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 != 0
+ case types.UntypedRune:
+ return t&(argInt|argRune) != 0
+ case types.UntypedString:
+ return t&argString != 0
+ case types.UntypedNil:
+ return t&argPointer != 0 // TODO?
+ case types.Invalid:
+ if *verbose {
+ f.Warnf(arg.Pos(), "printf argument %v has invalid or unknown type", arg)
+ }
+ return true // Probably a type check problem.
+ }
+ return false
+}
+
// checkPrint checks a call to an unformatted print routine such as Println.
-// The skip argument records how many arguments to ignore; that is,
-// call.Args[skip] is the first argument to be printed.
-func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {
+// call.Args[firstArg] is the first argument to be printed.
+func (f *File) checkPrint(call *ast.CallExpr, name string, firstArg int) {
isLn := strings.HasSuffix(name, "ln")
isF := strings.HasPrefix(name, "F")
args := call.Args
// check for Println(os.Stderr, ...)
- if skip == 0 && !isF && len(args) > 0 {
+ if firstArg == 0 && !isF && len(args) > 0 {
if sel, ok := args[0].(*ast.SelectorExpr); ok {
if x, ok := sel.X.(*ast.Ident); ok {
if x.Name == "os" && strings.HasPrefix(sel.Sel.Name, "Std") {
@@ -219,13 +371,23 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {
}
}
}
- if len(args) <= skip {
- if *verbose && !isLn {
- f.Badf(call.Pos(), "no args in %s call", name)
+ if len(args) <= firstArg {
+ // If we have a call to a method called Error that satisfies the Error interface,
+ // then it's ok. Otherwise it's something like (*T).Error from the testing package
+ // and we need to check it.
+ if name == "Error" && f.isErrorMethodCall(call) {
+ return
+ }
+ // If it's an Error call now, it's probably for printing errors.
+ if !isLn {
+ // Check the signature to be sure: there are niladic functions called "error".
+ if firstArg != 0 || f.numArgsInSignature(call) != firstArg {
+ f.Badf(call.Pos(), "no args in %s call", name)
+ }
}
return
}
- arg := args[skip]
+ arg := args[firstArg]
if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING {
if strings.Contains(lit.Value, "%") {
f.Badf(call.Pos(), "possible formatting directive in %s call", name)
@@ -242,37 +404,63 @@ func (f *File) checkPrint(call *ast.CallExpr, name string, skip int) {
}
}
-// This function never executes, but it serves as a simple test for the program.
-// Test with make test.
-func BadFunctionUsedInTests() {
- fmt.Println() // not an error
- fmt.Println("%s", "hi") // ERROR "possible formatting directive in Println call"
- fmt.Printf("%s", "hi", 3) // ERROR "wrong number of args in Printf call"
- fmt.Printf("%s%%%d", "hi", 3) // correct
- fmt.Printf("%.*d", 3, 3) // correct
- fmt.Printf("%.*d", 3, 3, 3) // ERROR "wrong number of args in Printf call"
- printf("now is the time", "buddy") // ERROR "no formatting directive"
- Printf("now is the time", "buddy") // ERROR "no formatting directive"
- Printf("hi") // ok
- f := new(File)
- f.Warn(0, "%s", "hello", 3) // ERROR "possible formatting directive in Warn call"
- f.Warnf(0, "%s", "hello", 3) // ERROR "wrong number of args in Warnf call"
- f.Warnf(0, "%r", "hello") // ERROR "unrecognized printf verb"
- f.Warnf(0, "%#s", "hello") // ERROR "unrecognized printf flag"
-}
-
-type BadTypeUsedInTests struct {
- X int "hello" // ERROR "struct field tag"
-}
-
-func (t *BadTypeUsedInTests) Scan(x fmt.ScanState, c byte) { // ERROR "method Scan[(]x fmt.ScanState, c byte[)] should have signature Scan[(]fmt.ScanState, rune[)] error"
-}
-
-type BadInterfaceUsedInTests interface {
- ReadByte() byte // ERROR "method ReadByte[(][)] byte should have signature ReadByte[(][)] [(]byte, error[)]"
+// numArgsInSignature tells how many formal arguments the function type
+// being called has.
+func (f *File) numArgsInSignature(call *ast.CallExpr) int {
+ // Check the type of the function or method declaration
+ typ := f.pkg.types[call.Fun]
+ if typ == nil {
+ return 0
+ }
+ // The type must be a signature, but be sure for safety.
+ sig, ok := typ.(*types.Signature)
+ if !ok {
+ return 0
+ }
+ return len(sig.Params)
}
-// printf is used by the test.
-func printf(format string, args ...interface{}) {
- panic("don't call - testing only")
+// isErrorMethodCall reports whether the call is of a method with signature
+// func Error() string
+// where "string" is the universe's string type. We know the method is called "Error".
+func (f *File) isErrorMethodCall(call *ast.CallExpr) bool {
+ // Is it a selector expression? Otherwise it's a function call, not a method call.
+ sel, ok := call.Fun.(*ast.SelectorExpr)
+ if !ok {
+ return false
+ }
+ // The package is type-checked, so if there are no arguments, we're done.
+ if len(call.Args) > 0 {
+ return false
+ }
+ // Check the type of the method declaration
+ typ := f.pkg.types[sel]
+ if typ == nil {
+ return false
+ }
+ // The type must be a signature, but be sure for safety.
+ sig, ok := typ.(*types.Signature)
+ if !ok {
+ return false
+ }
+ // There must be a receiver for it to be a method call. Otherwise it is
+ // a function, not something that satisfies the error interface.
+ if sig.Recv == nil {
+ return false
+ }
+ // There must be no arguments. Already verified by type checking, but be thorough.
+ if len(sig.Params) > 0 {
+ return false
+ }
+ // Finally the real questions.
+ // There must be one result.
+ if len(sig.Results) != 1 {
+ return false
+ }
+ // It must have return type "string" from the universe.
+ result := sig.Results[0].Type
+ if types.IsIdentical(result, types.Typ[types.String]) {
+ return true
+ }
+ return false
}