blob: c36a6492538ba89c385f9d747d8585fd3cbe748d [file] [log] [blame]
// Copyright 2015 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.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.devtools.build.lib.events.Location;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Objects;
/**
* EvalException with a stack trace.
*/
public class EvalExceptionWithStackTrace extends EvalException {
private StackFrame mostRecentElement;
public EvalExceptionWithStackTrace(Exception original, ASTNode culprit) {
super(extractLocation(original, culprit), getNonEmptyMessage(original), getCause(original));
registerNode(culprit);
}
@Override
public boolean canBeAddedToStackTrace() {
// Doesn't make any sense to add this exception to another instance of
// EvalExceptionWithStackTrace.
return false;
}
/**
* Returns the appropriate location for this exception.
*
* <p>If the {@code ASTNode} has a valid location, this one is used. Otherwise, we try to get the
* location of the exception.
*/
private static Location extractLocation(Exception original, ASTNode culprit) {
if (culprit != null && culprit.getLocation() != null) {
return culprit.getLocation();
}
return (original instanceof EvalException) ? ((EvalException) original).getLocation() : null;
}
/**
* Returns the "real" cause of this exception.
*
* <p>If the original exception is an EvalException, its cause is returned.
* Otherwise, the original exception itself is seen as the cause for this exception.
*/
private static Throwable getCause(Exception ex) {
return (ex instanceof EvalException) ? ex.getCause() : ex;
}
/**
* Adds an entry for the given {@code ASTNode} to the stack trace.
*/
public void registerNode(ASTNode node) {
addStackFrame(node.toString().trim(), node.getLocation());
}
/**
* Makes sure the stack trace is rooted in a function call.
*
* In some cases (rule implementation application, aspect implementation application)
* bazel calls into the function directly (using BaseFunction.call). In that case, since
* there is no FuncallExpression to evaluate, stack trace mechanism cannot record this call.
* This method allows to augument the stack trace with information about the call.
*/
public void registerPhantomFuncall(
String funcallDescription, Location location, BaseFunction function) {
/*
*
* We add two new frames to the stack:
* 1. Pseudo-function call (for example, rule definition)
* 2. Function entry (Rule implementation)
*
* Similar to Python, all functions that were entered (except for the top-level ones) appear
* twice in the stack trace output. This would lead to the following trace:
*
* File BUILD, line X, in <module>
* rule_definition()
* File BUILD, line X, in rule_definition
* rule_implementation()
* File bzl, line Y, in rule_implementation
* ...
*
* Please note that lines 3 and 4 are quite confusing since a) the transition from
* rule_definition to rule_implementation happens internally and b) the locations do not make
* any sense.
* Consequently, we decided to omit lines 3 and 4 from the output via canPrint = false:
*
* File BUILD, line X, in <module>
* rule_definition()
* File bzl, line Y, in rule_implementation
* ...
*
* */
addStackFrame(function.getName(), function.getLocation());
addStackFrame(funcallDescription, location, false);
}
/** Adds a line for the given frame. */
private void addStackFrame(String label, Location location, boolean canPrint) {
// TODO(bazel-team): This check was originally created to weed out duplicates in case the same
// node is added twice, but it's not clear if that is still a possibility. In any case, it would
// be better to eliminate the check and not create unwanted duplicates in the first place.
//
// The check is problematic because it suppresses tracebacks in the REPL, where line numbers
// can be reset within a single session.
if (mostRecentElement != null && isSameLocation(location, mostRecentElement.getLocation())) {
return;
}
mostRecentElement = new StackFrame(label, location, mostRecentElement, canPrint);
}
private void addStackFrame(String label, Location location) {
addStackFrame(label, location, true);
}
/**
* Checks two locations for equality in paths and start offsets.
*
* <p> LexerLocation#equals cannot be used since it cares about different end offsets.
*/
private boolean isSameLocation(Location first, Location second) {
try {
return Objects.equals(first.getPath(), second.getPath())
&& first.getStartOffset() == second.getStartOffset();
} catch (NullPointerException ex) {
return first == second;
}
}
/**
* Returns the exception message without the stack trace.
*/
public String getOriginalMessage() {
return super.getMessage();
}
@Override
public String getMessage() {
return print();
}
@Override
public String print() {
// Currently, we do not limit the text length per line.
return print(StackTracePrinter.INSTANCE);
}
/**
* Prints the stack trace iff it contains more than just one built-in function.
*/
public String print(StackTracePrinter printer) {
return canPrintStackTrace()
? printer.print(getOriginalMessage(), mostRecentElement)
: getOriginalMessage();
}
/**
* Returns true when there is at least one non-built-in element.
*/
protected boolean canPrintStackTrace() {
return mostRecentElement != null && mostRecentElement.getCause() != null;
}
/**
* An element in the stack trace which contains the name of the offending function / rule /
* statement and its location.
*/
protected static final class StackFrame {
private final String label;
private final Location location;
private final StackFrame cause;
private final boolean canPrint;
StackFrame(String label, Location location, StackFrame cause, boolean canPrint) {
this.label = label;
this.location = location;
this.cause = cause;
this.canPrint = canPrint;
}
String getLabel() {
return label;
}
Location getLocation() {
return location;
}
StackFrame getCause() {
return cause;
}
boolean canPrint() {
return canPrint;
}
@Override
public String toString() {
return String.format("%s @ %s -> %s", label, location, String.valueOf(cause));
}
}
/**
* Singleton class that prints stack traces similar to Python.
*/
public enum StackTracePrinter {
INSTANCE;
/**
* Turns the given message and StackTraceElements into a string.
*/
public final String print(String message, StackFrame mostRecentElement) {
Deque<String> output = new LinkedList<>();
// Adds dummy element for the rule call that uses the location of the top-most function.
mostRecentElement = new StackFrame("", mostRecentElement.getLocation(),
(mostRecentElement.getCause() == null) ? null : mostRecentElement, true);
while (mostRecentElement != null) {
if (mostRecentElement.canPrint()) {
String entry = print(mostRecentElement);
if (entry != null && entry.length() > 0) {
addEntry(output, entry);
}
}
mostRecentElement = mostRecentElement.getCause();
}
addMessage(output, message);
return Joiner.on(System.lineSeparator()).join(output);
}
/** Returns the string representation of the given element. */
protected String print(StackFrame element) {
// Similar to Python, the first (most-recent) entry in the stack frame is printed only once.
// Consequently, we skip it here.
if (element.getCause() == null) {
return "";
}
// Prints a two-line string, similar to Python.
Location location = getLocation(element.getCause());
return String.format(
"\tFile \"%s\", line %d%s%n\t\t%s",
printPath(location),
getLine(location),
printFunction(element.getLabel()),
element.getCause().getLabel());
}
/** Returns the location of the given element or Location.BUILTIN if the element is null. */
private Location getLocation(StackFrame element) {
return (element == null) ? Location.BUILTIN : element.getLocation();
}
private String printFunction(String func) {
if (func.isEmpty()) {
return "";
}
int pos = func.indexOf('(');
return String.format(", in %s", (pos < 0) ? func : func.substring(0, pos));
}
private String printPath(Location loc) {
return (loc == null || loc.getPath() == null) ? "<unknown>" : loc.getPath().getPathString();
}
private int getLine(Location loc) {
return (loc == null || loc.getStartLineAndColumn() == null)
? 0 : loc.getStartLineAndColumn().getLine();
}
/**
* Adds the given string to the specified Deque.
*/
protected void addEntry(Deque<String> output, String toAdd) {
output.addLast(toAdd);
}
/**
* Adds the given message to the given output dequeue after all stack trace elements have been
* added.
*/
protected void addMessage(Deque<String> output, String message) {
output.addFirst("Traceback (most recent call last):");
output.addLast(message);
}
}
/**
* Returns a non-empty message for the given exception.
*
* <p> If the exception itself does not have a message, a new message is constructed from the
* exception's class name.
* For example, an IllegalArgumentException will lead to "Illegal Argument".
* Additionally, the location in the Java code will be added, if applicable,
*/
private static String getNonEmptyMessage(Exception original) {
Preconditions.checkNotNull(original);
String msg = original.getMessage();
if (msg != null && !msg.isEmpty()) {
return msg;
}
char[] name = original.getClass().getSimpleName().replace("Exception", "").toCharArray();
boolean first = true;
StringBuilder builder = new StringBuilder();
for (char current : name) {
if (Character.isUpperCase(current) && !first) {
builder.append(" ");
}
builder.append(current);
first = false;
}
java.lang.StackTraceElement[] trace = original.getStackTrace();
if (trace.length > 0) {
builder.append(String.format(": %s.%s() in %s:%d", getShortClassName(trace[0]),
trace[0].getMethodName(), trace[0].getFileName(), trace[0].getLineNumber()));
}
return builder.toString();
}
private static String getShortClassName(java.lang.StackTraceElement element) {
String name = element.getClassName();
int pos = name.lastIndexOf('.');
return (pos < 0) ? name : name.substring(pos + 1);
}
}