icmd/vet: improved checking for variadic Println-like functions

- Automatically determine the first argument to check.
- Skip checking matching non-variadic functions.
- Skip checking matching functions accepting non-interface{}
  variadic arguments.
- Removed fragile 'magic' code for special cases such as math.Log
  and error interface.

Fixes #15067
Fixes #15099

Change-Id: Ib313557f18b12b36daa493f4b02c598b9503b55b
Reviewed-on: https://go-review.googlesource.com/21513
Run-TryBot: Rob Pike <r@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
diff --git a/src/cmd/vet/doc.go b/src/cmd/vet/doc.go
index d295fb4..2b5e8fc 100644
--- a/src/cmd/vet/doc.go
+++ b/src/cmd/vet/doc.go
@@ -188,13 +188,8 @@
 	-v
 		Verbose mode
 	-printfuncs
-		A comma-separated list of print-like functions to supplement the
-		standard list.  Each entry is in the form Name:N where N is the
-		zero-based argument position of the first argument involved in the
-		print: either the format or the first print argument for non-formatted
-		prints.  For example, if you have Warn and Warnf functions that
-		take an io.Writer as their first argument, like Fprintf,
-			-printfuncs=Warn:1,Warnf:1
+		A comma-separated list of print-like function names
+		to supplement the standard list.
 		For more information, see the discussion of the -printf flag.
 	-shadowstrict
 		Whether to be strict about shadowing; can be noisy.
diff --git a/src/cmd/vet/print.go b/src/cmd/vet/print.go
index 4e3252f..07499e6 100644
--- a/src/cmd/vet/print.go
+++ b/src/cmd/vet/print.go
@@ -35,20 +35,18 @@
 		if len(name) == 0 {
 			flag.Usage()
 		}
-		skip := 0
+
+		// Backwards compatibility: skip optional first argument
+		// index after the colon.
 		if colon := strings.LastIndex(name, ":"); colon > 0 {
-			var err error
-			skip, err = strconv.Atoi(name[colon+1:])
-			if err != nil {
-				errorf(`illegal format for "Func:N" argument %q; %s`, name, err)
-			}
 			name = name[:colon]
 		}
+
 		name = strings.ToLower(name)
 		if name[len(name)-1] == 'f' {
 			isFormattedPrint[name] = true
 		} else {
-			printList[name] = skip
+			isPrint[name] = true
 		}
 	}
 }
@@ -65,17 +63,20 @@
 	"sprintf": true,
 }
 
-// printList records the unformatted-print functions. The value is the location
-// of the first parameter to be printed. Names are lower-cased so the lookup is
-// case insensitive.
-var printList = map[string]int{
-	"error":  0,
-	"fatal":  0,
-	"fprint": 1, "fprintln": 1,
-	"log":   0,
-	"panic": 0, "panicln": 0,
-	"print": 0, "println": 0,
-	"sprint": 0, "sprintln": 0,
+// isPrint records the unformatted-print functions. Names are lower-cased
+// so the lookup is case insensitive.
+var isPrint = map[string]bool{
+	"error":    true,
+	"fatal":    true,
+	"fprint":   true,
+	"fprintln": true,
+	"log":      true,
+	"panic":    true,
+	"panicln":  true,
+	"print":    true,
+	"println":  true,
+	"sprint":   true,
+	"sprintln": true,
 }
 
 // formatString returns the format string argument and its index within
@@ -171,8 +172,8 @@
 		f.checkPrintf(call, Name)
 		return
 	}
-	if skip, ok := printList[name]; ok {
-		f.checkPrint(call, Name, skip)
+	if _, ok := isPrint[name]; ok {
+		f.checkPrint(call, Name)
 		return
 	}
 }
@@ -583,25 +584,36 @@
 }
 
 // checkPrint checks a call to an unformatted print routine such as Println.
-// 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
-	if name == "Log" && len(args) > 0 {
-		// Special case: Don't complain about math.Log or cmplx.Log.
-		// Not strictly necessary because the only complaint likely is for Log("%d")
-		// but it feels wrong to check that math.Log is a good print function.
-		if sel, ok := args[0].(*ast.SelectorExpr); ok {
-			if x, ok := sel.X.(*ast.Ident); ok {
-				if x.Name == "math" || x.Name == "cmplx" {
-					return
-				}
+func (f *File) checkPrint(call *ast.CallExpr, name string) {
+	firstArg := 0
+	typ := f.pkg.types[call.Fun].Type
+	if typ != nil {
+		if sig, ok := typ.(*types.Signature); ok {
+			if !sig.Variadic() {
+				// Skip checking non-variadic functions.
+				return
+			}
+			params := sig.Params()
+			firstArg = params.Len() - 1
+
+			typ := params.At(firstArg).Type()
+			typ = typ.(*types.Slice).Elem()
+			it, ok := typ.(*types.Interface)
+			if !ok || !it.Empty() {
+				// Skip variadic functions accepting non-interface{} args.
+				return
 			}
 		}
 	}
+	args := call.Args
+	if len(args) <= firstArg {
+		// Skip calls without variadic args.
+		return
+	}
+	args = args[firstArg:]
+
 	// check for Println(os.Stderr, ...)
-	if firstArg == 0 && !isF && len(args) > 0 {
+	if firstArg == 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") {
@@ -610,31 +622,15 @@
 			}
 		}
 	}
-	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[firstArg]
+	arg := args[0]
 	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)
 		}
 	}
