aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/syntax/EvalExceptionWithStackTrace.java
blob: bf25d702f00bf74c16e2f502f8ffe687c7a0745c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
// Copyright 2015 Google Inc. 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 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());
  }

  /**
   * Adds the given {@code Rule} to the stack trace.
   */
  public void registerRule(String rule, Location location, BaseFunction ruleImpl) {
    /* We have to model the transition from BUILD file to bzl file manually since the stack trace
     * mechanism cannot do that by itself (because, for example, the rule implementation does not
     * have a corresponding FuncallExpression).
     *
     * Consequently, we add two new frames to the stack:
     * 1. Rule definition
     * 2. 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(ruleImpl.getName(), ruleImpl.getLocation());
    addStackFrame(rule, 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("\n").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);
  }
}