| // Copyright 2017 The Bazel Authors. All Rights Reserved. |
| // |
| // 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 net.starlark.java.syntax; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** Tests {@link Node#toString} and {@code NodePrinter}. */ |
| @RunWith(JUnit4.class) |
| public final class NodePrinterTest { |
| private FileOptions fileOptions = FileOptions.DEFAULT; |
| |
| private void setFileOptions(FileOptions fileOptions) { |
| this.fileOptions = fileOptions; |
| } |
| |
| private StarlarkFile parseFile(String... lines) throws SyntaxError.Exception { |
| ParserInput input = ParserInput.fromLines(lines); |
| StarlarkFile file = StarlarkFile.parse(input, fileOptions); |
| if (!file.ok()) { |
| throw new SyntaxError.Exception(file.errors()); |
| } |
| return file; |
| } |
| |
| private Statement parseStatement(String... lines) throws SyntaxError.Exception { |
| return parseFile(lines).getStatements().get(0); |
| } |
| |
| private Expression parseExpression(String... lines) throws SyntaxError.Exception { |
| return Expression.parse(ParserInput.fromLines(lines), fileOptions); |
| } |
| |
| /** |
| * Asserts that the given node's pretty print at a given indent level matches the given string. |
| */ |
| private static void assertPrettyMatches(Node node, int indentLevel, String expected) { |
| StringBuilder buf = new StringBuilder(); |
| new NodePrinter(buf, indentLevel).printNode(node); |
| assertThat(buf.toString()).isEqualTo(expected); |
| } |
| |
| /** Asserts that the given node's pretty print with no indent matches the given string. */ |
| private static void assertPrettyMatches(Node node, String expected) { |
| assertPrettyMatches(node, 0, expected); |
| } |
| |
| /** Asserts that the given node's pretty print with one indent matches the given string. */ |
| private static void assertIndentedPrettyMatches(Node node, String expected) { |
| assertPrettyMatches(node, 1, expected); |
| } |
| |
| /** Asserts that the given node's {@code toString} matches the given string. */ |
| private static void assertTostringMatches(Node node, String expected) { |
| assertThat(node.toString()).isEqualTo(expected); |
| } |
| |
| /** |
| * Parses the given string as an expression, and asserts that its pretty print matches the given |
| * string. |
| */ |
| private void assertExprPrettyMatches(String source, String expected) |
| throws SyntaxError.Exception { |
| Expression node = parseExpression(source); |
| assertPrettyMatches(node, expected); |
| } |
| |
| /** |
| * Parses the given string as an expression, and asserts that its {@code toString} matches the |
| * given string. |
| */ |
| private void assertExprTostringMatches(String source, String expected) |
| throws SyntaxError.Exception { |
| Expression node = parseExpression(source); |
| assertThat(node.toString()).isEqualTo(expected); |
| } |
| |
| /** |
| * Parses the given string as an expression, and asserts that both its pretty print and {@code |
| * toString} return the original string. |
| */ |
| private void assertExprBothRoundTrip(String source) throws SyntaxError.Exception { |
| assertExprPrettyMatches(source, source); |
| assertExprTostringMatches(source, source); |
| } |
| |
| /** |
| * Parses the given string as a statement, and asserts that its pretty print with one indent |
| * matches the given string. |
| */ |
| private void assertStmtIndentedPrettyMatches(String source, String expected) |
| throws SyntaxError.Exception { |
| Statement node = parseStatement(source); |
| assertIndentedPrettyMatches(node, expected); |
| } |
| |
| /** |
| * Parses the given string as an statement, and asserts that its {@code toString} matches the |
| * given string. |
| */ |
| private void assertStmtTostringMatches(String source, String expected) |
| throws SyntaxError.Exception { |
| Statement node = parseStatement(source); |
| assertThat(node.toString()).isEqualTo(expected); |
| } |
| |
| // Expressions. |
| |
| @Test |
| public void abstractComprehension() throws SyntaxError.Exception { |
| // Covers DictComprehension and ListComprehension. |
| assertExprBothRoundTrip("[z for y in x if True for z in y]"); |
| assertExprBothRoundTrip("{z: x for y in x if True for z in y}"); |
| } |
| |
| @Test |
| public void binaryOperatorExpression() throws SyntaxError.Exception { |
| assertExprPrettyMatches("1 + 2", "(1 + 2)"); |
| assertExprTostringMatches("1 + 2", "1 + 2"); |
| |
| assertExprPrettyMatches("1 + (2 * 3)", "(1 + (2 * 3))"); |
| assertExprTostringMatches("1 + (2 * 3)", "1 + 2 * 3"); |
| } |
| |
| @Test |
| public void conditionalExpression() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("1 if True else 2"); |
| } |
| |
| @Test |
| public void dictExpression() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("{1: \"a\", 2: \"b\"}"); |
| } |
| |
| @Test |
| public void dotExpression() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("o.f"); |
| } |
| |
| @Test |
| public void funcallExpression() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("f()"); |
| assertExprBothRoundTrip("f(a)"); |
| assertExprBothRoundTrip("f(a, b = B, c = C, *d, **e)"); |
| assertExprBothRoundTrip("o.f()"); |
| assertExprBothRoundTrip("f(1 + 1)"); |
| } |
| |
| @Test |
| public void identifier() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("foo"); |
| } |
| |
| @Test |
| public void indexExpression() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("a[i]"); |
| assertExprBothRoundTrip("a[(1,)]"); |
| assertExprPrettyMatches("a[1,2]", "a[(1, 2)]"); |
| assertExprTostringMatches("a[1,2]", "a[(1, 2)]"); |
| } |
| |
| @Test |
| public void integerLiteral() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("5"); |
| } |
| |
| @Test |
| public void listLiteralShort() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("[]"); |
| assertExprBothRoundTrip("[5]"); |
| assertExprBothRoundTrip("[5, 6]"); |
| assertExprBothRoundTrip("()"); |
| assertExprBothRoundTrip("(5,)"); |
| assertExprBothRoundTrip("(5, 6)"); |
| } |
| |
| @Test |
| public void listLiteralLong() throws SyntaxError.Exception { |
| // List literals with enough elements to trigger the abbreviated toString() format. |
| assertExprPrettyMatches("[1, 2, 3, 4, 5, 6]", "[1, 2, 3, 4, 5, 6]"); |
| assertExprTostringMatches("[1, 2, 3, 4, 5, 6]", "[1, 2, 3, 4, +2 more]"); |
| |
| assertExprPrettyMatches("(1, 2, 3, 4, 5, 6)", "(1, 2, 3, 4, 5, 6)"); |
| assertExprTostringMatches("(1, 2, 3, 4, 5, 6)", "(1, 2, 3, 4, +2 more)"); |
| } |
| |
| @Test |
| public void listLiteralNested() throws SyntaxError.Exception { |
| // Make sure that the inner list doesn't get abbreviated when the outer list is printed using |
| // prettyPrint(). |
| assertExprPrettyMatches( |
| "[1, 2, 3, [10, 20, 30, 40, 50, 60], 4, 5, 6]", |
| "[1, 2, 3, [10, 20, 30, 40, 50, 60], 4, 5, 6]"); |
| // It doesn't matter as much what toString does. |
| assertExprTostringMatches("[1, 2, 3, [10, 20, 30, 40, 50, 60], 4, 5, 6]", "[1, 2, 3, +4 more]"); |
| } |
| |
| @Test |
| public void sliceExpression() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("a[b:c:d]"); |
| assertExprBothRoundTrip("a[b:c]"); |
| assertExprBothRoundTrip("a[b:]"); |
| assertExprBothRoundTrip("a[:c:d]"); |
| assertExprBothRoundTrip("a[:c]"); |
| assertExprBothRoundTrip("a[::d]"); |
| assertExprBothRoundTrip("a[:]"); |
| } |
| |
| @Test |
| public void stringLiteral() throws SyntaxError.Exception { |
| assertExprBothRoundTrip("\"foo\""); |
| assertExprBothRoundTrip("\"quo\\\"ted\""); |
| } |
| |
| @Test |
| public void unaryOperatorExpression() throws SyntaxError.Exception { |
| assertExprPrettyMatches("not True", "not (True)"); |
| assertExprTostringMatches("not True", "not True"); |
| assertExprPrettyMatches("-(5 + 3)", "-((5 + 3))"); |
| assertExprTostringMatches("-5", "-5"); |
| } |
| |
| // Statements. |
| |
| @Test |
| public void assignmentStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches("x = y", " x = y\n"); |
| assertStmtTostringMatches("x = y", "x = y\n"); |
| } |
| |
| @Test |
| public void augmentedAssignmentStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches("x += y", " x += y\n"); |
| assertStmtTostringMatches("x += y", "x += y\n"); |
| } |
| |
| @Test |
| public void assignmentStatementWithTypeAnnotation() throws SyntaxError.Exception { |
| setFileOptions(FileOptions.builder().allowTypeSyntax(true).build()); |
| assertStmtIndentedPrettyMatches("x : T = y", " x : T = y\n"); |
| assertStmtTostringMatches("x : T = y", "x : T = y\n"); |
| } |
| |
| @Test |
| public void expressionStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches("5", " 5\n"); |
| assertStmtTostringMatches("5", "5\n"); |
| } |
| |
| @Test |
| public void defStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches( |
| """ |
| def f(x): |
| print(x)\ |
| """, |
| """ |
| def f(x): |
| print(x) |
| """); |
| assertStmtTostringMatches( |
| """ |
| def f(x): |
| print(x)\ |
| """, |
| "def f(x): ...\n"); |
| |
| assertStmtIndentedPrettyMatches( |
| """ |
| def f(a, b=B, *c, d=D, **e): |
| print(x)\ |
| """, |
| """ |
| def f(a, b=B, *c, d=D, **e): |
| print(x) |
| """); |
| assertStmtTostringMatches( |
| """ |
| def f(a, b=B, *c, d=D, **e): |
| print(x)\ |
| """, |
| "def f(a, b=B, *c, d=D, **e): ...\n"); |
| |
| assertStmtIndentedPrettyMatches( |
| """ |
| def f(): |
| pass\ |
| """, |
| """ |
| def f(): |
| pass |
| """); |
| assertStmtTostringMatches( |
| """ |
| def f(): |
| pass\ |
| """, |
| "def f(): ...\n"); |
| } |
| |
| @Test |
| public void defStatementWithTypeAnnotations() throws SyntaxError.Exception { |
| setFileOptions(FileOptions.builder().allowTypeSyntax(true).build()); |
| assertStmtIndentedPrettyMatches( |
| """ |
| def f(x:int): |
| print(x)\ |
| """, |
| """ |
| def f(x: int): |
| print(x) |
| """); |
| assertStmtTostringMatches( |
| """ |
| def f(x:bool): |
| print(x)\ |
| """, |
| "def f(x: bool): ...\n"); |
| |
| assertStmtIndentedPrettyMatches( |
| """ |
| def f()->int: |
| print(x)\ |
| """, |
| """ |
| def f() -> int: |
| print(x) |
| """); |
| assertStmtTostringMatches( |
| """ |
| def f() -> bool: |
| print(x) |
| """, |
| "def f() -> bool: ...\n"); |
| assertStmtIndentedPrettyMatches( |
| """ |
| def f[T,U,](x:dict[T,U])->list[U]: |
| print(x)\ |
| """, |
| """ |
| def f[T, U](x: dict[T, U]) -> list[U]: |
| print(x) |
| """); |
| assertStmtTostringMatches( |
| """ |
| def f[T,U,](x:dict[T,U]|set[U]) -> bool: |
| print(x) |
| """, |
| "def f[T, U](x: dict[T, U] | set[U]) -> bool: ...\n"); |
| } |
| |
| @Test |
| public void typeAnnotations() throws SyntaxError.Exception { |
| // TODO(ilist@): replace with parsing type annotations directly (remove `def` from this test) |
| setFileOptions(FileOptions.builder().allowTypeSyntax(true).build()); |
| assertStmtTostringMatches("def f(x:bool): pass", "def f(x: bool): ...\n"); |
| assertStmtTostringMatches("def f(x:None | bool): pass", "def f(x: None | bool): ...\n"); |
| assertStmtTostringMatches("def f(x:list[str]): pass", "def f(x: list[str]): ...\n"); |
| assertStmtTostringMatches("def f(x:dict[str,int]): pass", "def f(x: dict[str, int]): ...\n"); |
| assertStmtTostringMatches( |
| "def f(x:Callable[[str],int]): pass", "def f(x: Callable[[str], int]): ...\n"); |
| assertStmtTostringMatches( |
| "def f(x:Callable[[str|int],int]): pass", "def f(x: Callable[[str | int], int]): ...\n"); |
| assertStmtTostringMatches( |
| "def f(x:TypedDict[{'field1': int}]): pass", |
| "def f(x: TypedDict[{\"field1\": int}]): ...\n"); |
| } |
| |
| @Test |
| public void typeAliasStatement() throws SyntaxError.Exception { |
| setFileOptions(FileOptions.builder().allowTypeSyntax(true).build()); |
| assertStmtTostringMatches("type my_int=int", "type my_int = ...\n"); |
| assertStmtIndentedPrettyMatches("type my_int=int", " type my_int = int\n"); |
| assertStmtTostringMatches("type X[T,U]=dict[T,U]|list[U]", "type X[T, U] = ...\n"); |
| assertStmtIndentedPrettyMatches( |
| "type X[T,U]=dict[T,U]|list[U]", " type X[T, U] = dict[T, U] | list[U]\n"); |
| } |
| |
| @Test |
| public void ellipsisExpression() throws SyntaxError.Exception { |
| setFileOptions( |
| FileOptions.builder().allowTypeSyntax(true).tolerateInvalidTypeExpressions(true).build()); |
| // Use `def` rather than `type` to wrap the type expression, because `type`'s toString() |
| // introduces its own metasyntactic "..." placeholder. |
| assertStmtTostringMatches( |
| "def f(x:Callable[...,int]): pass", "def f(x: Callable[(..., int)]): ...\n"); |
| assertStmtIndentedPrettyMatches("type x=...", " type x = ...\n"); |
| } |
| |
| @Test |
| public void castExpression() throws SyntaxError.Exception { |
| setFileOptions(FileOptions.builder().allowTypeSyntax(true).build()); |
| assertExprPrettyMatches("cast(list[int]|str,x+y)", "cast(list[int] | str, x + y)"); |
| assertExprTostringMatches("cast(set|None,bar(),)", "cast(set | None, bar())"); |
| } |
| |
| @Test |
| public void isinstanceExpression() throws SyntaxError.Exception { |
| setFileOptions(FileOptions.builder().allowTypeSyntax(true).build()); |
| assertExprPrettyMatches("isinstance(x+y, list|tuple)", "isinstance(x + y, list | tuple)"); |
| assertExprTostringMatches("isinstance(foo(), T[U],)", "isinstance(foo(), T[U])"); |
| } |
| |
| @Test |
| public void flowStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches( |
| """ |
| def f(): |
| pass |
| continue |
| break\ |
| """, |
| """ |
| def f(): |
| pass |
| continue |
| break |
| """); |
| } |
| |
| @Test |
| public void forStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches( |
| """ |
| for x in y: |
| print(x)\ |
| """, |
| """ |
| for x in y: |
| print(x) |
| """); |
| assertStmtTostringMatches( |
| """ |
| for x in y: |
| print(x)\ |
| """, |
| "for x in y: ...\n"); |
| |
| assertStmtIndentedPrettyMatches( |
| """ |
| for x in y: |
| pass\ |
| """, |
| """ |
| for x in y: |
| pass |
| """); |
| assertStmtTostringMatches( |
| """ |
| for x in y: |
| pass\ |
| """, |
| "for x in y: ...\n"); |
| } |
| |
| @Test |
| public void ifStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches( |
| """ |
| if True: |
| print(x)\ |
| """, |
| """ |
| if True: |
| print(x) |
| """); |
| assertStmtTostringMatches( |
| """ |
| if True: |
| print(x)\ |
| """, |
| "if True: ...\n"); |
| |
| assertStmtIndentedPrettyMatches( |
| """ |
| if True: |
| print(x) |
| elif False: |
| print(y) |
| else: |
| print(z)\ |
| """, |
| """ |
| if True: |
| print(x) |
| elif False: |
| print(y) |
| else: |
| print(z) |
| """); |
| assertStmtTostringMatches( |
| """ |
| if True: |
| print(x) |
| elif False: |
| print(y) |
| else: |
| print(z)\ |
| """, |
| "if True: ...\n"); |
| } |
| |
| @Test |
| public void loadStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches( |
| "load(\"foo.bzl\", a=\"A\", \"B\")", " load(\"foo.bzl\", a=\"A\", \"B\")\n"); |
| assertStmtTostringMatches( |
| "load(\"foo.bzl\", a=\"A\", \"B\")\n", "load(\"foo.bzl\", a=\"A\", \"B\")\n"); |
| } |
| |
| @Test |
| public void varStatement() throws SyntaxError.Exception { |
| setFileOptions(FileOptions.builder().allowTypeSyntax(true).build()); |
| assertStmtIndentedPrettyMatches("x : T", " x : T\n"); |
| assertStmtTostringMatches("x : T\n", "x : T\n"); |
| } |
| |
| @Test |
| public void returnStatement() throws SyntaxError.Exception { |
| assertStmtIndentedPrettyMatches("return \"foo\"", " return \"foo\"\n"); |
| assertStmtTostringMatches("return \"foo\"", "return \"foo\"\n"); |
| |
| assertStmtIndentedPrettyMatches("return None", " return None\n"); |
| assertStmtTostringMatches("return None", "return None\n"); |
| |
| assertStmtIndentedPrettyMatches("return", " return\n"); |
| assertStmtTostringMatches("return", "return\n"); |
| } |
| |
| // Miscellaneous. |
| |
| @Test |
| public void file() throws SyntaxError.Exception { |
| Node node = parseFile("print(x)\nprint(y)"); |
| assertIndentedPrettyMatches( |
| node, |
| """ |
| print(x) |
| print(y) |
| """); |
| assertTostringMatches(node, "<StarlarkFile with 2 statements>"); |
| } |
| |
| @Test |
| public void comment() throws SyntaxError.Exception { |
| ParserInput input = |
| ParserInput.fromLines( |
| "# foo", // |
| "expr # bar"); |
| Parser.ParseResult r = Parser.parseFile(input, FileOptions.DEFAULT); |
| Comment c0 = r.comments.get(0); |
| assertIndentedPrettyMatches(c0, " # foo"); |
| assertTostringMatches(c0, "# foo"); |
| Comment c1 = r.comments.get(1); |
| assertIndentedPrettyMatches(c1, " # bar"); |
| assertTostringMatches(c1, "# bar"); |
| } |
| |
| /* Not tested explicitly because they're covered implicitly by tests for other nodes: |
| * - DictExpression.Entry |
| * - Argument / Parameter |
| * - IfStatements |
| */ |
| } |