Support Safari 6.1's new stack trace format in pkg/stack_trace.

R=rnystrom@google.com
BUG=14534

Review URL: https://codereview.chromium.org//48273005

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/stack_trace@29399 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/stack_trace/lib/src/frame.dart b/pkgs/stack_trace/lib/src/frame.dart
index 0b4f577..b15f651 100644
--- a/pkgs/stack_trace/lib/src/frame.dart
+++ b/pkgs/stack_trace/lib/src/frame.dart
@@ -19,6 +19,10 @@
     r'^\s*at (?:([^\s].*?)(?: \[as [^\]]+\])? '
     r'\((.+):(\d+):(\d+)\)|(.+):(\d+):(\d+))$');
 
+/// foo$bar$0@http://pub.dartlang.org/stuff.dart.js:560:28
+/// http://pub.dartlang.org/stuff.dart.js:560:28
+final _safariFrame = new RegExp(r"^(?:([0-9A-Za-z_$]*)@)?(.*):(\d*):(\d*)$");
+
 // .VW.call$0@http://pub.dartlang.org/stuff.dart.js:560
 // .VW.call$0("arg")@http://pub.dartlang.org/stuff.dart.js:560
 // .VW.call$0/name<@http://pub.dartlang.org/stuff.dart.js:560
@@ -169,11 +173,27 @@
     return new Frame(uri, int.parse(match[4]), null, member);
   }
 
-  /// Parses a string representation of a Safari stack frame.
+  /// Parses a string representation of a Safari 6.0 stack frame.
   ///
-  /// Safari 6+ frames look just like Firefox frames. Prior to Safari 6, stack
-  /// traces can't be retrieved.
-  factory Frame.parseSafari(String frame) => new Frame.parseFirefox(frame);
+  /// Safari 6.0 frames look just like Firefox frames. Prior to Safari 6.0,
+  /// stack traces can't be retrieved.
+  factory Frame.parseSafari6_0(String frame) => new Frame.parseFirefox(frame);
+
+  /// Parses a string representation of a Safari 6.1+ stack frame.
+  factory Frame.parseSafari6_1(String frame) {
+    var match = _safariFrame.firstMatch(frame);
+    if (match == null) {
+      throw new FormatException(
+          "Couldn't parse Safari stack trace line '$frame'.");
+    }
+
+    var uri = Uri.parse(match[2]);
+    var member = match[1];
+    if (member == null) member = '<fn>';
+    var line = match[3] == '' ? null : int.parse(match[3]);
+    var column = match[4] == '' ? null : int.parse(match[4]);
+    return new Frame(uri, line, column, member);
+  }
 
   /// Parses this package's string representation of a stack frame.
   factory Frame.parseFriendly(String frame) {
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart
index 95d41ad..c3db43e 100644
--- a/pkgs/stack_trace/lib/src/trace.dart
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -28,6 +28,19 @@
 /// though it is possible for the message to match this as well.
 final _v8TraceLine = new RegExp(r"    ?at ");
 
+/// A RegExp to match Safari's stack traces.
+///
+/// Prior to version 6, Safari's stack traces were uncapturable. In v6 they were
+/// almost identical to Firefox traces, and so are handled by the Firefox code.
+/// In v6.1+, they have their own format that's similar to Firefox but distinct
+/// enough to warrant handling separately.
+///
+/// Most notably, Safari traces occasionally don't include the initial method
+/// name followed by "@", and they always have both the line and column number
+/// (or just a trailing colon if no column number is available).
+final _safariTrace = new RegExp(r"^([0-9A-Za-z_$]*@)?.*:\d*:\d*$",
+    multiLine: true);
+
 /// A RegExp to match Firefox's stack traces.
 ///
 /// Firefox's trace frames start with the name of the function in which the
@@ -36,7 +49,8 @@
 final _firefoxTrace = new RegExp(r"^([.0-9A-Za-z_$/<]|\(.*\))*@");
 
 /// A RegExp to match this package's stack traces.
-final _friendlyTrace = new RegExp(r"^[^\s]+( \d+(:\d+)?)?\s+[^\s]+($|\n)");
+final _friendlyTrace = new RegExp(r"^[^\s]+( \d+(:\d+)?)?[ \t]+[^\s]+$",
+    multiLine: true);
 
 /// A stack trace, comprised of a list of stack frames.
 class Trace implements StackTrace {
@@ -89,9 +103,15 @@
     try {
       if (trace.isEmpty) return new Trace(<Frame>[]);
       if (trace.contains(_v8Trace)) return new Trace.parseV8(trace);
-      // Valid Safari traces are a superset of valid Firefox traces.
-      if (trace.contains(_firefoxTrace)) return new Trace.parseSafari(trace);
-      if (trace.contains(_friendlyTrace)) return new Trace.parseFriendly(trace);
+      // Safari 6.1+ traces could be misinterpreted as Firefox traces, so we
+      // check for them first.
+      if (trace.contains(_safariTrace)) return new Trace.parseSafari6_1(trace);
+      // Safari 6.0 traces are a superset of Firefox traces, so we parse those
+      // two together.
+      if (trace.contains(_firefoxTrace)) return new Trace.parseSafari6_0(trace);
+      if (trace.contains(_friendlyTrace)) {
+        return new Trace.parseFriendly(trace);
+      }
 
       // Default to parsing the stack trace as a VM trace. This is also hit on
       // IE and Safari, where the stack trace is just an empty string (issue
@@ -129,11 +149,25 @@
 
   /// Parses a string representation of a Safari stack trace.
   ///
-  /// Safari 6+ stack traces look just like Firefox traces, except that they
+  /// This will automatically decide between [parseSafari6_0] and
+  /// [parseSafari6_1] based on the contents of [trace].
+  factory Trace.parseSafari(String trace) {
+    if (trace.contains(_safariTrace)) return new Trace.parseSafari6_1(trace);
+    return new Trace.parseSafari6_0(trace);
+  }
+
+  /// Parses a string representation of a Safari 6.1+ stack trace.
+  Trace.parseSafari6_1(String trace)
+      : this(trace.trim().split("\n")
+          .map((line) => new Frame.parseSafari6_1(line)));
+
+  /// Parses a string representation of a Safari 6.0 stack trace.
+  ///
+  /// Safari 6.0 stack traces look just like Firefox traces, except that they
   /// sometimes (e.g. in isolates) have a "[native code]" frame. We just ignore
   /// this frame to make the stack format more consistent between browsers.
-  /// Prior to Safari 6, stack traces can't be retrieved.
-  Trace.parseSafari(String trace)
+  /// Prior to Safari 6.0, stack traces can't be retrieved.
+  Trace.parseSafari6_0(String trace)
       : this(trace.trim().split("\n")
           .where((line) => line != '[native code]')
           .map((line) => new Frame.parseFirefox(line)));
diff --git a/pkgs/stack_trace/test/frame_test.dart b/pkgs/stack_trace/test/frame_test.dart
index ab7c401..97cf48a 100644
--- a/pkgs/stack_trace/test/frame_test.dart
+++ b/pkgs/stack_trace/test/frame_test.dart
@@ -226,6 +226,53 @@
     });
   });
 
