Fix when Ninja statements finishing earlier than parser expected
Closes #10758.
PiperOrigin-RevId: 294734381
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParserStep.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParserStep.java
index c625dc5..8640671 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParserStep.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/ninja/parser/NinjaParserStep.java
@@ -180,8 +180,7 @@
variablesBuilder.put(NinjaRuleVariable.NAME, NinjaVariableValue.createPlainText(name));
parseExpected(NinjaToken.NEWLINE);
- while (lexer.hasNextToken()) {
- parseExpected(NinjaToken.INDENT);
+ while (parseIndentOrFinishDeclaration()) {
String variableName = asString(parseExpected(NinjaToken.IDENTIFIER));
parseExpected(NinjaToken.EQUALS);
NinjaVariableValue value = parseVariableValue();
@@ -304,9 +303,7 @@
throws GenericParsingException {
Map<String, List<Pair<Integer, String>>> expandedVariables = Maps.newHashMap();
lexer.interpretPoolAsVariable();
- while (lexer.hasNextToken()) {
- parseExpected(NinjaToken.INDENT);
-
+ while (parseIndentOrFinishDeclaration()) {
Pair<String, NinjaVariableValue> pair = parseVariable();
String name = Preconditions.checkNotNull(pair.getFirst());
NinjaVariableValue value = Preconditions.checkNotNull(pair.getSecond());
@@ -402,30 +399,76 @@
return new String(value, StandardCharsets.ISO_8859_1);
}
+ /**
+ * It is expected that indent is preceding to the identifier in the scoped variable declaration.
+ * It can be, however, that it is just an empty line with spaces - in that case, we want to
+ * interpret it as the finish of the currently parsed lexeme.
+ *
+ * @return true if indent was parsed and there is something different than the newline after it.
+ */
+ private boolean parseIndentOrFinishDeclaration() throws GenericParsingException {
+ if (!lexer.hasNextToken()) {
+ return false;
+ }
+ NinjaToken token = lexer.nextToken();
+ boolean isIndent = NinjaToken.INDENT.equals(token);
+ if (!isIndent && !NinjaToken.NEWLINE.equals(token)) {
+ throwNotExpectedTokenError(NinjaToken.INDENT, token);
+ }
+ // Check for indent followed by newline, or end of file.
+ if (lexer.hasNextToken()) {
+ NinjaToken afterIndent = lexer.nextToken();
+ lexer.undo();
+ if (NinjaToken.NEWLINE.equals(afterIndent)) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ return isIndent;
+ }
+
private byte[] parseExpected(NinjaToken expectedToken) throws GenericParsingException {
+ checkLexerHasNextToken(expectedToken);
+ NinjaToken token = lexer.nextToken();
+ if (!expectedToken.equals(token)) {
+ throwNotExpectedTokenError(expectedToken, token);
+ }
+ return lexer.getTokenBytes();
+ }
+
+ private void throwNotExpectedTokenError(NinjaToken expectedToken, NinjaToken token)
+ throws GenericParsingException {
+ String actual =
+ NinjaToken.ERROR.equals(token)
+ ? String.format("error: '%s'", lexer.getError())
+ : asString(token.getBytes());
+ throw new GenericParsingException(
+ String.format(
+ "Expected %s, but got %s in fragment:\n%s\n",
+ asString(expectedToken.getBytes()),
+ actual,
+ lexer.getFragment().getFragmentAround(lexer.getLastStart())));
+ }
+
+ private void checkLexerHasNextToken(NinjaToken expectedToken) throws GenericParsingException {
if (!lexer.hasNextToken()) {
String message;
if (lexer.haveReadAnyTokens()) {
message =
String.format(
- "Expected %s after '%s'",
- asString(expectedToken.getBytes()), asString(lexer.getTokenBytes()));
+ "Expected %s after '%s' in fragment:\n%s\n",
+ asString(expectedToken.getBytes()),
+ asString(lexer.getTokenBytes()),
+ lexer.getFragment().getFragmentAround(lexer.getLastStart()));
} else {
message =
String.format(
- "Expected %s, but found no text to parse", asString(expectedToken.getBytes()));
+ "Expected %s, but found no text to parse after fragment:\n%s\n",
+ asString(expectedToken.getBytes()),
+ lexer.getFragment().getFragmentAround(lexer.getLastStart()));
}
throw new GenericParsingException(message);
}
- NinjaToken token = lexer.nextToken();
- if (!expectedToken.equals(token)) {
- String actual =
- NinjaToken.ERROR.equals(token)
- ? String.format("error: '%s'", lexer.getError())
- : asString(token.getBytes());
- throw new GenericParsingException(
- String.format("Expected %s, but got %s", asString(expectedToken.getBytes()), actual));
- }
- return lexer.getTokenBytes();
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaParserStepTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaParserStepTest.java
index 5ecdde7..ad54205 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaParserStepTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaParserStepTest.java
@@ -54,12 +54,12 @@
@Test
public void testVariableParsingException() {
- doTestVariableParsingException(" ", "Expected identifier, but got indent");
- doTestVariableParsingException("a", "Expected = after 'a'");
+ doTestVariableParsingException(" ", "Expected identifier, but got indent in fragment:\n \n");
+ doTestVariableParsingException("a", "Expected = after 'a' in fragment:\na\n");
doTestVariableParsingException(
"^a=",
"Expected identifier, but got error: 'Symbol '^' is not allowed in the identifier, "
- + "the text fragment with the symbol:\n^a=\n'");
+ + "the text fragment with the symbol:\n^a=\n' in fragment:\n^a=\n");
}
private static void doTestVariableParsingException(String text, String message) {
@@ -120,7 +120,7 @@
.hasMessageThat()
.isEqualTo(
"Expected newline, but got error: "
- + "'Bad $-escape (literal $ must be written as $$)'");
+ + "'Bad $-escape (literal $ must be written as $$)' in fragment:\ninclude x $\n");
GenericParsingException exception2 =
assertThrows(
@@ -136,6 +136,7 @@
@Test
public void testNinjaRule() throws Exception {
+ // Additionally test the situation when we get more line separators in the end.
NinjaParserStep parser =
createParser(
"rule testRule \n"
@@ -143,7 +144,7 @@
+ " description = Test rule for $TARGET\n"
+ " rspfile = $TARGET.in\n"
+ " deps = ${abc} $\n"
- + " ${cde}\n");
+ + " ${cde}\n\n\n");
NinjaRule ninjaRule = parser.parseNinjaRule();
ImmutableSortedMap<NinjaRuleVariable, NinjaVariableValue> variables = ninjaRule.getVariables();
assertThat(variables.keySet())
@@ -180,19 +181,41 @@
@Test
public void testNinjaRuleParsingException() {
doTestNinjaRuleParsingException(
- "rule testRule extra-word\n", "Expected newline, but got identifier");
+ "rule testRule extra-word\n",
+ String.join(
+ "\n",
+ "Expected newline, but got identifier in fragment:",
+ "rule testRule extra-word",
+ "",
+ ""));
doTestNinjaRuleParsingException(
- "rule testRule\ncommand =", "Expected indent, but got identifier");
+ "rule testRule\ncommand =",
+ String.join(
+ "\n",
+ "Expected indent, but got identifier in fragment:",
+ "rule testRule",
+ "command =",
+ ""));
doTestNinjaRuleParsingException(
"rule testRule\n ^custom = a",
- "Expected identifier, but got error: 'Symbol '^' is not allowed in the identifier, "
- + "the text fragment with the symbol:\nrule testRule\n ^custom = a\n'");
+ String.join(
+ "\n",
+ "Expected identifier, but got error: 'Symbol '^' is not allowed in the identifier, "
+ + "the text fragment with the symbol:",
+ "rule testRule",
+ " ^custom = a",
+ "' in fragment:",
+ "rule testRule",
+ " ^custom = a",
+ ""));
doTestNinjaRuleParsingException("rule testRule\n custom = a", "Unexpected variable 'custom'");
}
@Test
public void testNinjaTargets() throws Exception {
- NinjaTarget target = parseNinjaTarget("build output: command input");
+ // Additionally test the situation when the target does not have the variables section and
+ // we get more line separators in the end.
+ NinjaTarget target = parseNinjaTarget("build output: command input\n\n");
assertThat(target.getRuleName()).isEqualTo("command");
assertThat(target.getOutputs()).containsExactly(PathFragment.create("output"));
assertThat(target.getUsualInputs()).containsExactly(PathFragment.create("input"));
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaPipelineTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaPipelineTest.java
index 1ab3ed3..c1bbb5f 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaPipelineTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/ninja/NinjaPipelineTest.java
@@ -263,6 +263,36 @@
}
@Test
+ public void testSpacesInEmptyLine() throws Exception {
+ Path vfsPath =
+ tester.writeTmpFile(
+ "test.ninja",
+ "subfile=child",
+ " ",
+ "subninja_file=sub",
+ " ",
+ "rule r1",
+ " command = c $in $out",
+ // This could be interpreted as indent, but should be skipped as the empty line.
+ " ",
+ " \n\n\n\n",
+ "include ${subfile}.ninja",
+ " ",
+ "build t2: r1 in3",
+ " ",
+ "build t1: r1 in1 in2");
+ Path childFile = tester.writeTmpFile("child.ninja");
+ NinjaPipeline pipeline =
+ new NinjaPipeline(
+ vfsPath.getParentDirectory(),
+ tester.getService(),
+ ImmutableList.of(childFile),
+ "ninja_target");
+ List<NinjaTarget> targets = pipeline.pipeline(vfsPath);
+ checkTargets(targets);
+ }
+
+ @Test
public void testBigFile() throws Exception {
String[] lines = new String[1000];
for (int i = 0; i < lines.length - 1; i++) {