Optimize formatting large cascades.

Fix #811.
diff --git a/benchmark/after.dart.txt b/benchmark/after.dart.txt
index f72bd3d..8b8ec92 100644
--- a/benchmark/after.dart.txt
+++ b/benchmark/after.dart.txt
@@ -22,6 +22,96 @@
 import 'version_queue.dart';
 import 'version_solver.dart';
 
+class HttpRequest extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'HttpRequest',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'google.logging.type'),
+      createEmptyInstance: create)
+    ..aOS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'requestMethod')
+    ..aOS(
+        2,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'requestUrl')
+    ..aInt64(
+        3,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'requestSize')
+    ..a<$core.int>(
+        4,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'status',
+        $pb.PbFieldType.O3)
+    ..aInt64(
+        5,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'responseSize')
+    ..aOS(
+        6,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'userAgent')
+    ..aOS(
+        7,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'remoteIp')
+    ..aOS(
+        8,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'referer')
+    ..aOB(
+        9,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'cacheHit')
+    ..aOB(
+        10,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'cacheValidatedWithOriginServer')
+    ..aOB(
+        11,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'cacheLookup')
+    ..aInt64(
+        12,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'cacheFillBytes')
+    ..aOS(
+        13,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'serverIp')
+    ..aOM<$0.Duration>(
+        14,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'latency',
+        subBuilder: $0.Duration.create)
+    ..aOS(
+        15,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'protocol')
+    ..hasRequiredFields = false;
+}
+
 /// The top-level solver.
 ///
 /// Keeps track of the current potential solution, and the other possible
diff --git a/benchmark/before.dart.txt b/benchmark/before.dart.txt
index a542414..25f129a 100644
--- a/benchmark/before.dart.txt
+++ b/benchmark/before.dart.txt
@@ -22,6 +22,96 @@
 import 'version_queue.dart';
 import 'version_solver.dart';
 
+class HttpRequest extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(
+      const $core.bool.fromEnvironment('protobuf.omit_message_names')
+          ? ''
+          : 'HttpRequest',
+      package: const $pb.PackageName(
+          const $core.bool.fromEnvironment('protobuf.omit_message_names')
+              ? ''
+              : 'google.logging.type'),
+      createEmptyInstance: create)
+    ..aOS(
+        1,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'requestMethod')
+    ..aOS(
+        2,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'requestUrl')
+    ..aInt64(
+        3,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'requestSize')
+    ..a<$core.int>(
+        4,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'status',
+        $pb.PbFieldType.O3)
+    ..aInt64(
+        5,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'responseSize')
+    ..aOS(
+        6,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'userAgent')
+    ..aOS(
+        7,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'remoteIp')
+    ..aOS(
+        8,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'referer')
+    ..aOB(
+        9,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'cacheHit')
+    ..aOB(
+        10,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'cacheValidatedWithOriginServer')
+    ..aOB(
+        11,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'cacheLookup')
+    ..aInt64(
+        12,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'cacheFillBytes')
+    ..aOS(
+        13,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'serverIp')
+    ..aOM<$0.Duration>(
+        14,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'latency',
+        subBuilder: $0.Duration.create)
+    ..aOS(
+        15,
+        const $core.bool.fromEnvironment('protobuf.omit_field_names')
+            ? ''
+            : 'protocol')
+    ..hasRequiredFields = false;
+}
+
 /// The top-level solver.
 ///
 /// Keeps track of the current potential solution, and the other possible
diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart
index 90d5636..83ec33d 100644
--- a/lib/src/chunk.dart
+++ b/lib/src/chunk.dart
@@ -225,9 +225,9 @@
   }
 
   /// Turns this chunk into one that can contain a block of child chunks.
-  void makeBlock(Chunk? blockArgument) {
+  void makeBlock(Chunk? blockArgument, {required bool indent}) {
     assert(_block == null);
-    _block = ChunkBlock(blockArgument);
+    _block = ChunkBlock(blockArgument, indent);
   }
 
   /// Returns `true` if the block body owned by this chunk should be expression
@@ -290,10 +290,13 @@
   /// may need extra expression-level indentation.
   final Chunk? argument;
 
+  /// Whether the first chunk should have a level of indentation before it.
+  final bool indent;
+
   /// The child chunks in this block.
   final List<Chunk> chunks = [];
 
-  ChunkBlock(this.argument);
+  ChunkBlock(this.argument, this.indent);
 }
 
 /// Constants for the cost heuristics used to determine which set of splits is
