/* * Copyright 2009 Google Inc. * * 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 com.google.common.css.compiler.ast; import static com.google.common.truth.Truth.assertThat; import com.google.common.css.SourceCode; import junit.framework.TestCase; import java.util.ArrayList; import java.util.List; /** * Unit tests for error handling of {@link GssParser}. * * @author fbenz@google.com (Florian Benz) */ public class GssParserErrorTest extends TestCase { private void testError(String gss, int lineNumber, int indexInLine, String line, String caret) { try { parse(gss); fail(); } catch (GssParserException e) { assertEquals( "Parse error in test at line " + lineNumber + " column " + indexInLine + ":\n" + line + "\n" + caret + "\n", e.getMessage()); } } public void test1() { testError("a { exu7y&&rgx: url('http://test.com') }", 1, 10, "a { exu7y&&rgx: url('http://test.com') }", " ^"); } public void test2() { testError( "a {\n" + " exu7y&&rgx: url('http://test.com')\n" + " }", 2, 10, " exu7y&&rgx: url('http://test.com')", " ^"); } public void test3() { testError("a { b: c,,}", 1, 10, "a { b: c,,}", " ^"); } public void test4() { testError("a", 1, 1, "a", "^"); } public void test5() { testError("a { b: c;", 1, 9, "a { b: c;", " ^"); } public void test6() { testError("{}", 1, 1, "{}", "^"); } public void test7() { testError("\na { b: c,,}", 2, 10, "a { b: c,,}", " ^"); } public void testBadToken1() { // Should be > not <. testError(".foo .bar<td {}", 1, 10, ".foo .bar<td {}", " ^"); } public void testBadToken2() { testError("\n<td {}", 2, 1, "<td {}", "^"); } public void testBadToken3() { testError("<td {}", 1, 1, "<td {}", "^"); } public void testBadWebkitKeyframes1() { testError("@-webkit-keyframes bounce {\n" + " 0 {\n" + " left: 0px;\n" + " }\n" + " 100% {\n" + " left: 200px;\n" + " }\n" + "}\n", 2, 4, " 0 {", " ^"); } public void testBadWebkitKeyframes2() { testError("@-webkit-keyframes bounce {\n" + " 2.2 {\n" + " left: 0px;\n" + " }\n" + " 100% {\n" + " left: 200px;\n" + " }\n" + "}\n", 2, 6, " 2.2 {", " ^"); } public void testBadWebkitKeyframes3() { testError("@-webkit-keyframes foo;", 1, 23, "@-webkit-keyframes foo;", " ^"); } public void testBadPseudoNth1() { testError("div :nth-child(#id) { }", 1, 16, "div :nth-child(#id) { }", " ^"); } public void testBadPseudoNth2() { testError("div :nth-child(.class) { }", 1, 16, "div :nth-child(.class) { }", " ^"); } public void testBadPseudoNot1() { testError("div :not() { }", 1, 10, "div :not() { }", " ^"); } public void testBadPseudoNot2() { // :not can only take a simple selector as an argument. testError("div :not(div p) { }", 1, 14, "div :not(div p) { }", " ^"); } public void testBadMixinDefinition() { testError("@defmixin name($#%$var) {}", 1, 16, "@defmixin name($#%$var) {}", " ^"); } public void testBadGradient() { testError("div {" + "d:-invalid-gradient(bottom left, red 20px, yellow, green," + "blue 90%);" + "}", 1, 72, "div {d:-invalid-gradient(bottom left, red 20px, yellow, green,blue 90%);}", " ^"); } public void testInvalidSpaceInArgumentList() { // The parser marks the error at the semicolon because this is the token immediately following // the last successfully-consumed production. This is not ideal because the error occurs within // the argument list, but we validate the argument list after it is successfully parsed by the // grammar. testError("div { transform:rotate(180 deg); }", 1, 32, "div { transform:rotate(180 deg); }", " ^"); testError("div { background: rgba(255,0,0 1); }", 1, 34, "div { background: rgba(255,0,0 1); }", " ^"); } /** * Tests for error handling below */ private void testErrorHandling(String input, String expected, String... errors) throws GssParserException { List<GssParserException> handledErrors = new ArrayList<>(); CssTree tree = parse(input, true, handledErrors); List<String> errorMessages = new ArrayList<>(); for (GssParserException e : handledErrors) { errorMessages.add(e.getMessage()); } assertNotNull(tree); CssRootNode root = tree.getRoot(); assertNotNull(root); assertThat(errorMessages).containsExactly((Object[]) errors).inOrder(); assertEquals(expected, root.toString()); } public void testDeclarationErrorHandling() throws GssParserException { testErrorHandling("a { b: c,,; d: e }", "[[a]{[d:[e]]}]", "Parse error in test at line 1 column 10:\n" + "a { b: c,,; d: e }\n" + " ^\n"); testErrorHandling("a { b: c: d; e: f }", "[[a]{[e:[f]]}]", "Parse error in test at line 1 column 10:\n" + "a { b: c: d; e: f }\n" + " ^\n"); testErrorHandling("a { b: c; @at d: e; f: g }", "[[a]{[b:[c], f:[g]]}]", "Parse error in test at line 1 column 17:\n" + "a { b: c; @at d: e; f: g }\n" + " ^\n"); } public void testSelectorErrorHandling() throws GssParserException { testErrorHandling("a>>b { b: c } d { e: f }", "[[d]{[e:[f]]}]", "Parse error in test at line 1 column 2:\n" + "a>>b { b: c } d { e: f }\n" + " ^\n"); testErrorHandling("a @ b { c: d } e {}", "[[e]{[]}]", "Parse error in test at line 1 column 3:\n" + "a @ b { c: d } e {}\n" + " ^\n"); // No error; braces within quoted string are correctly parsed testErrorHandling("a{b:\"{,}\"}", "[[a]{[b:[\"{,}\"]]}]"); } public void testAtRuleErrorHandling() throws GssParserException { testErrorHandling("@a b (,,); c { d: e }", "[[c]{[d:[e]]}]", "Parse error in test at line 1 column 7:\n" + "@a b (,,); c { d: e }\n" + " ^\n"); testErrorHandling("@a { b,,{} c { d:: e; f: g } } h { i: j }", "[@a[]{[[c]{[f:[g]]}]}, [h]{[i:[j]]}]", "Parse error in test at line 1 column 8:\n" + "@a { b,,{} c { d:: e; f: g } } h { i: j }\n" + " ^\n", "Parse error in test at line 1 column 18:\n" + "@a { b,,{} c { d:: e; f: g } } h { i: j }\n" + " ^\n"); testErrorHandling("@a (b;) { c {} } d {}", "[[d]{[]}]", "Parse error in test at line 1 column 6:\n" + "@a (b;) { c {} } d {}\n" + " ^\n"); testErrorHandling("@a (b:c[]) { d[;}] {} e {} } f {}", "[[f]{[]}]", "Parse error in test at line 1 column 8:\n" + "@a (b:c[]) { d[;}] {} e {} } f {}\n" + " ^\n"); testErrorHandling("a { @b { c, {} d {} } e: f }", "[[a]{[@b[]{[]}, e:[f]]}]", "Parse error in test at line 1 column 11:\n" + "a { @b { c, {} d {} } e: f }\n" + " ^\n"); testErrorHandling("a[b=] { c {} } d {}", "[[d]{[]}]", "Parse error in test at line 1 column 5:\n" + "a[b=] { c {} } d {}\n" + " ^\n"); } public void testMatchingBraces() throws GssParserException { // Inner closed block ignored testErrorHandling("a{ b{} } c{}", "[[a]{[]}, [c]{[]}]", "Parse error in test at line 1 column 5:\n" + "a{ b{} } c{}\n" + " ^\n"); // Inner nested blocks ignored as well testErrorHandling("a{([b])} c{}", "[[c]{[]}]", "Parse error in test at line 1 column 3:\n" + "a{([b])} c{}\n" + " ^\n"); // Unmatched left brace consume until EOF testErrorHandling("a{([b)]} c{}", "[]", "Parse error in test at line 1 column 3:\n" + "a{([b)]} c{}\n" + " ^\n"); // Unmatched right brace ignored testErrorHandling("a{ (}) } b{}", "[[b]{[]}]", "Parse error in test at line 1 column 4:\n" + "a{ (}) } b{}\n" + " ^\n"); } public void testErrorRecoveryWithInvalidArgumentList() throws GssParserException { testErrorHandling("div { transform:rotate(180 deg); }", "[[div]{[]}]", "Parse error in test at line 1 column 32:\n" + "div { transform:rotate(180 deg); }\n" + " ^\n"); } public void testUnterminatedBlockCommentsWithoutErrorRecovery() throws GssParserException { testError("div {}/*comment**p {}", 1, 7, "div {}/*comment**p {}", " ^"); testError("div {}/*/p {}", 1, 7, "div {}/*/p {}", " ^"); testError("div {}/*", 1, 7, "div {}/*", " ^"); testError("div {}/* *\ndiv { color: red; }", 1, 7, "div {}/* *", " ^"); testError("div {} /* comment */ div {} /* unterminated comment", 1, 29, "div {} /* comment */ div {} /* unterminated comment", " ^"); testError("div {} /* comment */ /* unterminated comment", 1, 22, "div {} /* comment */ /* unterminated comment", " ^"); } public void testUnterminatedBlockCommentsWithErrorRecovery() throws GssParserException { testErrorHandling("div {}/*comment**p {}", "[[div]{[]}]", "Parse error in test at line 1 column 7:\n" + "div {}/*comment**p {}\n" + " ^\n"); testErrorHandling("div {}/*/p {}", "[[div]{[]}]", "Parse error in test at line 1 column 7:\n" + "div {}/*/p {}\n" + " ^\n"); testErrorHandling("div {}/*", "[[div]{[]}]", "Parse error in test at line 1 column 7:\n" + "div {}/*\n" + " ^\n"); testErrorHandling("div {}/* *\ndiv { color: red; }", "[[div]{[]}]", "Parse error in test at line 1 column 7:\n" + "div {}/* *\n" + " ^\n"); testErrorHandling( "div {} /* comment */ div {} /* unterminated comment", "[[div]{[]}, [div]{[]}]", "Parse error in test at line 1 column 29:\n" + "div {} /* comment */ div {} /* unterminated comment\n" + " ^\n"); testErrorHandling("div {} /* comment */ /* unterminated comment", "[[div]{[]}]", "Parse error in test at line 1 column 22:\n" + "div {} /* comment */ /* unterminated comment\n" + " ^\n"); } private CssTree parse(String gss, boolean shouldHandleError, List<GssParserException> handledErrors) throws GssParserException { GssParser parser = new GssParser(new SourceCode("test", gss)); CssTree tree = parser.parse(shouldHandleError); handledErrors.addAll(parser.getHandledErrors()); return tree; } private CssTree parse(String gss) throws GssParserException { return parse(gss, false, new ArrayList<GssParserException>()); } }