internal/protobuf: record position information

for both errors and in the AST.

Removes the error type, which now implements the
standard CUE error conventions.

Change-Id: I7a4198a9cf9b38622f14f63df827f3f9730fe4b5
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2341
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/internal/protobuf/errors.go b/internal/protobuf/errors.go
new file mode 100644
index 0000000..0c67560
--- /dev/null
+++ b/internal/protobuf/errors.go
@@ -0,0 +1,53 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package protobuf
+
+import (
+	"fmt"
+	"strings"
+
+	"cuelang.org/go/cue/token"
+)
+
+// protobufError implements cue/Error
+type protobufError struct {
+	path []string
+	pos  token.Pos
+	err  error
+}
+
+func (e *protobufError) Position() token.Pos {
+	return e.pos
+}
+
+func (e *protobufError) InputPositions() []token.Pos {
+	return nil
+}
+
+func (e *protobufError) Error() string {
+	if e.path == nil {
+		return fmt.Sprintf("protobuf: %s: %v", e.pos, e.err)
+	}
+	path := strings.Join(e.path, ".")
+	return fmt.Sprintf("protobuf: %s:%s: %v", e.pos, path, e.err)
+}
+
+func (e *protobufError) Path() []string {
+	return e.path
+}
+
+func (e *protobufError) Msg() (format string, args []interface{}) {
+	return "error parsing protobuf: %v", []interface{}{e.err}
+}
diff --git a/internal/protobuf/parse.go b/internal/protobuf/parse.go
index 0298db3..712c42e 100644
--- a/internal/protobuf/parse.go
+++ b/internal/protobuf/parse.go
@@ -15,18 +15,20 @@
 package protobuf
 
 import (
+	"bytes"
 	"fmt"
-	"io"
 	"os"
 	"path"
 	"path/filepath"
 	"sort"
 	"strconv"
 	"strings"
+	"text/scanner"
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/parser"
 	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/source"
 	"github.com/emicklei/proto"
 	"golang.org/x/xerrors"
 )
@@ -35,18 +37,10 @@
 	paths []string
 }
 
