cue/errors: move Handler to scanner

Handler is only used by scanner and just clutters
the API.

This change also improves localizability.

Change-Id: Idd963dc4be48dc53b58c63fb1f058aa0ee5ebd3a
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2215
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/errors/errors.go b/cue/errors/errors.go
index f512028..4f31959 100644
--- a/cue/errors/errors.go
+++ b/cue/errors/errors.go
@@ -32,11 +32,6 @@
 	return errors.New(msg)
 }
 
-// A Handler is a generic error handler used throughout CUE packages.
-//
-// The position points to the beginning of the offending value.
-type Handler func(pos token.Pos, msg string)
-
 // A Message implements the error interface as well as Message to allow
 // internationalized messages.
 type Message struct {
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 27b4e5b..630e00a 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -70,7 +70,9 @@
 	if p.mode&parseCommentsMode != 0 {
 		m = scanner.ScanComments
 	}
-	eh := func(pos token.Pos, msg string) { p.errors.AddNew(pos, msg) }
+	eh := func(pos token.Pos, msg string, args []interface{}) {
+		p.errors.AddNew(pos, fmt.Sprintf(msg, args...))
+	}
 	p.scanner.Init(p.file, src, eh, m)
 
 	p.trace = p.mode&traceMode != 0 // for convenience (p.trace is used frequently)
diff --git a/cue/scanner/scanner.go b/cue/scanner/scanner.go
index c0617d4..1591fd4 100644
--- a/cue/scanner/scanner.go
+++ b/cue/scanner/scanner.go
@@ -25,20 +25,24 @@
 	"unicode"
 	"unicode/utf8"
 
-	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
 )
 
+// An ErrorHandler is a generic error handler used throughout CUE packages.
+//
+// The position points to the beginning of the offending value.
+type ErrorHandler func(pos token.Pos, msg string, args []interface{})
+
 // A Scanner holds the Scanner's internal state while processing
 // a given text. It can be allocated as part of another data
 // structure but must be initialized via Init before use.
 type Scanner struct {
 	// immutable state
-	file *token.File    // source file handle
-	dir  string         // directory portion of file.Name()
-	src  []byte         // source
-	err  errors.Handler // error reporting; or nil
-	mode Mode           // scanning mode
+	file *token.File  // source file handle
+	dir  string       // directory portion of file.Name()
+	src  []byte       // source
+	errh ErrorHandler // error reporting; or nil
+	mode Mode         // scanning mode
 
 	// scanning state
 	ch              rune // current character
@@ -75,14 +79,14 @@
 		r, w := rune(s.src[s.rdOffset]), 1
 		switch {
 		case r == 0:
-			s.error(s.offset, "illegal character NUL")
+			s.errf(s.offset, "illegal character NUL")
 		case r >= utf8.RuneSelf:
 			// not ASCII
 			r, w = utf8.DecodeRune(s.src[s.rdOffset:])
 			if r == utf8.RuneError && w == 1 {
-				s.error(s.offset, "illegal UTF-8 encoding")
+				s.errf(s.offset, "illegal UTF-8 encoding")
 			} else if r == bom && s.offset > 0 {
-				s.error(s.offset, "illegal byte order mark")
+				s.errf(s.offset, "illegal byte order mark")
 			}
 		}
 		s.rdOffset += w
@@ -121,7 +125,7 @@
 //
 // Note that Init may call err if there is an error in the first character
 // of the file.
-func (s *Scanner) Init(file *token.File, src []byte, err errors.Handler, mode Mode) {
+func (s *Scanner) Init(file *token.File, src []byte, eh ErrorHandler, mode Mode) {
 	// Explicitly initialize all fields since a scanner may be reused.
 	if file.Size() != len(src) {
 		panic(fmt.Sprintf("file size (%d) does not match src len (%d)", file.Size(), len(src)))
@@ -129,7 +133,7 @@
 	s.file = file
 	s.dir, _ = filepath.Split(file.Name())
 	s.src = src
-	s.err = err
+	s.errh = eh
 	s.mode = mode
 
 	s.ch = ' '
@@ -145,9 +149,9 @@
 	}
 }
 
-func (s *Scanner) error(offs int, msg string) {
-	if s.err != nil {
-		s.err(s.file.Pos(offs, 0), msg)
+func (s *Scanner) errf(offs int, msg string, args ...interface{}) {
+	if s.errh != nil {
+		s.errh(s.file.Pos(offs, 0), msg, args)
 	}
 	s.ErrorCount++
 }
@@ -210,7 +214,7 @@
 		}
 	}
 
-	s.error(offs, "comment not terminated")
+	s.errf(offs, "comment not terminated")
 
 exit:
 	lit := s.src[offs:s.offset]
@@ -304,7 +308,7 @@
 		s.next()
 	}
 	if last == '_' {
-		s.error(s.offset-1, "illegal '_' in number")
+		s.errf(s.offset-1, "illegal '_' in number")
 	}
 }
 