diff --git a/lib/src/chunk_builder.dart b/lib/src/chunk_builder.dart
index c2ab67b..fb2317b 100644
--- a/lib/src/chunk_builder.dart
+++ b/lib/src/chunk_builder.dart
@@ -591,14 +591,12 @@
   /// Starts a new block as a child of the current chunk.
   ///
   /// Nested blocks are handled using their own independent [LineWriter].
-  ChunkBuilder startBlock(Chunk? argumentChunk) {
+  ChunkBuilder startBlock(Chunk? argumentChunk, {bool indent = true}) {
     var chunk = _chunks.last;
-    chunk.makeBlock(argumentChunk);
+    chunk.makeBlock(argumentChunk, indent: indent);
 
     var builder = ChunkBuilder._(this, _formatter, _source, chunk.block.chunks);
-
-    // A block always starts off indented one level.
-    builder.indent();
+    if (indent) builder.indent();
 
     return builder;
   }
diff --git a/lib/src/line_writer.dart b/lib/src/line_writer.dart
index e8874a8..13f883d 100644
--- a/lib/src/line_writer.dart
+++ b/lib/src/line_writer.dart
@@ -84,7 +84,8 @@
     // TODO(rnystrom): Passing in an initial indent here is hacky. The
     // LineWriter ensures all but the first chunk have a block indent, and this
     // handles the first chunk. Do something cleaner.
-    var result = writer.writeLines(Indent.block, flushLeft: chunk.flushLeft);
+    var result = writer.writeLines(chunk.block.indent ? Indent.block : 0,
+        flushLeft: chunk.flushLeft);
     return _blockCache[key] = result;
   }
 
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index bd8da7b..b03a87d 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -564,6 +564,12 @@
 
   @override
   void visitCascadeExpression(CascadeExpression node) {
+    // Optimized path if we know the cascade will split.
+    if (node.cascadeSections.length > 1) {
+      _visitSplitCascade(node);
+      return;
+    }
+
     // Whether a split in the cascade target expression forces the cascade to
     // move to the next line. It looks weird to move the cascade down if the
     // target expression is a collection, so we don't:
@@ -605,6 +611,90 @@
     builder.unnest();
   }
 
+  /// Format the cascade using a nested block instead of a single inline
+  /// expression.
+  ///
+  /// If the cascade has multiple sections, we know each section will be on its
+  /// own line and we know there will be at least one trailing section following
+  /// a preceding one. That let's us treat all of the earlier sections as a
+  /// separate block like we do with collections and functions, instead of a
+  /// monolithic expression. Using a block in turn makes big cascades much
+  /// faster to format (like 10x) since the block formatting is memoized and
+  /// each cascade section in it is formatted independently.
+  ///
+  /// The tricky part is that block formatting assumes the entire line will be
+  /// part of the block. This is not true of the last section in a cascade,
+  /// which may have other trailing code, like the `;` here:
+  ///
+  ///     var x = someLeadingExpression
+  ///       ..firstCascade()
+  ///       ..secondCascade()
+  ///       ..thirdCascade()
+  ///       ..fourthCascade();
+  ///
+  /// To handle that, we don't put the last section in the block and instead
+  /// format it with the surrounding expression. So, from the formatter's
+  /// view, a cascade like:
+  ///
+  ///     var x = someLeadingExpression
+  ///       ..firstCascade()
+  ///       ..secondCascade()
+  ///       ..thirdCascade()
+  ///       ..fourthCascade();
+  ///
+  /// Is formatted like:
+  ///
+  ///     var x = someLeadingExpression
+  ///       [ begin block ]
+  ///       ..firstCascade()
+  ///       ..secondCascade()
+  ///       ..thirdCascade()
+  ///       [ end block ]
+  ///       ..fourthCascade();
+  ///
+  /// This somewhere between clever and hacky, but it works and allows cascades
+  /// of essentially unbounded length to be formatted quickly.
+  void _visitSplitCascade(CascadeExpression node) {
+    // Rule to split before the first `..`.
+    builder.startLazyRule(Rule.hard());
+    visit(node.target);
+
+    builder.nestExpression(indent: Indent.cascade, now: true);
+    builder.startBlockArgumentNesting();
+
+    zeroSplit();
+
+    // If there are comments before the first section, keep them outside of the
+    // block.
+    var firstCommentToken = node.cascadeSections.first.beginToken;
+    writePrecedingCommentsAndNewlines(firstCommentToken);
+    _suppressPrecedingCommentsAndNewLines.add(firstCommentToken);
+
+    // Process the inner cascade sections as a separate block. This way the
+    // entire cascade expression isn't line split as a single monolithic unit,
+    // which is very slow.
+    builder = builder.startBlock(null, indent: false);
+
+    for (var i = 0; i < node.cascadeSections.length - 1; i++) {
+      if (i > 0) newline();
+      visit(node.cascadeSections[i]);
+    }
+
+    // Put comments before the last section inside the block.
+    var lastCommentToken = node.cascadeSections.last.beginToken;
+    writePrecedingCommentsAndNewlines(lastCommentToken);
+    _suppressPrecedingCommentsAndNewLines.add(lastCommentToken);
+
+    builder = builder.endBlock(null, forceSplit: true);
+
+    // The last section is outside of the block.
+    visit(node.cascadeSections.last);
+
+    builder.endRule();
+    builder.endBlockArgumentNesting();
+    builder.unnest();
+  }
+
   /// Whether [expression] is a collection literal, or a call with a trailing
   /// comma in an argument list.
   ///
