Add a "terse" argument to foldFrames().

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//917423002
diff --git a/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md
index edd8ba6..47bd9b1 100644
--- a/pkgs/stack_trace/CHANGELOG.md
+++ b/pkgs/stack_trace/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 1.2.0
+
+* Add a `terse` argument to `Trace.foldFrames()` and `Chain.foldFrames()`. This
+  allows them to inherit the behavior of `Trace.terse` and `Chain.terse` without
+  having to duplicate the logic.
+
 ## 1.1.3
 
 * Produce nicer-looking stack chains when using the VM's async/await
diff --git a/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart
index f027025..99726f3 100644
--- a/pkgs/stack_trace/lib/src/chain.dart
+++ b/pkgs/stack_trace/lib/src/chain.dart
@@ -136,21 +136,7 @@
   ///
   /// This calls [Trace.terse] on every trace in [traces], and discards any
   /// trace that contain only internal frames.
-  Chain get terse {
-    var terseTraces = traces.map((trace) => trace.terse);
-    var nonEmptyTraces = terseTraces.where((trace) {
-      // Ignore traces that contain only internal processing.
-      return trace.frames.length > 1;
-    });
-
-    // If all the traces contain only internal processing, preserve the last
-    // (top-most) one so that the chain isn't empty.
-    if (nonEmptyTraces.isEmpty && terseTraces.isNotEmpty) {
-      return new Chain([terseTraces.last]);
-    }
-
-    return new Chain(nonEmptyTraces);
-  }
+  Chain get terse => foldFrames((_) => false, terse: true);
 
   /// Returns a new [Chain] based on [this] where multiple stack frames matching
   /// [predicate] are folded together.
@@ -161,8 +147,13 @@
   ///
   /// This is useful for limiting the amount of library code that appears in a
   /// stack trace by only showing user code and code that's called by user code.
