Add a command-line option to set the exit code on a formatting change.

Fix #365.

R=kevmoo@google.com

Review URL: https://codereview.chromium.org//2333373003 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da338c4..0ce8399 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 * Handle metadata annotations before parameters with trailing commas (#520). 
 * Always split enum declarations if they end in a trailing comma (#529). 
+* Add `--set-exit-if-changed` to set the exit code on a change (#365).
 
 # 0.2.9
 
diff --git a/bin/format.dart b/bin/format.dart
index 72182bf..a8a00e2 100644
--- a/bin/format.dart
+++ b/bin/format.dart
@@ -33,6 +33,9 @@
       abbr: "n",
       negatable: false,
       help: "Show which files would be modified but make no changes.");
+  parser.addFlag("set-exit-if-changed",
+      negatable: false,
+      help: "Return exit code 1 if there are any formatting changes.");
   parser.addFlag("overwrite",
       abbr: "w",
       negatable: false,
@@ -119,6 +122,10 @@
     reporter = new ProfileReporter(reporter);
   }
 
+  if (argResults["set-exit-if-changed"]) {
+    reporter = new SetExitReporter(reporter);
+  }
+
   var pageWidth;
   try {
     pageWidth = int.parse(argResults["line-length"]);
diff --git a/lib/src/debug.dart b/lib/src/debug.dart
index 55734cf..b3ca65f 100644
--- a/lib/src/debug.dart
+++ b/lib/src/debug.dart
@@ -86,10 +86,10 @@
   var rules =
       chunks.map((chunk) => chunk.rule).where((rule) => rule != null).toSet();
 
-  var rows = [];
+  var rows = <List<String>>[];
 
   addChunk(List<Chunk> chunks, String prefix, int index) {
-    var row = [];
+    var row = <String>[];
     row.add("$prefix$index:");
 
     var chunk = chunks[index];
diff --git a/lib/src/formatter_options.dart b/lib/src/formatter_options.dart
index 6195d18..9899771 100644
--- a/lib/src/formatter_options.dart
+++ b/lib/src/formatter_options.dart
@@ -129,10 +129,35 @@
   }
 }
 
-/// A decorating reporter that reports how long it took for format each file.
-class ProfileReporter implements OutputReporter {
+/// Base clase for a reporter that decorates an inner reporter.
+abstract class _ReporterDecorator implements OutputReporter {
   final OutputReporter _inner;
 
+  _ReporterDecorator(this._inner);
+
+  void showDirectory(String path) {
+    _inner.showDirectory(path);
+  }
+
+  void showSkippedLink(String path) {
+    _inner.showSkippedLink(path);
+  }
+
+  void showHiddenPath(String path) {
+    _inner.showHiddenPath(path);
+  }
+
+  void beforeFile(File file, String label) {
+    _inner.beforeFile(file, label);
+  }
+
+  void afterFile(File file, String label, SourceCode output, {bool changed}) {
+    _inner.afterFile(file, label, output, changed: changed);
+  }
+}
+
+/// A decorating reporter that reports how long it took for format each file.
+class ProfileReporter extends _ReporterDecorator {
   /// The files that have been started but have not completed yet.
   ///
   /// Maps a file label to the time that it started being formatted.
@@ -145,7 +170,7 @@
   /// tracking.
   int _elided = 0;
 
-  ProfileReporter(this._inner);
+  ProfileReporter(OutputReporter inner) : super(inner);
 
   /// Show the times for the slowest files to format.
   void showProfile() {
@@ -165,25 +190,9 @@
     }
   }
 
-  /// Describe the directory whose contents are about to be processed.
-  void showDirectory(String path) {
-    _inner.showDirectory(path);
-  }
-
-  /// Describe the symlink at [path] that wasn't followed.
-  void showSkippedLink(String path) {
-    _inner.showSkippedLink(path);
-  }
-
-  /// Describe the hidden [path] that wasn't processed.
-  void showHiddenPath(String path) {
-    _inner.showHiddenPath(path);
-  }
-
   /// Called when [file] is about to be formatted.
   void beforeFile(File file, String label) {
-    _inner.beforeFile(file, label);
-
+    super.beforeFile(file, label);
     _ongoing[label] = new DateTime.now();
   }
 
@@ -199,6 +208,21 @@
       _elided++;
     }
 
-    _inner.afterFile(file, label, output, changed: changed);
+    super.afterFile(file, label, output, changed: changed);
+  }
+}
+
+/// A decorating reporter that sets the exit code to 1 if any changes are made.
+class SetExitReporter extends _ReporterDecorator {
+  SetExitReporter(OutputReporter inner) : super(inner);
+
+  /// Describe the processed file at [path] whose formatted result is [output].
+  ///
+  /// If the contents of the file are the same as the formatted output,
+  /// [changed] will be false.
+  void afterFile(File file, String label, SourceCode output, {bool changed}) {
+    if (changed) exitCode = 1;
+
+    super.afterFile(file, label, output, changed: changed);
   }
 }
diff --git a/test/command_line_test.dart b/test/command_line_test.dart
index 425143d..e105859 100644
--- a/test/command_line_test.dart
+++ b/test/command_line_test.dart
@@ -210,6 +210,29 @@
     });
   });
 
+  group("--set-exit-if-changed", () {
+    test("gives exit code 0 if there are no changes", () {
+      d.dir("code", [d.file("a.dart", formattedSource)]).create();
+
+      var process = runFormatterOnDir(["--set-exit-if-changed"]);
+      process.shouldExit(0);
+    });
+
+    test("gives exit code 1 if there are changes", () {
+      d.dir("code", [d.file("a.dart", unformattedSource)]).create();
+
+      var process = runFormatterOnDir(["--set-exit-if-changed"]);
+      process.shouldExit(1);
+    });
+
+    test("gives exit code 1 if there are changes even in dry run", () {
+      d.dir("code", [d.file("a.dart", unformattedSource)]).create();
+
+      var process = runFormatterOnDir(["--set-exit-if-changed", "--dry-run"]);
+      process.shouldExit(1);
+    });
+  });
+
   group("with no paths", () {
     test("errors on --overwrite", () {
       var process = runFormatter(["--overwrite"]);