| // Copyright 2017 The Bazel Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package syntax_test |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "go/build" |
| "io/ioutil" |
| "path/filepath" |
| "reflect" |
| "strings" |
| "testing" |
| |
| "go.starlark.net/internal/chunkedfile" |
| "go.starlark.net/starlarktest" |
| "go.starlark.net/syntax" |
| ) |
| |
| func TestExprParseTrees(t *testing.T) { |
| for _, test := range []struct { |
| input, want string |
| }{ |
| {`print(1)`, |
| `(CallExpr Fn=print Args=(1))`}, |
| {"print(1)\n", |
| `(CallExpr Fn=print Args=(1))`}, |
| {`x + 1`, |
| `(BinaryExpr X=x Op=+ Y=1)`}, |
| {`[x for x in y]`, |
| `(Comprehension Body=x Clauses=((ForClause Vars=x X=y)))`}, |
| {`[x for x in (a if b else c)]`, |
| `(Comprehension Body=x Clauses=((ForClause Vars=x X=(ParenExpr X=(CondExpr Cond=b True=a False=c)))))`}, |
| {`x[i].f(42)`, |
| `(CallExpr Fn=(DotExpr X=(IndexExpr X=x Y=i) Name=f) Args=(42))`}, |
| {`x.f()`, |
| `(CallExpr Fn=(DotExpr X=x Name=f))`}, |
| {`x+y*z`, |
| `(BinaryExpr X=x Op=+ Y=(BinaryExpr X=y Op=* Y=z))`}, |
| {`x%y-z`, |
| `(BinaryExpr X=(BinaryExpr X=x Op=% Y=y) Op=- Y=z)`}, |
| {`a + b not in c`, |
| `(BinaryExpr X=(BinaryExpr X=a Op=+ Y=b) Op=not in Y=c)`}, |
| {`lambda x, *args, **kwargs: None`, |
| `(LambdaExpr Params=(x (UnaryExpr Op=* X=args) (UnaryExpr Op=** X=kwargs)) Body=None)`}, |
| {`{"one": 1}`, |
| `(DictExpr List=((DictEntry Key="one" Value=1)))`}, |
| {`a[i]`, |
| `(IndexExpr X=a Y=i)`}, |
| {`a[i:]`, |
| `(SliceExpr X=a Lo=i)`}, |
| {`a[:j]`, |
| `(SliceExpr X=a Hi=j)`}, |
| {`a[::]`, |
| `(SliceExpr X=a)`}, |
| {`a[::k]`, |
| `(SliceExpr X=a Step=k)`}, |
| {`[]`, |
| `(ListExpr)`}, |
| {`[1]`, |
| `(ListExpr List=(1))`}, |
| {`[1,]`, |
| `(ListExpr List=(1))`}, |
| {`[1, 2]`, |
| `(ListExpr List=(1 2))`}, |
| {`()`, |
| `(TupleExpr)`}, |
| {`(4,)`, |
| `(ParenExpr X=(TupleExpr List=(4)))`}, |
| {`(4)`, |
| `(ParenExpr X=4)`}, |
| {`(4, 5)`, |
| `(ParenExpr X=(TupleExpr List=(4 5)))`}, |
| {`1, 2, 3`, |
| `(TupleExpr List=(1 2 3))`}, |
| {`1, 2,`, |
| `unparenthesized tuple with trailing comma`}, |
| {`{}`, |
| `(DictExpr)`}, |
| {`{"a": 1}`, |
| `(DictExpr List=((DictEntry Key="a" Value=1)))`}, |
| {`{"a": 1,}`, |
| `(DictExpr List=((DictEntry Key="a" Value=1)))`}, |
| {`{"a": 1, "b": 2}`, |
| `(DictExpr List=((DictEntry Key="a" Value=1) (DictEntry Key="b" Value=2)))`}, |
| {`{x: y for (x, y) in z}`, |
| `(Comprehension Curly Body=(DictEntry Key=x Value=y) Clauses=((ForClause Vars=(ParenExpr X=(TupleExpr List=(x y))) X=z)))`}, |
| {`{x: y for a in b if c}`, |
| `(Comprehension Curly Body=(DictEntry Key=x Value=y) Clauses=((ForClause Vars=a X=b) (IfClause Cond=c)))`}, |
| {`-1 + +2`, |
| `(BinaryExpr X=(UnaryExpr Op=- X=1) Op=+ Y=(UnaryExpr Op=+ X=2))`}, |
| {`"foo" + "bar"`, |
| `(BinaryExpr X="foo" Op=+ Y="bar")`}, |
| {`-1 * 2`, // prec(unary -) > prec(binary *) |
| `(BinaryExpr X=(UnaryExpr Op=- X=1) Op=* Y=2)`}, |
| {`-x[i]`, // prec(unary -) < prec(x[i]) |
| `(UnaryExpr Op=- X=(IndexExpr X=x Y=i))`}, |
| {`a | b & c | d`, // prec(|) < prec(&) |
| `(BinaryExpr X=(BinaryExpr X=a Op=| Y=(BinaryExpr X=b Op=& Y=c)) Op=| Y=d)`}, |
| {`a or b and c or d`, |
| `(BinaryExpr X=(BinaryExpr X=a Op=or Y=(BinaryExpr X=b Op=and Y=c)) Op=or Y=d)`}, |
| {`a and b or c and d`, |
| `(BinaryExpr X=(BinaryExpr X=a Op=and Y=b) Op=or Y=(BinaryExpr X=c Op=and Y=d))`}, |
| {`f(1, x=y)`, |
| `(CallExpr Fn=f Args=(1 (BinaryExpr X=x Op== Y=y)))`}, |
| {`f(*args, **kwargs)`, |
| `(CallExpr Fn=f Args=((UnaryExpr Op=* X=args) (UnaryExpr Op=** X=kwargs)))`}, |
| {`lambda *args, *, x=1, **kwargs: 0`, |
| `(LambdaExpr Params=((UnaryExpr Op=* X=args) (UnaryExpr Op=*) (BinaryExpr X=x Op== Y=1) (UnaryExpr Op=** X=kwargs)) Body=0)`}, |
| {`lambda *, a, *b: 0`, |
| `(LambdaExpr Params=((UnaryExpr Op=*) a (UnaryExpr Op=* X=b)) Body=0)`}, |
| {`a if b else c`, |
| `(CondExpr Cond=b True=a False=c)`}, |
| {`a and not b`, |
| `(BinaryExpr X=a Op=and Y=(UnaryExpr Op=not X=b))`}, |
| {`[e for x in y if cond1 if cond2]`, |
| `(Comprehension Body=e Clauses=((ForClause Vars=x X=y) (IfClause Cond=cond1) (IfClause Cond=cond2)))`}, // github.com/google/skylark/issues/53 |
| } { |
| e, err := syntax.ParseExpr("foo.star", test.input, 0) |
| var got string |
| if err != nil { |
| got = stripPos(err) |
| } else { |
| got = treeString(e) |
| } |
| if test.want != got { |
| t.Errorf("parse `%s` = %s, want %s", test.input, got, test.want) |
| } |
| } |
| } |
| |
| func TestStmtParseTrees(t *testing.T) { |
| for _, test := range []struct { |
| input, want string |
| }{ |
| {`print(1)`, |
| `(ExprStmt X=(CallExpr Fn=print Args=(1)))`}, |
| {`return 1, 2`, |
| `(ReturnStmt Result=(TupleExpr List=(1 2)))`}, |
| {`return`, |
| `(ReturnStmt)`}, |
| {`for i in "abc": break`, |
| `(ForStmt Vars=i X="abc" Body=((BranchStmt Token=break)))`}, |
| {`for i in "abc": continue`, |
| `(ForStmt Vars=i X="abc" Body=((BranchStmt Token=continue)))`}, |
| {`for x, y in z: pass`, |
| `(ForStmt Vars=(TupleExpr List=(x y)) X=z Body=((BranchStmt Token=pass)))`}, |
| {`if True: pass`, |
| `(IfStmt Cond=True True=((BranchStmt Token=pass)))`}, |
| {`if True: break`, |
| `(IfStmt Cond=True True=((BranchStmt Token=break)))`}, |
| {`if True: continue`, |
| `(IfStmt Cond=True True=((BranchStmt Token=continue)))`}, |
| {`if True: pass |
| else: |
| pass`, |
| `(IfStmt Cond=True True=((BranchStmt Token=pass)) False=((BranchStmt Token=pass)))`}, |
| {"if a: pass\nelif b: pass\nelse: pass", |
| `(IfStmt Cond=a True=((BranchStmt Token=pass)) False=((IfStmt Cond=b True=((BranchStmt Token=pass)) False=((BranchStmt Token=pass)))))`}, |
| {`x, y = 1, 2`, |
| `(AssignStmt Op== LHS=(TupleExpr List=(x y)) RHS=(TupleExpr List=(1 2)))`}, |
| {`x[i] = 1`, |
| `(AssignStmt Op== LHS=(IndexExpr X=x Y=i) RHS=1)`}, |
| {`x.f = 1`, |
| `(AssignStmt Op== LHS=(DotExpr X=x Name=f) RHS=1)`}, |
| {`(x, y) = 1`, |
| `(AssignStmt Op== LHS=(ParenExpr X=(TupleExpr List=(x y))) RHS=1)`}, |
| {`load("", "a", b="c")`, |
| `(LoadStmt Module="" From=(a c) To=(a b))`}, |
| {`if True: load("", "a", b="c")`, // load needn't be at toplevel |
| `(IfStmt Cond=True True=((LoadStmt Module="" From=(a c) To=(a b))))`}, |
| {`def f(x, *args, **kwargs): |
| pass`, |
| `(DefStmt Name=f Params=(x (UnaryExpr Op=* X=args) (UnaryExpr Op=** X=kwargs)) Body=((BranchStmt Token=pass)))`}, |
| {`def f(**kwargs, *args): pass`, |
| `(DefStmt Name=f Params=((UnaryExpr Op=** X=kwargs) (UnaryExpr Op=* X=args)) Body=((BranchStmt Token=pass)))`}, |
| {`def f(a, b, c=d): pass`, |
| `(DefStmt Name=f Params=(a b (BinaryExpr X=c Op== Y=d)) Body=((BranchStmt Token=pass)))`}, |
| {`def f(a, b=c, d): pass`, |
| `(DefStmt Name=f Params=(a (BinaryExpr X=b Op== Y=c) d) Body=((BranchStmt Token=pass)))`}, // TODO(adonovan): fix this |
| {`def f(): |
| def g(): |
| pass |
| pass |
| def h(): |
| pass`, |
| `(DefStmt Name=f Body=((DefStmt Name=g Body=((BranchStmt Token=pass))) (BranchStmt Token=pass)))`}, |
| {"f();g()", |
| `(ExprStmt X=(CallExpr Fn=f))`}, |
| {"f();", |
| `(ExprStmt X=(CallExpr Fn=f))`}, |
| {"f();g()\n", |
| `(ExprStmt X=(CallExpr Fn=f))`}, |
| {"f();\n", |
| `(ExprStmt X=(CallExpr Fn=f))`}, |
| } { |
| f, err := syntax.Parse("foo.star", test.input, 0) |
| if err != nil { |
| t.Errorf("parse `%s` failed: %v", test.input, stripPos(err)) |
| continue |
| } |
| if got := treeString(f.Stmts[0]); test.want != got { |
| t.Errorf("parse `%s` = %s, want %s", test.input, got, test.want) |
| } |
| } |
| } |
| |
| // TestFileParseTrees tests sequences of statements, and particularly |
| // handling of indentation, newlines, line continuations, and blank lines. |
| func TestFileParseTrees(t *testing.T) { |
| for _, test := range []struct { |
| input, want string |
| }{ |
| {`x = 1 |
| print(x)`, |
| `(AssignStmt Op== LHS=x RHS=1) |
| (ExprStmt X=(CallExpr Fn=print Args=(x)))`}, |
| {"if cond:\n\tpass", |
| `(IfStmt Cond=cond True=((BranchStmt Token=pass)))`}, |
| {"if cond:\n\tpass\nelse:\n\tpass", |
| `(IfStmt Cond=cond True=((BranchStmt Token=pass)) False=((BranchStmt Token=pass)))`}, |
| {`def f(): |
| pass |
| pass |
| |
| pass`, |
| `(DefStmt Name=f Body=((BranchStmt Token=pass))) |
| (BranchStmt Token=pass) |
| (BranchStmt Token=pass)`}, |
| {`pass; pass`, |
| `(BranchStmt Token=pass) |
| (BranchStmt Token=pass)`}, |
| {"pass\npass", |
| `(BranchStmt Token=pass) |
| (BranchStmt Token=pass)`}, |
| {"pass\n\npass", |
| `(BranchStmt Token=pass) |
| (BranchStmt Token=pass)`}, |
| {`x = (1 + |
| 2)`, |
| `(AssignStmt Op== LHS=x RHS=(ParenExpr X=(BinaryExpr X=1 Op=+ Y=2)))`}, |
| {`x = 1 \ |
| + 2`, |
| `(AssignStmt Op== LHS=x RHS=(BinaryExpr X=1 Op=+ Y=2))`}, |
| } { |
| f, err := syntax.Parse("foo.star", test.input, 0) |
| if err != nil { |
| t.Errorf("parse `%s` failed: %v", test.input, stripPos(err)) |
| continue |
| } |
| var buf bytes.Buffer |
| for i, stmt := range f.Stmts { |
| if i > 0 { |
| buf.WriteByte('\n') |
| } |
| writeTree(&buf, reflect.ValueOf(stmt)) |
| } |
| if got := buf.String(); test.want != got { |
| t.Errorf("parse `%s` = %s, want %s", test.input, got, test.want) |
| } |
| } |
| } |
| |
| // TestCompoundStmt tests handling of REPL-style compound statements. |
| func TestCompoundStmt(t *testing.T) { |
| for _, test := range []struct { |
| input, want string |
| }{ |
| // blank lines |
| {"\n", |
| ``}, |
| {" \n", |
| ``}, |
| {"# comment\n", |
| ``}, |
| // simple statement |
| {"1\n", |
| `(ExprStmt X=1)`}, |
| {"print(1)\n", |
| `(ExprStmt X=(CallExpr Fn=print Args=(1)))`}, |
| {"1;2;3;\n", |
| `(ExprStmt X=1)(ExprStmt X=2)(ExprStmt X=3)`}, |
| {"f();g()\n", |
| `(ExprStmt X=(CallExpr Fn=f))(ExprStmt X=(CallExpr Fn=g))`}, |
| {"f();\n", |
| `(ExprStmt X=(CallExpr Fn=f))`}, |
| {"f(\n\n\n\n\n\n\n)\n", |
| `(ExprStmt X=(CallExpr Fn=f))`}, |
| // complex statements |
| {"def f():\n pass\n\n", |
| `(DefStmt Name=f Body=((BranchStmt Token=pass)))`}, |
| {"if cond:\n pass\n\n", |
| `(IfStmt Cond=cond True=((BranchStmt Token=pass)))`}, |
| // Even as a 1-liner, the following blank line is required. |
| {"if cond: pass\n\n", |
| `(IfStmt Cond=cond True=((BranchStmt Token=pass)))`}, |
| // github.com/google/starlark-go/issues/121 |
| {"a; b; c\n", |
| `(ExprStmt X=a)(ExprStmt X=b)(ExprStmt X=c)`}, |
| {"a; b c\n", |
| `invalid syntax`}, |
| } { |
| |
| // Fake readline input from string. |
| // The ! suffix, which would cause a parse error, |
| // tests that the parser doesn't read more than necessary. |
| sc := bufio.NewScanner(strings.NewReader(test.input + "!")) |
| readline := func() ([]byte, error) { |
| if sc.Scan() { |
| return []byte(sc.Text() + "\n"), nil |
| } |
| return nil, sc.Err() |
| } |
| |
| var got string |
| f, err := syntax.ParseCompoundStmt("foo.star", readline) |
| if err != nil { |
| got = stripPos(err) |
| } else { |
| for _, stmt := range f.Stmts { |
| got += treeString(stmt) |
| } |
| } |
| if test.want != got { |
| t.Errorf("parse `%s` = %s, want %s", test.input, got, test.want) |
| } |
| } |
| } |
| |
| func stripPos(err error) string { |
| s := err.Error() |
| if i := strings.Index(s, ": "); i >= 0 { |
| s = s[i+len(": "):] // strip file:line:col |
| } |
| return s |
| } |
| |
| // treeString prints a syntax node as a parenthesized tree. |
| // Idents are printed as foo and Literals as "foo" or 42. |
| // Structs are printed as (type name=value ...). |
| // Only non-empty fields are shown. |
| func treeString(n syntax.Node) string { |
| var buf bytes.Buffer |
| writeTree(&buf, reflect.ValueOf(n)) |
| return buf.String() |
| } |
| |
| func writeTree(out *bytes.Buffer, x reflect.Value) { |
| switch x.Kind() { |
| case reflect.String, reflect.Int, reflect.Bool: |
| fmt.Fprintf(out, "%v", x.Interface()) |
| case reflect.Ptr, reflect.Interface: |
| if elem := x.Elem(); elem.Kind() == 0 { |
| out.WriteString("nil") |
| } else { |
| writeTree(out, elem) |
| } |
| case reflect.Struct: |
| switch v := x.Interface().(type) { |
| case syntax.Literal: |
| if v.Token == syntax.STRING { |
| fmt.Fprintf(out, "%q", v.Value) |
| } else if v.Token == syntax.INT { |
| fmt.Fprintf(out, "%d", v.Value) |
| } |
| return |
| case syntax.Ident: |
| out.WriteString(v.Name) |
| return |
| } |
| fmt.Fprintf(out, "(%s", strings.TrimPrefix(x.Type().String(), "syntax.")) |
| for i, n := 0, x.NumField(); i < n; i++ { |
| f := x.Field(i) |
| if f.Type() == reflect.TypeOf(syntax.Position{}) { |
| continue // skip positions |
| } |
| name := x.Type().Field(i).Name |
| if name == "commentsRef" { |
| continue // skip comments fields |
| } |
| if f.Type() == reflect.TypeOf(syntax.Token(0)) { |
| fmt.Fprintf(out, " %s=%s", name, f.Interface()) |
| continue |
| } |
| |
| switch f.Kind() { |
| case reflect.Slice: |
| if n := f.Len(); n > 0 { |
| fmt.Fprintf(out, " %s=(", name) |
| for i := 0; i < n; i++ { |
| if i > 0 { |
| out.WriteByte(' ') |
| } |
| writeTree(out, f.Index(i)) |
| } |
| out.WriteByte(')') |
| } |
| continue |
| case reflect.Ptr, reflect.Interface: |
| if f.IsNil() { |
| continue |
| } |
| case reflect.Int: |
| if f.Int() != 0 { |
| fmt.Fprintf(out, " %s=%d", name, f.Int()) |
| } |
| continue |
| case reflect.Bool: |
| if f.Bool() { |
| fmt.Fprintf(out, " %s", name) |
| } |
| continue |
| } |
| fmt.Fprintf(out, " %s=", name) |
| writeTree(out, f) |
| } |
| fmt.Fprintf(out, ")") |
| default: |
| fmt.Fprintf(out, "%T", x.Interface()) |
| } |
| } |
| |
| func TestParseErrors(t *testing.T) { |
| filename := starlarktest.DataFile("syntax", "testdata/errors.star") |
| for _, chunk := range chunkedfile.Read(filename, t) { |
| _, err := syntax.Parse(filename, chunk.Source, 0) |
| switch err := err.(type) { |
| case nil: |
| // ok |
| case syntax.Error: |
| chunk.GotError(int(err.Pos.Line), err.Msg) |
| default: |
| t.Error(err) |
| } |
| chunk.Done() |
| } |
| } |
| |
| // dataFile is the same as starlarktest.DataFile. |
| // We make a copy to avoid a dependency cycle. |
| var dataFile = func(pkgdir, filename string) string { |
| return filepath.Join(build.Default.GOPATH, "src/go.starlark.net", pkgdir, filename) |
| } |
| |
| func BenchmarkParse(b *testing.B) { |
| filename := dataFile("syntax", "testdata/scan.star") |
| b.StopTimer() |
| data, err := ioutil.ReadFile(filename) |
| if err != nil { |
| b.Fatal(err) |
| } |
| b.StartTimer() |
| |
| for i := 0; i < b.N; i++ { |
| _, err := syntax.Parse(filename, data, 0) |
| if err != nil { |
| b.Fatal(err) |
| } |
| } |
| } |