blob: 98ab05583ea0374861f51ed5fe8918c861e18265 [file] [log] [blame]
// 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;
}
}