| // Copyright 2014 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.syntax; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.base.Joiner; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.collect.ImmutableBiMap; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.hash.HashCode; | |
| import com.google.devtools.build.lib.events.Event; | |
| import com.google.devtools.build.lib.events.EventHandler; | |
| import com.google.devtools.build.lib.events.Location; | |
| import com.google.devtools.build.lib.syntax.Parser.ParseResult; | |
| import com.google.devtools.build.lib.syntax.SkylarkImports.SkylarkImportSyntaxException; | |
| import com.google.devtools.build.lib.util.Pair; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import java.io.IOException; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import javax.annotation.Nullable; | |
| /** | |
| * Abstract syntax node for an entire BUILD file. | |
| */ | |
| public class BuildFileAST extends ASTNode { | |
| private final ImmutableList<Statement> stmts; | |
| private final ImmutableList<Comment> comments; | |
| @Nullable private final Map<String, SkylarkImport> imports; | |
| /** | |
| * Whether any errors were encountered during scanning or parsing. | |
| */ | |
| private final boolean containsErrors; | |
| @Nullable private final String contentHashCode; | |
| private BuildFileAST( | |
| ImmutableList<Statement> stmts, | |
| boolean containsErrors, | |
| String contentHashCode, | |
| Location location, | |
| ImmutableList<Comment> comments, | |
| @Nullable Map<String, SkylarkImport> imports) { | |
| this.stmts = stmts; | |
| this.containsErrors = containsErrors; | |
| this.contentHashCode = contentHashCode; | |
| this.comments = comments; | |
| this.setLocation(location); | |
| this.imports = imports; | |
| } | |
| private static BuildFileAST create( | |
| List<Statement> preludeStatements, | |
| ParseResult result, | |
| String contentHashCode, | |
| EventHandler eventHandler) { | |
| ImmutableList<Statement> stmts = | |
| ImmutableList.<Statement>builder() | |
| .addAll(preludeStatements) | |
| .addAll(result.statements) | |
| .build(); | |
| boolean containsErrors = result.containsErrors; | |
| Pair<Boolean, Map<String, SkylarkImport>> skylarkImports = fetchLoads(stmts, eventHandler); | |
| containsErrors |= skylarkImports.first; | |
| return new BuildFileAST( | |
| stmts, | |
| containsErrors, | |
| contentHashCode, | |
| result.location, | |
| ImmutableList.copyOf(result.comments), | |
| skylarkImports.second); | |
| } | |
| /** | |
| * Extract a subtree containing only statements from {@code firstStatement} (included) up to | |
| * {@code lastStatement} excluded. | |
| */ | |
| public BuildFileAST subTree(int firstStatement, int lastStatement) { | |
| ImmutableList<Statement> stmts = this.stmts.subList(firstStatement, lastStatement); | |
| ImmutableMap.Builder<String, SkylarkImport> imports = ImmutableBiMap.builder(); | |
| for (Statement stmt : stmts) { | |
| if (stmt instanceof LoadStatement) { | |
| String str = ((LoadStatement) stmt).getImport(); | |
| imports.put( | |
| str, | |
| Preconditions.checkNotNull( | |
| this.imports.get(str), "%s cannot be found. This is an internal error.", str)); | |
| } | |
| } | |
| return new BuildFileAST( | |
| stmts, | |
| containsErrors, | |
| null, | |
| this.stmts.get(firstStatement).getLocation(), | |
| ImmutableList.<Comment>of(), | |
| imports.build()); | |
| } | |
| /** | |
| * Collects all load statements. Returns a pair with a boolean saying if there were errors and the | |
| * imports that could be resolved. | |
| */ | |
| @VisibleForTesting | |
| static Pair<Boolean, Map<String, SkylarkImport>> fetchLoads( | |
| List<Statement> stmts, EventHandler eventHandler) { | |
| Map<String, SkylarkImport> imports = new HashMap<>(); | |
| boolean error = false; | |
| for (Statement stmt : stmts) { | |
| if (stmt instanceof LoadStatement) { | |
| String importString = ((LoadStatement) stmt).getImport(); | |
| try { | |
| SkylarkImport imp = SkylarkImports.create(importString); | |
| imports.put(importString, imp); | |
| } catch (SkylarkImportSyntaxException e) { | |
| eventHandler.handle(Event.error(stmt.getLocation(), e.getMessage())); | |
| error = true; | |
| } | |
| } | |
| } | |
| return Pair.of(error, imports); | |
| } | |
| /** | |
| * Returns true if any errors were encountered during scanning or parsing. If | |
| * set, clients should not rely on the correctness of the AST for builds or | |
| * BUILD-file editing. | |
| */ | |
| public boolean containsErrors() { | |
| return containsErrors; | |
| } | |
| /** | |
| * Returns an (immutable, ordered) list of statements in this BUILD file. | |
| */ | |
| public ImmutableList<Statement> getStatements() { | |
| return stmts; | |
| } | |
| /** | |
| * Returns an (immutable, ordered) list of comments in this BUILD file. | |
| */ | |
| public ImmutableList<Comment> getComments() { | |
| return comments; | |
| } | |
| /** Returns a list of loads in this BUILD file. */ | |
| public ImmutableList<SkylarkImport> getImports() { | |
| Preconditions.checkNotNull(imports, "computeImports Should be called in parse* methods"); | |
| return ImmutableList.copyOf(imports.values()); | |
| } | |
| /** Returns a list of loads as strings in this BUILD file. */ | |
| public synchronized ImmutableList<String> getRawImports() { | |
| ImmutableList.Builder<String> imports = ImmutableList.builder(); | |
| for (Statement stmt : stmts) { | |
| if (stmt instanceof LoadStatement) { | |
| imports.add(((LoadStatement) stmt).getImport()); | |
| } | |
| } | |
| return imports.build(); | |
| } | |
| /** | |
| * Executes this build file in a given Environment. | |
| * | |
| * <p>If, for any reason, execution of a statement cannot be completed, an {@link EvalException} | |
| * is thrown by {@link Statement#exec(Environment)}. This exception is caught here and reported | |
| * through reporter and execution continues on the next statement. In effect, there is a | |
| * "try/except" block around every top level statement. Such exceptions are not ignored, though: | |
| * they are visible via the return value. Rules declared in a package containing any error | |
| * (including loading-phase semantical errors that cannot be checked here) must also be considered | |
| * "in error". | |
| * | |
| * <p>Note that this method will not affect the value of {@link #containsErrors()}; that refers | |
| * only to lexer/parser errors. | |
| * | |
| * @return true if no error occurred during execution. | |
| */ | |
| public boolean exec(Environment env, EventHandler eventHandler) throws InterruptedException { | |
| boolean ok = true; | |
| for (Statement stmt : stmts) { | |
| try { | |
| stmt.exec(env); | |
| } catch (EvalException e) { | |
| ok = false; | |
| // Do not report errors caused by a previous parsing error, as it has already been | |
| // reported. | |
| if (e.isDueToIncompleteAST()) { | |
| continue; | |
| } | |
| // When the exception is raised from another file, report first the location in the | |
| // BUILD file (as it is the most probable cause for the error). | |
| Location exnLoc = e.getLocation(); | |
| Location nodeLoc = stmt.getLocation(); | |
| eventHandler.handle(Event.error( | |
| (exnLoc == null || !nodeLoc.getPath().equals(exnLoc.getPath())) ? nodeLoc : exnLoc, | |
| e.getMessage())); | |
| } | |
| } | |
| return ok; | |
| } | |
| @Override | |
| public String toString() { | |
| return "BuildFileAST" + getStatements(); | |
| } | |
| @Override | |
| public void accept(SyntaxTreeVisitor visitor) { | |
| visitor.visit(this); | |
| } | |
| /** | |
| * Parse the specified build file, returning its AST. All errors during | |
| * scanning or parsing will be reported to the reporter. | |
| * | |
| * @throws IOException if the file cannot not be read. | |
| */ | |
| public static BuildFileAST parseBuildFile(Path buildFile, EventHandler eventHandler) | |
| throws IOException { | |
| return parseBuildFile(buildFile, buildFile.getFileSize(), eventHandler); | |
| } | |
| public static BuildFileAST parseBuildFile(Path buildFile, long fileSize, | |
| EventHandler eventHandler) | |
| throws IOException { | |
| ParserInputSource inputSource = ParserInputSource.create(buildFile, fileSize); | |
| return parseBuildFile(inputSource, eventHandler); | |
| } | |
| /** | |
| * Parse the specified build file, returning its AST. All errors during | |
| * scanning or parsing will be reported to the reporter. | |
| */ | |
| public static BuildFileAST parseBuildFile(ParserInputSource input, | |
| List<Statement> preludeStatements, | |
| EventHandler eventHandler) { | |
| Parser.ParseResult result = Parser.parseFile(input, eventHandler, false); | |
| return create(preludeStatements, result, /*contentHashCode=*/ null, eventHandler); | |
| } | |
| public static BuildFileAST parseBuildFile(ParserInputSource input, EventHandler eventHandler) { | |
| Parser.ParseResult result = Parser.parseFile(input, eventHandler, false); | |
| return create(ImmutableList.<Statement>of(), result, /*contentHashCode=*/ null, eventHandler); | |
| } | |
| /** | |
| * Parse the specified Skylark file, returning its AST. All errors during scanning or parsing will | |
| * be reported to the reporter. | |
| * | |
| * @throws IOException if the file cannot not be read. | |
| */ | |
| public static BuildFileAST parseSkylarkFile(Path file, EventHandler eventHandler) | |
| throws IOException { | |
| return parseSkylarkFile(file, file.getFileSize(), eventHandler); | |
| } | |
| public static BuildFileAST parseSkylarkFile(Path file, long fileSize, EventHandler eventHandler) | |
| throws IOException { | |
| ParserInputSource input = ParserInputSource.create(file, fileSize); | |
| Parser.ParseResult result = Parser.parseFileForSkylark(input, eventHandler); | |
| return create( | |
| ImmutableList.<Statement>of(), result, | |
| HashCode.fromBytes(file.getMD5Digest()).toString(), eventHandler); | |
| } | |
| /** | |
| * Parse the specified non-build Skylark file but avoid the validation of the imports, returning | |
| * its AST. All errors during scanning or parsing will be reported to the reporter. | |
| * | |
| * <p>This method should not be used in Bazel code, since it doesn't validate that the imports are | |
| * syntactically valid. | |
| * | |
| * @throws IOException if the file cannot not be read. | |
| */ | |
| public static BuildFileAST parseSkylarkFileWithoutImports( | |
| ParserInputSource input, EventHandler eventHandler) throws IOException { | |
| ParseResult result = Parser.parseFileForSkylark(input, eventHandler); | |
| return new BuildFileAST( | |
| ImmutableList.<Statement>builder() | |
| .addAll(ImmutableList.<Statement>of()) | |
| .addAll(result.statements) | |
| .build(), | |
| result.containsErrors, /*contentHashCode=*/ | |
| null, | |
| result.location, | |
| ImmutableList.copyOf(result.comments), /*imports=*/ | |
| null); | |
| } | |
| /** | |
| * Run static checks on the AST. | |
| * | |
| * @return a new AST (or the same), with the containsErrors flag updated. | |
| */ | |
| public BuildFileAST validate(ValidationEnvironment validationEnv, EventHandler eventHandler) { | |
| boolean valid = validationEnv.validateAst(stmts, eventHandler); | |
| if (valid || containsErrors) { | |
| return this; | |
| } | |
| return new BuildFileAST(stmts, true, contentHashCode, getLocation(), comments, imports); | |
| } | |
| public static BuildFileAST parseBuildString(EventHandler eventHandler, String... content) { | |
| String str = Joiner.on("\n").join(content); | |
| ParserInputSource input = ParserInputSource.create(str, null); | |
| Parser.ParseResult result = Parser.parseFile(input, eventHandler, false); | |
| return create(ImmutableList.<Statement>of(), result, null, eventHandler); | |
| } | |
| // TODO(laurentlb): Merge parseSkylarkString and parseBuildString. | |
| public static BuildFileAST parseSkylarkString(EventHandler eventHandler, String... content) { | |
| String str = Joiner.on("\n").join(content); | |
| ParserInputSource input = ParserInputSource.create(str, null); | |
| Parser.ParseResult result = Parser.parseFileForSkylark(input, eventHandler); | |
| return create(ImmutableList.<Statement>of(), result, null, eventHandler); | |
| } | |
| /** | |
| * Parse the specified build file, without building the AST. | |
| * | |
| * @return true if the input file is syntactically valid | |
| */ | |
| public static boolean checkSyntax( | |
| ParserInputSource input, EventHandler eventHandler, boolean parsePython) { | |
| Parser.ParseResult result = Parser.parseFile(input, eventHandler, parsePython); | |
| return !result.containsErrors; | |
| } | |
| /** | |
| * Evaluates the code and return the value of the last statement if it's an | |
| * Expression or else null. | |
| */ | |
| @Nullable public Object eval(Environment env) throws EvalException, InterruptedException { | |
| Object last = null; | |
| for (Statement statement : stmts) { | |
| if (statement instanceof ExpressionStatement) { | |
| last = ((ExpressionStatement) statement).getExpression().eval(env); | |
| } else { | |
| statement.exec(env); | |
| last = null; | |
| } | |
| } | |
| return last; | |
| } | |
| /** | |
| * Returns a hash code calculated from the string content of the source file of this AST. | |
| */ | |
| @Nullable public String getContentHashCode() { | |
| return contentHashCode; | |
| } | |
| } |