// 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); } }