-	if isLn {
+	if strings.HasSuffix(name, "ln") {
 		// The last item, if a string, should not have a newline.
-		arg = args[len(call.Args)-1]
+		arg = args[len(args)-1]
 		if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING {
 			if strings.HasSuffix(lit.Value, `\n"`) {
 				f.Badf(call.Pos(), "%s call ends with newline", name)
diff --git a/src/cmd/vet/testdata/print.go b/src/cmd/vet/testdata/print.go
index 5c7ff35..261ee78 100644
--- a/src/cmd/vet/testdata/print.go
+++ b/src/cmd/vet/testdata/print.go
@@ -185,11 +185,11 @@
 	// Something that looks like an error interface but isn't, such as the (*T).Error method
 	// in the testing package.
 	var et1 errorTest1
-	fmt.Println(et1.Error())        // ERROR "no args in Error call"
+	fmt.Println(et1.Error())        // ok
 	fmt.Println(et1.Error("hi"))    // ok
 	fmt.Println(et1.Error("%d", 3)) // ERROR "possible formatting directive in Error call"
 	var et2 errorTest2
-	et2.Error()        // ERROR "no args in Error call"
+	et2.Error()        // ok
 	et2.Error("hi")    // ok, not an error method.
 	et2.Error("%d", 3) // ERROR "possible formatting directive in Error call"
 	var et3 errorTest3
@@ -231,11 +231,41 @@
 	externalprintf.Logf(level, "%d", 42)                        // OK
 	externalprintf.Errorf(level, level, "foo %q bar", "foobar") // OK
 	externalprintf.Logf(level, "%d")                            // ERROR "format reads arg 1, have only 0 args"
+
+	// user-defined Println-like functions
+	ss := &someStruct{}
+	ss.Log(someFunction, "foo")          // OK
+	ss.Error(someFunction, someFunction) // OK
+	ss.Println()                         // OK
+	ss.Println(1.234, "foo")             // OK
+	ss.Println(1, someFunction)          // ERROR "arg someFunction in Println call is a function value, not a function call"
+	ss.log(someFunction)                 // OK
+	ss.log(someFunction, "bar", 1.33)    // OK
+	ss.log(someFunction, someFunction)   // ERROR "arg someFunction in log call is a function value, not a function call"
 }
 
+type someStruct struct{}
+
+// Log is non-variadic user-define Println-like function.
+// Calls to this func must be skipped when checking
+// for Println-like arguments.
+func (ss *someStruct) Log(f func(), s string) {}
+
+// Error is variadic user-define Println-like function.
+// Calls to this func mustn't be checked for Println-like arguments,
+// since variadic arguments type isn't interface{}.
+func (ss *someStruct) Error(args ...func()) {}
+
+// Println is variadic user-defined Println-like function.
+// Calls to this func must be checked for Println-like arguments.
+func (ss *someStruct) Println(args ...interface{}) {}
+
+// log is variadic user-defined Println-like function.
+// Calls to this func must be checked for Println-like arguments.
+func (ss *someStruct) log(f func(), args ...interface{}) {}
+
 // A function we use as a function value; it has no other purpose.
-func someFunction() {
-}
+func someFunction() {}
 
 // Printf is used by the test so we must declare it.
 func Printf(format string, args ...interface{}) {
diff --git a/src/cmd/vet/types.go b/src/cmd/vet/types.go
index 692bae6..4358955 100644
--- a/src/cmd/vet/types.go
+++ b/src/cmd/vet/types.go
@@ -292,72 +292,6 @@
 	return true
 }
 
-// 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].Type
-	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 sig.Params().Len()
-}
-
-// 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 {
-	typ := f.pkg.types[call].Type
-	if typ != nil {
-		// We know it's called "Error", so just check the function signature
-		// (stringerType has exactly one method, String).
-		if stringerType != nil && stringerType.NumMethods() == 1 {
-			return types.Identical(f.pkg.types[call.Fun].Type, stringerType.Method(0).Type())
-		}
-	}
-	// Without types, we can still check by hand.
-	// 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].Type
-	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 sig.Params().Len() > 0 {
-		return false
-	}
-	// Finally the real questions.
-	// There must be one result.
-	if sig.Results().Len() != 1 {
-		return false
-	}
-	// It must have return type "string" from the universe.
-	return sig.Results().At(0).Type() == types.Typ[types.String]
-}
-
 // hasMethod reports whether the type contains a method with the given name.
 // It is part of the workaround for Formatters and should be deleted when
 // that workaround is no longer necessary.