@@ -640,9 +730,13 @@
         !hasCommaAfter(arguments.arguments.last);
   }
 
-  /// Whether a cascade should be allowed to be inline as opposed to one
-  /// expression per line.
+  /// Whether a cascade should be allowed to be inline as opposed to moving the
+  /// section to the next line.
   bool _allowInlineCascade(CascadeExpression node) {
+    // Cascades with multiple sections are handled elsewhere and are never
+    // inline.
+    assert(node.cascadeSections.length == 1);
+
     // If the receiver is an expression that makes the cascade's very low
     // precedence confusing, force it to split. For example:
     //
@@ -654,9 +748,7 @@
     if (node.target is PrefixExpression) return false;
     if (node.target is AwaitExpression) return false;
 
-    // Only allow a single cascade to be inline.
-    if (node.cascadeSections.length < 2) return true;
-    return false;
+    return true;
   }
 
   @override
diff --git a/test/comments/cascades.stmt b/test/comments/cascades.stmt
new file mode 100644
index 0000000..ab01fe6
--- /dev/null
+++ b/test/comments/cascades.stmt
@@ -0,0 +1,67 @@
+>>> comments on single cascade lines
+receiver..cascade(); // comment
+<<<
+receiver..cascade(); // comment
+>>> comments on split cascade lines
+receiver
+  ..cascade() // a
+  ..cascade() // b
+  ..more(); // c
+<<<
+receiver
+  ..cascade() // a
+  ..cascade() // b
+  ..more(); // c
+>>> comment before first multi-line cascade section stays on line
+receiver // comment
+  ..cascade()
+  ..more();
+<<<
+receiver // comment
+  ..cascade()
+  ..more();
+>>> collapse blank lines before comment before first cascade
+receiver
+
+
+
+
+  // comment
+  ..cascade()
+  ..cascade();
+<<<
+receiver
+
+  // comment
+  ..cascade()
+  ..cascade();
+>>> preserve one blank line before comments on other cascades
+receiver
+
+
+
+
+  // comment
+  ..cascade()
+  // no blank
+  ..cascade()
+
+  // comment
+  ..cascade()
+
+
+  // comment
+  ..cascade();
+<<<
+receiver
+
+  // comment
+  ..cascade()
+  // no blank
+  ..cascade()
+
+  // comment
+  ..cascade()
+
+  // comment
+  ..cascade();
\ No newline at end of file
diff --git a/test/regression/0800/0881.unit b/test/regression/0800/0881.unit
new file mode 100644
index 0000000..f420d35
--- /dev/null
+++ b/test/regression/0800/0881.unit
@@ -0,0 +1,160 @@
+>>>
+void main() async {
+  group('my group', () {
+    setUp(() async {
+      final requestHandler = FakeRequestHandler()
+        ..when(withServiceName(Ng2ProtoFooBarBazService.serviceName))
+            .thenRespond(FooBarBazListResponse()
+              ..entities.add(FooBarBaz()
+                ..fooBarBazEntityId = entityId
+                ..name = 'Test entity'
+                ..baseFooBarBazId = baseFooBarBazId
+                ..entityFooBarBazId = entityFooBarBazId))
+        ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+                hasFooBarBazId(baseFooBarBazId)))
+            .thenRespond(FooBarBazListResponse()..entities.add(baseFooBarBaz))
+        ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+                hasFooBarBazId(entityFooBarBazId)))
+            .thenRespond(FooBarBazListResponse()..entities.add(entityFooBarBaz))
+        ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+                hasFooBarBazIds(Set.from([baseFooBarBazId, entityFooBarBazId]))))
+            .thenRespond(
+                FooBarBazListResponse()..entities.addAll([baseFooBarBaz, entityFooBarBaz]));
+    });
+  });
+}
+<<<
+void main() async {
+  group('my group', () {
+    setUp(() async {
+      final requestHandler = FakeRequestHandler()
+        ..when(withServiceName(Ng2ProtoFooBarBazService.serviceName))
+            .thenRespond(FooBarBazListResponse()
+              ..entities.add(FooBarBaz()
+                ..fooBarBazEntityId = entityId
+                ..name = 'Test entity'
+                ..baseFooBarBazId = baseFooBarBazId
+                ..entityFooBarBazId = entityFooBarBazId))
+        ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+                hasFooBarBazId(baseFooBarBazId)))
+            .thenRespond(FooBarBazListResponse()..entities.add(baseFooBarBaz))
+        ..when(allOf(withServiceName(Ng2ProtoFooBarBazService.serviceName),
+                hasFooBarBazId(entityFooBarBazId)))
+            .thenRespond(FooBarBazListResponse()..entities.add(entityFooBarBaz))
+        ..when(allOf(
+            withServiceName(Ng2ProtoFooBarBazService.serviceName),
+            hasFooBarBazIds(
+                Set.from([baseFooBarBazId, entityFooBarBazId])))).thenRespond(
+            FooBarBazListResponse()
+              ..entities.addAll([baseFooBarBaz, entityFooBarBaz]));
+    });
+  });
+}
+>>>
+aaaa() {
+  {
+    {
+      {
+        aaaaa aaaaa = AaaAaaaa.aaaaaaa().aaaaaaa((a) => a
+          ..aaaaAaaaa
+                  .aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa.AAA_AAAAAAAAAAA_AAAAA_AAAAAAA.aaaa] =
+              aaaa
+          ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+              .AAA_AAAAAAAAAAA_AAAAAAAA_AAAA_AAAAAAAAA.aaaa] = aaaa
+          ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+              .AAA_AAAAAAAAAAA_AAAAAAA_AAAA_AAAAAAAAA.aaaa] = aaaa
+          ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+              .AAA_AAAAAAAAAAA_AAAAAA_AAAAAAA_AAA_AAAAAAAAAAA_AAAAA.aaaa] = aaaa
+          ..aaaaaaaaAaaaaaAaaaa.aaaaaaa(AaaaaaaaAaaaaaAaaaaAaaaa.aaaaaaa())
+          ..aaaaaaaaAaaaa.aaaaaaaa =
+              (Aaaaaaaa()..aaaaaaaaAaaa = (AaaaaaaaAaaa()..aaAaaaaaa = aaaaa)));
+      }
+    }
+  }
+}
+<<<
+aaaa() {
+  {
+    {
+      {
+        aaaaa aaaaa = AaaAaaaa.aaaaaaa().aaaaaaa((a) => a
+          ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[
+              AaaaAaaaaaaaaaAaaa_Aaaa.AAA_AAAAAAAAAAA_AAAAA_AAAAAAA.aaaa] = aaaa
+          ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+              .AAA_AAAAAAAAAAA_AAAAAAAA_AAAA_AAAAAAAAA.aaaa] = aaaa
+          ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+              .AAA_AAAAAAAAAAA_AAAAAAA_AAAA_AAAAAAAAA.aaaa] = aaaa
+          ..aaaaAaaaa.aaaaaaaaaaaAaAaaa[AaaaAaaaaaaaaaAaaa_Aaaa
+              .AAA_AAAAAAAAAAA_AAAAAA_AAAAAAA_AAA_AAAAAAAAAAA_AAAAA.aaaa] = aaaa
+          ..aaaaaaaaAaaaaaAaaaa.aaaaaaa(AaaaaaaaAaaaaaAaaaaAaaaa.aaaaaaa())
+          ..aaaaaaaaAaaaa.aaaaaaaa =
+              (Aaaaaaaa()..aaaaaaaaAaaa = (AaaaaaaaAaaa()..aaAaaaaaa = aaaaa)));
+      }
+    }
+  }
+}
+>>>
+void main() {
+  Example()
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName(), ClassWithLongName());
+}
+<<<
+void main() {
+  Example()
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName())
+    ..methodWithLongName(ClassWithLongName(), ClassWithLongName(),
+        ClassWithLongName(), ClassWithLongName(), ClassWithLongName());
+}
\ No newline at end of file
diff --git a/test/regression/other/cascades.unit b/test/regression/other/cascades.unit
new file mode 100644
index 0000000..78c7eed
--- /dev/null
+++ b/test/regression/other/cascades.unit
@@ -0,0 +1,48 @@
+>>> over_react-4.1.0/tools/analyzer_plugin/playground/web/pseudo_static_lifecycle.dart
+class C {
+  get defaultProps {
+    return newProps() // This newProps() call should not lint
+      ..addProps(
+          super.defaultProps) // This super.defaultProps access should not lint
+      ..somethingThatCanBeTouched =
+          mcHammer; // This mcHammer access SHOULD lint
+  }
+}
+<<<
+class C {
+  get defaultProps {
+    return newProps() // This newProps() call should not lint
+      ..addProps(
+          super.defaultProps) // This super.defaultProps access should not lint
+      ..somethingThatCanBeTouched =
+          mcHammer; // This mcHammer access SHOULD lint
+  }
+}
+>>> sass-1.32.8/lib/src/executable/options.dart
+class C {
+  static final ArgParser _parser = () {
+    var parser = ArgParser(allowTrailingOptions: true)
+
+      // This is used for compatibility with sass-spec, even though we don't
+      // support setting the precision.
+      ..addOption('precision', hide: true)
+
+      // This is used when testing to ensure that the asynchronous evaluator path
+      // works the same as the synchronous one.
+      ..addFlag('async', hide: true);
+  };
+}
+<<<
+class C {
+  static final ArgParser _parser = () {
+    var parser = ArgParser(allowTrailingOptions: true)
+
+      // This is used for compatibility with sass-spec, even though we don't
+      // support setting the precision.
+      ..addOption('precision', hide: true)
+
+      // This is used when testing to ensure that the asynchronous evaluator path
+      // works the same as the synchronous one.
+      ..addFlag('async', hide: true);
+  };
+}
\ No newline at end of file
diff --git a/tool/change_metrics.dart b/tool/change_metrics.dart
new file mode 100644
index 0000000..ab67523
--- /dev/null
+++ b/tool/change_metrics.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+import 'dart:io';
+
+/// Calculates the amount of formatting changes in a given directory of code.
+///
+/// This should be run with a path to a directory. That directory should
+/// contain a Git repo. The committed state of the repo should be the formatted
+/// output before the change in question. Then the result of the new formatting
+/// should be unstaged changes.
+///
+/// Uses `git diff --shortstat` to calculate the number of changed lines.
+///
+/// Counts the number of lines of Dart code by reading the files.
+void main(List<String> arguments) {
+  if (arguments.length != 1) {
+    print('Usage: change_metrics.dart <dir>');
+    exit(1);
+  }
+
+  var directory = arguments[0];
+  var totalLines = 0;
+  var totalFiles = 0;
+
+  print('Counting lines...');
+  for (var entry in Directory(directory).listSync(recursive: true)) {
+    if (entry is File && entry.path.endsWith('.dart')) {
+      try {
+        var lines = entry.readAsLinesSync();
+        totalFiles++;
+        totalLines += lines.length;
+      } catch (error) {
+        print('Could not read ${entry.path}:\n$error');
+      }
+    }
+  }
+
+  print('Getting diff stats...');
+  var result = Process.runSync('git', ['diff', '--shortstat'],
+      workingDirectory: directory);
+  if (result.exitCode != 0) {
+    print('Git failure:\n${result.stdout}\n${result.stderr}');
+    exit(1);
+  }
+
+  var stdout = result.stdout as String;
+  var insertions = _parseGitStdout(stdout, r'(\d+) insertions');
+  var deletions = _parseGitStdout(stdout, r'(\d+) deletions');
+  var changes = insertions + deletions;
+
+  print('$totalLines lines in $totalFiles files');
+  print('$insertions insertions + $deletions deletions = $changes changes');
+  var linesPerChange = totalLines / changes;
+  print('1 changed line for every ${linesPerChange.toStringAsFixed(2)} '
+      'lines of code');
+
+  var changesPerLine = 1000.0 * changes / totalLines;
+  print('${changesPerLine.toStringAsFixed(4)} '
+      'changed lines for every 1,000 lines of code');
+}
+
+int _parseGitStdout(String stdout, String pattern) {
+  var match = RegExp(pattern).firstMatch(stdout);
+  if (match == null) {
+    print('Could not parse Git output:\n$stdout');
+    exit(1);
+  }
+
+  return int.parse(match[1]!);
+}