-  Chain foldFrames(bool predicate(Frame frame)) {
-    var foldedTraces = traces.map((trace) => trace.foldFrames(predicate));
+  ///
+  /// If [terse] is true, this will also fold together frames from the core
+  /// library or from this package, and simplify core library frames as in
+  /// [Trace.terse].
+  Chain foldFrames(bool predicate(Frame frame), {bool terse: false}) {
+    var foldedTraces = traces.map(
+        (trace) => trace.foldFrames(predicate, terse: terse));
     var nonEmptyTraces = foldedTraces.where((trace) {
       // Ignore traces that contain only folded frames. These traces will be
       // folded into a single frame each.
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart
index 5cd09e9..53dbe33 100644
--- a/pkgs/stack_trace/lib/src/trace.dart
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -203,36 +203,43 @@
   /// core library or from this package, as in [foldFrames]. Remaining core
   /// library frames have their libraries, "-patch" suffixes, and line numbers
   /// removed.
-  Trace get terse {
-    return new Trace(foldFrames((frame) {
-      if (frame.isCore) return true;
-      if (frame.package == 'stack_trace') return true;
-
-      // Ignore async stack frames without any line or column information. These
-      // come from the VM's async/await implementation and represent internal
-      // frames. They only ever show up in stack chains and are always
-      // surrounded by other traces that are actually useful, so we can just get
-      // rid of them.
-      // TODO(nweiz): Get rid of this logic some time after issue 22009 is
-      // fixed.
-      if (!frame.member.contains('<async>')) return false;
-      return frame.line == null;
-    }).frames.map((frame) {
-      if (!frame.isCore) return frame;
-      var library = frame.library.replaceAll(_terseRegExp, '');
-      return new Frame(Uri.parse(library), null, null, frame.member);
-    }));
-  }
+  ///
+  /// For custom folding, see [foldFrames].
+  Trace get terse => foldFrames((_) => false, terse: true);
 
   /// Returns a new [Trace] based on [this] where multiple stack frames matching
-  /// [predicate] are folded together. This means that whenever there are
-  /// multiple frames in a row that match [predicate], only the last one is
-  /// kept.
+  /// [predicate] are folded together.
   ///
-  /// This is useful for limiting the amount of library code that appears in a
-  /// stack trace by only showing user code and code that's called by user code.
-  Trace foldFrames(bool predicate(Frame frame)) {
-    var newFrames = <Frame>[];
+  /// This means that whenever there are multiple frames in a row that match
+  /// [predicate], only the last one is kept. This is useful for limiting the
+  /// amount of library code that appears in a stack trace by only showing user
+  /// code and code that's called by user code.
+  ///
+  /// If [terse] is true, this will also fold together frames from the core
+  /// library or from this package, and simplify core library frames as in
+  /// [Trace.terse].
+  Trace foldFrames(bool predicate(Frame frame), {bool terse: false}) {
+    if (terse) {
+      var oldPredicate = predicate;
+      predicate = (frame) {
+        if (oldPredicate(frame)) return true;
+
+        if (frame.isCore) return true;
+        if (frame.package == 'stack_trace') return true;
+
+        // Ignore async stack frames without any line or column information.
+        // These come from the VM's async/await implementation and represent
+        // internal frames. They only ever show up in stack chains and are
+        // always surrounded by other traces that are actually useful, so we can
+        // just get rid of them.
+        // TODO(nweiz): Get rid of this logic some time after issue 22009 is
+        // fixed.
+        if (!frame.member.contains('<async>')) return false;
+        return frame.line == null;
+      };
+    }
+
+    var newFrames = [];
     for (var frame in frames.reversed) {
       if (!predicate(frame)) {
         newFrames.add(frame);
@@ -242,6 +249,14 @@
       }
     }
 
+    if (terse) {
+      newFrames = newFrames.map((frame) {
+        if (!frame.isCore) return frame;
+        var library = frame.library.replaceAll(_terseRegExp, '');
+        return new Frame(Uri.parse(library), null, null, frame.member);
+      }).toList();
+    }
+
     return new Trace(newFrames.reversed);
   }
 
diff --git a/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml
index ded96f6..01b1550 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.1.3
+version: 1.2.0
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://github.com/dart-lang/stack_trace
 description: >
diff --git a/pkgs/stack_trace/test/chain_test.dart b/pkgs/stack_trace/test/chain_test.dart
index 6af3c6b..54bc5f5 100644
--- a/pkgs/stack_trace/test/chain_test.dart
+++ b/pkgs/stack_trace/test/chain_test.dart
@@ -572,6 +572,33 @@
           'b.dart 10:11  Zop.zoop\n'));
     });
 
+    test('with terse: true, folds core frames as well', () {
+      var chain = new Chain([
+        new Trace.parse(
+            'a.dart 10:11                        Foo.bar\n'
+            'dart:async-patch/future.dart 10:11  Zip.zap\n'
+            'b.dart 10:11                        Bang.qux\n'
+            'dart:core 10:11                     Bar.baz\n'
+            'a.dart 10:11                        Zop.zoop'),
+        new Trace.parse(
+            'a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'a.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart',
+          terse: true);
+      expect(folded.toString(), equals(
+          'dart:async    Zip.zap\n'
+          'b.dart 10:11  Bang.qux\n'
+          'a.dart 10:11  Zop.zoop\n'
+          '===== asynchronous gap ===========================\n'
+          'a.dart 10:11  Zip.zap\n'
+          'b.dart 10:11  Zop.zoop\n'));
+    });
+
     test('eliminates completely-folded traces', () {
       var chain = new Chain([
         new Trace.parse(
diff --git a/pkgs/stack_trace/test/trace_test.dart b/pkgs/stack_trace/test/trace_test.dart
index c8a848b..125ba95 100644
--- a/pkgs/stack_trace/test/trace_test.dart
+++ b/pkgs/stack_trace/test/trace_test.dart
@@ -315,4 +315,24 @@
 dart:async-patch/future.dart 9:11  fooBottom
 '''));
   });
+
+  test('.foldFrames with terse: true, folds core frames as well', () {
+    var trace = new Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 coreBottom (dart:async/future.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 coreBottom (dart:async-patch/future.dart:9:11)
+''');
+
+    var folded = trace.foldFrames((frame) => frame.member.startsWith('foo'),
+        terse: true);
+    expect(folded.toString(), equals('''
+foo.dart 42:21  notFoo
+dart:async      coreBottom
+bar.dart 10:20  alsoNotFoo
+dart:async      coreBottom
+'''));
+  });
 }