+  group('.parseSafari6_1', () {
+    test('parses a simple stack frame correctly', () {
+      var frame = new Frame.parseSafari6_1(
+          "foo\$bar@http://dartlang.org/foo/bar.dart:10:11");
+      expect(frame.uri, equals(Uri.parse("http://dartlang.org/foo/bar.dart")));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses an anonymous stack frame correctly', () {
+      var frame = new Frame.parseSafari6_1(
+          "http://dartlang.org/foo/bar.dart:10:11");
+      expect(frame.uri, equals(Uri.parse("http://dartlang.org/foo/bar.dart")));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('<fn>'));
+    });
+
+    test('parses a stack frame with no line correctly', () {
+      var frame = new Frame.parseSafari6_1(
+          "foo\$bar@http://dartlang.org/foo/bar.dart::11");
+      expect(frame.uri, equals(Uri.parse("http://dartlang.org/foo/bar.dart")));
+      expect(frame.line, isNull);
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses a stack frame with no column correctly', () {
+      var frame = new Frame.parseSafari6_1(
+          "foo\$bar@http://dartlang.org/foo/bar.dart:10:");
+      expect(frame.uri, equals(Uri.parse("http://dartlang.org/foo/bar.dart")));
+      expect(frame.line, equals(10));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses a stack frame with no line or column correctly', () {
+      var frame = new Frame.parseSafari6_1(
+          "foo\$bar@http://dartlang.org/foo/bar.dart:10:11");
+      expect(frame.uri, equals(Uri.parse("http://dartlang.org/foo/bar.dart")));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+  });
+
   group('.parseFriendly', () {
     test('parses a simple stack frame correctly', () {
       var frame = new Frame.parseFriendly(
diff --git a/pkgs/stack_trace/test/trace_test.dart b/pkgs/stack_trace/test/trace_test.dart
index c3ce039..ad916f1 100644
--- a/pkgs/stack_trace/test/trace_test.dart
+++ b/pkgs/stack_trace/test/trace_test.dart
@@ -130,7 +130,7 @@
           equals(Uri.parse("http://pub.dartlang.org/stuff.js")));
     });
 
-    test('.parseSafari', () {
+    test('parses a Safari 6.0 stack trace correctly', () {
       var trace = new Trace.parse(
           'Foo._bar@http://pub.dartlang.org/stuff.js:42\n'
           'zip/<@http://pub.dartlang.org/stuff.js:0\n'
@@ -146,6 +146,20 @@
       expect(trace.frames.length, equals(3));
     });
 
+    test('parses a Safari 6.1 stack trace correctly', () {
+      var trace = new Trace.parse(
+          'http://pub.dartlang.org/stuff.js:42:43\n'
+          'zip@http://pub.dartlang.org/stuff.js:0:1\n'
+          'zip\$zap@http://pub.dartlang.org/thing.js:1:2');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse("http://pub.dartlang.org/stuff.js")));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse("http://pub.dartlang.org/stuff.js")));
+      expect(trace.frames[2].uri,
+          equals(Uri.parse("http://pub.dartlang.org/thing.js")));
+    });
+
     test('parses a package:stack_trace stack trace correctly', () {
       var trace = new Trace.parse(
           'http://dartlang.org/foo/bar.dart 10:11  Foo.<fn>.bar\n'