Unify parsing of Firefox and Safari stack traces.

This releases stack_trace 1.1.0.

R=rnystrom@google.com

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/stack_trace@40470 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md
index 56c614a..bbb7eb5 100644
--- a/pkgs/stack_trace/CHANGELOG.md
+++ b/pkgs/stack_trace/CHANGELOG.md
@@ -1,3 +1,13 @@
+## 1.1.0
+
+* Unify the parsing of Safari and Firefox stack traces. This fixes an error in
+  Firefox trace parsing.
+
+* Deprecate `Trace.parseSafari6_0`, `Trace.parseSafari6_1`,
+  `Frame.parseSafari6_0`, and `Frame.parseSafari6_1`.
+
+* Add `Frame.parseSafari`.
+
 ## 1.0.3
 
 * Use `Zone.errorCallback` to attach stack chains to all errors without the need
diff --git a/pkgs/stack_trace/lib/src/frame.dart b/pkgs/stack_trace/lib/src/frame.dart
index b1db230..4f5b16c 100644
--- a/pkgs/stack_trace/lib/src/frame.dart
+++ b/pkgs/stack_trace/lib/src/frame.dart
@@ -30,15 +30,26 @@
 final _v8EvalLocation = new RegExp(
     r'^eval at (?:\S.*?) \((.*)\)(?:, .*?:\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
-final _firefoxFrame = new RegExp(
-    r'^([^@(/]*)(?:\(.*\))?((?:/[^/]*)*)(?:\(.*\))?@(.*):(\d+)$');
+// .VW.call$0@http://pub.dartlang.org/stuff.dart.js:560:36
+// http://pub.dartlang.org/stuff.dart.js:560
+final _firefoxSafariFrame = new RegExp(
+    r'^'
+    r'(?:' // Member description. Not present in some Safari frames.
+      r'([^@(/]*)' // The actual name of the member.
+      r'(?:\(.*\))?' // Arguments to the member, sometimes captured by Firefox.
+      r'((?:/[^/]*)*)' // Extra characters indicating a nested closure.
+      r'(?:\(.*\))?' // Arguments to the closure.
+      r'@'
+    r')?'
+    r'(.*?)' // The frame's URL.
+    r':'
+    r'(\d*)' // The line number. Empty in Safari if it's unknown.
+    r'(?::(\d*))?' // The column number. Not present in older browsers and
+                   // empty in Safari if it's unknown.
+    r'$');
 
 // foo/bar.dart 10:11 in Foo._bar
 // http://dartlang.org/foo/bar.dart in Foo._bar
@@ -187,45 +198,45 @@
 
   /// Parses a string representation of a Firefox stack frame.
   factory Frame.parseFirefox(String frame) {
-    var match = _firefoxFrame.firstMatch(frame);
+    var match = _firefoxSafariFrame.firstMatch(frame);
     if (match == null) {
       throw new FormatException(
-          "Couldn't parse Firefox stack trace line '$frame'.");
+          "Couldn't parse Firefox/Safari stack trace line '$frame'.");
     }
 
     // Normally this is a URI, but in a jsshell trace it can be a path.
     var uri = _uriOrPathToUri(match[3]);
-    var member = match[1];
-    member += new List.filled('/'.allMatches(match[2]).length, ".<fn>").join();
-    if (member == '') member = '<fn>';
 
-    // Some Firefox members have initial dots. We remove them for consistency
-    // with other platforms.
-    member = member.replaceFirst(_initialDot, '');
-    return new Frame(uri, int.parse(match[4]), null, member);
+    var member;
+    if (match[1] != null) {
+      member = match[1];
+      member +=
+          new List.filled('/'.allMatches(match[2]).length, ".<fn>").join();
+      if (member == '') member = '<fn>';
+
+      // Some Firefox members have initial dots. We remove them for consistency
+      // with other platforms.
+      member = member.replaceFirst(_initialDot, '');
+    } else {
+      member = '<fn>';
+    }
+
+    var line = match[4] == '' ? null : int.parse(match[4]);
+    var column = match[5] == null || match[5] == '' ?
+        null : int.parse(match[5]);
+    return new Frame(uri, line, column, member);
   }
 
   /// Parses a string representation of a Safari 6.0 stack frame.
