Handle function arguments inside function calls.

Fixes #366.

Also, I'm calling it. This is the 0.2.0 release.

R=kevmoo@google.com, pquitslund@google.com

Review URL: https://chromiumcodereview.appspot.com//1258203006 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 602970f..731fa32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 0.2.0
+
+* Treat functions nested inside function calls like block arguments (#366).
+
 # 0.2.0-rc.4
 
 * Smarter indentation for function arguments (#369).
diff --git a/lib/src/argument_list_visitor.dart b/lib/src/argument_list_visitor.dart
index 4acb3b4..c45b737 100644
--- a/lib/src/argument_list_visitor.dart
+++ b/lib/src/argument_list_visitor.dart
@@ -97,12 +97,8 @@
         new ArgumentSublist(node.arguments, argumentsAfter));
   }
 
-  ArgumentListVisitor._(
-      this._visitor,
-      this._node,
-      this._arguments,
-      this._functions,
-      this._argumentsAfterFunctions);
+  ArgumentListVisitor._(this._visitor, this._node, this._arguments,
+      this._functions, this._argumentsAfterFunctions);
 
   /// Builds chunks for the call chain.
   void visit() {
@@ -162,11 +158,36 @@
       expression = (expression as NamedExpression).expression;
     }
 
+    // Allow functions wrapped in dotted method calls like "a.b.c(() { ... })".
+    if (expression is MethodInvocation) {
+      if (!_isValidWrappingTarget(expression.target)) return false;
+      if (expression.argumentList.arguments.length != 1) return false;
+
+      return _isBlockFunction(expression.argumentList.arguments.single);
+    }
+
     // Curly body functions are.
     if (expression is! FunctionExpression) return false;
     var function = expression as FunctionExpression;
     return function.body is BlockFunctionBody;
   }
+
+  /// Returns `true` if [expression] is a valid method invocation target for
+  /// an invocation that wraps a function literal argument.
+  static bool _isValidWrappingTarget(Expression expression) {
+    // Allow bare function calls.
+    if (expression == null) return true;
+
+    // Allow property accesses.
+    while (expression is PropertyAccess) {
+      expression = (expression as PropertyAccess).target;
+    }
+
+    if (expression is PrefixedIdentifier) return true;
+    if (expression is SimpleIdentifier) return true;
+
+    return false;
+  }
 }
 
 /// A range of arguments from a complete argument list.
@@ -333,8 +354,8 @@
     }
 
     // Split before the first named argument.
-    namedRule.beforeArguments(visitor.builder.split(
-        space: !_isFirstArgument(_named.first)));
+    namedRule.beforeArguments(
+        visitor.builder.split(space: !_isFirstArgument(_named.first)));
 
     for (var argument in _named) {
       _visitArgument(visitor, namedRule, argument);
@@ -346,7 +367,8 @@
     visitor.builder.endRule();
   }
 
-  void _visitArgument(SourceVisitor visitor, ArgumentRule rule, Expression argument) {
+  void _visitArgument(
+      SourceVisitor visitor, ArgumentRule rule, Expression argument) {
     // If we're about to write a collection argument, handle it specially.
     if (_collections.contains(argument)) {
       if (rule != null) rule.beforeCollection();
@@ -394,4 +416,4 @@
 
     return expression is ListLiteral || expression is MapLiteral;
   }
-}
\ No newline at end of file
+}
diff --git a/lib/src/line_splitting/solve_state.dart b/lib/src/line_splitting/solve_state.dart
index 04a9b11..b419756 100644
--- a/lib/src/line_splitting/solve_state.dart
+++ b/lib/src/line_splitting/solve_state.dart
@@ -218,7 +218,7 @@
     // Lines that contain both bound and unbound rules must have the same
     // bound values.
     if (_boundRulesInUnboundLines.length !=
-    other._boundRulesInUnboundLines.length) {
+        other._boundRulesInUnboundLines.length) {
       return false;
     }
 
