blob: cd50c4a84737253a474a639d2a021ce38077afcd [file] [log] [blame]
// Copyright 2019 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 com.google.devtools.build.lib.bazel.rules.ninja.parser;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSortedMap;
import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
import com.google.devtools.build.lib.bazel.rules.ninja.lexer.NinjaLexer;
import com.google.devtools.build.lib.bazel.rules.ninja.lexer.NinjaToken;
import com.google.devtools.build.lib.util.Pair;
import java.nio.charset.StandardCharsets;
/** Ninja files parser. The types of tokens: {@link NinjaToken}. Ninja lexer: {@link NinjaLexer}. */
public class NinjaParser {
private final NinjaLexer lexer;
public NinjaParser(NinjaLexer lexer) {
this.lexer = lexer;
}
/** Parses variable at the current lexer position. */
public Pair<String, NinjaVariableValue> parseVariable() throws GenericParsingException {
String name = asString(parseExpected(NinjaToken.IDENTIFIER));
parseExpected(NinjaToken.EQUALS);
NinjaVariableValue value = parseVariableValue(true, name);
return Pair.of(name, value);
}
@VisibleForTesting
public NinjaVariableValue parseVariableValue(boolean allowUnescapedColon, String name)
throws GenericParsingException {
return parseVariableValueImpl(
allowUnescapedColon, () -> String.format("Variable '%s' has no value.", name));
}
private NinjaVariableValue parseVariableValueImpl(
boolean allowUnescapedColon, Supplier<String> messageForNoValue)
throws GenericParsingException {
NinjaVariableValue.Builder varBuilder = NinjaVariableValue.builder();
int previous = -1;
while (lexer.hasNextToken()) {
lexer.expectTextUntilEol();
NinjaToken token = lexer.nextToken();
if (NinjaToken.VARIABLE.equals(token)) {
if (previous >= 0) {
// add space interval between tokens
varBuilder.addText(
asString(lexer.getFragment().getBytes(previous, lexer.getLastStart())));
}
varBuilder.addVariable(normalizeVariableName(asString(lexer.getTokenBytes())));
} else if (NinjaToken.TEXT.equals(token)
|| NinjaToken.ESCAPED_TEXT.equals(token)
|| (allowUnescapedColon && NinjaToken.COLON.equals(token))) {
// Add text together with the spaces between current and previous token.
int start = previous >= 0 ? previous : lexer.getLastStart();
String rawText = asString(lexer.getFragment().getBytes(start, lexer.getLastEnd()));
String text = NinjaToken.ESCAPED_TEXT.equals(token) ? unescapeText(rawText) : rawText;
varBuilder.addText(text);
} else {
lexer.undo();
break;
}
previous = lexer.getLastEnd();
}
if (previous == -1) {
// We read no value.
throw new GenericParsingException(messageForNoValue.get());
}
return varBuilder.build();
}
private static String unescapeText(String text) {
StringBuilder sb = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch == '$') {
Preconditions.checkState(i + 1 < text.length());
sb.append(text.charAt(i + 1));
i++;
} else {
sb.append(ch);
}
}
return sb.toString();
}
public NinjaVariableValue parseIncludeStatement() throws GenericParsingException {
return parseIncludeOrSubNinja(NinjaToken.INCLUDE);
}
public NinjaVariableValue parseSubNinjaStatement() throws GenericParsingException {
return parseIncludeOrSubNinja(NinjaToken.SUBNINJA);
}
private NinjaVariableValue parseIncludeOrSubNinja(NinjaToken token)
throws GenericParsingException {
parseExpected(token);
NinjaVariableValue value =
parseVariableValueImpl(
true,
() -> String.format("%s statement has no path.", Ascii.toLowerCase(token.name())));
if (lexer.hasNextToken()) {
parseExpected(NinjaToken.NEWLINE);
lexer.undo();
}
return value;
}
/** Parses Ninja rule at the current lexer position. */
public NinjaRule parseNinjaRule() throws GenericParsingException {
parseExpected(NinjaToken.RULE);
String name = asString(parseExpected(NinjaToken.IDENTIFIER));
ImmutableSortedMap.Builder<NinjaRuleVariable, NinjaVariableValue> variablesBuilder =
ImmutableSortedMap.naturalOrder();
variablesBuilder.put(NinjaRuleVariable.NAME, NinjaVariableValue.createPlainText(name));
parseExpected(NinjaToken.NEWLINE);
while (lexer.hasNextToken()) {
parseExpected(NinjaToken.INDENT);
String variableName = asString(parseExpected(NinjaToken.IDENTIFIER));
parseExpected(NinjaToken.EQUALS);
NinjaVariableValue value = parseVariableValue(true, variableName);
NinjaRuleVariable ninjaRuleVariable = NinjaRuleVariable.nullOrValue(variableName);
if (ninjaRuleVariable == null) {
throw new GenericParsingException(String.format("Unexpected variable '%s'", variableName));
}
variablesBuilder.put(ninjaRuleVariable, value);
if (lexer.hasNextToken()) {
parseExpected(NinjaToken.NEWLINE);
}
}
return new NinjaRule(variablesBuilder.build());
}
@VisibleForTesting
public static String normalizeVariableName(String raw) {
// We start from 1 because it is always at least $ marker symbol in the beginning
int start = 1;
for (; start < raw.length(); start++) {
char ch = raw.charAt(start);
if (' ' != ch && '$' != ch && '{' != ch) {
break;
}
}
int end = raw.length() - 1;
for (; end > start; end--) {
char ch = raw.charAt(end);
if (' ' != ch && '}' != ch) {
break;
}
}
return raw.substring(start, end + 1);
}
private static String asString(byte[] value) {
return new String(value, StandardCharsets.ISO_8859_1);
}
private byte[] parseExpected(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()));
} else {
message =
String.format(
"Expected %s, but found no text to parse", asString(expectedToken.getBytes()));
}
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();
}
}