internal: hoist attribute parsing logic

Change-Id: I76b2163dc717aba19a51b9daa0544792c7a8d599
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4900
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/attr.go b/cue/attr.go
index 03ec8d3..235bed7 100644
--- a/cue/attr.go
+++ b/cue/attr.go
@@ -19,7 +19,7 @@
 	"strings"
 
 	"cuelang.org/go/cue/ast"
-	"cuelang.org/go/cue/literal"
+	"cuelang.org/go/internal"
 )
 
 // This file includes functionality for parsing attributes.
@@ -58,8 +58,8 @@
 		}
 		as = append(as, attr{a.Text[:n], index})
 
-		if err := parseAttrBody(ctx, src, a.Text[index+1:n-1], nil); err != nil {
-			return nil, err
+		if err := internal.ParseAttrBody(src.Pos(), a.Text[index+1:n-1]).Err; err != nil {
+			return nil, ctx.mkErr(newNode(a), err)
 		}
 	}
 
@@ -130,99 +130,3 @@
 func (kv *keyValue) text() string  { return kv.data }
 func (kv *keyValue) key() string   { return kv.data[:kv.equal] }
 func (kv *keyValue) value() string { return kv.data[kv.equal+1:] }
-
-func parseAttrBody(ctx *context, src source, s string, a *parsedAttr) (err *bottom) {
-	i := 0
-	for {
-		// always scan at least one, possibly empty element.
-		n, err := scanAttributeElem(ctx, src, s[i:], a)
-		if err != nil {
-			return err
-		}
-		if i += n; i >= len(s) {
-			break
-		}
-		if s[i] != ',' {
-			return ctx.mkErr(src, "invalid attribute: expected comma")
-		}
-		i++
-	}
-	return nil
-}
-
-func scanAttributeElem(ctx *context, src source, s string, a *parsedAttr) (n int, err *bottom) {
-	// try CUE string
-	kv := keyValue{}
-	if n, kv.data, err = scanAttributeString(ctx, src, s); n == 0 {
-		// try key-value pair
-		p := strings.IndexAny(s, ",=") // ) is assumed to be stripped.
-		switch {
-		case p < 0:
-			kv.data = s
-			n = len(s)
-
-		default: // ','
-			n = p
-			kv.data = s[:n]
-
-		case s[p] == '=':
-			kv.equal = p
-			offset := p + 1
-			var str string
-			if p, str, err = scanAttributeString(ctx, src, s[offset:]); p > 0 {
-				n = offset + p
-				kv.data = s[:offset] + str
-			} else {
-				n = len(s)
-				if p = strings.IndexByte(s[offset:], ','); p >= 0 {
-					n = offset + p
-				}
-				kv.data = s[:n]
-			}
-		}
-	}
-	if a != nil {
-		a.fields = append(a.fields, kv)
-	}
-	return n, err
-}
-
-func scanAttributeString(ctx *context, src source, s string) (n int, str string, err *bottom) {
-	if s == "" || (s[0] != '#' && s[0] != '"' && s[0] != '\'') {
-		return 0, "", nil
-	}
-
-	nHash := 0
-	for {
-		if nHash < len(s) {
-			if s[nHash] == '#' {
-				nHash++
-				continue
-			}
-			if s[nHash] == '\'' || s[nHash] == '"' {
-				break
-			}
-		}
-		return nHash, s[:nHash], ctx.mkErr(src, "invalid attribute string")
-	}
-
-	// Determine closing quote.
-	nQuote := 1
-	if c := s[nHash]; nHash+6 < len(s) && s[nHash+1] == c && s[nHash+2] == c {
-		nQuote = 3
-	}
-	close := s[nHash:nHash+nQuote] + s[:nHash]
-
-	// Search for closing quote.
-	index := strings.Index(s[len(close):], close)
-	if index == -1 {
-		return len(s), "", ctx.mkErr(src, "attribute string not terminated")
-	}
-
-	index += 2 * len(close)
-	s, err2 := literal.Unquote(s[:index])
-	if err2 != nil {
-		return index, "", ctx.mkErr(src, "invalid attribute string: %v", err2)
-	}
-	return index, s, nil
-}
diff --git a/cue/attr_test.go b/cue/attr_test.go
index c48ce96..3a929c4 100644
--- a/cue/attr_test.go
+++ b/cue/attr_test.go
@@ -15,7 +15,6 @@
 package cue
 
 import (
-	"fmt"
 	"reflect"
 	"strings"
 	"testing"
@@ -23,87 +22,6 @@
 	"cuelang.org/go/cue/ast"
 )
 
