Better support for async VM frames.

Closes #1

R=kevmoo@google.com

Review URL: https://codereview.chromium.org//912043002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2850cfc..25abd8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 1.1.2
+
+* Support VM frames without line *or* column numbers, which async/await programs
+  occasionally generate.
+
+* Replace `<<anonymous closure>_async_body>` in VM frames' members with the
+  terser `<async>`.
+
 ## 1.1.1
 
 * Widen the SDK constraint to include 1.7.0-dev.4.0.
diff --git a/lib/src/frame.dart b/lib/src/frame.dart
index 4f5b16c..8ecd19f 100644
--- a/lib/src/frame.dart
+++ b/lib/src/frame.dart
@@ -10,8 +10,9 @@
 import 'trace.dart';
 
 // #1      Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)
-final _vmFrame = new RegExp(
-    r'^#\d+\s+(\S.*) \((.+?):(\d+)(?::(\d+))?\)$');
+// #1      Foo._bar (file:///home/nweiz/code/stuff.dart:42)
+// #1      Foo._bar (file:///home/nweiz/code/stuff.dart)
+final _vmFrame = new RegExp(r'^#\d+\s+(\S.*) \((.+?)((?::\d+){0,2})\)$');
 
 //     at VW.call$0 (http://pub.dartlang.org/stuff.dart.js:560:28)
 //     at VW.call$0 (eval as fn
@@ -135,14 +136,14 @@
 
     // Get the pieces out of the regexp match. Function, URI and line should
     // always be found. The column is optional.
-    var member = match[1].replaceAll("<anonymous closure>", "<fn>");
+    var member = match[1]
+        .replaceAll("<<anonymous closure>_async_body>", "<async>")
+        .replaceAll("<anonymous closure>", "<fn>");
     var uri = Uri.parse(match[2]);
-    var line = int.parse(match[3]);
-    var column = null;
-    var columnMatch = match[4];
-    if (columnMatch != null) {
-      column = int.parse(columnMatch);
-    }
+
+    var lineAndColumn = match[3].split(':');
+    var line = lineAndColumn.length > 1 ? int.parse(lineAndColumn[1]) : null;
+    var column = lineAndColumn.length > 2 ? int.parse(lineAndColumn[2]) : null;
     return new Frame(uri, line, column, member);
   }
 
diff --git a/lib/src/vm_trace.dart b/lib/src/vm_trace.dart
index 8116015..89474f5 100644
--- a/lib/src/vm_trace.dart
+++ b/lib/src/vm_trace.dart
@@ -22,7 +22,9 @@
     var i = 1;
     return frames.map((frame) {
       var number = padRight("#${i++}", 8);
-      var member = frame.member.replaceAll("<fn>", "<anonymous closure>");
+      var member = frame.member
+          .replaceAll("<fn>", "<anonymous closure>")
+          .replaceAll("<async>", "<<anonymous closure>_async_body>");
       var line = frame.line == null ? 0 : frame.line;
       var column = frame.column == null ? 0 : frame.column;
       return "$number$member (${frame.uri}:$line:$column)\n";
diff --git a/pubspec.yaml b/pubspec.yaml
index 8adfb2f..2fee061 100644
--- a/pubspec.yaml
+++ b/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.1.1
+version: 1.1.2
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://github.com/dart-lang/stack_trace
 description: >
diff --git a/test/frame_test.dart b/test/frame_test.dart
index b132368..638d1d7 100644
--- a/test/frame_test.dart
+++ b/test/frame_test.dart
@@ -30,6 +30,17 @@
       expect(frame.member, equals('Foo._bar'));
     });
 
+    // This can happen with async stack traces. See issue 22009.
+    test('parses a stack frame without line or column correctly', () {
+      var frame = new Frame.parseVM("#1      Foo._bar "
+          "(file:///home/nweiz/code/stuff.dart)");
+      expect(frame.uri,
+          equals(Uri.parse("file:///home/nweiz/code/stuff.dart")));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Foo._bar'));
+    });
+
     test('converts "<anonymous closure>" to "<fn>"', () {
       String parsedMember(String member) =>
           new Frame.parseVM('#0 $member (foo:0:0)').member;
@@ -39,6 +50,18 @@
           equals('<fn>.<fn>.bar'));
     });
 
+    test('converts "<<anonymous closure>_async_body>" to "<async>"', () {
+      var frame = new Frame.parseVM(
+          '#0 Foo.<<anonymous closure>_async_body> (foo:0:0)');
+      expect(frame.member, equals('Foo.<async>'));
+    });
+
+    test('converts "<<anonymous closure>_async_body>" to "<async>"', () {
+      var frame = new Frame.parseVM(
+          '#0 Foo.<<anonymous closure>_async_body> (foo:0:0)');
+      expect(frame.member, equals('Foo.<async>'));
+    });
+
     test('parses a folded frame correctly', () {
       var frame = new Frame.parseVM('...');
 
@@ -52,8 +75,6 @@
       expect(() => new Frame.parseVM(''), throwsFormatException);
       expect(() => new Frame.parseVM('#1'), throwsFormatException);
       expect(() => new Frame.parseVM('#1      Foo'), throwsFormatException);
-      expect(() => new Frame.parseVM('#1      Foo (dart:async/future.dart)'),
-          throwsFormatException);
       expect(() => new Frame.parseVM('#1      (dart:async/future.dart:10:15)'),
           throwsFormatException);
       expect(() => new Frame.parseVM('Foo (dart:async/future.dart:10:15)'),