Automatically harden splits containing too much nesting. Fix #108.

R=pquitslund@google.com

Review URL: https://chromiumcodereview.appspot.com//996033002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78a6042..88e5e30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 * Optimize formatting deeply nested expressions (#108).
 * Discard unused nesting level to improve performance (#108).
 * Discard unused spans to improve performance (#108).
+* Harden splits that containg too much nesting (#108).
 * Try to avoid splitting single-element lists (#211).
 * Avoid splitting when the first argument is a function expression (#211). 
 
diff --git a/lib/src/line_splitter.dart b/lib/src/line_splitter.dart
index e0a8de1..3291e28 100644
--- a/lib/src/line_splitter.dart
+++ b/lib/src/line_splitter.dart
@@ -103,9 +103,29 @@
   List<int> apply(StringBuffer buffer) {
     if (debugFormatter) dumpLine(_chunks, _indent);
 
-    _flattenNestingLevels();
+    var nestingDepth = _flattenNestingLevels();
+
+    // Hack. The formatter doesn't handle formatting very deeply nested code
+    // well. It can make performance spiral into a pit of sadness. Fortunately,
+    // we only tend to see expressions pathologically deeply nested in
+    // generated code that isn't read by humans much anyway. To avoid burning
+    // too much time on these, harden any splits containing more than a certain
+    // level of nesting.
+    //
+    // The number here was chosen empirically based on formatting the repo. It
+    // was picked to get the best performance while affecting the minimum amount
+    // of results.
+    // TODO(rnystrom): Do something smarter.
+    if (nestingDepth > 9) {
+      for (var chunk in _chunks) {
+        if (chunk.param != null && nestingDepth - chunk.nesting > 9) {
+          chunk.harden();
+        }
+      }
+    }
 
     var splits = _findBestSplits(new LinePrefix());
+
     var selection = [null, null];
 
     // Write each chunk and the split after it.
@@ -161,7 +181,11 @@
   /// Worse, if the splitter *does* consider these levels, it can dramatically
   /// increase solving time. To avoid that, this renumbers all of the nesting
   /// levels in the chunks to not have any of these unused gaps.
-  void _flattenNestingLevels() {
+  ///
+  /// Returns the number of distinct nesting levels remaining after flattening.
+  /// This may be zero if the chunks have no nesting (i.e. just statement-level
+  /// indentation).
+  int _flattenNestingLevels() {
     var nestingLevels = _chunks
         .map((chunk) => chunk.nesting)
         .where((nesting) => nesting != -1)
@@ -177,6 +201,8 @@
     for (var chunk in _chunks) {
       chunk.nesting = nestingMap[chunk.nesting];
     }
+
+    return nestingLevels.length;
   }
 
   /// Finds the best set of splits to apply to the remainder of the line
diff --git a/test/regression/108.unit b/test/regression/108.unit
new file mode 100644
index 0000000..9349cb6
--- /dev/null
+++ b/test/regression/108.unit
@@ -0,0 +1,257 @@
+>>>
+class ResolutionCopier {
+  @override
+  bool visitClassDeclaration(ClassDeclaration node) {
+    ClassDeclaration toNode = this._toNode as ClassDeclaration;
+    return javaBooleanAnd(
+        javaBooleanAnd(
+            javaBooleanAnd(
+                javaBooleanAnd(javaBooleanAnd(javaBooleanAnd(
+                        javaBooleanAnd(javaBooleanAnd(
+                            javaBooleanAnd(javaBooleanAnd(javaBooleanAnd(
+                                    _isEqualNodes(node.documentationComment,
+                                        toNode.documentationComment),
+                                    _isEqualNodeLists(
+                                        node.metadata, toNode.metadata)),
+                                _isEqualTokens(node.abstractKeyword,
+                                    toNode.abstractKeyword)), _isEqualTokens(
+                                node.classKeyword, toNode.classKeyword)),
+                            _isEqualNodes(
+                                node.name, toNode.name)), _isEqualNodes(
+                            node.typeParameters, toNode.typeParameters)),
+                        _isEqualNodes(
+                            node.extendsClause, toNode.extendsClause)),
+                    _isEqualNodes(
+                        node.withClause, toNode.withClause)), _isEqualNodes(
+                    node.implementsClause, toNode.implementsClause)),
+                _isEqualTokens(node.leftBracket, toNode.leftBracket)),
+            _isEqualNodeLists(
+                node.members,
+                toNode.members)),
+        _isEqualTokens(
+            node.rightBracket,
+            toNode.rightBracket));
+  }
+}
+<<<
+class ResolutionCopier {
+  @override
+  bool visitClassDeclaration(ClassDeclaration node) {
+    ClassDeclaration toNode = this._toNode as ClassDeclaration;
+    return javaBooleanAnd(
+        javaBooleanAnd(
+            javaBooleanAnd(
+                javaBooleanAnd(javaBooleanAnd(javaBooleanAnd(
+                        javaBooleanAnd(javaBooleanAnd(
+                            javaBooleanAnd(javaBooleanAnd(javaBooleanAnd(
+                                    _isEqualNodes(node.documentationComment,
+                                        toNode.documentationComment),
+                                    _isEqualNodeLists(
+                                        node.metadata, toNode.metadata)),
+                                _isEqualTokens(node.abstractKeyword,
+                                    toNode.abstractKeyword)), _isEqualTokens(
+                                node.classKeyword, toNode.classKeyword)),
+                            _isEqualNodes(
+                                node.name, toNode.name)), _isEqualNodes(
+                            node.typeParameters, toNode.typeParameters)),
+                        _isEqualNodes(
+                            node.extendsClause, toNode.extendsClause)),
+                    _isEqualNodes(
+                        node.withClause, toNode.withClause)), _isEqualNodes(
+                    node.implementsClause, toNode.implementsClause)),
+                _isEqualTokens(node.leftBracket, toNode.leftBracket)),
+            _isEqualNodeLists(
+                node.members,
+                toNode.members)),
+        _isEqualTokens(
+            node.rightBracket,
+            toNode.rightBracket));
+  }
+}
+>>> pathologically deep
+class ResolutionCopier {
+  @override
+  bool visitClassDeclaration(ClassDeclaration node) {
+    ClassDeclaration toNode = this._toNode as ClassDeclaration;
+    return javaBooleanAnd(
+        javaBooleanAnd(
+            javaBooleanAnd(
+                javaBooleanAnd(javaBooleanAnd(javaBooleanAnd(
+                        javaBooleanAnd(javaBooleanAnd(
+                            javaBooleanAnd(javaBooleanAnd(javaBooleanAnd(
+                                    _isEqualNodes(node.documentationComment,
+                                        toNode.documentationComment),
+                                    _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, _isEqualNodeLists(
+                                        node.metadata, toNode.metadata))))))))))))))))))))))),
+                                _isEqualTokens(node.abstractKeyword,
+                                    toNode.abstractKeyword)), _isEqualTokens(
+                                node.classKeyword, toNode.classKeyword)),
+                            _isEqualNodes(
+                                node.name, toNode.name)), _isEqualNodes(
+                            node.typeParameters, toNode.typeParameters)),
+                        _isEqualNodes(
+                            node.extendsClause, toNode.extendsClause)),
+                    _isEqualNodes(
+                        node.withClause, toNode.withClause)), _isEqualNodes(
+                    node.implementsClause, toNode.implementsClause)),
+                _isEqualTokens(node.leftBracket, toNode.leftBracket)),
+            _isEqualNodeLists(
+                node.members,
+                toNode.members)),
+        _isEqualTokens(
+            node.rightBracket,
+            toNode.rightBracket));
+  }
+}
+<<<
+class ResolutionCopier {
+  @override
+  bool visitClassDeclaration(ClassDeclaration node) {
+    ClassDeclaration toNode = this._toNode as ClassDeclaration;
+    return javaBooleanAnd(
+        javaBooleanAnd(
+            javaBooleanAnd(
+                javaBooleanAnd(
+                    javaBooleanAnd(
+                        javaBooleanAnd(
+                            javaBooleanAnd(
+                                javaBooleanAnd(
+                                    javaBooleanAnd(
+                                        javaBooleanAnd(
+                                            javaBooleanAnd(
+                                                _isEqualNodes(
+                                                    node.documentationComment,
+                                                    toNode.documentationComment),
+                                                _isEqualNodeLists(
+                                                    node.metadata,
+                                                    _isEqualNodeLists(
+                                                        node.metadata,
+                                                        _isEqualNodeLists(
+                                                            node.metadata,
+                                                            _isEqualNodeLists(
+                                                                node.metadata,
+                                                                _isEqualNodeLists(
+                                                                    node.metadata,
+                                                                    _isEqualNodeLists(
+                                                                        node.metadata,
+                                                                        _isEqualNodeLists(
+                                                                            node.metadata,
+                                                                            _isEqualNodeLists(
+                                                                                node.metadata,
+                                                                                _isEqualNodeLists(
+                                                                                    node.metadata,
+                                                                                    _isEqualNodeLists(
+                                                                                        node.metadata,
+                                                                                        _isEqualNodeLists(
+                                                                                            node.metadata,
+                                                                                            _isEqualNodeLists(
+                                                                                                node.metadata,
+                                                                                                _isEqualNodeLists(
+                                                                                                    node.metadata,
+                                                                                                    _isEqualNodeLists(node.metadata, _isEqualNodeLists(node.metadata, _isEqualNodeLists(node.metadata, _isEqualNodeLists(node.metadata, _isEqualNodeLists(node.metadata, _isEqualNodeLists(node.metadata, _isEqualNodeLists(node.metadata, _isEqualNodeLists(node.metadata, _isEqualNodeLists(node.metadata, toNode.metadata))))))))))))))))))))))),
+                                            _isEqualTokens(
+                                                node.abstractKeyword,
+                                                toNode.abstractKeyword)),
+                                        _isEqualTokens(
+                                            node.classKeyword,
+                                            toNode.classKeyword)),
+                                    _isEqualNodes(
+                                        node.name,
+                                        toNode.name)),
+                                _isEqualNodes(
+                                    node.typeParameters,
+                                    toNode.typeParameters)),
+                            _isEqualNodes(
+                                node.extendsClause,
+                                toNode.extendsClause)),
+                        _isEqualNodes(
+                            node.withClause,
+                            toNode.withClause)),
+                    _isEqualNodes(
+                        node.implementsClause,
+                        toNode.implementsClause)),
+                _isEqualTokens(
+                    node.leftBracket,
+                    toNode.leftBracket)),
+            _isEqualNodeLists(
+                node.members,
+                toNode.members)),
+        _isEqualTokens(
+            node.rightBracket,
+            toNode.rightBracket));
+  }
+}
+>>>
+class ElementBinder {
+  DirectiveInjector bind(
+      View view, Scope scope, DirectiveInjector parentInjector, dom.Node node) {
+    if (bindAssignableProps.isNotEmpty) {
+      _bindAssignablePropsOn.forEach((String eventName) => node
+          .addEventListener(eventName, (_) => zone.run(() => bindAssignableProps
+              .forEach((propAndExp) => propAndExp[1].assign(
+                  scope.context, jsNode[propAndExp[0]])))));
+    }
+  }
+}
+<<<
+class ElementBinder {
+  DirectiveInjector bind(
+      View view, Scope scope, DirectiveInjector parentInjector, dom.Node node) {
+    if (bindAssignableProps.isNotEmpty) {
+      _bindAssignablePropsOn
+          .forEach(
+              (String eventName) =>
+                  node
+                      .addEventListener(
+                          eventName,
+                          (_) =>
+                              zone.run(() => bindAssignableProps.forEach(
+                                  (propAndExp) => propAndExp[1].assign(
+                                      scope.context, jsNode[propAndExp[0]])))));
+    }
+  }
+}
+>>>
+async.Future<List<dom.StyleElement>> call(String tag, List<String> cssUrls, {Type type}) =>
+(DDC$RT.cast(async.Future.wait((DDC$RT.cast(cssUrls.map((url) => _styleElement(tag,
+(DDC$RT.cast(url, String, key: "Cast failed: package:angular/core_dom/component_css_loader.dart:17:65")), type)),
+DDC$RT.type((Iterable<Future<dynamic>> _) {}), key: "Cast failed: package:angular/core_dom/component_css_loader.dart:17:25"))),
+DDC$RT.type((Future<List<StyleElement>> _) {}), key: "Cast failed: package:angular/core_dom/component_css_loader.dart:17:7"));
+<<<
+async.Future<List<dom.StyleElement>> call(
+        String tag,
+        List<String> cssUrls,
+        {Type type}) =>
+    (DDC$RT
+        .cast(
+            async.Future
+                .wait((DDC$RT.cast(cssUrls.map((url) => _styleElement(tag,
+                    (DDC$RT.cast(url, String,
+                        key: "Cast failed: package:angular/core_dom/component_css_loader.dart:17:65")),
+                    type)), DDC$RT.type((Iterable<Future<dynamic>> _) {
+}), key: "Cast failed: package:angular/core_dom/component_css_loader.dart:17:25"))),
+    DDC$RT
+        .type((Future<List<StyleElement>> _) {
+}),
+    key: "Cast failed: package:angular/core_dom/component_css_loader.dart:17:7"));
\ No newline at end of file