blob: 9f968be577cf9d4531b0cd7cf2e12153a58a32b0 [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.Supplier;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Range;
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.collect.ImmutableSortedKeyListMultimap;
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(name);
return Pair.of(name, value);
}
private NinjaVariableValue parseVariableValue(String name) throws GenericParsingException {
return parseVariableValueImpl(() -> String.format("Variable '%s' has no value.", name));
}
private NinjaVariableValue parseVariableValueImpl(Supplier<String> messageForNoValue)
throws GenericParsingException {
// We are skipping starting spaces.
int valueStart = -1;
ImmutableSortedKeyListMultimap.Builder<String, Range<Integer>> builder =
ImmutableSortedKeyListMultimap.builder();
while (lexer.hasNextToken()) {
lexer.expectTextUntilEol();
NinjaToken token = lexer.nextToken();
if (NinjaToken.VARIABLE.equals(token)) {
if (valueStart == -1) {
valueStart = lexer.getLastStart();
}
builder.put(
normalizeVariableName(asString(lexer.getTokenBytes())),
Range.openClosed(lexer.getLastStart(), lexer.getLastEnd()));
} else if (NinjaToken.TEXT.equals(token)) {
if (valueStart == -1) {
valueStart = lexer.getLastStart();
}
} else {
lexer.undo();
break;
}
}
if (valueStart == -1) {
// We read no value.
throw new GenericParsingException(messageForNoValue.get());
}
String text = asString(lexer.getFragment().getBytes(valueStart, lexer.getLastEnd()));
return new NinjaVariableValue(text, builder.build());
}
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(
() -> 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, new NinjaVariableValue(name, ImmutableSortedKeyListMultimap.of()));
parseExpected(NinjaToken.NEWLINE);
while (lexer.hasNextToken()) {
parseExpected(NinjaToken.INDENT);
String variableName = asString(parseExpected(NinjaToken.IDENTIFIER));
parseExpected(NinjaToken.EQUALS);
NinjaVariableValue value = parseVariableValue(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();
}
}