// Copyright 2018 The 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 parser import ( "fmt" "strings" "testing" "cuelang.org/go/cue/ast" ) func TestParse(t *testing.T) { testCases := []struct{ desc, in, out string }{{ "ellipsis in structs", `#Def: { b: "2" ... } ... #Def2: { ... b: "2" } #Def3: {... _} ... `, `#Def: {b: "2", ...}, ..., #Def2: {..., b: "2"}, #Def3: {..., _}, ...`, }, { "empty file", "", "", }, { "empty struct", "{}", "{}", }, { "empty structs", "{},{},", "{}, {}", }, { "empty structs; elided comma", "{}\n{}", "{}, {}", }, { "basic lits", `"a","b", 3,3.4,5,2_3`, `"a", "b", 3, 3.4, 5, 2_3`, }, { "keyword basic lits", `true,false,null,for,in,if,let,if`, `true, false, null, for, in, if, let, if`, }, { "keyword basic newline", ` true false null for in if let if `, `true, false, null, for, in, if, let, if`, }, { "keywords as labels", `if: 0, for: 1, in: 2, where: 3, div: 4, quo: 5, func: 6 for: if: func: let: 3 `, `if: 0, for: 1, in: 2, where: 3, div: 4, quo: 5, func: 6, for: {if: {func: {let: 3}}}`, }, { "keywords as alias", `if=foo: 0 for=bar: 2 let=bar: 3 func=baz: 4 `, `if=foo: 0, for=bar: 2, let=bar: 3, func=baz: 4`, }, { "json", `{ "a": 1, "b": "2", "c": 3 }`, `{"a": 1, "b": "2", "c": 3}`, }, { "json:extra comma", `{ "a": 1, "b": "2", "c": 3, }`, `{"a": 1, "b": "2", "c": 3}`, }, { "json:simplified", `{ a: 1 b: "2" c: 3 }`, `{a: 1, b: "2", c: 3}`, }, { "attributes", `a: 1 @xml(,attr) b: 2 @foo(a,b=4) @go(Foo) c: { d: "x" @go(D) @json(,omitempty) e: "y" @ts(,type=string,"str") }`, `a: 1 @xml(,attr), b: 2 @foo(a,b=4) @go(Foo), c: {d: "x" @go(D) @json(,omitempty), e: "y" @ts(,type=string,"str")}`, }, { "not emitted", `a: true b?: "2" c?: 3 d!: 2 e: f!: 3 "g\("en")"?: 4 "h\("en")"!: 4 `, `a: true, b?: "2", c?: 3, d!: 2, e: {f!: 3}, "g\("en")"?: 4, "h\("en")"!: 4`, }, { "definition", `#Def: { b: "2" c: 3 embedding } #Def: {} `, `#Def: {b: "2", c: 3, embedding}, #Def: {}`, }, { "one-line embedding", `{ V1, V2 }`, `{V1, V2}`, }, { "selectors", `a.b. "str"`, `a.b."str"`, }, { "selectors", `a.b. "str"`, `a.b."str"`, }, { "faulty bytes selector", `a.b.'str'`, "a.b._\nexpected selector, found 'STRING' 'str'", }, { "faulty multiline string selector", `a.b.""" """`, "a.b._\nexpected selector, found 'STRING' \"\"\"\n\t\t\t\"\"\"", }, { "expression embedding", `#Def: { a.b.c a > b < c -1<2 foo: 2 }`, `#Def: {a.b.c, a>b=1 & <=2 b: >2.0 & <= 40.0 c: >"a" & <="b" v: (>=1 & <=2) & <=(>=5 & <=10) w: >1 & <=2 & <=3 d: >=3T & <=5M `, "a: >=1&<=2, b: >2.0&<=40.0, c: >\"a\"&<=\"b\", v: (>=1&<=2)&<=(>=5&<=10), w: >1&<=2&<=3, d: >=3T&<=5M", }, { "indices", `{ a: b[2] b: c[1:2] c: "asdf" d: c ["a"] }`, `{a: b[2], b: c[1:2], c: "asdf", d: c["a"]}`, }, { "calls", `{ a: b(a.b, c.d) b: a.b(c) }`, `{a: b(a.b, c.d), b: a.b(c)}`, }, { "lists", `{ a: [ 1, 2, 3, b, c, ... ] b: [ 1, 2, 3, ], c: [ 1, 2, 3 ], d: [ 1+2, 2, 4,] }`, `{a: [1, 2, 3, b, c, ...], b: [1, 2, 3], c: [1, 2, 3], d: [1+2, 2, 4]}`, }, { "list types", `{ a: 4*[int] b: <=5*[{a: 5}] c1: [...int] c2: [...] c3: [1, 2, ...int,] }`, `{a: 4*[int], b: <=5*[{a: 5}], c1: [...int], c2: [...], c3: [1, 2, ...int]}`, }, { "list comprehensions", `{ y: [1,2,3] b: [for x in y if x == 1 { x }], }`, `{y: [1, 2, 3], b: [for x in y if x==1 {x}]}`, }, { "field comprehensions", `{ y: { a: 1, b: 2} a: { for k, v in y if v > 2 { "\(k)": v } } }`, `{y: {a: 1, b: 2}, a: {for k: v in y if v>2 {"\(k)": v}}}`, }, { "nested comprehensions", `{ y: { a: 1, b: 2} a: { for k, v in y let x = v+2 if x > 2 { "\(k)": v } } }`, `{y: {a: 1, b: 2}, a: {for k: v in y let x=v+2 if x>2 {"\(k)": v}}}`, }, { "let declaration", `{ let X = 42 let Y = "42", let Z = 10 + 12 }`, `{let X=42, let Y="42", let Z=10+12}`, }, { "duplicates allowed", `{ a: b: 3 a: { b: 3 } }`, "{a: {b: 3}, a: {b: 3}}", }, { "templates", // TODO: remove `{ [foo=_]: { a: int } a: { a: 1 } }`, "{[foo=_]: {a: int}, a: {a: 1}}", }, { "value alias", ` { a: X=foo b: Y={foo} c: d: e: X=5 } `, `{a: X=foo, b: Y={foo}, c: {d: {e: X=5}}}`, }, { "dynamic labels", `{ (x): a: int x: "foo" a: { (a.b) } (x)?: 1 y: (x)!: 2 }`, `{(x): {a: int}, x: "foo", a: {(a.b)}, (x)?: 1, y: {(x)!: 2}}`, }, { "foo", `[ [1], [1, 2], [1, 2, 3], ]`, "[[1], [1, 2], [1, 2, 3]]", }, { "interpolation", `a: "foo \(ident)" b: "bar \(bar) $$$ " c: "nest \( { a: "\( nest ) "}.a ) \(5)" m1: """ multi \(bar) """ m2: ''' \(bar) multi '''`, `a: "foo \(ident)", b: "bar \(bar) $$$ ", c: "nest \({a: "\(nest) "}.a) \(5)", ` + "m1: \"\"\"\n\t\t\t multi \\(bar)\n\t\t\t \"\"\", m2: '''\n\t\t\t \\(bar) multi\n\t\t\t '''", }, { "file comments", `// foo // uni package foo // uniline // file.1 // file.2 `, "<[0// foo] <[d0// uni] [l3// uniline] [3// file.1 // file.2] package foo>>", }, { "line comments", `// doc a: 5 // line b: 6 // lineb // next `, // next is followed by EOF. Ensure it doesn't move to file. "<[d0// doc] [l5// line] a: 5>, " + "<[l5// lineb] [5// next] b: 6>", }, { "alt comments", `// a ... a: 5 // line a // about a // b ... b: // lineb 6 // about b c: 7 // about c // about d d: // about e e: 3 `, "<[d0// a ...] [l5// line a] [5// about a] a: 5>, " + "<[d0// b ...] [l2// lineb] [5// about b] b: 6>, " + "<[5// about c] c: 7>, " + "<[d0// about d] d: {<[d0// about e] e>: 3}>", }, { "expr comments", ` a: 2 + // 2 + 3 + // 3 + 4 // 4 `, "<[l5// 4] a: <[l2// 3 +] <[l2// 2 +] 2+3>+4>>", }, { "composit comments", `a : { a: 1, b: 2, c: 3, d: 4 // end } b: [ 1, 2, 3, 4, 5, // end ] c: [ 1, 2, 3, 4, // here { a: 3 }, // here 5, 6, 7, 8 // and here ] d: { a: 1 // Hello // Doc b: 2 } e1: [ // comment in list body ] e2: { // comment in struct body } `, "a: {a: 1, b: 2, c: 3, <[d5// end] d: 4>}, " + "b: [1, 2, 3, 4, <[d2// end] 5>], " + "c: [1, 2, 3, <[l2// here] 4>, <[l4// here] {a: 3}>, 5, 6, 7, <[l2// and here] 8>], " + "d: {<[l5// Hello] a: 1>, <[d0// Doc] b: 2>}, " + "e1: <[d1// comment in list body] []>, " + "e2: <[d1// comment in struct body] {}>", }, { "attribute comments", ` a: 1 @a() @b() // d `, `<[l5// d] a: 1 @a() @b()>`, }, { "attribute declarations", ` @foo() package bar @bar() import "strings" @baz() `, `@foo(), package bar, @bar(), import "strings", @baz()`, }, { "comprehension comments", ` if X { // Comment 1 Field: 2 // Comment 2 } `, `if X <[d2// Comment 2] {<[d0// Comment 1] Field: 2>}>`, }, { "let comments", `let X = foo // Comment 1`, `<[5// Comment 1] let X=foo>`, }, { "emit comments", `// a comment at the beginning of the file // a second comment // comment a: 5 {} // a comment at the end of the file `, "<[0// a comment at the beginning of the file] [0// a second comment] <[d0// comment] a: 5>, <[2// a comment at the end of the file] {}>>", }, { "composite comments 2", ` { // foo // fooo foo: 1 bar: 2 } [ {"name": "value"}, // each element has a long {"name": "next"} // optional next element ] `, `{<[0// foo] [d0// fooo] foo: 1>, bar: 2}, [<[l4// each element has a long] {"name": "value"}>, <[l4// optional next element] {"name": "next"}>]`, }, { desc: "field aliasing", in: ` I="\(k)": v S="foo-bar": w L=foo: x X=[0]: { foo: X | null } [Y=string]: { name: Y } X1=[X2=<"d"]: { name: X2 } Y1=foo: Y2=bar: [Y1, Y2] `, out: `I="\(k)": v, ` + `S="foo-bar": w, ` + `L=foo: x, ` + `X=[0]: {foo: X|null}, ` + `[Y=string]: {name: Y}, ` + `X1=[X2=<"d"]: {name: X2}, ` + `Y1=foo: {Y2=bar: [Y1, Y2]}`, }, { desc: "allow keyword in expression", in: ` foo: in & 2 `, out: "foo: in&2", }, { desc: "dot import", in: ` import . "foo" `, out: "import , \"foo\"\nexpected 'STRING', found '.'", }, { desc: "attributes", in: ` package name @t1(v1) { @t2(v2) } a: { a: 1 @t3(v3) @t4(v4) c: 2 } `, out: "package name, @t1(v1), {@t2(v2)}, a: {a: 1, @t3(v3), @t4(v4), c: 2}", }, { desc: "Issue #276", in: ` a: int=>2 `, out: "a: int=>2", }, { desc: "struct comments", in: ` struct: { // This is a comment // This is a comment // Another comment something: { } // extra comment }`, out: `struct: {<[0// This is a comment] [0// This is a comment] [d0// Another comment] [d5// extra comment] something: {}>}`, }, { desc: "list comments", in: ` list: [ // Comment1 // Comment2 // Another comment { }, // Comment 3 ]`, out: "list: [<[0// Comment1] [0// Comment2] [d0// Another comment] [d3// Comment 3] {}>]", }, { desc: "call comments", in: ` funcArg1: foo( {}, // Comment1 // Comment2 {} // Comment3 )`, out: "funcArg1: foo(<[1// Comment1] {}>, <[d0// Comment2] [d1// Comment3] {}>)", }, { desc: "front-style commas", in: ` frontStyle: { "key": "value" , "key2": "value2" , "foo" : bar } `, out: "frontStyle: {\"key\": \"value\", \"key2\": \"value2\", \"foo\": bar}", }, { desc: "function types", in: ` f0: func(): int f1: func(int): int f2: func(int, string): int f3: func({a: int, b: string}): bool f4: func(bool, func(int, string): int): string f5: func(int, int): func(bool, bool): bool f6: func(func(bool, bool): bool, func(string, string): string): func(int, func(int, string): int): func(int, string): int `, out: "f0: func(): int, f1: func(int): int, f2: func(int, string): int, f3: func({a: int, b: string}): bool, f4: func(bool, func(int, string): int): string, f5: func(int, int): func(bool, bool): bool, f6: func(func(bool, bool): bool, func(string, string): string): func(int, func(int, string): int): func(int, string): int", }} for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { mode := []Option{AllErrors} if strings.Contains(tc.desc, "comments") { mode = append(mode, ParseComments) } if strings.Contains(tc.desc, "function") { mode = append(mode, ParseFuncs) } f, err := ParseFile("input", tc.in, mode...) got := debugStr(f) if err != nil { got += "\n" + err.Error() } if got != tc.out { t.Errorf("\ngot %q;\nwant %q", got, tc.out) } }) } } func TestStrict(t *testing.T) { testCases := []struct{ desc, in string }{ {"block comments", `a: 1 /* a */`}, {"space separator", `a b c: 2`}, {"reserved identifiers", `__foo: 3`}, {"old-style alias 1", `X=3`}, {"old-style alias 2", `X={}`}, // Not yet supported {"additional typed not yet supported", `{...int}`}, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { mode := []Option{AllErrors, ParseComments, FromVersion(Latest)} _, err := ParseFile("input", tc.in, mode...) if err == nil { t.Errorf("unexpected success: %v", tc.in) } }) } } func TestParseExpr(t *testing.T) { // just kicking the tires: // a valid arithmetic expression src := "a + b" x, err := parseExprString(src) if err != nil { t.Errorf("ParseExpr(%q): %v", src, err) } // sanity check if _, ok := x.(*ast.BinaryExpr); !ok { t.Errorf("ParseExpr(%q): got %T, want *BinaryExpr", src, x) } // an invalid expression src = "a + *" if _, err := parseExprString(src); err == nil { t.Errorf("ParseExpr(%q): got no error", src) } // a comma is not permitted unless automatically inserted src = "a + b\n" if _, err := parseExprString(src); err != nil { t.Errorf("ParseExpr(%q): got error %s", src, err) } src = "a + b;" if _, err := parseExprString(src); err == nil { t.Errorf("ParseExpr(%q): got no error", src) } // check resolution src = "{ foo: bar, bar: foo }" x, err = parseExprString(src) if err != nil { t.Fatalf("ParseExpr(%q): %v", src, err) } for _, d := range x.(*ast.StructLit).Elts { v := d.(*ast.Field).Value.(*ast.Ident) if v.Scope == nil { t.Errorf("ParseExpr(%q): scope of field %v not set", src, v.Name) } if v.Node == nil { t.Errorf("ParseExpr(%q): scope of node %v not set", src, v.Name) } } // various other stuff following a valid expression const validExpr = "a + b" const anything = "dh3*#D)#_" for _, c := range "!)]};," { src := validExpr + string(c) + anything if _, err := parseExprString(src); err == nil { t.Errorf("ParseExpr(%q): got no error", src) } } // ParseExpr must not crash for _, src := range valids { _, _ = parseExprString(src) } } func TestImports(t *testing.T) { var imports = map[string]bool{ `"a"`: true, `"a/b"`: true, `"a.b"`: true, `'m\x61th'`: true, `"greek/αβ"`: true, `""`: false, // Each of these pairs tests both #""# vs "" strings // and also use of invalid characters spelled out as // escape sequences and written directly. // For example `"\x00"` tests import "\x00" // while "`\x00`" tests import ``. `#"a"#`: true, `"\x00"`: false, "'\x00'": false, `"\x7f"`: false, "`\x7f`": false, `"a!"`: false, "#'a!'#": false, `"a b"`: false, `#"a b"#`: false, `"a\\b"`: false, "#\"a\\b\"#": false, "\"`a`\"": false, "#'\"a\"'#": false, `"\x80\x80"`: false, "#'\x80\x80'#": false, `"\xFFFD"`: false, "#'\xFFFD'#": false, } for path, isValid := range imports { t.Run(path, func(t *testing.T) { src := fmt.Sprintf("package p, import %s", path) _, err := ParseFile("", src) switch { case err != nil && isValid: t.Errorf("ParseFile(%s): got %v; expected no error", src, err) case err == nil && !isValid: t.Errorf("ParseFile(%s): got no error; expected one", src) } }) } } // TestIncompleteSelection ensures that an incomplete selector // expression is parsed as a (blank) *SelectorExpr, not a // *BadExpr. func TestIncompleteSelection(t *testing.T) { for _, src := range []string{ "{ a: fmt. }", // at end of object "{ a: fmt.\n0.0: x }", // not at end of struct } { t.Run("", func(t *testing.T) { f, err := ParseFile("", src) if err == nil { t.Fatalf("ParseFile(%s) succeeded unexpectedly", src) } const wantErr = "expected selector" if !strings.Contains(err.Error(), wantErr) { t.Errorf("ParseFile returned wrong error %q, want %q", err, wantErr) } var sel *ast.SelectorExpr ast.Walk(f, func(n ast.Node) bool { if n, ok := n.(*ast.SelectorExpr); ok { sel = n } return true }, nil) if sel == nil { t.Fatalf("found no *SelectorExpr: %#v %s", f.Decls[0], debugStr(f)) } const wantSel = "&{fmt _ {} {{}}}" if fmt.Sprint(sel) != wantSel { t.Fatalf("found selector %v, want %s", sel, wantSel) } }) } } // For debugging, do not delete. func TestX(t *testing.T) { t.Skip() f, err := ParseFile("input", ` `, ParseComments) if err != nil { t.Errorf("unexpected error: %v", err) } t.Error(debugStr(f)) }