Merge pull request #1033 from dart-lang/simpler-cascades

Simpler cascades
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/call_chain_visitor.dart b/lib/src/call_chain_visitor.dart
index 4151cb7..fa4a779 100644
--- a/lib/src/call_chain_visitor.dart
+++ b/lib/src/call_chain_visitor.dart
@@ -173,14 +173,7 @@
       this._blockCalls, this._hangingCall);
 
   /// Builds chunks for the call chain.
-  ///
-  /// If [unnest] is `false` than this will not close the expression nesting
-  /// created for the call chain and the caller must end it. Used by cascades
-  /// to force a cascade after a method chain to be more deeply nested than
-  /// the methods.
-  void visit({bool? unnest}) {
-    unnest ??= true;
-
+  void visit() {
     _visitor.builder.nestExpression();
 
     // Try to keep the entire method invocation one line.
@@ -259,8 +252,7 @@
 
     _disableRule();
     _endSpan();
-
-    if (unnest) _visitor.builder.unnest();
+    _visitor.builder.unnest();
   }
 
   /// Returns `true` if the method chain should split if a split occurs inside
diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart
index 7786d21..72681fa 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 71d955b..249250e 100644
--- a/lib/src/chunk_builder.dart
+++ b/lib/src/chunk_builder.dart
@@ -588,15 +588,15 @@
 
   /// Starts a new block as a child of the current chunk.
   ///
-  /// Nested blocks are handled using their own independent [LineWriter].
-  ChunkBuilder startBlock(Chunk? argumentChunk) {
+  /// Nested blocks are handled using their own independent [LineWriter]. If
+  /// [indent] is `false` then the first line of the block will not get a level
+  /// of leading indentation. Otherwise it does.
+  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 a74e222..9b0517d 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -564,39 +564,27 @@
 
   @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:
+    //
+    //     var list = [
+    //       stuff
+    //     ]
+    //       ..add(more);
     var splitIfOperandsSplit =
         node.cascadeSections.length > 1 || _isCollectionLike(node.target);
-
-    // If the cascade sections have consistent names they can be broken
-    // normally otherwise they always get their own line.
     if (splitIfOperandsSplit) {
       builder.startLazyRule(_allowInlineCascade(node) ? Rule() : Rule.hard());
     }
 
-    // If the target of the cascade is a method call (or chain of them), we
-    // treat the nesting specially. Normally, you would end up with:
-    //
-    //     receiver
-    //           .method()
-    //           .method()
-    //       ..cascade()
-    //       ..cascade();
-    //
-    // This is logical, since the method chain is an operand of the cascade
-    // expression, so it's more deeply nested. But it looks wrong, so we leave
-    // the method chain's nesting active until after the cascade sections to
-    // force the *cascades* to be deeper because it looks better:
-    //
-    //     receiver
-    //         .method()
-    //         .method()
-    //           ..cascade()
-    //           ..cascade();
-    if (node.target is MethodInvocation) {
-      CallChainVisitor(this, node.target).visit(unnest: false);
-    } else {
-      visit(node.target);
-    }
+    visit(node.target);
 
     builder.nestExpression(indent: Indent.cascade, now: true);
     builder.startBlockArgumentNesting();
@@ -621,8 +609,87 @@
 
     builder.endBlockArgumentNesting();
     builder.unnest();
+  }
 
-    if (node.target is MethodInvocation) 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, the above casade 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. That way code like:
+    //
+    //     receiver // comment
+    //       ..cascade();
+    //
+    // Keeps the comment on the first line.
+    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
@@ -660,9 +727,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:
     //
@@ -674,23 +745,6 @@
     if (node.target is PrefixExpression) return false;
     if (node.target is AwaitExpression) return false;
 
-    if (node.cascadeSections.length < 2) return true;
-
-    var name;
-    // We could be more forgiving about what constitutes sections with
-    // consistent names but for now we require all sections to have the same
-    // method name.
-    for (var expression in node.cascadeSections) {
-      if (expression is MethodInvocation) {
-        if (name == null) {
-          name = expression.methodName.name;
-        } else if (name != expression.methodName.name) {
-          return false;
-        }
-      } else {
-        return false;
-      }
-    }
     return true;
   }
 
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/0100/0137.stmt b/test/regression/0100/0137.stmt
index 159624f..d1c0906 100644
--- a/test/regression/0100/0137.stmt
+++ b/test/regression/0100/0137.stmt
@@ -16,11 +16,11 @@
       lib.declarations.values
           .where((d) => d is ClassMirror || d is MethodMirror)
           .toList()