-  ///
-  /// Safari 6.0 frames look just like Firefox frames. Prior to Safari 6.0,
-  /// stack traces can't be retrieved.
+  @Deprecated("Use Frame.parseSafari instead.")
   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'.");
-    }
+  @Deprecated("Use Frame.parseSafari instead.")
+  factory Frame.parseSafari6_1(String frame) => new Frame.parseFirefox(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 a string representation of a Safari stack frame.
+  factory Frame.parseSafari(String frame) => new Frame.parseFirefox(frame);
 
   /// 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 c540d78..2690ece 100644
--- a/pkgs/stack_trace/lib/src/trace.dart
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -29,25 +29,28 @@
 /// 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.
+/// A RegExp to match Firefox and 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 and Safari have very similar stack trace formats, so we use the same
+/// logic for parsing them.
 ///
 /// Firefox's trace frames start with the name of the function in which the
 /// error occurred, possibly including its parameters inside `()`. For example,
 /// `.VW.call$0("arg")@http://pub.dartlang.org/stuff.dart.js:560`.
-final _firefoxTrace = new RegExp(r"^([.0-9A-Za-z_$/<]|\(.*\))*@");
+///
+/// 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). They can also contain
+/// empty lines or lines consisting only of `[native code]`.
+final _firefoxSafariTrace = new RegExp(
+    r"^"
+    r"(" // Member description. Not present in some Safari frames.
+      r"([.0-9A-Za-z_$/<]|\(.*\))*" // Member name and arguments.
+      r"@"
+    r")?"
+    r"[^\s]*" // Frame URL.
+    r":\d*" // Line or column number. Some older frames only have a line number.
+    r"$", multiLine: true);
 
 /// A RegExp to match this package's stack traces.
 final _friendlyTrace = new RegExp(r"^[^\s]+( \d+(:\d+)?)?[ \t]+[^\s]+$",
@@ -112,12 +115,9 @@
     try {
       if (trace.isEmpty) return new Trace(<Frame>[]);
       if (trace.contains(_v8Trace)) return new Trace.parseV8(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(_firefoxSafariTrace)) {
+        return new Trace.parseFirefox(trace);
+      }
       if (trace.contains(_friendlyTrace)) {
         return new Trace.parseFriendly(trace);
       }
@@ -157,29 +157,20 @@
   /// Parses a string representation of a Firefox stack trace.
   Trace.parseFirefox(String trace)
       : this(trace.trim().split("\n")
+          .where((line) => line.isNotEmpty && line != '[native code]')
           .map((line) => new Frame.parseFirefox(line)));
 
   /// Parses a string representation of a Safari stack trace.
-  ///
-  /// 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);
-  }
+  Trace.parseSafari(String trace)
+      : this.parseFirefox(trace);
 
   /// Parses a string representation of a Safari 6.1+ stack trace.
+  @Deprecated("Use Trace.parseSafari instead.")
   Trace.parseSafari6_1(String trace)
-      : this(trace.trim().split("\n")
-          .where((line) => line.isNotEmpty)
-          .map((line) => new Frame.parseSafari6_1(line)));
+      : this.parseSafari(trace);
 
   /// 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.0, stack traces can't be retrieved.
+  @Deprecated("Use Trace.parseSafari instead.")
   Trace.parseSafari6_0(String trace)
       : this(trace.trim().split("\n")
           .where((line) => line != '[native code]')
diff --git a/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml
index bf28770..cd798bf 100644
--- a/pkgs/stack_trace/pubspec.yaml
+++ b/pkgs/stack_trace/pubspec.yaml
@@ -7,7 +7,7 @@
 #
 # When the major version is upgraded, you *must* update that version constraint
 # in pub to stay in sync with this.
-version: 1.0.3
+version: 1.1.0
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://www.dartlang.org
 description: >
diff --git a/pkgs/stack_trace/test/frame_test.dart b/pkgs/stack_trace/test/frame_test.dart
index 1205d74..b132368 100644
--- a/pkgs/stack_trace/test/frame_test.dart
+++ b/pkgs/stack_trace/test/frame_test.dart
@@ -210,7 +210,7 @@
     });
   });
 
