Format parameter lists with trailing commas like argument lists.

Fix #447.

R=jakemac@google.com

Review URL: https://codereview.chromium.org//2196863002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0fe3947..fcccee8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 0.2.8
+
+* Format parameter lists with trailing commas like argument lists (#447).
+
 # 0.2.7
 
 * Make it strong mode clean.
diff --git a/bin/format.dart b/bin/format.dart
index 410d0c8..a25680e 100644
--- a/bin/format.dart
+++ b/bin/format.dart
@@ -14,7 +14,7 @@
 import 'package:dart_style/src/source_code.dart';
 
 // Note: The following line of code is modified by tool/grind.dart.
-const version = "0.2.7";
+const version = "0.2.8";
 
 void main(List<String> args) {
   var parser = new ArgParser(allowTrailingOptions: true);
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index a80a960..cb19267 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -879,6 +879,14 @@
       return;
     }
 
+    // If the parameter list has a trailing comma, format it like a collection
+    // literal where each parameter goes on its own line, they are indented +2,
+    // and the ")" ends up on its own line.
+    if (node.parameters.last.endToken.next.type == TokenType.COMMA) {
+      _visitTrailingCommaParameterList(node);
+      return;
+    }
+
     var requiredParams = node.parameters
         .where((param) => param is! DefaultFormalParameter)
         .toList();
@@ -1854,11 +1862,7 @@
       builder.startLazyRule(new Rule(Cost.arrow));
     }
 
-    if (parameters != null) {
-      builder.nestExpression();
-      visit(parameters);
-      builder.unnest();
-    }
+    if (parameters != null) visit(parameters);
 
     if (beforeBody != null) beforeBody();
     visit(body);
@@ -2020,6 +2024,77 @@
     _endLiteralBody(rightBracket, ignoredRule: rule, forceSplit: force);
   }
 
+  /// Writes [parameters], which is assumed to have a trailing comma after the
+  /// last parameter.
+  ///
+  /// Parameter lists with trailing commas are formatted differently from
+  /// regular parameter lists. They are treated more like collection literals.
+  ///
+  /// We don't reuse [_visitCollectionLiteral] here because there are enough
+  /// weird differences around optional parameters that it's easiest just to
+  /// give them their own method.
+  void _visitTrailingCommaParameterList(FormalParameterList parameters) {
+    // Can't have a trailing comma if there are no parameters.
+    assert(parameters.parameters.isNotEmpty);
+
+    // Always split the parameters.
+    builder.startRule(new Rule.hard());
+
+    token(parameters.leftParenthesis);
+
+    // Find the parameter immediately preceding the optional parameters (if
+    // there are any).
+    FormalParameter lastRequired;
+    for (var i = 0; i < parameters.parameters.length; i++) {
+      if (parameters.parameters[i] is DefaultFormalParameter) {
+        if (i > 0) lastRequired = parameters.parameters[i - 1];
+        break;
+      }
+    }
+
+    // If all parameters are optional, put the "[" or "{" right after "(".
+    if (parameters.parameters.first is DefaultFormalParameter) {
+      token(parameters.leftDelimiter);
+    }
+
+    // Process the parameters as a separate set of chunks.
+    builder = builder.startBlock(null);
+
+    for (var parameter in parameters.parameters) {
+      builder.nestExpression();
+      visit(parameter);
+
+      // The comma after the parameter.
+      if (parameter.endToken.next.type == TokenType.COMMA) {
+        token(parameter.endToken.next);
+      }
+
+      // If the optional parameters start after this one, put the delimiter
+      // at the end of its line.
+      if (parameter == lastRequired) {
+        space();
+        token(parameters.leftDelimiter);
+        lastRequired = null;
+      }
+
+      builder.unnest();
+      newline();
+    }
+
+    // Put comments before the closing ")", "]", or "}" inside the block.
+    var firstDelimiter =
+        parameters.rightDelimiter ?? parameters.rightParenthesis;
+    writePrecedingCommentsAndNewlines(firstDelimiter);
+    builder = builder.endBlock(null, forceSplit: true);
+    builder.endRule();
+
+    // Now write the delimiter itself.
+    _writeText(firstDelimiter.lexeme, firstDelimiter.offset);
+    if (firstDelimiter != parameters.rightParenthesis) {
+      token(parameters.rightParenthesis);
+    }
+  }
+
   /// Gets the cost to split at an assignment (or `:` in the case of a named
   /// default value) with the given [rightHandSide].
   ///
