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)
+ }
+ })
+ }
+}