-  group('.parseFirefox', () {
+  group('.parseFirefox/.parseSafari', () {
     test('parses a simple stack frame correctly', () {
       var frame = new Frame.parseFirefox(
           ".VW.call\$0@http://pub.dartlang.org/stuff.dart.js:560");
@@ -356,11 +356,9 @@
       expect(() => new Frame.parseFirefox('@dart:async/future.dart'),
           throwsFormatException);
     });
-  });
 
-  group('.parseSafari6_1', () {
     test('parses a simple stack frame correctly', () {
-      var frame = new Frame.parseSafari6_1(
+      var frame = new Frame.parseFirefox(
           "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));
@@ -369,7 +367,7 @@
     });
 
     test('parses an anonymous stack frame correctly', () {
-      var frame = new Frame.parseSafari6_1(
+      var frame = new Frame.parseFirefox(
           "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));
@@ -378,7 +376,7 @@
     });
 
     test('parses a stack frame with no line correctly', () {
-      var frame = new Frame.parseSafari6_1(
+      var frame = new Frame.parseFirefox(
           "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);
@@ -387,7 +385,7 @@
     });
 
     test('parses a stack frame with no column correctly', () {
-      var frame = new Frame.parseSafari6_1(
+      var frame = new Frame.parseFirefox(
           "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));
@@ -396,7 +394,7 @@
     });
 
     test('parses a stack frame with no line or column correctly', () {
-      var frame = new Frame.parseSafari6_1(
+      var frame = new Frame.parseFirefox(
           "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));
diff --git a/pkgs/stack_trace/test/trace_test.dart b/pkgs/stack_trace/test/trace_test.dart
index bc2db6b..17f9c22 100644
--- a/pkgs/stack_trace/test/trace_test.dart
+++ b/pkgs/stack_trace/test/trace_test.dart
@@ -92,7 +92,7 @@
           equals(Uri.parse("http://pub.dartlang.org/thing.js")));
     });
 
-    test('parses a Firefox stack trace correctly', () {
+    test('parses a Firefox/Safari 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'
@@ -130,7 +130,8 @@
           equals(Uri.parse("http://pub.dartlang.org/stuff.js")));
     });
 
-    test('parses a Safari 6.0 stack trace correctly', () {
+    test('parses a Firefox/Safari stack trace containing native code 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,11 +147,29 @@
       expect(trace.frames.length, equals(3));
     });
 
-    test('parses a Safari 6.1 stack trace correctly', () {
+    test('parses a Firefox/Safari stack trace without a method name 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');
+          'http://pub.dartlang.org/stuff.js:42\n'
+          'zip/<@http://pub.dartlang.org/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@http://pub.dartlang.org/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse("http://pub.dartlang.org/stuff.js")));
+      expect(trace.frames[0].member, equals('<fn>'));
+      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 Firefox/Safari stack trace with an empty line correctly',
+        () {
+      var trace = new Trace.parse(
+          'Foo._bar@http://pub.dartlang.org/stuff.js:42\n'
+          '\n'
+          'zip/<@http://pub.dartlang.org/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@http://pub.dartlang.org/thing.js:1');
 
       expect(trace.frames[0].uri,
           equals(Uri.parse("http://pub.dartlang.org/stuff.js")));
@@ -160,15 +179,17 @@
           equals(Uri.parse("http://pub.dartlang.org/thing.js")));
     });
 
-    test('parses a Safari 6.1 stack trace with an empty line correctly', () {
+    test('parses a Firefox/Safari stack trace with a column number correctly',
+        () {
       var trace = new Trace.parse(
-          'http://pub.dartlang.org/stuff.js:42:43\n'
-          '\n'
-          'zip@http://pub.dartlang.org/stuff.js:0:1\n'
-          'zip\$zap@http://pub.dartlang.org/thing.js:1:2');
+          'Foo._bar@http://pub.dartlang.org/stuff.js:42:2\n'
+          'zip/<@http://pub.dartlang.org/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@http://pub.dartlang.org/thing.js:1');
 
       expect(trace.frames[0].uri,
           equals(Uri.parse("http://pub.dartlang.org/stuff.js")));
+      expect(trace.frames[0].line, equals(42));
+      expect(trace.frames[0].column, equals(2));
       expect(trace.frames[1].uri,
           equals(Uri.parse("http://pub.dartlang.org/stuff.js")));
       expect(trace.frames[2].uri,