-            ..sort((a, b) {
-              if (a.runtimeType == b.runtimeType) {
-                return _declarationName(a).compareTo(_declarationName(b));
-              }
-              if (a is MethodMirror && b is ClassMirror) return -1;
-              if (a is ClassMirror && b is MethodMirror) return 1;
-              return 0;
-            });
\ No newline at end of file
+        ..sort((a, b) {
+          if (a.runtimeType == b.runtimeType) {
+            return _declarationName(a).compareTo(_declarationName(b));
+          }
+          if (a is MethodMirror && b is ClassMirror) return -1;
+          if (a is ClassMirror && b is MethodMirror) return 1;
+          return 0;
+        });
\ No newline at end of file
diff --git a/test/regression/0500/0591.unit b/test/regression/0500/0591.unit
index d91626e..f5112be 100644
--- a/test/regression/0500/0591.unit
+++ b/test/regression/0500/0591.unit
@@ -26,12 +26,11 @@
 <<<
 void fn80() {
   // The number of characters in the `four` line is ..........................80
-  var list = (one
-      .two()
-      .three()
+  var list =
+      (one.two().three()
         ..four(
             '_15___20___25___30___35___40___45___50___55___60___65___70___75__'))
-    ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
+        ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
   ;
 }
 >>>
@@ -45,12 +44,11 @@
 <<<
 void fn79() {
   // The number of characters in the `four` line is .........................79
-  var list = (one
-      .two()
-      .three()
+  var list =
+      (one.two().three()
         ..four(
             '_15___20___25___30___35___40___45___50___55___60___65___70___75_'))
-    ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
+        ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
   ;
 }
 >>>
@@ -64,12 +62,11 @@
 <<<
 void fn78() {
   // The number of characters in the `four` line is ........................78
-  var list = (one
-      .two()
-      .three()
+  var list =
+      (one.two().three()
         ..four(
             '_15___20___25___30___35___40___45___50___55___60___65___70___75'))
-    ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
+        ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
   ;
 }
 >>>
@@ -83,12 +80,11 @@
 <<<
 void fn77() {
   // The number of characters in the `four` line is .......................77
-  var list = (one
-      .two()
-      .three()
+  var list =
+      (one.two().three()
         ..four(
             '_15___20___25___30___35___40___45___50___55___60___65___70___7'))
-    ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
+        ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
   ;
 }
 >>>
@@ -102,10 +98,8 @@
 <<<
 void fn76() {
   // The number of characters in the `four` line is ......................76
-  var list = (one
-      .two()
-      .three()
-        ..four('_15___20___25___30___35___40___45___50___55___60___65___70___'))
-    ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
+  var list =
+      (one.two().three()..four('_15___20___25___30___35___40___45___50___55___60___65___70___'))
+        ..six('zzzzzzzzzzzzzzzzzzzzzzzzzzzz');
   ;
 }
\ 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/test/splitting/invocations.stmt b/test/splitting/invocations.stmt
index 6650f93..8ce34f1 100644
--- a/test/splitting/invocations.stmt
+++ b/test/splitting/invocations.stmt
@@ -131,7 +131,9 @@
 >>> unsplit cascade unsplit method
 object.method().method()..c()..c();
 <<<
-object.method().method()..c()..c();
+object.method().method()
+  ..c()
+  ..c();
 >>> split cascade unsplit method
 object.method().method()..cascade()..cascade();
 <<<
@@ -146,9 +148,9 @@
     .method()
     .method()
     .method()
-      ..cascade()
-      ..cascade()
-      ..cascade();
+  ..cascade()
+  ..cascade()
+  ..cascade();
 >>> cascade setters on method chain
 object.method().method().method().method()..x=1..y=2;
 <<<
@@ -157,8 +159,8 @@
     .method()
     .method()
     .method()
-      ..x = 1
-      ..y = 2;
+  ..x = 1
+  ..y = 2;
 >>> cascade index
 object..[index]..method()..[index]=value;
 <<<
diff --git a/test/whitespace/cascades.stmt b/test/whitespace/cascades.stmt
index 03377d2..c34560d 100644
--- a/test/whitespace/cascades.stmt
+++ b/test/whitespace/cascades.stmt
@@ -14,7 +14,9 @@
   ..add("baz")
   ..add("bar");
 <<<
-list..add("baz")..add("bar");
+list
+  ..add("baz")
+  ..add("bar");
 >>> cascades indent contained blocks (and force multi-line) multiple cascades get their own line when method names are different
 foo..fooBar()..toString();
 <<<
@@ -148,7 +150,9 @@
   ?..add("baz")
   ..add("bar");
 <<<
-list?..add("baz")..add("bar");
+list
+  ?..add("baz")
+  ..add("bar");
 >>> mixed
 foo?..a()..b()..c();
 <<<
diff --git a/test/whitespace/functions.unit b/test/whitespace/functions.unit
index e17e716..a58d0ed 100644
--- a/test/whitespace/functions.unit
+++ b/test/whitespace/functions.unit
@@ -35,7 +35,9 @@
   ..add(1)
   ..add(2);
 <<<
-fish() => []..add(1)..add(2);
+fish() => []
+  ..add(1)
+  ..add(2);
 >>>
 fish() => []..add(1);
 <<<