doc/ref/spec.md: simplify attributes
Simplify the specifcation of attributes. The only requirement
of attribute bodies is now that parentheses, brackets, and
curly braces are matched.
This approach is adopted from Swift attributes. It is compatible
with a wide range of attribute formats of other languages,
allowing for easier interoperability.
Parsing in the cue API remains the same.
This allows for more tolerance of faulty inputs
as well, allowing for bugs in external specs, such
as mentioned in Issue #181 to not affect the basic
operation of CUE itself. This is especially useful
as the external specs are often not used at all.
Change-Id: I1f0dc63fe4ebf468d4b041481a1d15f999232dab
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4223
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/cue/ast_test.go b/cue/ast_test.go
index 9417ea6..042d49f 100644
--- a/cue/ast_test.go
+++ b/cue/ast_test.go
@@ -327,9 +327,9 @@
out: "<0>{a: int @foo(1,\"str\")}",
}, {
in: `
- a: int @b('' ,b) // invalid
+ a: int @b([,b) // invalid
`,
- out: "attribute missing ')':\n test:2:16\nmissing ',' in struct literal:\n test:3:3\n<0>{}",
+ out: "unexpected ')':\n test:2:18\nattribute missing ')':\n test:3:3\n<0>{}",
}, {
in: `
a d: {
diff --git a/cue/attr_test.go b/cue/attr_test.go
index 3f30f79..5f1f7f0 100644
--- a/cue/attr_test.go
+++ b/cue/attr_test.go
@@ -52,6 +52,9 @@
in: `bar="str"`,
out: "[{bar=str 3}]",
}, {
+ in: `foo.bar="str"`,
+ out: "[{foo.bar=str 7}]",
+ }, {
in: `bar=,baz=`,
out: "[{bar= 3} {baz= 3}]",
}, {
diff --git a/cue/scanner/scanner.go b/cue/scanner/scanner.go
index 29cb0cf..c9b9b06 100644
--- a/cue/scanner/scanner.go
+++ b/cue/scanner/scanner.go
@@ -619,64 +619,37 @@
s.scanIdentifier()
- if s.ch != '(' {
- s.errf(s.offset, "invalid attribute: expected '('")
- return token.ATTRIBUTE, string(s.src[offs:s.offset])
- }
- s.next()
-
- for {
- s.scanAttributeElem()
- if s.ch != ',' {
- break
- }
- s.next()
- }
-
- if s.ch == ')' {
- s.next()
+ if _, tok, _ := s.Scan(); tok == token.LPAREN {
+ s.scanAttributeTokens(token.RPAREN)
} else {
- s.errf(s.offset, "attribute missing ')'")
+ s.errf(s.offset, "invalid attribute: expected '('")
}
return token.ATTRIBUTE, string(s.src[offs:s.offset])
}
-func (s *Scanner) scanAttributeElem() {
- // try CUE string
- if s.scanAttributeString() {
- return
- }
-
- // try key-value pair
- if s.scanIdentifier() != "" && s.ch == '=' {
- s.next()
- if s.scanAttributeString() {
+func (s *Scanner) scanAttributeTokens(close token.Token) {
+ for {
+ switch _, tok, _ := s.Scan(); tok {
+ case close:
return
- }
- }
-
- // raw element or key-value pair with raw value
- for s.ch != ',' && s.ch != ')' && s.ch != '\n' && s.ch != -1 {
- switch s.ch {
- case '#', '\'', '"', '(', '=':
- s.errf(s.offset, "illegal character in attribute")
- s.recoverParen(1)
+ case token.EOF:
+ s.errf(s.offset, "attribute missing '%s'", close)
return
- }
- s.next()
- }
-}
-func (s *Scanner) scanAttributeString() bool {
- if s.ch == '#' || s.ch == '"' || s.ch == '\'' {
- if _, tok, _ := s.Scan(); tok == token.INTERPOLATION {
+ case token.INTERPOLATION:
s.errf(s.offset, "interpolation not allowed in attribute")
s.popInterpolation()
s.recoverParen(1)
+ case token.LPAREN:
+ s.scanAttributeTokens(token.RPAREN)
+ case token.LBRACE:
+ s.scanAttributeTokens(token.RBRACE)
+ case token.LBRACK:
+ s.scanAttributeTokens(token.RBRACK)
+ case token.RPAREN, token.RBRACK, token.RBRACE:
+ s.errf(s.offset, "unexpected '%s'", tok)
}
- return true
}
- return false
}
// recoverParen is an approximate recovery mechanism to recover from invalid
diff --git a/cue/scanner/scanner_test.go b/cue/scanner/scanner_test.go
index 11a377b..ac5e9be 100644
--- a/cue/scanner/scanner_test.go
+++ b/cue/scanner/scanner_test.go
@@ -69,6 +69,8 @@
{token.ATTRIBUTE, `@foo(",a=b")`, special},
{token.ATTRIBUTE, `@foo(##"\(),a=b"##)`, special},
{token.ATTRIBUTE, `@foo("",a="")`, special},
+ {token.ATTRIBUTE, `@foo(2,bytes,a.b=c)`, special},
+ {token.ATTRIBUTE, `@foo([{()}]())`, special},
// Identifiers and basic type literals
{token.BOTTOM, "_|_", literal},
@@ -747,9 +749,9 @@
{`@foo`, token.ATTRIBUTE, 4, `@foo`, "invalid attribute: expected '('"},
{`@foo(`, token.ATTRIBUTE, 5, `@foo(`, "attribute missing ')'"},
{`@foo( `, token.ATTRIBUTE, 6, `@foo( `, "attribute missing ')'"},
- {`@foo( "")`, token.ATTRIBUTE, 6, `@foo( "")`, "illegal character in attribute"},
- {`@foo(a=b=c)`, token.ATTRIBUTE, 8, `@foo(a=b=c)`, "illegal character in attribute"},
- {`@foo("" )`, token.ATTRIBUTE, 7, `@foo(""`, "attribute missing ')'"},
+ {`@foo( ""])`, token.ATTRIBUTE, 9, `@foo( ""])`, "unexpected ']'"},
+ {`@foo(3})`, token.ATTRIBUTE, 7, `@foo(3})`, "unexpected '}'"},
+ {`@foo(["")])`, token.ATTRIBUTE, 9, `@foo(["")])`, "unexpected ')'"},
{`@foo(""`, token.ATTRIBUTE, 7, `@foo(""`, "attribute missing ')'"},
{`@foo(aa`, token.ATTRIBUTE, 7, `@foo(aa`, "attribute missing ')'"},
{`@foo("\(())")`, token.ATTRIBUTE, 7, `@foo("\(())")`, "interpolation not allowed in attribute"},
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index db7916a..a7bf68b 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -1118,13 +1118,12 @@
Label = LabelName [ "?" ] | "[" AliasExpr "]".
LabelName = identifier | simple_string_lit .
-attribute = "@" identifier "(" attr_elems ")" .
-attr_elems = attr_elem { "," attr_elem }
-attr_elem = attr_string | attr_label | attr_nest .
-attr_label = identifier "=" attr_string .
-attr_nest = identifier "(" attr_elems ")" .
-attr_string = { attr_char } | string_lit .
-attr_char = /* an arbitrary Unicode code point except newline, ',', '"', `'`, '#', '=', '(', and ')' */ .
+attribute = "@" identifier "(" attr_tokens ")" .
+attr_tokens = { attr_token |
+ "(" attr_tokens ")" |
+ "[" attr_tokens "]" |
+ "{" attr_tokens "}" } .
+attr_token = /* any token except '(', ')', '[', ']', '{', or '}' */
```
<!--