| // 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.devtools.build.lib.events.Location; | |
| import com.google.devtools.build.lib.util.Preconditions; | |
| import java.util.Deque; | |
| import java.util.LinkedList; | |
| import java.util.Objects; | |
| /** | |
| * EvalException with a stack trace. | |
| */ | |
| public class EvalExceptionWithStackTrace extends EvalException { | |
| private StackTraceElement 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) { | |
| // We have to watch out for duplicate since ExpressionStatements add themselves twice: | |
| // Statement#exec() calls Expression#eval(), both of which call this method. | |
| if (mostRecentElement != null && isSameLocation(location, mostRecentElement.getLocation())) { | |
| return; | |
| } | |
| mostRecentElement = new StackTraceElement(label, location, mostRecentElement, canPrint); | |
| } | |
| /** | |
| * 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()) | |
| && Objects.equals(first.getStartOffset(), second.getStartOffset()); | |
| } catch (NullPointerException ex) { | |
| return first == second; | |
| } | |
| } | |
| private void addStackFrame(String label, Location location) { | |
| addStackFrame(label, location, true); | |
| } | |
| /** | |
| * 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 StackTraceElement { | |
| private final String label; | |
| private final Location location; | |
| private final StackTraceElement cause; | |
| private final boolean canPrint; | |
| StackTraceElement(String label, Location location, StackTraceElement cause, boolean canPrint) { | |
| this.label = label; | |
| this.location = location; | |
| this.cause = cause; | |
| this.canPrint = canPrint; | |
| } | |
| String getLabel() { | |
| return label; | |
| } | |
| Location getLocation() { | |
| return location; | |
| } | |
| StackTraceElement getCause() { | |
| return cause; | |
| } | |
| boolean canPrint() { | |
| return canPrint; | |
| } | |
| @Override | |
| public String toString() { | |
| return String.format( | |
| "%s @ %s -> %s", label, location, (cause == null) ? "null" : cause.toString()); | |
| } | |
| } | |
| /** | |
| * 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, StackTraceElement mostRecentElement) { | |
| Deque<String> output = new LinkedList<>(); | |
| // Adds dummy element for the rule call that uses the location of the top-most function. | |
| mostRecentElement = new StackTraceElement("", 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 location of the given element or Location.BUILTIN if the element is null. | |
| */ | |
| private Location getLocation(StackTraceElement element) { | |
| return (element == null) ? Location.BUILTIN : element.getLocation(); | |
| } | |
| /** | |
| * Returns the string representation of the given element. | |
| */ | |
| protected String print(StackTraceElement 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()); | |
| } | |
| 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); | |
| } | |
| } |