@@ -415,7 +415,6 @@
     var hasUnbound = false;
 
     for (var i = 0; i < _splitter.chunks.length - 1; i++) {
-
       if (splits.shouldSplitAt(i)) {
         if (hasUnbound) _boundRulesInUnboundLines.addAll(boundInLine);
 
@@ -439,25 +438,23 @@
   String toString() {
     var buffer = new StringBuffer();
 
-    buffer.writeAll(
-        _splitter.rules.map((rule) {
-          var valueLength = "${rule.fullySplitValue}".length;
+    buffer.writeAll(_splitter.rules.map((rule) {
+      var valueLength = "${rule.fullySplitValue}".length;
 
-          var value = "?";
-          if (_ruleValues.contains(rule)) {
-            value = "${_ruleValues.getValue(rule)}";
-          }
+      var value = "?";
+      if (_ruleValues.contains(rule)) {
+        value = "${_ruleValues.getValue(rule)}";
+      }
 
-          value = value.padLeft(valueLength);
-          if (_liveRules.contains(rule)) {
-            value = debug.bold(value);
-          } else {
-            value = debug.gray(value);
-          }
+      value = value.padLeft(valueLength);
+      if (_liveRules.contains(rule)) {
+        value = debug.bold(value);
+      } else {
+        value = debug.gray(value);
+      }
 
-          return value;
-        }),
-        " ");
+      return value;
+    }), " ");
 
     buffer.write("   \$${splits.cost}");
 
diff --git a/lib/src/rule/argument.dart b/lib/src/rule/argument.dart
index 8e08fba..813bfb1 100644
--- a/lib/src/rule/argument.dart
+++ b/lib/src/rule/argument.dart
@@ -113,9 +113,8 @@
   /// split before the argument if the argument itself contains a split.
   SinglePositionalRule(Rule collectionRule, {bool splitsOnInnerRules})
       : super(collectionRule),
-        splitsOnInnerRules = splitsOnInnerRules
-            != null
-            ? splitsOnInnerRules : false;
+        splitsOnInnerRules =
+            splitsOnInnerRules != null ? splitsOnInnerRules : false;
 
   bool isSplit(int value, Chunk chunk) => value == 1;
 
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index a59ff5e..524db2e 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -647,11 +647,9 @@
     visit(node.name);
     space();
 
-    _writeBody(node.leftBracket, node.rightBracket,
-        space: true,
-        body: () {
-          visitCommaSeparatedNodes(node.constants, between: split);
-        });
+    _writeBody(node.leftBracket, node.rightBracket, space: true, body: () {
+      visitCommaSeparatedNodes(node.constants, between: split);
+    });
   }
 
   visitExportDirective(ExportDirective node) {
diff --git a/pubspec.yaml b/pubspec.yaml
index 086999d..7d5bf57 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dart_style
-version: 0.2.0-rc.4
+version: 0.2.0
 author: Dart Team <misc@dartlang.org>
 description: Opinionated, automatic Dart source code formatter.
 homepage: https://github.com/dart-lang/dart_style
diff --git a/test/formatter_test.dart b/test/formatter_test.dart
index 859cf6b..d227133 100644
--- a/test/formatter_test.dart
+++ b/test/formatter_test.dart
@@ -39,12 +39,14 @@
 
   test("FormatterException describes parse errors", () {
     try {
-      new DartFormatter().format("""
+      new DartFormatter().format(
+          """
 
       var a = some error;
 
       var b = another one;
-      """, uri: "my_file.dart");
+      """,
+          uri: "my_file.dart");
 
       fail("Should throw.");
     } on FormatterException catch (err) {
@@ -90,7 +92,8 @@
   test('preserves initial indent', () {
     var formatter = new DartFormatter(indent: 3);
     expect(
-        formatter.formatStatement('if (foo) {bar;}'), equals('   if (foo) {\n'
+        formatter.formatStatement('if (foo) {bar;}'),
+        equals('   if (foo) {\n'
             '     bar;\n'
             '   }'));
   });
@@ -119,7 +122,8 @@
       expect(
           new DartFormatter(lineEnding: "\r\n").formatStatement('  """first\r\n'
               'second\r\n'
-              'third"""  ;'), equals('"""first\r\n'
+              'third"""  ;'),
+          equals('"""first\r\n'
               'second\r\n'
               'third""";'));
     });
diff --git a/test/regression/0100/0108.unit b/test/regression/0100/0108.unit
index 2e0bfeb..64522f2 100644
--- a/test/regression/0100/0108.unit
+++ b/test/regression/0100/0108.unit
@@ -207,19 +207,11 @@
 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}) =>
+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"))),
+        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
+        key: "Cast failed: package:angular/core_dom/component_css_loader.dart:17:7"));
\ No newline at end of file
diff --git a/test/regression/0300/0366.stmt b/test/regression/0300/0366.stmt
new file mode 100644
index 0000000..e14856e
--- /dev/null
+++ b/test/regression/0300/0366.stmt
@@ -0,0 +1,10 @@
+>>> (indent 4)
+    zoop(
+        "zoop description here",
+        spang(() {
+            ;
+        }));
+<<<
+    zoop("zoop description here", spang(() {
+      ;
+    }));
\ No newline at end of file
diff --git a/test/splitting/function_arguments.stmt b/test/splitting/function_arguments.stmt
index 3da9e71..89c0956 100644
--- a/test/splitting/function_arguments.stmt
+++ b/test/splitting/function_arguments.stmt
@@ -137,34 +137,30 @@
 }, b: () {
   ;
 });
->>> don't nest because of nested 1-arg fn
+>>> do not nest because of nested 1-arg fn
 outer(inner(() {body;}));
 <<<
 outer(inner(() {
   body;
 }));
->>> do nest because of nested many-arg fn
+>>> do not nest because of nested many-arg fn
 outer(argument, inner(() {body;}));
 <<<
-outer(
-    argument,
-    inner(() {
-      body;
-    }));
->>> don't nest because of nested 1-arg method call
-obj.outer(obj.inner(() {body;}));
-<<<
-obj.outer(obj.inner(() {
+outer(argument, inner(() {
   body;
 }));
->>> do nest because of nested many-arg method call
-obj.outer(argument, obj.inner(() {body;}));
+>>> do not nest because of nested 1-arg method call
+obj.outer(a.b.c.fn(() {body;}));
 <<<
-obj.outer(
-    argument,
-    obj.inner(() {
-      body;
-    }));
+obj.outer(a.b.c.fn(() {
+  body;
+}));
+>>> do not nest because of nested many-arg method call
+obj.outer(argument, a.b.c.fn(() {body;}));
+<<<
+obj.outer(argument, a.b.c.fn(() {
+  body;
+}));
 >>> do not force named args to split on positional function
 function(argument, () {;},
     named: argument, another: argument);
diff --git a/test/splitting/mixed.stmt b/test/splitting/mixed.stmt
index 2c57dae..0ef02fd 100644
--- a/test/splitting/mixed.stmt
+++ b/test/splitting/mixed.stmt
@@ -71,11 +71,9 @@
 >>> unnested function inside nested expression
 function(argument, function(() {;}));
 <<<
-function(
-    argument,
-    function(() {
-      ;
-    }));
+function(argument, function(() {
+  ;
+}));
 >>> nested function inside nested expression
 function(argument, function(() {;}, argument, () {;}));
 <<<