| /* |
| * 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.JavaScriptObject; |
| import com.google.gwt.core.client.JsArray; |
| import com.google.gwt.core.client.JsArrayString; |
| |
| import javaemul.internal.ArrayHelper; |
| |
| /** |
| * Encapsulates logic to create a stack trace. This class should only be used in |
| * Production Mode. |
| */ |
| public class StackTraceCreator { |
| |
| /** |
| * Maximum # of frames to look for {@link Throwable#fillInStackTrace()} in the generated stack |
| * trace. This is just a safe guard just in case if {@code fillInStackTrace} doesn't show up in |
| * the stack trace for some reason. |
| */ |
| private static final int DROP_FRAME_LIMIT = 5; |
| |
| /** |
| * Line number used in a stack trace when it is unknown. |
| */ |
| private static final int LINE_NUMBER_UNKNOWN = -1; |
| |
| /** |
| * Replacement for function names that cannot be extracted from a stack. |
| */ |
| private static final String ANONYMOUS = "anonymous"; |
| |
| /** |
| * Replacement for class or file names that cannot be extracted from a stack. |
| */ |
| private static final String UNKNOWN = "Unknown"; |
| |
| /** |
| * This class acts as a deferred-binding hook point to allow more optimal versions to be |
| * substituted. |
| */ |
| abstract static class Collector { |
| |
| public abstract void collect(Object error); |
| |
| public abstract StackTraceElement[] getStackTrace(Object t); |
| } |
| |
| /** |
| * This legacy {@link Collector} simply crawls <code>arguments.callee.caller</code> for browsers |
| * that doesn't support {@code Error.stack} property. |
| */ |
| static class CollectorLegacy extends Collector { |
| |
| @Override |
| public native void collect(Object error) /*-{ |
| var seen = {}; |
| var fnStack = []; |
| error["fnStack"] = fnStack; |
| |
| // Ignore the collect() call |
| var callee = arguments.callee.caller; |
| while (callee) { |
| var name = @StackTraceCreator::getFunctionName(*)(callee); |
| fnStack.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; |
| } |
| } |
| } |
| |
| (withThisName || (seen[keyName] = [])).push(callee); |
| callee = callee.caller; |
| } |
| }-*/; |
| |
| @Override |
| public StackTraceElement[] getStackTrace(Object t) { |
| JsArrayString stack = getFnStack(t); |
| |
| int length = stack.length(); |
| StackTraceElement[] stackTrace = new StackTraceElement[length]; |
| for (int i = 0; i < length; i++) { |
| stackTrace[i] = new StackTraceElement(UNKNOWN, stack.get(i), null, LINE_NUMBER_UNKNOWN); |
| } |
| return stackTrace; |
| } |
| } |
| |
| /** |
| * Collaborates with JsStackEmulator. |
| */ |
| static final class CollectorEmulated extends Collector { |
| |
| @Override |
| public native void collect(Object error) /*-{ |
| var fnStack = []; |
| error["fnStack"] = fnStack; |
| for (var i = 0; i < $stackDepth; i++) { |
| var location = $location[i]; |
| var fn = $stack[i]; |
| var name = fn ? @StackTraceCreator::getFunctionName(*)(fn) : @StackTraceCreator::ANONYMOUS; |
| |
| // Reverse the order |
| fnStack[$stackDepth - i - 1] = [name, location]; |
| } |
| }-*/; |
| |
| @Override |
| public StackTraceElement[] getStackTrace(Object t) { |
| JsArray<JsArrayString> stack = getFnStack(t).cast(); |
| |
| StackTraceElement[] stackTrace = new StackTraceElement[stack.length()]; |
| for (int i = 0; i < stackTrace.length; i++) { |
| JsArrayString frame = stack.get(i); |
| String name = frame.get(0); |
| String location = frame.get(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 = parseInt(location.substring(idx + 1)); |
| } else { |
| lineNumber = parseInt(location); |
| } |
| } |
| stackTrace[i] = new StackTraceElement(UNKNOWN, name, fileName, lineNumber); |
| } |
| return stackTrace; |
| } |
| } |
| |
| /** |
| * Modern browsers provide a <code>stack</code> property in thrown objects. |
| */ |
| static class CollectorModern extends Collector { |
| |
| @Override |
| public void collect(Object error) { |
| // No op, already collected by the error itself. |
| } |
| |
| @Override |
| public StackTraceElement[] getStackTrace(Object t) { |
| JsArrayString stack = split(t); |
| |
| // We are in script-mode - let the array auto grow. |
| StackTraceElement[] stackTrace = new StackTraceElement[0]; |
| int addIndex = 0, length = stack.length(); |
| |
| if (length == 0) { |
| // Nothing to parse... |
| return stackTrace; |
| } |
| |
| // Chrome & IE10+ contains the error msg as the first line of stack (iOS, Firefox doesn't). |
| StackTraceElement ste = parse(stack.get(0)); |
| if (!ste.getMethodName().equals(ANONYMOUS)) { |
| stackTrace[addIndex++] = ste; |
| } |
| |
| // Parse and put the rest of the elements in to the stack trace. |
| for (int i = 1; i < length; i++) { |
| stackTrace[addIndex++] = parse(stack.get(i)); |
| } |
| |
| return stackTrace; |
| } |
| |
| /** |
| * Parses a stack trace line from the browser and returns a new {@link StackTraceElement} |
| * constructed with the extracted data. |
| */ |
| private StackTraceElement parse(String stString) { |
| String location = ""; |
| |
| if (stString.isEmpty()) { |
| return createSte(UNKNOWN, ANONYMOUS, LINE_NUMBER_UNKNOWN, -1); |
| } |
| |
| String toReturn = stString.trim(); |
| |
| // Strip the "at " prefix: |
| if (toReturn.startsWith("at ")) { |
| toReturn = toReturn.substring(3); |
| } |
| |
| toReturn = stripSquareBrackets(toReturn); |
| |
| int index = toReturn.indexOf("("); |
| if (index == -1) { |
| // No bracketed items found, try '@' (used by iOS & Firefox). |
| index = toReturn.indexOf("@"); |
| if (index == -1) { |
| // No bracketed items nor '@' found, hence no function name available |
| location = toReturn; |
| toReturn = ""; |
| } else { |
| location = toReturn.substring(index + 1).trim(); |
| toReturn = toReturn.substring(0, index).trim(); |
| } |
| } else { |
| // Bracketed items found: strip them off, parse location info |
| int closeParen = toReturn.indexOf(")", index); |
| location = toReturn.substring(index + 1, closeParen); |
| toReturn = toReturn.substring(0, index).trim(); |
| } |
| |
| // Strip the Type off t |
| index = toReturn.indexOf('.'); |
| if (index != -1) { |
| toReturn = toReturn.substring(index + 1); |
| } |
| |
| final String ieAnonymousFunctionName = "Anonymous function"; |
| if (toReturn.isEmpty() || toReturn.equals(ieAnonymousFunctionName)) { |
| toReturn = ANONYMOUS; |
| } |
| |
| // colon between line and column |
| int lastColonIndex = location.lastIndexOf(':'); |
| // colon between file url and line number |
| int endFileUrlIndex = location.lastIndexOf(':', lastColonIndex - 1); |
| |
| int line = LINE_NUMBER_UNKNOWN; |
| int col = -1; |
| String fileName = UNKNOWN; |
| |
| if (lastColonIndex != -1 && endFileUrlIndex != -1) { |
| fileName = location.substring(0, endFileUrlIndex); |
| line = parseInt(location.substring(endFileUrlIndex + 1, lastColonIndex)); |
| col = parseInt(location.substring(lastColonIndex + 1)); |
| } |
| |
| return createSte(fileName, toReturn, line, col); |
| } |
| |
| protected StackTraceElement createSte(String fileName, String method, int line, int col) { |
| return new StackTraceElement(UNKNOWN, method, fileName + "@" + col, |
| line < 0 ? LINE_NUMBER_UNKNOWN : line); |
| } |
| |
| private native String stripSquareBrackets(String toReturn) /*-{ |
| return toReturn.replace(/\[.*?\]/g,"") |
| }-*/; |
| } |
| |
| /** |
| * Subclass that forces reported line numbers to -1 (fetch from symbolMap) if source maps are |
| * disabled. |
| */ |
| static class CollectorModernNoSourceMap extends CollectorModern { |
| @Override |
| protected StackTraceElement createSte(String fileName, String method, int line, int col) { |
| return new StackTraceElement(UNKNOWN, method, fileName, LINE_NUMBER_UNKNOWN); |
| } |
| } |
| |
| private static native int parseInt(String number) /*-{ |
| return parseInt(number) || @StackTraceCreator::LINE_NUMBER_UNKNOWN; |
| }-*/; |
| |
| /** |
| * When compiler.stackMode = strip, we stub out the collector. |
| */ |
| static class CollectorNull extends Collector { |
| @Override |
| public void collect(Object error) { |
| // Nothing to do |
| } |
| |
| @Override |
| public StackTraceElement[] getStackTrace(Object ignored) { |
| return new StackTraceElement[0]; |
| } |
| } |
| |
| /** |
| * Collect necessary information to construct stack trace trace later in time. |
| */ |
| public static void captureStackTrace(Object error) { |
| collector.collect(error); |
| } |
| |
| public static StackTraceElement[] constructJavaStackTrace(Throwable thrown) { |
| StackTraceElement[] stackTrace = collector.getStackTrace(thrown); |
| return dropInternalFrames(stackTrace); |
| } |
| |
| private static StackTraceElement[] dropInternalFrames(StackTraceElement[] stackTrace) { |
| final String dropFrameUntilFnName = |
| Impl.getNameOf("@com.google.gwt.core.client.impl.StackTraceCreator::captureStackTrace(*)"); |
| final String dropFrameUntilFnName2 = |
| Impl.getNameOf("@java.lang.Throwable::initializeBackingError(*)"); |
| |
| int numberOfFramesToSearch = Math.min(stackTrace.length, DROP_FRAME_LIMIT); |
| for (int i = numberOfFramesToSearch - 1; i >= 0; i--) { |
| if (stackTrace[i].getMethodName().equals(dropFrameUntilFnName) |
| || stackTrace[i].getMethodName().equals(dropFrameUntilFnName2)) { |
| splice(stackTrace, i + 1); |
| break; |
| } |
| } |
| |
| return stackTrace; |
| } |
| |
| private static <T> void splice(Object[] arr, int length) { |
| if (arr.length >= length) { |
| ArrayHelper.removeFrom(arr, 0, length); |
| } |
| } |
| |
| // Visible for testing |
| static final Collector collector; |
| |
| static { |
| // Ensure old Safari falls back to legacy Collector implementation. |
| boolean enforceLegacy = !supportsErrorStack(); |
| Collector c = GWT.create(Collector.class); |
| collector = (c instanceof CollectorModern && enforceLegacy) ? new CollectorLegacy() : c; |
| } |
| |
| private static native boolean supportsErrorStack() /*-{ |
| // Error.stackTraceLimit is cheaper to check and available in both IE and Chrome |
| if (Error.stackTraceLimit > 0) { |
| $wnd.Error.stackTraceLimit = Error.stackTraceLimit = 64; |
| return true; |
| } |
| |
| return "stack" in new Error(); |
| }-*/; |
| |
| private static native JsArrayString getFnStack(Object e) /*-{ |
| return (e && e["fnStack"]) ? e["fnStack"] : []; |
| }-*/; |
| |
| private static native String getFunctionName(JavaScriptObject fn) /*-{ |
| return fn.name || (fn.name = @StackTraceCreator::extractFunctionName(*)(fn.toString())); |
| }-*/; |
| |
| // Visible for testing |
| static native String extractFunctionName(String fnName) /*-{ |
| var fnRE = /function(?:\s+([\w$]+))?\s*\(/; |
| var match = fnRE.exec(fnName); |
| return (match && match[1]) || @StackTraceCreator::ANONYMOUS; |
| }-*/; |
| |
| private static native JsArrayString split(Object t) /*-{ |
| var e = t.@Throwable::backingJsObject; |
| if (e && e.stack) { |
| var stack = e.stack; |
| // If the stack starts with toString of Error, drop it. |
| var toString = e + "\n"; |
| if (stack.substring(0, toString.length) == toString) { |
| stack = stack.substring(toString.length); |
| } |
| return stack.split('\n'); |
| } |
| return []; |
| }-*/; |
| } |