-func TestAttributeBody(t *testing.T) {
-	testdata := []struct {
-		in, out string
-		err     string
-	}{{
-		in:  "",
-		out: "[{ 0}]",
-	}, {
-		in:  "bb",
-		out: "[{bb 0}]",
-	}, {
-		in:  "a,",
-		out: "[{a 0} { 0}]",
-	}, {
-		in:  `"a",`,
-		out: "[{a 0} { 0}]",
-	}, {
-		in:  "a,b",
-		out: "[{a 0} {b 0}]",
-	}, {
-		in:  `foo,"bar",#"baz"#`,
-		out: "[{foo 0} {bar 0} {baz 0}]",
-	}, {
-		in:  `bar=str`,
-		out: "[{bar=str 3}]",
-	}, {
-		in:  `bar="str"`,
-		out: "[{bar=str 3}]",
-	}, {
-		in:  `foo.bar="str"`,
-		out: "[{foo.bar=str 7}]",
-	}, {
-		in:  `bar=,baz=`,
-		out: "[{bar= 3} {baz= 3}]",
-	}, {
-		in:  `foo=1,bar="str",baz=free form`,
-		out: "[{foo=1 3} {bar=str 3} {baz=free form 3}]",
-	}, {
-		in: `"""
-		"""`,
-		out: "[{ 0}]",
-	}, {
-		in: `#'''
-			\#x20
-			'''#`,
-		out: "[{  0}]",
-	}, {
-		in:  "'' ,b",
-		err: "invalid attribute",
-	}, {
-		in:  "' ,b",
-		err: "not terminated",
-	}, {
-		in:  `"\ "`,
-		err: "invalid attribute",
-	}, {
-		in:  `# `,
-		err: "invalid attribute",
-	}}
-	for _, tc := range testdata {
-		t.Run(tc.in, func(t *testing.T) {
-			pa := &parsedAttr{}
-			err := parseAttrBody(&context{}, baseValue{}, tc.in, pa)
-
-			if tc.err != "" {
-				if !strings.Contains(debugStr(&context{}, err), tc.err) {
-					t.Errorf("error was %v; want %v", err, tc.err)
-				}
-				return
-			}
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			if got := fmt.Sprint(pa.fields); got != tc.out {
-				t.Errorf("got %v; want %v", got, tc.out)
-			}
-		})
-	}
-}
-
 func TestCreateAttrs(t *testing.T) {
 	testdata := []struct {
 		// space-separated lists of attributes
diff --git a/cue/types.go b/cue/types.go
index e8ffdd1..0ba8a4f 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -32,6 +32,7 @@
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal"
 )
 
 // Kind determines the underlying type of a Value.
@@ -1804,95 +1805,55 @@
 // The returned attribute will return an error for any of its methods if there
 // is no attribute for the requested key.
 func (v Value) Attribute(key string) Attribute {
-	const msgNotExist = "attribute %q does not exist"
 	// look up the attributes
 	if v.path == nil || v.path.attrs == nil {
-		return Attribute{err: errors.Newf(token.NoPos, msgNotExist, key)}
+		return Attribute{internal.NewNonExisting(key)}
 	}
 	for _, a := range v.path.attrs.attr {
 		if a.key() != key {
 			continue
 		}
-		at := Attribute{}
-		if err := parseAttrBody(v.ctx(), nil, a.body(), &at.attr); err != nil {
-			return Attribute{err: v.toErr(err)}
-		}
-		return at
+		return Attribute{internal.ParseAttrBody(token.NoPos, a.body())}
 	}
-	return Attribute{err: errors.Newf(token.NoPos, msgNotExist, key)}
+	return Attribute{internal.NewNonExisting(key)}
 }
 
 // An Attribute contains meta data about a field.
 type Attribute struct {
-	attr parsedAttr
-	err  error
+	attr internal.Attr
 }
 
 // Err returns the error associated with this Attribute or nil if this
 // attribute is valid.
 func (a *Attribute) Err() error {
-	return a.err
-}
-
-func (a *Attribute) hasPos(p int) error {
-	if a.err != nil {
-		return a.err
-	}
-	if p >= len(a.attr.fields) {
-		return fmt.Errorf("field does not exist")
-	}
-	return nil
+	return a.attr.Err
 }
 
 // String reports the possibly empty string value at the given position or
 // an error the attribute is invalid or if the position does not exist.
 func (a *Attribute) String(pos int) (string, error) {
-	if err := a.hasPos(pos); err != nil {
-		return "", err
-	}
-	return a.attr.fields[pos].text(), nil
+	return a.attr.String(pos)
 }
 
 // Int reports the integer at the given position or an error if the attribute is
 // invalid, the position does not exist, or the value at the given position is
 // not an integer.
 func (a *Attribute) Int(pos int) (int64, error) {
-	if err := a.hasPos(pos); err != nil {
-		return 0, err
-	}
-	// TODO: use CUE's literal parser once it exists, allowing any of CUE's
-	// number types.
-	return strconv.ParseInt(a.attr.fields[pos].text(), 10, 64)
+	return a.attr.Int(pos)
 }
 
 // Flag reports whether an entry with the given name exists at position pos or
 // onwards or an error if the attribute is invalid or if the first pos-1 entries
 // are not defined.
 func (a *Attribute) Flag(pos int, key string) (bool, error) {
-	if err := a.hasPos(pos - 1); err != nil {
-		return false, err
-	}
-	for _, kv := range a.attr.fields[pos:] {
-		if kv.text() == key {
-			return true, nil
-		}
-	}
-	return false, nil
+	return a.attr.Flag(pos, key)
 }
 
 // Lookup searches for an entry of the form key=value from position pos onwards
 // and reports the value if found. It reports an error if the attribute is
 // invalid or if the first pos-1 entries are not defined.
 func (a *Attribute) Lookup(pos int, key string) (val string, found bool, err error) {
-	if err := a.hasPos(pos - 1); err != nil {
-		return "", false, err
-	}
-	for _, kv := range a.attr.fields[pos:] {
-		if kv.key() == key {
-			return kv.value(), true, nil
-		}
-	}
-	return "", false, nil
+	return a.attr.Lookup(pos, key)
 }
 
 // Expr reports the operation of the underlying expression and the values it
diff --git a/internal/attrs.go b/internal/attrs.go
new file mode 100644
index 0000000..c0d03c8
--- /dev/null
+++ b/internal/attrs.go
@@ -0,0 +1,205 @@
+// Copyright 2020 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 internal
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/literal"
+	"cuelang.org/go/cue/token"
+)
+
+// Attr holds positional information for a single Attr.
+type Attr struct {
+	Fields []keyValue
+	Err    error
+}
+
+// NewNonExisting creates a non-existing attribute.
+func NewNonExisting(key string) Attr {
+	const msgNotExist = "attribute %q does not exist"
+	return Attr{Err: errors.Newf(token.NoPos, msgNotExist, key)}
+}
+
+type keyValue struct {
+	data  string
+	equal int // index of equal sign or 0 if non-existing
+}
+
+func (kv *keyValue) Text() string { return kv.data }
+func (kv *keyValue) Key() string  { return kv.data[:kv.equal] }
+func (kv *keyValue) Value() string {
+	return strings.TrimSpace(kv.data[kv.equal+1:])
+}
+
+func (a *Attr) hasPos(p int) error {
+	if a.Err != nil {
+		return a.Err
+	}
+	if p >= len(a.Fields) {
+		return fmt.Errorf("field does not exist")
+	}
+	return nil
+}
+
+// String reports the possibly empty string value at the given position or
+// an error the attribute is invalid or if the position does not exist.
+func (a *Attr) String(pos int) (string, error) {
+	if err := a.hasPos(pos); err != nil {
+		return "", err
+	}
+	return a.Fields[pos].Text(), nil
+}
+
+// Int reports the integer at the given position or an error if the attribute is
+// invalid, the position does not exist, or the value at the given position is
+// not an integer.
+func (a *Attr) Int(pos int) (int64, error) {
+	if err := a.hasPos(pos); err != nil {
+		return 0, err
+	}
+	// TODO: use CUE's literal parser once it exists, allowing any of CUE's
+	// number types.
+	return strconv.ParseInt(a.Fields[pos].Text(), 10, 64)
+}
+
+// Flag reports whether an entry with the given name exists at position pos or
+// onwards or an error if the attribute is invalid or if the first pos-1 entries
+// are not defined.
+func (a *Attr) Flag(pos int, key string) (bool, error) {
+	if err := a.hasPos(pos - 1); err != nil {
+		return false, err
+	}
+	for _, kv := range a.Fields[pos:] {
+		if kv.Text() == key {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+// Lookup searches for an entry of the form key=value from position pos onwards
+// and reports the value if found. It reports an error if the attribute is
+// invalid or if the first pos-1 entries are not defined.
+func (a *Attr) Lookup(pos int, key string) (val string, found bool, err error) {
+	if err := a.hasPos(pos - 1); err != nil {
+		return "", false, err
+	}
+	for _, kv := range a.Fields[pos:] {
+		if kv.Key() == key {
+			return kv.Value(), true, nil
+		}
+	}
+	return "", false, nil
+}
+
+func ParseAttrBody(pos token.Pos, s string) (a Attr) {
+	i := 0
+	for {
+		// always scan at least one, possibly empty element.
+		n, err := scanAttributeElem(pos, s[i:], &a)
+		if err != nil {
+			return Attr{Err: err}
+		}
+		if i += n; i >= len(s) {
+			break
+		}
+		if s[i] != ',' {
+			return Attr{Err: errors.Newf(pos, "invalid attribute: expected comma")}
+		}
+		i++
+	}
+	return a
+}
+
+func scanAttributeElem(pos token.Pos, s string, a *Attr) (n int, err errors.Error) {
+	// try CUE string
+	kv := keyValue{}
+	if n, kv.data, err = scanAttributeString(pos, s); n == 0 {
+		// try key-value pair
+		p := strings.IndexAny(s, ",=") // ) is assumed to be stripped.
+		switch {
+		case p < 0:
+			kv.data = s
+			n = len(s)
+
+		default: // ','
+			n = p
+			kv.data = s[:n]
+
+		case s[p] == '=':
+			kv.equal = p
+			offset := p + 1
+			var str string
+			if p, str, err = scanAttributeString(pos, s[offset:]); p > 0 {
+				n = offset + p
+				kv.data = s[:offset] + str
+			} else {
+				n = len(s)
+				if p = strings.IndexByte(s[offset:], ','); p >= 0 {
+					n = offset + p
+				}
+				kv.data = s[:n]
+			}
+		}
+	}
+	if a != nil {
+		a.Fields = append(a.Fields, kv)
+	}
+	return n, err
+}
+
+func scanAttributeString(pos token.Pos, s string) (n int, str string, err errors.Error) {
+	if s == "" || (s[0] != '#' && s[0] != '"' && s[0] != '\'') {
+		return 0, "", nil
+	}
+
+	nHash := 0
+	for {
+		if nHash < len(s) {
+			if s[nHash] == '#' {
+				nHash++
+				continue
+			}
+			if s[nHash] == '\'' || s[nHash] == '"' {
+				break
+			}
+		}
+		return nHash, s[:nHash], errors.Newf(pos, "invalid attribute string")
+	}
+
+	// Determine closing quote.
+	nQuote := 1
+	if c := s[nHash]; nHash+6 < len(s) && s[nHash+1] == c && s[nHash+2] == c {
+		nQuote = 3
+	}
+	close := s[nHash:nHash+nQuote] + s[:nHash]
+
+	// Search for closing quote.
+	index := strings.Index(s[len(close):], close)
+	if index == -1 {
+		return len(s), "", errors.Newf(pos, "attribute string not terminated")
+	}
+
+	index += 2 * len(close)
+	s, err2 := literal.Unquote(s[:index])
+	if err2 != nil {
+		return index, "", errors.Newf(pos, "invalid attribute string: %v", err2)
+	}
+	return index, s, nil
+}
diff --git a/internal/attrs_test.go b/internal/attrs_test.go
new file mode 100644
index 0000000..01cba60
--- /dev/null
+++ b/internal/attrs_test.go
@@ -0,0 +1,104 @@
+// Copyright 2020 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 internal
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"cuelang.org/go/cue/token"
+)
+
+func TestAttributeBody(t *testing.T) {
+	testdata := []struct {
+		in, out string
+		err     string
+	}{{
+		in:  "",
+		out: "[{ 0}]",
+	}, {
+		in:  "bb",
+		out: "[{bb 0}]",
+	}, {
+		in:  "a,",
+		out: "[{a 0} { 0}]",
+	}, {
+		in:  `"a",`,
+		out: "[{a 0} { 0}]",
+	}, {
+		in:  "a,b",
+		out: "[{a 0} {b 0}]",
+	}, {
+		in:  `foo,"bar",#"baz"#`,
+		out: "[{foo 0} {bar 0} {baz 0}]",
+	}, {
+		in:  `bar=str`,
+		out: "[{bar=str 3}]",
+	}, {
+		in:  `bar="str"`,
+		out: "[{bar=str 3}]",
+	}, {
+		in:  `foo.bar="str"`,
+		out: "[{foo.bar=str 7}]",
+	}, {
+		in:  `bar=,baz=`,
+		out: "[{bar= 3} {baz= 3}]",
+	}, {
+		in:  `foo=1,bar="str",baz=free form`,
+		out: "[{foo=1 3} {bar=str 3} {baz=free form 3}]",
+	}, {
+		in: `"""
+		"""`,
+		out: "[{ 0}]",
+	}, {
+		in: `#'''
+			\#x20
+			'''#`,
+		out: "[{  0}]",
+	}, {
+		in:  "'' ,b",
+		err: "invalid attribute",
+	}, {
+		in:  "' ,b",
+		err: "not terminated",
+	}, {
+		in:  `"\ "`,
+		err: "invalid attribute",
+	}, {
+		in:  `# `,
+		err: "invalid attribute",
+	}}
+	for _, tc := range testdata {
+		t.Run(tc.in, func(t *testing.T) {
+			pa := ParseAttrBody(token.NoPos, tc.in)
+			err := pa.Err
+
+			if tc.err != "" {
+				if !strings.Contains(err.Error(), tc.err) {
+					t.Errorf("error was %v; want %v", err, tc.err)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if got := fmt.Sprint(pa.Fields); got != tc.out {
+				t.Errorf("got %v; want %v", got, tc.out)
+			}
+		})
+	}
+}