@@ -330,7 +334,7 @@
 			s.scanMantissa(16)
 			if s.offset-offs <= 2 {
 				// only scanned "0x" or "0X"
-				s.error(offs, "illegal hexadecimal number")
+				s.errf(offs, "illegal hexadecimal number")
 			}
 		} else if s.ch == 'b' {
 			// binary int
@@ -338,7 +342,7 @@
 			s.scanMantissa(2)
 			if s.offset-offs <= 2 {
 				// only scanned "0b"
-				s.error(offs, "illegal binary number")
+				s.errf(offs, "illegal binary number")
 			}
 		} else if s.ch == 'o' {
 			// octal int
@@ -346,7 +350,7 @@
 			s.scanMantissa(8)
 			if s.offset-offs <= 2 {
 				// only scanned "0o"
-				s.error(offs, "illegal octal number")
+				s.errf(offs, "illegal octal number")
 			}
 		} else {
 			// 0 or float
@@ -360,7 +364,7 @@
 			}
 			if seenDigits {
 				// integer other than 0 may not start with 0
-				s.error(offs, "illegal integer number")
+				s.errf(offs, "illegal integer number")
 			}
 		}
 		goto exit
@@ -450,7 +454,7 @@
 		if s.ch < 0 {
 			msg = "escape sequence not terminated"
 		}
-		s.error(offs, msg)
+		s.errf(offs, msg)
 		return false, false
 	}
 