-func (s *sharedState) parse(filename string, r io.Reader) (p *protoConverter, err error) {
-	// Determine files to convert.
-	if r == nil {
-		f, err := os.Open(filename)
-		if err != nil {
-			return nil, xerrors.Errorf("protobuf: %w", err)
-		}
-		defer f.Close()
-		r = f
-	}
+func (s *sharedState) parse(filename string, src interface{}) (p *protoConverter, err error) {
+	b, err := source.Read(filename, src)
 
-	parser := proto.NewParser(r)
+	parser := proto.NewParser(bytes.NewReader(b))
 	if filename != "" {
 		parser.Filename(filename)
 	}
@@ -57,6 +51,7 @@
 
 	p = &protoConverter{
 		state:   s,
+		tfile:   token.NewFile(filename, 0, len(b)),
 		used:    map[string]bool{},
 		symbols: map[string]bool{},
 	}
@@ -65,10 +60,10 @@
 		switch x := recover().(type) {
 		case nil:
 		case protoError:
-			err = &Error{
-				Filename: filename,
-				Path:     strings.Join(p.path, "."),
-				Err:      x.error,
+			err = &protobufError{
+				path: p.path,
+				pos:  p.toCUEPos(x.pos),
+				err:  x.error,
 			}
 		default:
 			panic(x)
@@ -88,7 +83,7 @@
 			if x.Name == "go_package" {
 				str, err := strconv.Unquote(x.Constant.SourceRepresentation())
 				if err != nil {
-					failf("unquoting package filed: %v", err)
+					failf(x.Position, "unquoting package filed: %v", err)
 				}
 				split := strings.Split(str, ";")
 				p.goPkgPath = split[0]
@@ -98,7 +93,7 @@
 				case 2:
 					p.goPkg = split[1]
 				default:
-					failf("unexpected ';' in %q", str)
+					failf(x.Position, "unexpected ';' in %q", str)
 				}
 				p.file.Name = ast.NewIdent(p.goPkg)
 				// name.AddComment(comment(x.Comment, true))
@@ -144,6 +139,7 @@
 // CUE files one to one.
 type protoConverter struct {
 	state *sharedState
+	tfile *token.File
 
 	proto3 bool
 
@@ -174,10 +170,14 @@
 	shortName  string // Used for the cue package path, default is base of goPath
 }
 
-func (p *protoConverter) addRef(from, to string) {
+func (p *protoConverter) toCUEPos(pos scanner.Position) token.Pos {
+	return p.tfile.Pos(pos.Offset, 0)
+}
+
+func (p *protoConverter) addRef(pos scanner.Position, from, to string) {
 	top := p.scope[len(p.scope)-1]
 	if _, ok := top[from]; ok {
-		failf("entity %q already defined", from)
+		failf(pos, "entity %q already defined", from)
 	}
 	top[from] = mapping{ref: to}
 }
@@ -185,6 +185,7 @@
 func (p *protoConverter) addNames(elems []proto.Visitee) {
 	p.scope = append(p.scope, map[string]mapping{})
 	for _, e := range elems {
+		var pos scanner.Position
 		var name string
 		switch x := e.(type) {
 		case *proto.Message:
@@ -192,14 +193,16 @@
 				continue
 			}
 			name = x.Name
+			pos = x.Position
 		case *proto.Enum:
 			name = x.Name
+			pos = x.Position
 		default:
 			continue
 		}
 		sym := strings.Join(append(p.path, name), ".")
 		p.symbols[sym] = true
-		p.addRef(name, strings.Join(append(p.path, name), "_"))
+		p.addRef(pos, name, strings.Join(append(p.path, name), "_"))
 	}
 }
 
@@ -207,19 +210,19 @@
 	p.scope = p.scope[:len(p.scope)-1]
 }
 
-func (p *protoConverter) resolve(name string, options []*proto.Option) string {
+func (p *protoConverter) resolve(pos scanner.Position, name string, options []*proto.Option) string {
 	if strings.HasPrefix(name, ".") {
-		return p.resolveTopScope(name[1:], options)
+		return p.resolveTopScope(pos, name[1:], options)
 	}
 	for i := len(p.scope) - 1; i > 0; i-- {
 		if m, ok := p.scope[i][name]; ok {
 			return m.ref
 		}
 	}
-	return p.resolveTopScope(name, options)
+	return p.resolveTopScope(pos, name, options)
 }
 
-func (p *protoConverter) resolveTopScope(name string, options []*proto.Option) string {
+func (p *protoConverter) resolveTopScope(pos scanner.Position, name string, options []*proto.Option) string {
 	for i := 0; i < len(name); i++ {
 		k := strings.IndexByte(name[i:], '.')
 		i += k
@@ -236,7 +239,7 @@
 	if s, ok := protoToCUE(name, options); ok {
 		return s
 	}
-	failf("name %q not found", name)
+	failf(pos, "name %q not found", name)
 	return ""
 }
 
@@ -257,13 +260,13 @@
 	}
 
 	if filename == "" {
-		p.mustBuiltinPackage(v.Filename)
+		p.mustBuiltinPackage(v.Position, v.Filename)
 		return
 	}
 
 	imp, err := p.state.parse(filename, nil)
 	if err != nil {
-		fail(err)
+		fail(v.Position, err)
 	}
 
 	prefix := ""
@@ -298,16 +301,26 @@
 	}
 }
 
-func (p *protoConverter) stringLit(s string) *ast.BasicLit {
-	return &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(s)}
+func (p *protoConverter) stringLit(pos scanner.Position, s string) *ast.BasicLit {
+	return &ast.BasicLit{
+		ValuePos: p.toCUEPos(pos),
+		Kind:     token.STRING,
+		Value:    strconv.Quote(s)}
 }
 
-func (p *protoConverter) ref() *ast.Ident {
-	return ast.NewIdent(strings.Join(p.path, "_"))
+func (p *protoConverter) ident(pos scanner.Position, name string) *ast.Ident {
+	return &ast.Ident{NamePos: p.toCUEPos(pos), Name: labelName(name)}
 }
 
-func (p *protoConverter) subref(name string) *ast.Ident {
-	return ast.NewIdent(strings.Join(append(p.path, name), "_"))
+func (p *protoConverter) ref(pos scanner.Position) *ast.Ident {
+	return &ast.Ident{NamePos: p.toCUEPos(pos), Name: strings.Join(p.path, "_")}
+}
+
+func (p *protoConverter) subref(pos scanner.Position, name string) *ast.Ident {
+	return &ast.Ident{
+		NamePos: p.toCUEPos(pos),
+		Name:    strings.Join(append(p.path, name), "_"),
+	}
 }
 
 func (p *protoConverter) addTag(f *ast.Field, body string) {
@@ -344,7 +357,7 @@
 		// already handled.
 
 	default:
-		failf("unsupported type %T", x)
+		failf(scanner.Position{}, "unsupported type %T", x)
 	}
 }
 
@@ -358,10 +371,12 @@
 	// TODO: handle IsExtend/ proto2
 
 	s := &ast.StructLit{
+		Lbrace: p.toCUEPos(v.Position),
 		// TOOD: set proto file position.
+		Rbrace: token.Newline.Pos(),
 	}
 
-	ref := p.ref()
+	ref := p.ref(v.Position)
 	if v.Comment == nil {
 		ref.NamePos = newSection
 	}
@@ -386,6 +401,7 @@
 
 		if x.Repeated {
 			f.Value = &ast.ListLit{
+				Lbrack:   p.toCUEPos(x.Position),
 				Ellipsis: token.NoSpace.Pos(),
 				Type:     f.Value,
 			}
@@ -397,18 +413,18 @@
 		// All keys are converted to strings.
 		// TODO: support integer keys.
 		f.Label = &ast.TemplateLabel{Ident: ast.NewIdent("_")}
-		f.Value = ast.NewIdent(p.resolve(x.Type, x.Options))
+		f.Value = ast.NewIdent(p.resolve(x.Position, x.Type, x.Options))
 
-		name := labelName(x.Name)
+		name := p.ident(x.Position, x.Name)
 		f = &ast.Field{
-			Label: ast.NewIdent(name),
+			Label: name,
 			Value: &ast.StructLit{Elts: []ast.Decl{f}},
 		}
 		addComments(f, i, x.Comment, x.InlineComment)
 
 		o := optionParser{message: s, field: f}
 		o.tags = fmt.Sprintf("%d,type=map<%s,%s>", x.Sequence, x.KeyType, x.Type)
-		if x.Name != name {
+		if x.Name != name.Name {
 			o.tags += "," + x.Name
 		}
 		s.Elts = append(s.Elts, f)
@@ -425,7 +441,7 @@
 		p.oneOf(x)
 
 	default:
-		failf("unsupported type %T", v)
+		failf(scanner.Position{}, "unsupported type %T", v)
 	}
 }
 
@@ -449,10 +465,10 @@
 // will be prefixed with the name of its parent and an underscore.
 func (p *protoConverter) enum(x *proto.Enum) {
 	if len(x.Elements) == 0 {
-		failf("empty enum")
+		failf(x.Position, "empty enum")
 	}
 
-	name := p.subref(x.Name)
+	name := p.subref(x.Position, x.Name)
 
 	p.addNames(x.Elements)
 
@@ -482,13 +498,13 @@
 		case *proto.EnumField:
 			// Add enum value to map
 			f := &ast.Field{
-				Label: p.stringLit(y.Name),
+				Label: p.stringLit(y.Position, y.Name),
 				Value: &ast.BasicLit{Value: strconv.Itoa(y.Integer)},
 			}
 			valueMap.Elts = append(valueMap.Elts, f)
 
 			// add to enum disjunction
-			value := p.stringLit(y.Name)
+			value := p.stringLit(y.Position, y.Name)
 
 			var e ast.Expr = value
 			// Make the first value the default value.
@@ -523,14 +539,17 @@
 
 func (p *protoConverter) oneOf(x *proto.Oneof) {
 	f := &ast.Field{
-		Label: p.ref(),
+		Label: p.ref(x.Position),
 	}
 	f.AddComment(comment(x.Comment, true))
 
 	p.file.Decls = append(p.file.Decls, f)
 
 	for _, v := range x.Elements {
-		s := &ast.StructLit{}
+		s := &ast.StructLit{
+			// TODO: make this the default in the formatter.
+			Rbrace: token.Newline.Pos(),
+		}
 		switch x := v.(type) {
 		case *proto.OneOfField:
 			f := p.parseField(s, 0, x.Field)
@@ -551,9 +570,9 @@
 	f := &ast.Field{}
 	addComments(f, i, x.Comment, x.InlineComment)
 
-	name := labelName(x.Name)
-	f.Label = ast.NewIdent(name)
-	typ := p.resolve(x.Type, x.Options)
+	name := p.ident(x.Position, x.Name)
+	f.Label = name
+	typ := p.resolve(x.Position, x.Type, x.Options)
 	f.Value = ast.NewIdent(typ)
 	s.Elts = append(s.Elts, f)
 
@@ -564,7 +583,7 @@
 	if x.Type != typ {
 		o.tags += ",type=" + x.Type
 	}
-	if x.Name != name {
+	if x.Name != name.Name {
 		o.tags += ",name=" + x.Name
 	}
 	o.parse(x.Options)
@@ -598,7 +617,7 @@
 			// TODO: set filename and base offset.
 			expr, err := parser.ParseExpr("", o.Constant.Source)
 			if err != nil {
-				failf("invalid cue.val value: %v", err)
+				failf(o.Position, "invalid cue.val value: %v", err)
 			}
 			// Any further checks will be done at the end.
 			constraint := &ast.Field{Label: p.field.Label, Value: expr}
diff --git a/internal/protobuf/protobuf.go b/internal/protobuf/protobuf.go
index 958e6fb..bf95bc4 100644
--- a/internal/protobuf/protobuf.go
+++ b/internal/protobuf/protobuf.go
@@ -19,9 +19,6 @@
 package protobuf
 
 import (
-	"fmt"
-	"io"
-
 	"cuelang.org/go/cue/ast"
 )
 
@@ -30,10 +27,11 @@
 	Paths []string
 }
 
-// Parse parses a single proto file and returns its contents translated to
-// a CUE file. Imports are resolved using the path define in Config.
-// If body is not nil, it will use this as the contents of the file. Otherwise
-// Parse will open the given file name at the fully qualified path.
+// Parse parses a single proto file and returns its contents translated to a CUE
+// file. Imports are resolved using the paths defined in Config. If src is not
+// nil, it will use this as the contents of the file. It may be a string, []byte
+// or io.Reader. Otherwise Parse will open the given file name at the fully
+// qualified path.
 //
 // The following field options are supported:
 //    (cue.val)     string        CUE constraint for this field. The string may
@@ -41,33 +39,17 @@
 //    (cue.opt)     FieldOptions
 //       required   bool          Defines the field is required. Use with
 //                                caution.
-func Parse(filename string, body io.Reader, c *Config) (f *ast.File, err error) {
+func Parse(filename string, src interface{}, c *Config) (f *ast.File, err error) {
 	state := &sharedState{
 		paths: c.Paths,
 	}
-	p, err := state.parse(filename, body)
+	p, err := state.parse(filename, src)
 	if err != nil {
 		return nil, err
 	}
 	return p.file, nil
 }
 
-// Error describes the location and cause of an error.
-type Error struct {
-	Filename string
-	Path     string
-	Err      error
-}
-
-func (p *Error) Unwrap() error { return p.Err }
-
-func (p *Error) Error() string {
-	if p.Path == "" {
-		return fmt.Sprintf("parse of file %q failed: %v", p.Filename, p.Err)
-	}
-	return fmt.Sprintf("parse of file %q failed at %s: %v", p.Filename, p.Path, p.Err)
-}
-
 // TODO
 // func GenDefinition
 
diff --git a/internal/protobuf/protobuf_test.go b/internal/protobuf/protobuf_test.go
index e21a126..5049281 100644
--- a/internal/protobuf/protobuf_test.go
+++ b/internal/protobuf/protobuf_test.go
@@ -26,7 +26,7 @@
 	"github.com/kr/pretty"
 )
 
-var update *bool = flag.Bool("update", false, "update the test output")
+var update = flag.Bool("update", false, "update the test output")
 
 func TestParseDefinitions(t *testing.T) {
 	testCases := []string{
diff --git a/internal/protobuf/types.go b/internal/protobuf/types.go
index 99e5af4..230e211 100644
--- a/internal/protobuf/types.go
+++ b/internal/protobuf/types.go
@@ -14,7 +14,11 @@
 
 package protobuf
 
-import "github.com/emicklei/proto"
+import (
+	"text/scanner"
+
+	"github.com/emicklei/proto"
+)
 
 func protoToCUE(typ string, options []*proto.Option) (ref string, ok bool) {
 	t, ok := scalars[typ]
@@ -53,7 +57,7 @@
 	p.scope[0][from] = mapping{to, pkg}
 }
 
-func (p *protoConverter) mustBuiltinPackage(file string) {
+func (p *protoConverter) mustBuiltinPackage(pos scanner.Position, file string) {
 	// Map some builtin types to their JSON/CUE mappings.
 	switch file {
 	case "gogoproto/gogo.proto":
@@ -71,6 +75,6 @@
 		p.setBuiltin("google.protobuf.Empty", "{}", nil)
 
 	default:
-		failf("import %q not found", file)
+		failf(pos, "import %q not found", file)
 	}
 }
diff --git a/internal/protobuf/util.go b/internal/protobuf/util.go
index 980a7d3..d1040d0 100644
--- a/internal/protobuf/util.go
+++ b/internal/protobuf/util.go
@@ -16,6 +16,7 @@
 
 import (
 	"strings"
+	"text/scanner"
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/token"
@@ -25,15 +26,16 @@
 
 // failf panics with a marked error that can be intercepted upon returning
 // from parsing.
-func failf(format string, args ...interface{}) {
-	panic(protoError{xerrors.Errorf(format, args...)})
+func failf(pos scanner.Position, format string, args ...interface{}) {
+	panic(protoError{pos, xerrors.Errorf(format, args...)})
 }
 
-func fail(err error) {
-	panic(protoError{err})
+func fail(pos scanner.Position, err error) {
+	panic(protoError{pos, err})
 }
 
 type protoError struct {
+	pos scanner.Position
 	error
 }