| // 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.devtools.build.lib.bazel.rules.ninja.file.DeclarationConsumer; |
| import com.google.devtools.build.lib.bazel.rules.ninja.file.FileFragment; |
| 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.bazel.rules.ninja.parser.NinjaFileParseResult.NinjaPromise; |
| import com.google.devtools.build.lib.bazel.rules.ninja.pipeline.NinjaPipeline; |
| import com.google.devtools.build.lib.util.Pair; |
| import java.io.IOException; |
| |
| /** |
| * Ninja parser: an instance will be created per a fragment of Ninja file, to avoid synchronization |
| * while parsing independent values. |
| * |
| * <p>Populates the {@link NinjaFileParseResult} with variables and rules. |
| */ |
| public class NinjaParser implements DeclarationConsumer { |
| private final NinjaPipeline pipeline; |
| private final NinjaFileParseResult parseResult; |
| private final String ninjaFileName; |
| |
| private static final String UNSUPPORTED_TOKEN_MESSAGE = |
| " is an unsupported type of Ninja lexeme for the parser at this state. " |
| + "These are unexpected lexemes at this state for the parser, and running into a " |
| + "token of this lexeme hints that the parser did not progress the lexer to a " |
| + "valid state during a previous parsing step."; |
| |
| public NinjaParser( |
| NinjaPipeline pipeline, NinjaFileParseResult parseResult, String ninjaFileName) { |
| this.pipeline = pipeline; |
| this.parseResult = parseResult; |
| this.ninjaFileName = ninjaFileName; |
| } |
| |
| @Override |
| public void declaration(FileFragment fragment) throws GenericParsingException, IOException { |
| long offset = fragment.getFragmentOffset(); |
| |
| NinjaLexer lexer = new NinjaLexer(fragment); |
| if (!lexer.hasNextToken()) { |
| throw new IllegalStateException("Empty fragment passed as declaration."); |
| } |
| NinjaToken token = lexer.nextToken(); |
| // Skip possible leading newlines in the fragment for parsing. |
| while (lexer.hasNextToken() && NinjaToken.NEWLINE.equals(token)) { |
| token = lexer.nextToken(); |
| } |
| if (!lexer.hasNextToken()) { |
| // If fragment contained only newlines. |
| return; |
| } |
| long declarationStart = offset + lexer.getLastStart(); |
| lexer.undo(); |
| NinjaParserStep parser = |
| new NinjaParserStep(lexer, pipeline.getPathFragmentInterner(), pipeline.getNameInterner()); |
| |
| switch (token) { |
| case IDENTIFIER: |
| Pair<String, NinjaVariableValue> variable = parser.parseVariable(); |
| parseResult.addVariable(variable.getFirst(), declarationStart, variable.getSecond()); |
| break; |
| case RULE: |
| NinjaRule rule = parser.parseNinjaRule(); |
| parseResult.addRule(declarationStart, rule); |
| break; |
| case POOL: |
| parseResult.addPool(declarationStart, parser.parseNinjaPool()); |
| break; |
| case INCLUDE: |
| NinjaVariableValue includeStatement = parser.parseIncludeStatement(); |
| NinjaPromise<NinjaFileParseResult> includeFuture = |
| pipeline.createChildFileParsingPromise( |
| includeStatement, declarationStart, ninjaFileName); |
| parseResult.addIncludeScope(declarationStart, includeFuture); |
| break; |
| case SUBNINJA: |
| NinjaVariableValue subNinjaStatement = parser.parseSubNinjaStatement(); |
| NinjaPromise<NinjaFileParseResult> subNinjaFuture = |
| pipeline.createChildFileParsingPromise( |
| subNinjaStatement, declarationStart, ninjaFileName); |
| parseResult.addSubNinjaScope(declarationStart, subNinjaFuture); |
| break; |
| case BUILD: |
| FileFragment targetFragment; |
| if (declarationStart == offset) { |
| targetFragment = fragment; |
| } else { |
| // Method subFragment accepts only the offset *inside fragment*. |
| // So we should subtract the offset of fragment's buffer in file |
| // (fragment.getFileOffset()), |
| // and start of fragment inside buffer (fragment.getStartIncl()). |
| long fragmentStart = |
| declarationStart - fragment.getFileOffset() - fragment.getStartIncl(); |
| |
| // While the absolute offset is typed as long (because of larger ninja files), the |
| // fragments are only at most Integer.MAX_VALUE long, so fragmentStart cannot be |
| // larger than that. Sanity check this here. |
| if (fragmentStart > Integer.MAX_VALUE) { |
| throw new GenericParsingException( |
| String.format( |
| "The fragmentStart value %s is not expected to be larger than max-int, " |
| + "since each fragment is at most max-int long.", |
| fragmentStart)); |
| } |
| targetFragment = fragment.subFragment((int) fragmentStart, fragment.length()); |
| } |
| parseResult.addTarget(targetFragment); |
| break; |
| case DEFAULT: |
| // Do nothing. |
| break; |
| case ZERO: |
| case EOF: |
| return; |
| case COLON: |
| case EQUALS: |
| case ESCAPED_TEXT: |
| case INDENT: |
| case NEWLINE: |
| case PIPE: |
| case PIPE2: |
| case PIPE_AT: |
| case TEXT: |
| case VARIABLE: |
| throw new UnsupportedOperationException(token.name() + UNSUPPORTED_TOKEN_MESSAGE); |
| case ERROR: |
| throw new GenericParsingException(lexer.getError()); |
| // Omit default case on purpose. Explicitly specify *all* NinjaToken enum cases above or the |
| // compilation will fail. |
| } |
| } |
| } |