diff --git a/pubspec.yaml b/pubspec.yaml
index 8ae5477..9464315 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: dart_style
 # Note: See tool/grind.dart for how to bump the version.
-version: 0.2.7
+version: 0.2.8
 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/comments/functions.unit b/test/comments/functions.unit
index b2d0bc9..00c235b 100644
--- a/test/comments/functions.unit
+++ b/test/comments/functions.unit
@@ -126,4 +126,21 @@
 <<<
 main() {
   // comment
+}
+>>> comment before "]" with trailing comma
+function([parameter,/* c */]) {;}
+<<<
+function([
+  parameter,
+  /* c */
+]) {
+  ;
+}
+>>> comment before ")" with trailing comma
+function([parameter,]/* c */) {;}
+<<<
+function([
+  parameter,
+] /* c */) {
+  ;
 }
\ No newline at end of file
diff --git a/test/whitespace/functions.unit b/test/whitespace/functions.unit
index 2e8f487..206ec2b 100644
--- a/test/whitespace/functions.unit
+++ b/test/whitespace/functions.unit
@@ -77,10 +77,88 @@
   var lambda = () sync* {};
 }
 >>> trailing comma in single parameter list
-function(argument   ,   ) {}
+function(parameter   ,   ) {;}
 <<<
-function(argument,) {}
+function(
+  parameter,
+) {
+  ;
+}
 >>> trailing comma in parameter list
-function(argument,argument  ,  ) {}
+function(parameter,parameter  ,  ) {;}
 <<<
-function(argument, argument,) {}
\ No newline at end of file
+function(
+  parameter,
+  parameter,
+) {
+  ;
+}
+>>> trailing comma in all optional parameter list
+function([parameter,parameter  ,  ]) {;}
+<<<
+function([
+  parameter,
+  parameter,
+]) {
+  ;
+}
+>>> trailing comma in all named parameter list
+function({parameter,parameter  ,  }) {;}
+<<<
+function({
+  parameter,
+  parameter,
+}) {
+  ;
+}
+>>> trailing comma in mixed optional parameter list
+function(parameter,[parameter,parameter  ,  ]) {;}
+<<<
+function(
+  parameter, [
+  parameter,
+  parameter,
+]) {
+  ;
+}
+>>> trailing comma in mixed named parameter list
+function(parameter,{parameter,parameter  ,  }) {;}
+<<<
+function(
+  parameter, {
+  parameter,
+  parameter,
+}) {
+  ;
+}
+>>> trailing comma with => function containing split
+function(parameter,parameter,) => veryLongBodyThatWraps(argument, argument, argument, argument);
+<<<
+function(
+  parameter,
+  parameter,
+) =>
+    veryLongBodyThatWraps(argument,
+        argument, argument, argument);
+>>> trailing comma with wrap at =>
+function(parameter,parameter,) /* comment */ => "a very very long string";
+<<<
+function(
+  parameter,
+  parameter,
+) /* comment */ =>
+    "a very very long string";
+>>> trailing comma function nested in expression
+main() {
+  someVeryLongFunction(argument, argument, (parameter, parameter,) {;});
+}
+<<< (this looks weird, but it should rare and at least we test it)
+main() {
+  someVeryLongFunction(
+      argument, argument, (
+    parameter,
+    parameter,
+  ) {
+    ;
+  });
+}
\ No newline at end of file