go/types, types2: test that error format strings have matching parentheses/brackets

Also, for go/types, switch to using syntax.Inspect instead of
(deprecated) syntax.Crawl.

Change-Id: I8333079040e9676e0a61c23d09d41ca790526eeb
Reviewed-on: https://go-review.googlesource.com/c/go/+/460759
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
Reviewed-by: Robert Griesemer <gri@google.com>
Run-TryBot: Robert Griesemer <gri@google.com>
Auto-Submit: Robert Griesemer <gri@google.com>
diff --git a/src/cmd/compile/internal/types2/errorcalls_test.go b/src/cmd/compile/internal/types2/errorcalls_test.go
index edf2a51..6153b42 100644
--- a/src/cmd/compile/internal/types2/errorcalls_test.go
+++ b/src/cmd/compile/internal/types2/errorcalls_test.go
@@ -6,13 +6,18 @@
 
 import (
 	"cmd/compile/internal/syntax"
+	"strconv"
 	"testing"
 )
 
-const errorfMinArgCount = 4
+const (
+	errorfMinArgCount = 4
+	errorfFormatIndex = 2
+)
 
 // TestErrorCalls makes sure that check.errorf calls have at least
-// errorfMinArgCount arguments (otherwise we should use check.error).
+// errorfMinArgCount arguments (otherwise we should use check.error)
+// and use balanced parentheses/brackets.
 func TestErrorCalls(t *testing.T) {
 	files, err := pkgFiles(".")
 	if err != nil {
@@ -20,17 +25,17 @@
 	}
 
 	for _, file := range files {
-		syntax.Crawl(file, func(n syntax.Node) bool {
+		syntax.Inspect(file, func(n syntax.Node) bool {
 			call, _ := n.(*syntax.CallExpr)
 			if call == nil {
-				return false
+				return true
 			}
 			selx, _ := call.Fun.(*syntax.SelectorExpr)
 			if selx == nil {
-				return false
+				return true
 			}
 			if !(isName(selx.X, "check") && isName(selx.Sel, "errorf")) {
-				return false
+				return true
 			}
 			// check.errorf calls should have at least errorfMinArgCount arguments:
 			// position, code, format string, and arguments to format
@@ -38,6 +43,18 @@
 				t.Errorf("%s: got %d arguments, want at least %d", call.Pos(), n, errorfMinArgCount)
 				return false
 			}
+			format := call.ArgList[errorfFormatIndex]
+			syntax.Inspect(format, func(n syntax.Node) bool {
+				if lit, _ := n.(*syntax.BasicLit); lit != nil && lit.Kind == syntax.StringLit {
+					if s, err := strconv.Unquote(lit.Value); err == nil {
+						if !balancedParentheses(s) {
+							t.Errorf("%s: unbalanced parentheses/brackets", lit.Pos())
+						}
+					}
+					return false
+				}
+				return true
+			})
 			return false
 		})
 	}
@@ -49,3 +66,30 @@
 	}
 	return false
 }
+
+func balancedParentheses(s string) bool {
+	var stack []byte
+	for _, ch := range s {
+		var open byte
+		switch ch {
+		case '(', '[', '{':
+			stack = append(stack, byte(ch))
+			continue
+		case ')':
+			open = '('
+		case ']':
+			open = '['
+		case '}':
+			open = '{'
+		default:
+			continue
+		}
+		// closing parenthesis/bracket must have matching opening
+		top := len(stack) - 1
+		if top < 0 || stack[top] != open {
+			return false
+		}
+		stack = stack[:top]
+	}
+	return len(stack) == 0
+}
diff --git a/src/go/types/errorcalls_test.go b/src/go/types/errorcalls_test.go
index 6d6bd60..ea9a122 100644
--- a/src/go/types/errorcalls_test.go
+++ b/src/go/types/errorcalls_test.go
@@ -7,13 +7,18 @@
 import (
 	"go/ast"
 	"go/token"
+	"strconv"
 	"testing"
 )
 
-const errorfMinArgCount = 4
+const (
+	errorfMinArgCount = 4
+	errorfFormatIndex = 2
+)
 
 // TestErrorCalls makes sure that check.errorf calls have at least
-// errorfMinArgCount arguments (otherwise we should use check.error).
+// errorfMinArgCount arguments (otherwise we should use check.error)
+// and use balanced parentheses/brackets.
 func TestErrorCalls(t *testing.T) {
 	fset := token.NewFileSet()
 	files, err := pkgFiles(fset, ".")
@@ -40,7 +45,19 @@
 				t.Errorf("%s: got %d arguments, want at least %d", fset.Position(call.Pos()), n, errorfMinArgCount)
 				return false
 			}
-			return true
+			format := call.Args[errorfFormatIndex]
+			ast.Inspect(format, func(n ast.Node) bool {
+				if lit, _ := n.(*ast.BasicLit); lit != nil && lit.Kind == token.STRING {
+					if s, err := strconv.Unquote(lit.Value); err == nil {
+						if !balancedParentheses(s) {
+							t.Errorf("%s: unbalanced parentheses/brackets", fset.Position(lit.ValuePos))
+						}
+					}
+					return false
+				}
+				return true
+			})
+			return false
 		})
 	}
 }
@@ -51,3 +68,30 @@
 	}
 	return false
 }
+
+func balancedParentheses(s string) bool {
+	var stack []byte
+	for _, ch := range s {
+		var open byte
+		switch ch {
+		case '(', '[', '{':
+			stack = append(stack, byte(ch))
+			continue
+		case ')':
+			open = '('
+		case ']':
+			open = '['
+		case '}':
+			open = '{'
+		default:
+			continue
+		}
+		// closing parenthesis/bracket must have matching opening
+		top := len(stack) - 1
+		if top < 0 || stack[top] != open {
+			return false
+		}
+		stack = stack[:top]
+	}
+	return len(stack) == 0
+}
\ No newline at end of file