blob: bbfcb27628390e09d62ae0005ab6cfc7d2e1d4d9 [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 java.util.Deque;
import java.util.LinkedList;
/** EvalException with a stack trace. */
// TODO(adonovan): get rid of this. Every EvalException should record the stack.
// This requires several steps:
// - break dependency on syntax. Use only call frames (function names and locations).
// That means to print the source we must reopen the file, which may require
// heuristics as it may reside in a fake file system.
// - eliminate or at least de-emphasize EvalException constructors that accept
// a Location. Most exceptions should be created with no location; the function
// call machinery fills in the details the first time exception bubbles up out of
// a call. The only exceptions (ha!) are (a) exceptions thrown by the interpreter at a
// particular statement/operator location, and (b) when client code wants to add
// a fake alternative (or additional?) location, such as the place a data structure
// was created.
// - get rid of the "phantom node" machinery. In the rare cases where it is needed,
// it is trivial to call a built-in wrapper function that presents the desired
// name and location and simply calls the desired function. There is no need to
// complicate the API.
// - clarify the various string methods (toString, getMessage, getOriginalMessage, print),
// and don't let this subclass totally redefine them.
// Printing the stack should be an explicit operation, as in Java.
// - make this class private; the parent should define the complete API.
// (For internal catch statements, it may be helpful to keep the two classes.)
public class EvalExceptionWithStackTrace extends EvalException {
private StackFrame mostRecentElement;
// Called only from Eval.maybeTransformException.
EvalExceptionWithStackTrace(EvalException original, Node culprit) {
// The 'message' here must be non-empty in case getCause() is null,
// as the super(-fragile) constructor crashes if both are empty.
super(nodeLocation(culprit), getNonEmptyMessage(original), original.getCause());
registerNode(culprit);
}
@Override
protected boolean canBeAddedToStackTrace() {
// Doesn't make any sense to add this exception to another instance of
// EvalExceptionWithStackTrace.
return false;
}
private static Location nodeLocation(Node node) {
return node instanceof CallExpression
? ((CallExpression) node).getLparenLocation()
: node.getStartLocation();
}
/** Adds an entry for the given {@code Node} to the stack trace. */
void registerNode(Node node) {
addStackFrame(node.toString().trim(), nodeLocation(node), /*canPrint=*/ true);
}
/**
* Makes sure the stack trace is rooted in a function call.
*
* <p>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
* CallExpression to evaluate, stack trace mechanism cannot record this call. This method allows
* to augument the stack trace with information about the call.
*/
public void registerPhantomCall(
String callDescription, Location location, StarlarkCallable 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(), /*canPrint=*/ true);
addStackFrame(callDescription, location, /*canPrint=*/ false);
}
/** Adds a line for the given frame. */
private void addStackFrame(String text, 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.
//
// [I suspect the real reason it was added is not because of duplicate nodes,
// but because the stack corresponds to the stack of expressions in the tree-walking
// evaluator's recursion, which often includes several subexpressions within
// the same line, e.g. f().g()+1. If the stack had one entry per function call,
// like StarlarkThread.CallStack, there would be no problem.
// This was revealed when we started recording operator positions precisely,
// causing the f(), .g(), and + operations in the example above to have different
// locations within the same line. --adonovan]
//
// 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
&& location.file().equals(mostRecentElement.location.file())
&& location.line() == mostRecentElement.location.line()) {
return;
}
mostRecentElement = new StackFrame(text, location, mostRecentElement, canPrint);
}
/**
* Returns the exception message without the stack trace.
*/
public String getOriginalMessage() {
return super.getMessage();
}
@Override
public String getMessage() {
// TODO(adonovan): don't change the meaning of getMessage (and toString) in the subclass.
// Printing the stack should be an explicit operation.
return print();
}
@Override
public String print() {
// Currently, we do not limit the text length per line.
// Prints the stack trace iff it contains more than just one built-in function.
return canPrintStackTrace()
? StackTracePrinter.print(getOriginalMessage(), mostRecentElement)
: getOriginalMessage();
}
/**
* Returns true when there is at least one non-built-in element.
*/
protected boolean canPrintStackTrace() {
return mostRecentElement != null && mostRecentElement.cause != null;
}
/**
* An element in the stack trace which contains the name of the offending function / rule /
* statement and its location.
*/
private static final class StackFrame {
final String text;
final Location location;
final StackFrame cause; // tail of linked list
final boolean canPrint;
StackFrame(String text, Location location, StackFrame cause, boolean canPrint) {
this.text = text;
this.location = location;
this.cause = cause;
this.canPrint = canPrint;
}
@Override
public String toString() {
return String.format("%s @ %s -> %s", text, location, String.valueOf(cause));
}
}
/** A collection of stateless of functions to print stack traces similar to Python. */
private static final class StackTracePrinter {
/** Turns the given message and StackTraceElements into a string. */
static 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.location,
mostRecentElement.cause == null ? null : mostRecentElement,
true);
while (mostRecentElement != null) {
if (mostRecentElement.canPrint) {
String entry = printElement(mostRecentElement);
if (entry != null && entry.length() > 0) {
addEntry(output, entry);
}
}
mostRecentElement = mostRecentElement.cause;
}
addMessage(output, message);
return Joiner.on(System.lineSeparator()).join(output);
}
/** Returns the string representation of the given element. */
static String printElement(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.cause == null) {
return "";
}
// Prints a two-line string, similar to Python.
Location location = getLocation(element.cause);
return String.format(
"\tFile \"%s\", line %d%s%n\t\t%s",
printPath(location), getLine(location), printFunction(element.text), element.cause.text);
}
/** Returns the location of the given element or Location.BUILTIN if the element is null. */
static Location getLocation(StackFrame element) {
return element == null ? Location.BUILTIN : element.location;
}
static String printFunction(String func) {
if (func.isEmpty()) {
return "";
}
int pos = func.indexOf('(');
return String.format(", in %s", (pos < 0) ? func : func.substring(0, pos));
}
static String printPath(Location loc) {
return loc == null ? "<unknown>" : loc.file();
}
static int getLine(Location loc) {
return loc == null ? 0 : loc.line();
}
/** Adds the given string to the specified Deque. */
static 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.
*/
static 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,
*/
// TODO(adonovan): eliminate this function. We have no business interpreting Java exceptions.
private static String getNonEmptyMessage(EvalException 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);
}
}