Add a stack trace collector for Chrome.
This is mapped into the webkit user.agent, but will fall back to the standard collector when running on Safari.

Patch by: schuck
Review by: bobv

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5979 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/core/CoreWithUserAgent.gwt.xml b/user/src/com/google/gwt/core/CoreWithUserAgent.gwt.xml
index cd34b6b..723063a 100644
--- a/user/src/com/google/gwt/core/CoreWithUserAgent.gwt.xml
+++ b/user/src/com/google/gwt/core/CoreWithUserAgent.gwt.xml
@@ -15,7 +15,17 @@
 <!-- Deferred binding rules for core classes based on user agent. -->
 <module>
   <inherits name="com.google.gwt.core.Core"/>
-  
+
+  <replace-with class="com.google.gwt.core.client.impl.StackTraceCreator.CollectorChrome">
+    <when-type-is class="com.google.gwt.core.client.impl.StackTraceCreator.Collector" />
+    <when-property-is name="compiler.emulatedStack" value="false" />
+    <any>
+      <!-- For now, only Chrome provides Error.stack support, so we hijack the
+           entire WebKit permutation -->
+      <when-property-is name="user.agent" value="safari" />
+    </any>
+  </replace-with>
+
   <replace-with class="com.google.gwt.core.client.impl.StackTraceCreator.CollectorMoz">
     <when-type-is class="com.google.gwt.core.client.impl.StackTraceCreator.Collector" />
     <when-property-is name="compiler.emulatedStack" value="false" />
@@ -32,4 +42,4 @@
       <when-property-is name="user.agent" value="opera" />
     </any>
   </replace-with>
-</module>
\ No newline at end of file
+</module>
diff --git a/user/src/com/google/gwt/core/client/impl/StackTraceCreator.java b/user/src/com/google/gwt/core/client/impl/StackTraceCreator.java
index 3dc8a5e..1847972 100644
--- a/user/src/com/google/gwt/core/client/impl/StackTraceCreator.java
+++ b/user/src/com/google/gwt/core/client/impl/StackTraceCreator.java
@@ -209,6 +209,88 @@
   }
 
   /**
+   * 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 {
diff --git a/user/test/com/google/gwt/core/client/impl/StackTraceCreatorTest.java b/user/test/com/google/gwt/core/client/impl/StackTraceCreatorTest.java
index 9083605..b3f9dde 100644
--- a/user/test/com/google/gwt/core/client/impl/StackTraceCreatorTest.java
+++ b/user/test/com/google/gwt/core/client/impl/StackTraceCreatorTest.java
@@ -19,6 +19,7 @@
 import com.google.gwt.core.client.JavaScriptException;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.impl.StackTraceCreator.CollectorChrome;
 import com.google.gwt.junit.DoNotRunWith;
 import com.google.gwt.junit.Platform;
 import com.google.gwt.junit.client.GWTTestCase;
@@ -64,7 +65,7 @@
   /**
    * Just make sure that reentrant behavior doesn't fail.
    */
-  @DoNotRunWith({Platform.Htmlunit})
+  @DoNotRunWith(value = {Platform.Htmlunit})
   public static void testReentrantCalls() {
     if (!GWT.isScript()) {
       // sample is useless in hosted mode
@@ -81,7 +82,7 @@
     assertEquals(start, end);
   }
 
-  @DoNotRunWith({Platform.Htmlunit})
+  @DoNotRunWith(value = {Platform.Htmlunit})
   public static void testStackTraces() {
     JsArrayString start = sample();
 
@@ -193,4 +194,16 @@
     assertEquals("foo",
         StackTraceCreator.extractNameFromToString("  function foo (){}"));
   }
+
+  public void testChromeExtractName() {
+    CollectorChrome c = new CollectorChrome();
+
+    assertEquals("anonymous", c.extractName(" at file.js:1:2"));
+    assertEquals("functionName",
+        c.extractName(" at functionName (file.js:1:2)"));
+    assertEquals("functionName",
+        c.extractName(" at Type.functionName (file.js:1:2)"));
+    assertEquals("functionName",
+        c.extractName(" at Type.functionName [as methodName] (file.js:1:2)"));
+  }
 }