| /* |
| * Copyright 2009 Google Inc. |
| * |
| * 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.gwt.core.client.impl; |
| |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.core.client.JavaScriptException; |
| import com.google.gwt.core.client.JavaScriptObject; |
| import com.google.gwt.core.client.JsArray; |
| import com.google.gwt.core.client.JsArrayString; |
| |
| /** |
| * Encapsulates logic to create a stack trace. This class should only be used in |
| * Production Mode. |
| */ |
| public class StackTraceCreator { |
| /** |
| * This class acts as a deferred-binding hook point to allow more optimal |
| * versions to be substituted. This base version simply crawls |
| * <code>arguments.callee.caller</code>. |
| */ |
| static class Collector { |
| public native JsArrayString collect() /*-{ |
| var seen = {}; |
| var toReturn = []; |
| |
| // Ignore the collect() and fillInStackTrace call |
| var callee = arguments.callee.caller.caller; |
| while (callee) { |
| var name = this.@com.google.gwt.core.client.impl.StackTraceCreator.Collector::extractName(Ljava/lang/String;)(callee.toString()); |
| toReturn.push(name); |
| |
| // Avoid infinite loop by associating names to function objects. We |
| // record each caller in the withThisName variable to handle functions |
| // with identical names but separate identity (such as 'anonymous') |
| var keyName = ':' + name; |
| var withThisName = seen[keyName]; |
| if (withThisName) { |
| var i, j; |
| for (i = 0, j = withThisName.length; i < j; i++) { |
| if (withThisName[i] === callee) { |
| return toReturn; |
| } |
| } |
| } |
| |
| (withThisName || (seen[keyName] = [])).push(callee); |
| callee = callee.caller; |
| } |
| return toReturn; |
| }-*/; |
| |
| public void createStackTrace(JavaScriptException e) { |
| JsArrayString stack = inferFrom(e.getException()); |
| |
| StackTraceElement[] stackTrace = new StackTraceElement[stack.length()]; |
| for (int i = 0, j = stackTrace.length; i < j; i++) { |
| stackTrace[i] = new StackTraceElement("Unknown", stack.get(i), null, |
| LINE_NUMBER_UNKNOWN); |
| } |
| e.setStackTrace(stackTrace); |
| } |
| |
| public void fillInStackTrace(Throwable t) { |
| JsArrayString stack = StackTraceCreator.createStackTrace(); |
| StackTraceElement[] stackTrace = new StackTraceElement[stack.length()]; |
| for (int i = 0, j = stackTrace.length; i < j; i++) { |
| stackTrace[i] = new StackTraceElement("Unknown", stack.get(i), null, |
| LINE_NUMBER_UNKNOWN); |
| } |
| t.setStackTrace(stackTrace); |
| } |
| |
| /** |
| * Returns the list of properties of an unexpected JavaScript exception. |
| */ |
| public native String getProperties(JavaScriptObject e) /*-{ |
| var result = ""; |
| try { |
| for (var prop in e) { |
| if (prop != "name" && prop != "message" && prop != "toString") { |
| try { |
| result += "\n " + prop + ": " + e[prop]; |
| } catch (ignored) { |
| // Skip the property if it threw an exception. |
| } |
| } |
| } |
| } catch (ignored) { |
| // If we can't do "in" on the exception, just return what we have. |
| } |
| return result; |
| }-*/; |
| |
| /** |
| * Attempt to infer the stack from an unknown JavaScriptObject that had been |
| * thrown. The default implementation just returns an empty array. |
| * |
| * @param e a JavaScriptObject |
| */ |
| public JsArrayString inferFrom(JavaScriptObject e) { |
| return JavaScriptObject.createArray().cast(); |
| } |
| |
| /** |
| * Extract the name of a function from it's toString() representation. |
| * Package-access for testing. |
| */ |
| protected String extractName(String fnToString) { |
| return extractNameFromToString(fnToString); |
| } |
| |
| /** |
| * Raise an exception and return it. |
| */ |
| protected native JavaScriptObject makeException() /*-{ |
| try { |
| null.a(); |
| } catch (e) { |
| return e; |
| } |
| }-*/; |
| } |
| |
| /** |
| * Collaborates with JsStackEmulator. |
| */ |
| static class CollectorEmulated extends Collector { |
| |
| @Override |
| public JsArrayString collect() { |
| JsArrayString toReturn = JsArrayString.createArray().cast(); |
| JsArray<JavaScriptObject> stack = getStack(); |
| for (int i = 0, j = getStackDepth(); i < j; i++) { |
| String name = stack.get(i) == null ? "anonymous" |
| : extractName(stack.get(i).toString()); |
| // Reverse the order |
| toReturn.set(j - i - 1, name); |
| } |
| |
| return toReturn; |
| } |
| |
| @Override |
| public void createStackTrace(JavaScriptException e) { |
| // No-op, relying on initializer call to collect() |
| } |
| |
| @Override |
| public void fillInStackTrace(Throwable t) { |
| JsArrayString stack = collect(); |
| JsArrayString locations = getLocation(); |
| StackTraceElement[] stackTrace = new StackTraceElement[stack.length()]; |
| for (int i = 0, j = stackTrace.length; i < j; i++) { |
| // Locations is also backwards |
| String location = locations.get(j - i - 1); |
| String fileName = null; |
| int lineNumber = LINE_NUMBER_UNKNOWN; |
| if (location != null) { |
| int idx = location.indexOf(':'); |
| if (idx != -1) { |
| fileName = location.substring(0, idx); |
| lineNumber = Integer.parseInt(location.substring(idx + 1)); |
| } else { |
| lineNumber = Integer.parseInt(location); |
| } |
| } |
| stackTrace[i] = new StackTraceElement("Unknown", stack.get(i), |
| fileName, lineNumber); |
| } |
| t.setStackTrace(stackTrace); |
| } |
| |
| /** |
| * When compiler.stackMode = emulated, return an empty string, rather than a |
| * list of properties, since the additional information regarding the origin |
| * of the JavaScriptException, relative to compiled JavaScript source code, |
| * adds no real value, since we have fully emulated stack traces. |
| */ |
| @Override |
| public String getProperties(JavaScriptObject e) { |
| return ""; |
| } |
| |
| @Override |
| public JsArrayString inferFrom(JavaScriptObject e) { |
| throw new RuntimeException("Should not reach here"); |
| } |
| |
| private native JsArrayString getLocation()/*-{ |
| return $location; |
| }-*/; |
| |
| private native JsArray<JavaScriptObject> getStack()/*-{ |
| return $stack; |
| }-*/; |
| |
| private native int getStackDepth() /*-{ |
| return $stackDepth; |
| }-*/; |
| } |
| |
| /** |
| * Mozilla provides a <code>stack</code> property in thrown objects. |
| */ |
| static class CollectorMoz extends Collector { |
| /** |
| * This implementation doesn't suffer from the limitations of crawling |
| * <code>caller</code> since Mozilla provides proper activation records. |
| */ |
| @Override |
| public JsArrayString collect() { |
| return splice(inferFrom(makeException()), toSplice()); |
| } |
| |
| @Override |
| public JsArrayString inferFrom(JavaScriptObject e) { |
| JsArrayString stack = getStack(e); |
| for (int i = 0, j = stack.length(); i < j; i++) { |
| stack.set(i, extractName(stack.get(i))); |
| } |
| return stack; |
| } |
| |
| protected native JsArrayString getStack(JavaScriptObject e) /*-{ |
| return (e && e.stack) ? e.stack.split('\n') : []; |
| }-*/; |
| |
| protected int toSplice() { |
| return 2; |
| } |
| } |
| |
| /** |
| * Chrome uses a slightly different format to Mozilla. |
| * |
| * See http://code.google.com/p/v8/source/browse/branches/bleeding_edge/src/ |
| * messages.js?r=2340#712 for formatting code. |
| * |
| * Function calls can be of the four following forms: |
| * |
| * <pre> |
| * at file.js:1:2 |
| * at functionName (file.js:1:2) |
| * at Type.functionName (file.js:1:2) |
| * at Type.functionName [as methodName] (file.js:1:2) |
| * </pre> |
| */ |
| static class CollectorChrome extends CollectorMoz { |
| @Override |
| public JsArrayString collect() { |
| JsArrayString res = super.collect(); |
| if (res.length() == 0) { |
| /* |
| * Ensure Safari falls back to default Collector implementation. |
| * Remember to remove this method call from the stack: |
| */ |
| res = splice(new Collector().collect(), 1); |
| } |
| return res; |
| } |
| |
| @Override |
| public JsArrayString inferFrom(JavaScriptObject e) { |
| JsArrayString stack = super.inferFrom(e); |
| if (stack.length() == 0) { |
| // Safari should fall back to default Collector: |
| return new Collector().inferFrom(e); |
| } else { |
| // Chrome contains the error itself as the first line of the stack: |
| return splice(stack, 1); |
| } |
| } |
| |
| @Override |
| protected String extractName(String fnToString) { |
| if (fnToString.length() == 0) { |
| return "anonymous"; |
| } |
| |
| String toReturn = fnToString.trim(); |
| |
| // Strip the "at " prefix: |
| if (toReturn.startsWith("at ")) { |
| toReturn = toReturn.substring(3); |
| } |
| |
| // Strip bracketed items from the end: |
| int index = toReturn.indexOf("["); |
| if (index == -1) { |
| index = toReturn.indexOf("("); |
| } |
| if (index == -1) { |
| // No bracketed items found, hence no function name available: |
| return "anonymous"; |
| } else { |
| // Bracketed items found: strip them off. |
| toReturn = toReturn.substring(0, index).trim(); |
| } |
| |
| // Strip the Type off to leave just the functionName: |
| index = toReturn.indexOf('.'); |
| if (index != -1) { |
| toReturn = toReturn.substring(index + 1); |
| } |
| |
| return toReturn.length() > 0 ? toReturn : "anonymous"; |
| } |
| |
| @Override |
| protected int toSplice() { |
| return 3; |
| } |
| } |
| |
| /** |
| * Opera encodes stack trace information in the error's message. |
| */ |
| static class CollectorOpera extends CollectorMoz { |
| /** |
| * We have much a much simpler format to work with. |
| */ |
| @Override |
| protected String extractName(String fnToString) { |
| return fnToString.length() == 0 ? "anonymous" : fnToString; |
| } |
| |
| /** |
| * Opera has the function name on every-other line. |
| */ |
| @Override |
| protected JsArrayString getStack(JavaScriptObject e) { |
| JsArrayString toReturn = getMessage(e); |
| assert toReturn.length() % 2 == 0 : "Expecting an even number of lines"; |
| |
| int i, i2, j; |
| for (i = 0, i2 = 0, j = toReturn.length(); i2 < j; i++, i2 += 2) { |
| int idx = toReturn.get(i2).lastIndexOf("function "); |
| if (idx == -1) { |
| toReturn.set(i, ""); |
| } else { |
| toReturn.set(i, toReturn.get(i2).substring(idx + 9).trim()); |
| } |
| } |
| setLength(toReturn, i); |
| |
| return toReturn; |
| } |
| |
| @Override |
| protected int toSplice() { |
| return 3; |
| } |
| |
| private native JsArrayString getMessage(JavaScriptObject e) /*-{ |
| return (e && e.message) ? e.message.split('\n') : []; |
| }-*/; |
| |
| private native void setLength(JsArrayString obj, int length) /*-{ |
| obj.length = length; |
| }-*/; |
| } |
| |
| /** |
| * When compiler.stackMode = strip, we stub out the collector. |
| */ |
| static class CollectorNull extends Collector { |
| @Override |
| public JsArrayString collect() { |
| return JsArrayString.createArray().cast(); |
| } |
| |
| @Override |
| public void createStackTrace(JavaScriptException e) { |
| // empty, since Throwable.getStackTrace() properly handles null |
| } |
| |
| @Override |
| public void fillInStackTrace(Throwable t) { |
| // empty, since Throwable.getStackTrace() properly handles null |
| } |
| } |
| |
| private static final int LINE_NUMBER_UNKNOWN = -1; |
| |
| /** |
| * Create a stack trace based on a JavaScriptException. This method should |
| * only be called in Production Mode. |
| */ |
| public static void createStackTrace(JavaScriptException e) { |
| if (!GWT.isScript()) { |
| throw new RuntimeException( |
| "StackTraceCreator should only be called in Production Mode"); |
| } |
| |
| GWT.<Collector> create(Collector.class).createStackTrace(e); |
| } |
| |
| /** |
| * Fill in a stack trace based on the current execution stack. This method |
| * should only be called in Production Mode. |
| */ |
| public static void fillInStackTrace(Throwable t) { |
| if (!GWT.isScript()) { |
| throw new RuntimeException( |
| "StackTraceCreator should only be called in Production Mode"); |
| } |
| |
| GWT.<Collector> create(Collector.class).fillInStackTrace(t); |
| } |
| |
| /** |
| * Returns the list of properties of an unexpected JavaScript exception, |
| * unless compiler.stackMode = emulated, in which case the empty string is |
| * returned. This method should only be called in Production Mode. |
| */ |
| public static String getProperties(JavaScriptObject e) { |
| if (!GWT.isScript()) { |
| throw new RuntimeException( |
| "StackTraceCreator should only be called in Production Mode"); |
| } |
| |
| return GWT.<Collector> create(Collector.class).getProperties(e); |
| } |
| |
| /** |
| * Create a stack trace based on the current execution stack. This method |
| * should only be called in Production Mode. |
| */ |
| static JsArrayString createStackTrace() { |
| if (!GWT.isScript()) { |
| throw new RuntimeException( |
| "StackTraceCreator should only be called in Production Mode"); |
| } |
| |
| return GWT.<Collector> create(Collector.class).collect(); |
| } |
| |
| static String extractNameFromToString(String fnToString) { |
| String toReturn = ""; |
| fnToString = fnToString.trim(); |
| int index = fnToString.indexOf("("); |
| if (index != -1) { |
| int start = fnToString.startsWith("function") ? 8 : 0; |
| toReturn = fnToString.substring(start, index).trim(); |
| } |
| |
| return toReturn.length() > 0 ? toReturn : "anonymous"; |
| } |
| |
| private static native JsArrayString splice(JsArrayString arr, int length) /*-{ |
| (arr.length >= length) && arr.splice(0, length); |
| return arr; |
| }-*/; |
| } |