@@ -458,11 +462,11 @@
 	for n > 0 {
 		d := uint32(digitVal(s.ch))
 		if d >= base {
-			msg := fmt.Sprintf("illegal character %#U in escape sequence", s.ch)
 			if s.ch < 0 {
-				msg = "escape sequence not terminated"
+				s.errf(s.offset, "escape sequence not terminated")
+			} else {
+				s.errf(s.offset, "illegal character %#U in escape sequence", s.ch)
 			}
-			s.error(s.offset, msg)
 			return false, false
 		}
 		x = x*base + d
@@ -473,7 +477,7 @@
 	// TODO: this is valid JSON, so remove, but normalize and report an error
 	// if for unmatched surrogate pairs .
 	if x > max {
-		s.error(offs, "escape sequence is invalid Unicode code point")
+		s.errf(offs, "escape sequence is invalid Unicode code point")
 		return false, false
 	}
 
@@ -494,7 +498,7 @@
 			break
 		}
 		if (quote.numChar != 3 && ch == '\n') || ch < 0 {
-			s.error(offs, "string literal not terminated")
+			s.errf(offs, "string literal not terminated")
 			lit := s.src[offs:s.offset]
 			if hasCR {
 				lit = stripCR(lit)
@@ -573,7 +577,7 @@
 	s.scanIdentifier()
 
 	if s.ch != '(' {
-		s.error(s.offset, "invalid attribute: expected '('")
+		s.errf(s.offset, "invalid attribute: expected '('")
 		return token.ATTRIBUTE, string(s.src[offs:s.offset])
 	}
 	s.next()
@@ -589,7 +593,7 @@
 	if s.ch == ')' {
 		s.next()
 	} else {
-		s.error(s.offset, "attribute missing ')'")
+		s.errf(s.offset, "attribute missing ')'")
 	}
 	return token.ATTRIBUTE, string(s.src[offs:s.offset])
 }
@@ -612,7 +616,7 @@
 	for s.ch != ',' && s.ch != ')' && s.ch != '\n' && s.ch != -1 {
 		switch s.ch {
 		case '#', '\'', '"', '(', '=':
-			s.error(s.offset, "illegal character in attribute")
+			s.errf(s.offset, "illegal character in attribute")
 			s.recoverParen(1)
 			return
 		}
@@ -623,7 +627,7 @@
 func (s *Scanner) scanAttributeString() bool {
 	if s.ch == '#' || s.ch == '"' || s.ch == '\'' {
 		if _, tok, _ := s.Scan(); tok == token.INTERPOLATION {
-			s.error(s.offset, "interpolation not allowed in attribute")
+			s.errf(s.offset, "interpolation not allowed in attribute")
 			s.popInterpolation()
 			s.recoverParen(1)
 		}
@@ -784,7 +788,7 @@
 				// Note that `_|x` is always equal to _.
 				s.next()
 				if s.ch != '_' {
-					s.error(s.file.Offset(pos), "illegal token '_|'; expected '_'")
+					s.errf(s.file.Offset(pos), "illegal token '_|'; expected '_'")
 					insertEOL = s.insertEOL // preserve insertComma info
 					tok = token.ILLEGAL
 					lit = "_|"
@@ -823,7 +827,7 @@
 			case 1:
 				if ch == '"' || ch == '\'' {
 					if !s.hashCount(quote) {
-						s.error(offs, "string literal not terminated")
+						s.errf(offs, "string literal not terminated")
 					}
 					tok, lit = token.STRING, string(s.src[offs:s.offset])
 				}
@@ -852,7 +856,7 @@
 					s.next()
 					tok = token.ELLIPSIS
 				} else {
-					s.error(s.file.Offset(pos), "illegal token '..'; expected '.'")
+					s.errf(s.file.Offset(pos), "illegal token '..'; expected '.'")
 				}
 			} else {
 				tok = token.PERIOD
@@ -946,7 +950,7 @@
 		default:
 			// next reports unexpected BOMs - don't repeat
 			if ch != bom {
-				s.error(s.file.Offset(pos), fmt.Sprintf("illegal character %#U", ch))
+				s.errf(s.file.Offset(pos), "illegal character %#U", ch)
 			}
 			insertEOL = s.insertEOL // preserve insertSemi info
 			tok = token.ILLEGAL
diff --git a/cue/scanner/scanner_test.go b/cue/scanner/scanner_test.go
index 9fc91d0..122ddf0 100644
--- a/cue/scanner/scanner_test.go
+++ b/cue/scanner/scanner_test.go
@@ -208,8 +208,8 @@
 	whitespace_linecount := newlineCount(whitespace)
 
 	// error handler
-	eh := func(_ token.Pos, msg string) {
-		t.Errorf("error handler called (msg = %s)", msg)
+	eh := func(_ token.Pos, msg string, args []interface{}) {
+		t.Errorf("error handler called (msg = %s)", fmt.Sprintf(msg, args...))
 	}
 
 	// verify scan
@@ -591,7 +591,8 @@
 
 func TestScanInterpolation(t *testing.T) {
 	// error handler
-	eh := func(pos token.Pos, msg string) {
+	eh := func(pos token.Pos, msg string, args []interface{}) {
+		msg = fmt.Sprintf(msg, args...)
 		t.Errorf("error handler called (pos = %v, msg = %s)", pos, msg)
 	}
 	trim := func(s string) string { return strings.Trim(s, `#"\()`) }
@@ -646,7 +647,9 @@
 		"~ ~ ~" // original file, line 1 again
 
 	var list errors.List
-	eh := func(pos token.Pos, msg string) { list.AddNew(pos, msg) }
+	eh := func(pos token.Pos, msg string, args []interface{}) {
+		list.AddNew(pos, fmt.Sprintf(msg, args...))
+	}
 
 	var s Scanner
 	s.Init(token.NewFile("File1", 1, len(src)), []byte(src), eh, dontInsertCommas)
@@ -688,9 +691,9 @@
 	t.Helper()
 	var s Scanner
 	var h errorCollector
-	eh := func(pos token.Pos, msg string) {
+	eh := func(pos token.Pos, msg string, args []interface{}) {
 		h.cnt++
-		h.msg = msg
+		h.msg = fmt.Sprintf(msg, args...)
 		h.pos = pos
 	}
 	s.Init(token.NewFile("", 1, len(src)), []byte(src), eh, ScanComments|dontInsertCommas)