blob: 713f57fafc11e707f42c167743b579b2c0956bf2 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2015 The Bazel Authors. All rights reserved.
Florian Weikert3f610e82015-08-18 14:37:46 +00002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package com.google.devtools.build.lib.syntax;
15
Florian Weikertc1d54ec2015-08-26 14:06:58 +000016import com.google.common.base.Joiner;
Florian Weikert3f610e82015-08-18 14:37:46 +000017import com.google.devtools.build.lib.events.Location;
Mark Schaller6df81792015-12-10 18:47:47 +000018import com.google.devtools.build.lib.util.Preconditions;
Florian Weikertc1d54ec2015-08-26 14:06:58 +000019import java.util.Deque;
20import java.util.LinkedList;
Florian Weikert90a15962015-09-11 13:43:10 +000021import java.util.Objects;
Florian Weikert3f610e82015-08-18 14:37:46 +000022
23/**
Florian Weikerta37e86c2015-08-27 12:57:25 +000024 * EvalException with a stack trace.
Florian Weikert3f610e82015-08-18 14:37:46 +000025 */
26public class EvalExceptionWithStackTrace extends EvalException {
Florian Weikertc1d54ec2015-08-26 14:06:58 +000027
28 private StackTraceElement mostRecentElement;
Florian Weikert3f610e82015-08-18 14:37:46 +000029
Florian Weikert90a15962015-09-11 13:43:10 +000030 public EvalExceptionWithStackTrace(Exception original, ASTNode culprit) {
31 super(extractLocation(original, culprit), getNonEmptyMessage(original), getCause(original));
32 registerNode(culprit);
Laurent Le Brun6e5eecb2015-09-10 11:37:32 +000033 }
34
Florian Weikert4b67d4f2015-09-14 13:35:34 +000035 @Override
36 public boolean canBeAddedToStackTrace() {
37 // Doesn't make any sense to add this exception to another instance of
38 // EvalExceptionWithStackTrace.
39 return false;
40 }
41
Laurent Le Brun6e5eecb2015-09-10 11:37:32 +000042 /**
Florian Weikert90a15962015-09-11 13:43:10 +000043 * Returns the appropriate location for this exception.
44 *
45 * <p>If the {@code ASTNode} has a valid location, this one is used. Otherwise, we try to get the
46 * location of the exception.
Laurent Le Brun6e5eecb2015-09-10 11:37:32 +000047 */
Florian Weikert90a15962015-09-11 13:43:10 +000048 private static Location extractLocation(Exception original, ASTNode culprit) {
49 if (culprit != null && culprit.getLocation() != null) {
50 return culprit.getLocation();
Laurent Le Brun6e5eecb2015-09-10 11:37:32 +000051 }
Florian Weikert90a15962015-09-11 13:43:10 +000052 return (original instanceof EvalException) ? ((EvalException) original).getLocation() : null;
Florian Weikerta37e86c2015-08-27 12:57:25 +000053 }
54
55 /**
56 * Returns the "real" cause of this exception.
57 *
58 * <p>If the original exception is an EvalException, its cause is returned.
59 * Otherwise, the original exception itself is seen as the cause for this exception.
60 */
61 private static Throwable getCause(Exception ex) {
62 return (ex instanceof EvalException) ? ex.getCause() : ex;
Florian Weikert3f610e82015-08-18 14:37:46 +000063 }
64
65 /**
Florian Weikert90a15962015-09-11 13:43:10 +000066 * Adds an entry for the given {@code ASTNode} to the stack trace.
Florian Weikert3f610e82015-08-18 14:37:46 +000067 */
Florian Weikert90a15962015-09-11 13:43:10 +000068 public void registerNode(ASTNode node) {
69 addStackFrame(node.toString().trim(), node.getLocation());
Florian Weikert3f610e82015-08-18 14:37:46 +000070 }
71
72 /**
Dmitry Lomov2aa1a982015-10-20 12:18:36 +000073 * Makes sure the stack trace is rooted in a function call.
74 *
75 * In some cases (rule implementation application, aspect implementation application)
76 * bazel calls into the function directly (using BaseFunction.call). In that case, since
77 * there is no FuncallExpression to evaluate, stack trace mechanism cannot record this call.
78 * This method allows to augument the stack trace with information about the call.
Florian Weikertc1d54ec2015-08-26 14:06:58 +000079 */
Dmitry Lomov2aa1a982015-10-20 12:18:36 +000080 public void registerPhantomFuncall(
81 String funcallDescription, Location location, BaseFunction function) {
82 /*
Florian Weikert90a15962015-09-11 13:43:10 +000083 *
Dmitry Lomov2aa1a982015-10-20 12:18:36 +000084 * We add two new frames to the stack:
85 * 1. Pseudo-function call (for example, rule definition)
86 * 2. Function entry (Rule implementation)
Florian Weikert90a15962015-09-11 13:43:10 +000087 *
88 * Similar to Python, all functions that were entered (except for the top-level ones) appear
89 * twice in the stack trace output. This would lead to the following trace:
90 *
91 * File BUILD, line X, in <module>
92 * rule_definition()
93 * File BUILD, line X, in rule_definition
94 * rule_implementation()
95 * File bzl, line Y, in rule_implementation
96 * ...
97 *
98 * Please note that lines 3 and 4 are quite confusing since a) the transition from
99 * rule_definition to rule_implementation happens internally and b) the locations do not make
100 * any sense.
101 * Consequently, we decided to omit lines 3 and 4 from the output via canPrint = false:
102 *
103 * File BUILD, line X, in <module>
104 * rule_definition()
105 * File bzl, line Y, in rule_implementation
106 * ...
107 *
108 * */
Dmitry Lomov2aa1a982015-10-20 12:18:36 +0000109 addStackFrame(function.getName(), function.getLocation());
110 addStackFrame(funcallDescription, location, false);
Florian Weikert3f610e82015-08-18 14:37:46 +0000111 }
112
113 /**
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000114 * Adds a line for the given frame.
Florian Weikert3f610e82015-08-18 14:37:46 +0000115 */
Florian Weikert90a15962015-09-11 13:43:10 +0000116 private void addStackFrame(String label, Location location, boolean canPrint) {
117 // We have to watch out for duplicate since ExpressionStatements add themselves twice:
118 // Statement#exec() calls Expression#eval(), both of which call this method.
119 if (mostRecentElement != null && isSameLocation(location, mostRecentElement.getLocation())) {
120 return;
121 }
122 mostRecentElement = new StackTraceElement(label, location, mostRecentElement, canPrint);
123 }
124
125 /**
126 * Checks two locations for equality in paths and start offsets.
127 *
128 * <p> LexerLocation#equals cannot be used since it cares about different end offsets.
129 */
130 private boolean isSameLocation(Location first, Location second) {
131 try {
132 return Objects.equals(first.getPath(), second.getPath())
laurentlb3d2a68c2017-06-30 00:32:04 +0200133 && first.getStartOffset() == second.getStartOffset();
Florian Weikert90a15962015-09-11 13:43:10 +0000134 } catch (NullPointerException ex) {
135 return first == second;
136 }
137 }
138
139 private void addStackFrame(String label, Location location) {
140 addStackFrame(label, location, true);
Florian Weikert3f610e82015-08-18 14:37:46 +0000141 }
142
143 /**
144 * Returns the exception message without the stack trace.
145 */
146 public String getOriginalMessage() {
147 return super.getMessage();
148 }
149
150 @Override
151 public String getMessage() {
152 return print();
153 }
154
155 @Override
156 public String print() {
Florian Weikert90a15962015-09-11 13:43:10 +0000157 // Currently, we do not limit the text length per line.
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000158 return print(StackTracePrinter.INSTANCE);
159 }
160
161 /**
162 * Prints the stack trace iff it contains more than just one built-in function.
163 */
164 public String print(StackTracePrinter printer) {
165 return canPrintStackTrace()
166 ? printer.print(getOriginalMessage(), mostRecentElement)
167 : getOriginalMessage();
168 }
169
170 /**
171 * Returns true when there is at least one non-built-in element.
172 */
173 protected boolean canPrintStackTrace() {
174 return mostRecentElement != null && mostRecentElement.getCause() != null;
175 }
176
177 /**
178 * An element in the stack trace which contains the name of the offending function / rule /
179 * statement and its location.
180 */
Florian Weikert90a15962015-09-11 13:43:10 +0000181 protected static final class StackTraceElement {
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000182 private final String label;
183 private final Location location;
184 private final StackTraceElement cause;
Florian Weikert90a15962015-09-11 13:43:10 +0000185 private final boolean canPrint;
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000186
Florian Weikert90a15962015-09-11 13:43:10 +0000187 StackTraceElement(String label, Location location, StackTraceElement cause, boolean canPrint) {
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000188 this.label = label;
189 this.location = location;
190 this.cause = cause;
Florian Weikert90a15962015-09-11 13:43:10 +0000191 this.canPrint = canPrint;
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000192 }
193
194 String getLabel() {
195 return label;
196 }
197
198 Location getLocation() {
199 return location;
200 }
201
202 StackTraceElement getCause() {
203 return cause;
204 }
Florian Weikert90a15962015-09-11 13:43:10 +0000205
206 boolean canPrint() {
207 return canPrint;
208 }
209
210 @Override
211 public String toString() {
212 return String.format(
213 "%s @ %s -> %s", label, location, (cause == null) ? "null" : cause.toString());
214 }
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000215 }
216
217 /**
218 * Singleton class that prints stack traces similar to Python.
219 */
220 public enum StackTracePrinter {
221 INSTANCE;
222
223 /**
224 * Turns the given message and StackTraceElements into a string.
225 */
226 public final String print(String message, StackTraceElement mostRecentElement) {
227 Deque<String> output = new LinkedList<>();
228
Florian Weikert90a15962015-09-11 13:43:10 +0000229 // Adds dummy element for the rule call that uses the location of the top-most function.
230 mostRecentElement = new StackTraceElement("", mostRecentElement.getLocation(),
231 (mostRecentElement.getCause() == null) ? null : mostRecentElement, true);
232
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000233 while (mostRecentElement != null) {
Florian Weikert90a15962015-09-11 13:43:10 +0000234 if (mostRecentElement.canPrint()) {
235 String entry = print(mostRecentElement);
236 if (entry != null && entry.length() > 0) {
237 addEntry(output, entry);
238 }
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000239 }
240
241 mostRecentElement = mostRecentElement.getCause();
242 }
243
244 addMessage(output, message);
Yun Penge7e55bb2016-09-09 09:11:42 +0000245 return Joiner.on(System.lineSeparator()).join(output);
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000246 }
247
248 /**
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000249 * Returns the location of the given element or Location.BUILTIN if the element is null.
250 */
251 private Location getLocation(StackTraceElement element) {
252 return (element == null) ? Location.BUILTIN : element.getLocation();
253 }
254
255 /**
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000256 * Returns the string representation of the given element.
257 */
258 protected String print(StackTraceElement element) {
259 // Similar to Python, the first (most-recent) entry in the stack frame is printed only once.
260 // Consequently, we skip it here.
261 if (element.getCause() == null) {
262 return "";
263 }
264
265 // Prints a two-line string, similar to Python.
Florian Weikert90a15962015-09-11 13:43:10 +0000266 Location location = getLocation(element.getCause());
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000267 return String.format(
Florian Weikert90a15962015-09-11 13:43:10 +0000268 "\tFile \"%s\", line %d%s%n\t\t%s",
269 printPath(location),
270 getLine(location),
271 printFunction(element.getLabel()),
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000272 element.getCause().getLabel());
273 }
274
Florian Weikert90a15962015-09-11 13:43:10 +0000275 private String printFunction(String func) {
276 if (func.isEmpty()) {
277 return "";
278 }
279
280 int pos = func.indexOf('(');
281 return String.format(", in %s", (pos < 0) ? func : func.substring(0, pos));
282 }
283
284 private String printPath(Location loc) {
285 return (loc == null || loc.getPath() == null) ? "<unknown>" : loc.getPath().getPathString();
286 }
287
288 private int getLine(Location loc) {
289 return (loc == null || loc.getStartLineAndColumn() == null)
290 ? 0 : loc.getStartLineAndColumn().getLine();
Florian Weikertc1d54ec2015-08-26 14:06:58 +0000291 }
292
293 /**
294 * Adds the given string to the specified Deque.
295 */
296 protected void addEntry(Deque<String> output, String toAdd) {
297 output.addLast(toAdd);
298 }
299
300 /**
301 * Adds the given message to the given output dequeue after all stack trace elements have been
302 * added.
303 */
304 protected void addMessage(Deque<String> output, String message) {
305 output.addFirst("Traceback (most recent call last):");
306 output.addLast(message);
307 }
Florian Weikert3f610e82015-08-18 14:37:46 +0000308 }
Florian Weikert77303b42015-08-27 08:24:24 +0000309
310 /**
311 * Returns a non-empty message for the given exception.
312 *
313 * <p> If the exception itself does not have a message, a new message is constructed from the
314 * exception's class name.
315 * For example, an IllegalArgumentException will lead to "Illegal Argument".
Florian Weikert6a663392015-09-02 14:04:33 +0000316 * Additionally, the location in the Java code will be added, if applicable,
Florian Weikert77303b42015-08-27 08:24:24 +0000317 */
318 private static String getNonEmptyMessage(Exception original) {
319 Preconditions.checkNotNull(original);
320 String msg = original.getMessage();
321 if (msg != null && !msg.isEmpty()) {
322 return msg;
323 }
324
325 char[] name = original.getClass().getSimpleName().replace("Exception", "").toCharArray();
326 boolean first = true;
327 StringBuilder builder = new StringBuilder();
328
329 for (char current : name) {
330 if (Character.isUpperCase(current) && !first) {
331 builder.append(" ");
332 }
333 builder.append(current);
334 first = false;
335 }
336
Florian Weikert6a663392015-09-02 14:04:33 +0000337 java.lang.StackTraceElement[] trace = original.getStackTrace();
338 if (trace.length > 0) {
339 builder.append(String.format(": %s.%s() in %s:%d", getShortClassName(trace[0]),
340 trace[0].getMethodName(), trace[0].getFileName(), trace[0].getLineNumber()));
341 }
342
Florian Weikert77303b42015-08-27 08:24:24 +0000343 return builder.toString();
344 }
Florian Weikert6a663392015-09-02 14:04:33 +0000345
346 private static String getShortClassName(java.lang.StackTraceElement element) {
347 String name = element.getClassName();
348 int pos = name.lastIndexOf('.');
349 return (pos < 0) ? name : name.substring(pos + 1);
350 }
Florian Weikert3f610e82015-08-18 14:37:46 +0000351}