Merge tag 'google-java-format-1.8' into master am: 0e3175afa9 am: 66bca70ecd

Original change: https://android-review.googlesource.com/c/platform/external/google-java-format/+/1342207

Change-Id: Ia13bed6e92c85968362685391324497d7348081c
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..235f69d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+.idea/
+*.ims
+*.iml
+
+.classpath
+.project
+.factorypath
+.settings/
+.apt_generated/
+
+target/
+
+bin/
+out/
+eclipse_plugin/lib/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b8a7c33
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,42 @@
+language: java
+
+notifications:
+  email:
+    recipients:
+      - google-java-format-dev+ci@google.com
+    on_success: change
+    on_failure: always
+
+jdk:
+  - openjdk11
+  - openjdk14
+  - openjdk-ea
+
+matrix:
+  allow_failures:
+    - jdk: openjdk-ea
+
+# see https://github.com/travis-ci/travis-ci/issues/8408
+before_install:
+- unset _JAVA_OPTIONS
+
+install: echo "The default Travis install script is being skipped!"
+
+# use travis-ci docker based infrastructure
+sudo: false
+
+cache:
+  directories:
+    - $HOME/.m2
+
+script:
+- mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
+- mvn test -B
+
+env:
+  global:
+  - secure: KkUX74NDDk95WR60zwN6x6pz49KAfR0zUu1thxl8Kke0+WVoIv1EBo7/e4ZXTdBKxlzQCX9Aa0OlIyUlhGJeuNIGtX16lcNyZNmKSacfrT68MpZqi+nAiYp8tnOyW/zuI+shSKHkGQOFq6c9KTtR9vG8kjr1Q9dNl/H5QjGaG1ZMiU/mGH9ompf+0nQTMDLKaEWV+SpKGjK5U1Zs2p08I9KKePbZoi9L2oAw5cH9wW8Q3pQJds6Rkwy9aecxRd4xmTza7Lb04dmByjqY8gsIjrTN0onOndBmLKTHiH5NVLKf0ilEVGiMQ1x4eCQolcRpGzxdTTKI0ahiWS59UABVoy1sXYqkIbZjpmMuGhHvbRir7YEXaG8LRUAxdWd9drJfvKQeBphQlIJKwajHSiMAdc9zisQg1UW75HSGKoPDHpzq+P7YBil2PUjk+5yUy5OytX6IebFT4KdeCO2ayu338yqb2t8q98elMoD5jwFVD0tpkLQ6xsYodClSGfMCVfP2zTkB7c4sHZV7tJS68CiNt7sCwz9CTNApFiSWMBxLKkKQ7VSBTy9bAn+phvW0u/maGsrRnehmsV3PVPtEsMlrqeMGwaPqIwx1l6otVQCnGRt3e8z3HoxY6AaBPaX0Z8lH2y+BxYhWTYzGhRxyyV666u/9yekTXmH53c7at7mau6Q=
+  - secure: VWnZcPA4esdaMJgh0Mui7K5O++AGZY3AYswufd0UUbAmnK60O6cDBOSelnr7hImDgZ09L2RWMXIVCt4b+UFXoIhqrvZKVitUXPldS6uNJeGT9p6quFf36o8Wf0ppKWnPd66AY6ECnE75Ujn1Maw899kb3zY2SvIvzA7HlXqtmowHCVGoJ4ou6LQxJpVEJ4hjvS2gQMF9W31uOzRzMI1JhdZioYmqe6eq9sGmRZZiYON7jBqX8f4XW0tTZoK+dVRNwYQcwyqcvQpxeI15VWDq5cqjBw3ps5XSEYTNIFUXREnEEi+vLdCuw/YRZp1ij7LiQKp6bcb2KROXaWii4VpUNWxIAflm4Nvn/8pa/3CUwqIbxTSAL+Qkb2iEzuYuNPGLr72mQgGEnlSpaqzUx0miwjJ41x3Q8mf72ihIME7YQGMDJL7TA7/GjXFeSxroPk65tbssAGmbjwGGJX67NHUzeQPW2QPA2cohCHyopKB9GqhKgKwKjenkCUaBGCaZReZz9XkSkHTXlxxSakMTmgJnA9To9d2lPOy0nppUvrd/0uAbPuxxCZqXElRvOvHKzpV1ZpKpqSxvjh63mCQRTi2rFiPn8uFaajai9mHaPoGmNwQwIUbAviNqifuIEPpc6cOuyV0MWJwdFLo1SKamJya/MwQz+IwXuY2TX7Fmv9HovdM=
+
+after_success:
+- util/publish-snapshot-on-commit.sh
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0f5b8cf
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,29 @@
+Want to contribute? Great! First, read this page (including the small print at
+the end).
+
+### Before you contribute
+
+Before we can use your code, you must sign the [Google Individual Contributor
+License
+Agreement](https://developers.google.com/open-source/cla/individual?csw=1)
+(CLA), which you can do online. The CLA is necessary mainly because you own the
+copyright to your changes, even after your contribution becomes part of our
+codebase, so we need your permission to use and distribute your code. We also
+need to be sure of various other things—for instance that you'll tell us if you
+know that your code infringes on other people's patents. You don't have to sign
+the CLA until after you've submitted your code for review and a member has
+approved it, but you must do it before we can put your code into our codebase.
+Before you start working on a larger contribution, you should get in touch with
+us first through the issue tracker with your idea so that we can help out and
+possibly guide you. Coordinating up front makes it much easier to avoid
+frustration later on.
+
+### Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose.
+
+### The small print
+
+Contributions made by corporations are covered by a different agreement than the
+one above, the Software Grant and Corporate Contributor License Agreement.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6c4ca5a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,272 @@
+The following Apache 2.0 license applies to all code in this package except
+google-java-format-diff.py.
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+------------------------------------------------------------------------------
+
+The following NCSA license applies only to google-java-format-diff.py.
+
+==============================================================================
+LLVM Release License
+==============================================================================
+University of Illinois/NCSA
+Open Source License
+
+Copyright (c) 2007-2015 University of Illinois at Urbana-Champaign.
+All rights reserved.
+
+Developed by:
+
+    LLVM Team
+
+    University of Illinois at Urbana-Champaign
+
+    http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal with
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimers.
+
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimers in the
+      documentation and/or other materials provided with the distribution.
+
+    * Neither the names of the LLVM Team, University of Illinois at
+      Urbana-Champaign, nor the names of its contributors may be used to
+      endorse or promote products derived from this Software without specific
+      prior written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+SOFTWARE.
+
+==============================================================================
+The LLVM software contains code written by third parties.  Such software will
+have its own individual LICENSE.TXT file in the directory in which it appears.
+This file will describe the copyrights, license, and restrictions which apply
+to that code.
+
+The disclaimer of warranty in the University of Illinois Open Source License
+applies to all code in the LLVM Distribution, and nothing in any of the
+other licenses gives permission to use the names of the LLVM Team or the
+University of Illinois to endorse or promote products derived from this
+Software.
+
+The following pieces of software have additional or alternate copyrights,
+licenses, and/or restrictions:
+
+Program             Directory
+-------             ---------
+<none yet>
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c5fdee2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,152 @@
+# google-java-format
+
+`google-java-format` is a program that reformats Java source code to comply with
+[Google Java Style][].
+
+[Google Java Style]: https://google.github.io/styleguide/javaguide.html
+
+## Using the formatter
+
+### from the command-line
+
+[Download the formatter](https://github.com/google/google-java-format/releases)
+and run it with:
+
+```
+java -jar /path/to/google-java-format-1.7-all-deps.jar <options> [files...]
+```
+
+The formatter can act on whole files, on limited lines (`--lines`), on specific
+offsets (`--offset`), passing through to standard-out (default) or altered
+in-place (`--replace`).
+
+To reformat changed lines in a specific patch, use
+[`google-java-format-diff.py`](https://github.com/google/google-java-format/blob/master/scripts/google-java-format-diff.py).
+
+***Note:*** *There is no configurability as to the formatter's algorithm for
+formatting. This is a deliberate design decision to unify our code formatting on
+a single format.*
+
+### IntelliJ, Android Studio, and other JetBrains IDEs
+
+A
+[google-java-format IntelliJ plugin](https://plugins.jetbrains.com/plugin/8527)
+is available from the plugin repository. To install it, go to your IDE's
+settings and select the `Plugins` category. Click the `Marketplace` tab, search
+for the `google-java-format` plugin, and click the `Install` button.
+
+The plugin will be disabled by default. To enable it in the current project, go
+to `File→Settings...→google-java-format Settings` (or `IntelliJ
+IDEA→Preferences...→Other Settings→google-java-format Settings` on macOS) and
+check the `Enable google-java-format` checkbox. (A notification will be
+presented when you first open a project offering to do this for you.)
+
+To enable it by default in new projects, use `File→Other Settings→Default
+Settings...`.
+
+When enabled, it will replace the normal `Reformat Code` action, which can be
+triggered from the `Code` menu or with the Ctrl-Alt-L (by default) keyboard
+shortcut.
+
+The import ordering is not handled by this plugin, unfortunately. To fix the
+import order, download the
+[IntelliJ Java Google Style file](https://raw.githubusercontent.com/google/styleguide/gh-pages/intellij-java-google-style.xml)
+and import it into File→Settings→Editor→Code Style.
+
+### Eclipse
+
+A
+[google-java-format Eclipse plugin](https://github.com/google/google-java-format/releases/download/google-java-format-1.6/google-java-format-eclipse-plugin_1.6.0.jar)
+can be downloaded from the releases page. Drop it into the Eclipse
+[drop-ins folder](http://help.eclipse.org/neon/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Freference%2Fmisc%2Fp2_dropins_format.html)
+to activate the plugin.
+
+The plugin adds a `google-java-format` formatter implementation that can be
+configured in `Window > Preferences > Java > Code Style > Formatter > Formatter
+Implementation`.
+
+### Third-party integrations
+
+*   Gradle plugins
+    *   [Spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle#applying-to-java-source-google-java-format):
+    *   [sherter/google-java-format-gradle-plugin](https://github.com/sherter/google-java-format-gradle-plugin)
+*   Apache Maven plugins
+    *   [coveo/fmt-maven-plugin](https://github.com/coveo/fmt-maven-plugin)
+    *   [talios/googleformatter-maven-plugin](https://github.com/talios/googleformatter-maven-plugin)
+    *   [Cosium/maven-git-code-format](https://github.com/Cosium/maven-git-code-format):
+        A maven plugin that automatically deploys google-java-format as a
+        pre-commit git hook.
+*   SBT plugins
+    *   [sbt/sbt-java-formatter](https://github.com/sbt/sbt-java-formatter)
+*   [maltzj/google-style-precommit-hook](https://github.com/maltzj/google-style-precommit-hook):
+    A pre-commit (pre-commit.com) hook that will automatically run GJF whenever
+    you commit code to your repository
+
+### as a library
+
+The formatter can be used in software which generates java to output more
+legible java code. Just include the library in your maven/gradle/etc.
+configuration.
+
+#### Maven
+
+```xml
+<dependency>
+  <groupId>com.google.googlejavaformat</groupId>
+  <artifactId>google-java-format</artifactId>
+  <version>1.7</version>
+</dependency>
+```
+
+#### Gradle
+
+```groovy
+dependencies {
+  compile 'com.google.googlejavaformat:google-java-format:1.7'
+}
+```
+
+You can then use the formatter through the `formatSource` methods. E.g.
+
+```java
+String formattedSource = new Formatter().formatSource(sourceString);
+```
+
+or
+
+```java
+CharSource source = ...
+CharSink output = ...
+new Formatter().formatSource(source, output);
+```
+
+Your starting point should be the instance methods of
+`com.google.googlejavaformat.java.Formatter`.
+
+## Building from source
+
+```
+mvn install
+```
+
+## Contributing
+
+Please see [the contributors guide](CONTRIBUTING.md) for details.
+
+## License
+
+```text
+Copyright 2015 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+```
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..3522155
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,33 @@
+os: Visual Studio 2015
+install:
+  - ps: |
+      Add-Type -AssemblyName System.IO.Compression.FileSystem
+      if (!(Test-Path -Path "C:\maven" )) {
+        (new-object System.Net.WebClient).DownloadFile(
+          'http://www.us.apache.org/dist/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip',
+          'C:\maven-bin.zip'
+        )
+        [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\maven-bin.zip", "C:\maven")
+      }
+  - cmd: SET JAVA_HOME=C:\Program Files\Java\jdk11
+  - cmd: SET PATH=C:\maven\apache-maven-3.3.9\bin;%JAVA_HOME%\bin;%PATH%
+  - cmd: SET MAVEN_OPTS=-XX:MaxPermSize=2g -Xmx4g
+  - cmd: SET JAVA_OPTS=-XX:MaxPermSize=2g -Xmx4g
+
+build_script:
+  - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
+
+test_script:
+  - mvn test -B
+
+cache:
+  - C:\maven\
+  - C:\Users\appveyor\.m2
+
+notifications:
+  - provider: Email
+    to:
+      - google-java-format-dev+ci@google.com
+    on_build_success: false
+    on_build_failure: true
+    on_build_status_changed: true
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 0000000..88b3cf9
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Google Inc.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>com.google.googlejavaformat</groupId>
+    <artifactId>google-java-format-parent</artifactId>
+    <version>1.8-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>google-java-format</artifactId>
+
+  <name>Google Java Format</name>
+
+  <description>
+    A Java source code formatter that follows Google Java Style.
+  </description>
+
+  <dependencies>
+    <!-- Required runtime dependencies -->
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
+    <!-- Compile-time dependencies -->
+    <dependency>
+      <groupId>org.checkerframework</groupId>
+      <artifactId>checker-qual</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>com.google.errorprone</groupId>
+      <artifactId>error_prone_annotations</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava-testlib</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.testing.compile</groupId>
+      <artifactId>compile-testing</artifactId>
+      <version>0.15</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <configuration>
+          <source>11</source>
+          <encoding>UTF-8</encoding>
+          <docencoding>UTF-8</docencoding>
+          <charset>UTF-8</charset>
+          <links>
+            <link>https://guava.dev/releases/${guava.version}/api/docs/</link>
+            <link>https://docs.oracle.com/en/java/javase/11/docs/api</link>
+          </links>
+          <additionalJOptions>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+            <additionalJOption>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED,com.google.googlejavaformat</additionalJOption>
+          </additionalJOptions>
+        </configuration>
+        <executions>
+          <execution>
+            <id>attach-docs</id>
+            <phase>post-integration-test</phase>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>2.4.3</version>
+        <executions>
+          <execution>
+            <id>shade-all-deps</id>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <shadedArtifactAttached>true</shadedArtifactAttached>
+              <shadedClassifierName>all-deps</shadedClassifierName>
+              <createDependencyReducedPom>false</createDependencyReducedPom>
+              <!-- http://stackoverflow.com/a/6743609 -->
+              <filters>
+                <filter>
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <exclude>META-INF/*.SF</exclude>
+                    <exclude>META-INF/*.DSA</exclude>
+                    <exclude>META-INF/*.RSA</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifest>
+              <mainClass>com.google.googlejavaformat.java.Main</mainClass>
+              <addDefaultImplementationEntries>
+                true
+              </addDefaultImplementationEntries>
+            </manifest>
+            <manifestEntries>
+              <Automatic-Module-Name>
+                com.google.googlejavaformat
+              </Automatic-Module-Name>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>3.0.1</version>
+        <executions>
+          <execution>
+            <id>copy-resources</id>
+            <phase>package</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>../eclipse_plugin/lib</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>target</directory>
+                  <include>${project.artifactId}-${project.version}.jar</include>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.10</version>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <phase>package</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>../eclipse_plugin/lib</outputDirectory>
+              <overWriteReleases>true</overWriteReleases>
+              <overWriteSnapshots>true</overWriteSnapshots>
+              <excludeTransitive>true</excludeTransitive>
+              <excludeArtifactIds>org.eclipse.jdt.core</excludeArtifactIds>
+              <excludeScope>compile</excludeScope>
+              <excludeScope>provided</excludeScope>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>com.google.code.maven-replacer-plugin</groupId>
+        <artifactId>replacer</artifactId>
+        <version>1.5.3</version>
+        <executions>
+          <execution>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>replace</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <file>${project.basedir}/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template</file>
+          <outputFile>${project.build.directory}/generated-sources/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java</outputFile>
+          <replacements>
+            <replacement>
+              <token>%VERSION%</token>
+              <value>${project.version}</value>
+            </replacement>
+          </replacements>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <version>3.0.0</version>
+        <executions>
+          <execution>
+            <id>add-source</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>${project.build.directory}/generated-sources/java/</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>11</source>
+          <target>11</target>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>jdk11</id>
+      <activation>
+        <jdk>(,14)</jdk>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+              <excludes>
+                <exclude>**/Java14InputAstVisitor.java</exclude>
+              </excludes>
+            </configuration>
+          </plugin>
+          <plugin>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <excludePackageNames>com.google.googlejavaformat.java.java14</excludePackageNames>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/core/src/main/java/com/google/googlejavaformat/CloseOp.java b/core/src/main/java/com/google/googlejavaformat/CloseOp.java
new file mode 100644
index 0000000..59e7e5a
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/CloseOp.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * A {@code CloseOp} closes a level. It is an {@link Op} in the sequence of {@link Op}s generated by
+ * {@link OpsBuilder}. When the sequence is turned into a {@link Doc} by {@link DocBuilder}, ranges
+ * delimited by {@link OpenOp}-{@code CloseOp} pairs turn into nested {@link Doc.Level}s.
+ */
+public enum CloseOp implements Op {
+  CLOSE;
+
+  /**
+   * Make a {@code CloseOp}, returning a singleton since they are all the same.
+   *
+   * @return the singleton {@code CloseOp}
+   */
+  public static Op make() {
+    return CLOSE;
+  }
+
+  @Override
+  public void add(DocBuilder builder) {
+    builder.close();
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java
new file mode 100644
index 0000000..45e507b
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+/**
+ * Rewrite comments. This interface is implemented by {@link
+ * com.google.googlejavaformat.java.JavaCommentsHelper JavaCommentsHelper}.
+ */
+public interface CommentsHelper {
+  /**
+   * Try to rewrite comments, returning rewritten text.
+   *
+   * @param tok the comment's tok
+   * @param maxWidth the line length for the output
+   * @param column0 the current column
+   * @return the rewritten comment
+   */
+  String rewrite(Input.Tok tok, int maxWidth, int column0);
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Doc.java b/core/src/main/java/com/google/googlejavaformat/Doc.java
new file mode 100644
index 0000000..e663c96
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Doc.java
@@ -0,0 +1,770 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import static com.google.common.collect.Iterables.getLast;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Range;
+import com.google.googlejavaformat.Output.BreakTag;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * {@link com.google.googlejavaformat.java.JavaInputAstVisitor JavaInputAstVisitor} outputs a
+ * sequence of {@link Op}s using {@link OpsBuilder}. This linear sequence is then transformed by
+ * {@link DocBuilder} into a tree-structured {@code Doc}. The top-level {@code Doc} is a {@link
+ * Level}, which contains a sequence of {@code Doc}s, including other {@link Level}s. Leaf {@code
+ * Doc}s are {@link Token}s, representing language-level tokens; {@link Tok}s, which may also
+ * represent non-token {@link Input.Tok}s, including comments and other white-space; {@link Space}s,
+ * representing single spaces; and {@link Break}s, which represent optional line-breaks.
+ */
+public abstract class Doc {
+  /**
+   * Each {@link Break} in a {@link Level} is either {@link FillMode#UNIFIED} or {@link
+   * FillMode#INDEPENDENT}.
+   */
+  public enum FillMode {
+    /**
+     * If a {@link Level} will not fit on one line, all of its {@code UNIFIED} {@link Break}s will
+     * be broken.
+     */
+    UNIFIED,
+
+    /**
+     * If a {@link Level} will not fit on one line, its {@code INDEPENDENT} {@link Break}s will be
+     * broken independently of each other, to fill in the {@link Level}.
+     */
+    INDEPENDENT,
+
+    /**
+     * A {@code FORCED} {@link Break} will always be broken, and a {@link Level} it appears in will
+     * not fit on one line.
+     */
+    FORCED
+  }
+
+  /** State for writing. */
+  public static final class State {
+    final int lastIndent;
+    final int indent;
+    final int column;
+    final boolean mustBreak;
+
+    State(int lastIndent, int indent, int column, boolean mustBreak) {
+      this.lastIndent = lastIndent;
+      this.indent = indent;
+      this.column = column;
+      this.mustBreak = mustBreak;
+    }
+
+    public State(int indent0, int column0) {
+      this(indent0, indent0, column0, false);
+    }
+
+    State withColumn(int column) {
+      return new State(lastIndent, indent, column, mustBreak);
+    }
+
+    State withMustBreak(boolean mustBreak) {
+      return new State(lastIndent, indent, column, mustBreak);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("lastIndent", lastIndent)
+          .add("indent", indent)
+          .add("column", column)
+          .add("mustBreak", mustBreak)
+          .toString();
+    }
+  }
+
+  private static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
+  private static final DiscreteDomain<Integer> INTEGERS = DiscreteDomain.integers();
+
+  // Memoized width; Float.POSITIVE_INFINITY if contains forced breaks.
+  private boolean widthComputed = false;
+  private float width = 0.0F;
+
+  // Memoized flat; not defined (and never computed) if contains forced breaks.
+  private boolean flatComputed = false;
+  private String flat = "";
+
+  // Memoized Range.
+  private boolean rangeComputed = false;
+  private Range<Integer> range = EMPTY_RANGE;
+
+  /**
+   * Return the width of a {@code Doc}, or {@code Float.POSITIVE_INFINITY} if it must be broken.
+   *
+   * @return the width
+   */
+  final float getWidth() {
+    if (!widthComputed) {
+      width = computeWidth();
+      widthComputed = true;
+    }
+    return width;
+  }
+
+  /**
+   * Return a {@code Doc}'s flat-string value; not defined (and never called) if the (@code Doc}
+   * contains forced breaks.
+   *
+   * @return the flat-string value
+   */
+  final String getFlat() {
+    if (!flatComputed) {
+      flat = computeFlat();
+      flatComputed = true;
+    }
+    return flat;
+  }
+
+  /**
+   * Return the {@link Range} of a {@code Doc}.
+   *
+   * @return the {@code Doc}'s {@link Range}
+   */
+  final Range<Integer> range() {
+    if (!rangeComputed) {
+      range = computeRange();
+      rangeComputed = true;
+    }
+    return range;
+  }
+
+  /**
+   * Compute the {@code Doc}'s width.
+   *
+   * @return the width, or {@code Float.POSITIVE_INFINITY} if it must be broken
+   */
+  abstract float computeWidth();
+
+  /**
+   * Compute the {@code Doc}'s flat value. Not defined (and never called) if contains forced breaks.
+   *
+   * @return the flat value
+   */
+  abstract String computeFlat();
+
+  /**
+   * Compute the {@code Doc}'s {@link Range} of {@link Input.Token}s.
+   *
+   * @return the {@link Range}
+   */
+  abstract Range<Integer> computeRange();
+
+  /**
+   * Make breaking decisions for a {@code Doc}.
+   *
+   * @param maxWidth the maximum line width
+   * @param state the current output state
+   * @return the new output state
+   */
+  public abstract State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state);
+
+  /** Write a {@code Doc} to an {@link Output}, after breaking decisions have been made. */
+  public abstract void write(Output output);
+
+  /** A {@code Level} inside a {@link Doc}. */
+  static final class Level extends Doc {
+    private final Indent plusIndent; // The extra indent following breaks.
+    private final List<Doc> docs = new ArrayList<>(); // The elements of the level.
+
+    private Level(Indent plusIndent) {
+      this.plusIndent = plusIndent;
+    }
+
+    /**
+     * Factory method for {@code Level}s.
+     *
+     * @param plusIndent the extra indent inside the {@code Level}
+     * @return the new {@code Level}
+     */
+    static Level make(Indent plusIndent) {
+      return new Level(plusIndent);
+    }
+
+    /**
+     * Add a {@link Doc} to the {@code Level}.
+     *
+     * @param doc the {@link Doc} to add
+     */
+    void add(Doc doc) {
+      docs.add(doc);
+    }
+
+    @Override
+    float computeWidth() {
+      float thisWidth = 0.0F;
+      for (Doc doc : docs) {
+        thisWidth += doc.getWidth();
+      }
+      return thisWidth;
+    }
+
+    @Override
+    String computeFlat() {
+      StringBuilder builder = new StringBuilder();
+      for (Doc doc : docs) {
+        builder.append(doc.getFlat());
+      }
+      return builder.toString();
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      Range<Integer> docRange = EMPTY_RANGE;
+      for (Doc doc : docs) {
+        docRange = union(docRange, doc.range());
+      }
+      return docRange;
+    }
+
+    // State that needs to be preserved between calculating breaks and
+    // writing output.
+    // TODO(cushon): represent phases as separate immutable data.
+
+    /** True if the entire {@link Level} fits on one line. */
+    boolean oneLine = false;
+
+    /**
+     * Groups of {@link Doc}s that are children of the current {@link Level}, separated by {@link
+     * Break}s.
+     */
+    List<List<Doc>> splits = new ArrayList<>();
+
+    /** {@link Break}s between {@link Doc}s in the current {@link Level}. */
+    List<Break> breaks = new ArrayList<>();
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      float thisWidth = getWidth();
+      if (state.column + thisWidth <= maxWidth) {
+        oneLine = true;
+        return state.withColumn(state.column + (int) thisWidth);
+      }
+      State broken =
+          computeBroken(
+              commentsHelper, maxWidth, new State(state.indent + plusIndent.eval(), state.column));
+      return state.withColumn(broken.column);
+    }
+
+    private static void splitByBreaks(List<Doc> docs, List<List<Doc>> splits, List<Break> breaks) {
+      splits.clear();
+      breaks.clear();
+      splits.add(new ArrayList<>());
+      for (Doc doc : docs) {
+        if (doc instanceof Break) {
+          breaks.add((Break) doc);
+          splits.add(new ArrayList<>());
+        } else {
+          getLast(splits).add(doc);
+        }
+      }
+    }
+
+    /** Compute breaks for a {@link Level} that spans multiple lines. */
+    private State computeBroken(CommentsHelper commentsHelper, int maxWidth, State state) {
+      splitByBreaks(docs, splits, breaks);
+
+      state =
+          computeBreakAndSplit(
+              commentsHelper, maxWidth, state, /* optBreakDoc= */ Optional.empty(), splits.get(0));
+
+      // Handle following breaks and split.
+      for (int i = 0; i < breaks.size(); i++) {
+        state =
+            computeBreakAndSplit(
+                commentsHelper, maxWidth, state, Optional.of(breaks.get(i)), splits.get(i + 1));
+      }
+      return state;
+    }
+
+    /** Lay out a Break-separated group of Docs in the current Level. */
+    private static State computeBreakAndSplit(
+        CommentsHelper commentsHelper,
+        int maxWidth,
+        State state,
+        Optional<Break> optBreakDoc,
+        List<Doc> split) {
+      float breakWidth = optBreakDoc.isPresent() ? optBreakDoc.get().getWidth() : 0.0F;
+      float splitWidth = getWidth(split);
+      boolean shouldBreak =
+          (optBreakDoc.isPresent() && optBreakDoc.get().fillMode == FillMode.UNIFIED)
+              || state.mustBreak
+              || state.column + breakWidth + splitWidth > maxWidth;
+
+      if (optBreakDoc.isPresent()) {
+        state = optBreakDoc.get().computeBreaks(state, state.lastIndent, shouldBreak);
+      }
+      boolean enoughRoom = state.column + splitWidth <= maxWidth;
+      state = computeSplit(commentsHelper, maxWidth, split, state.withMustBreak(false));
+      if (!enoughRoom) {
+        state = state.withMustBreak(true); // Break after, too.
+      }
+      return state;
+    }
+
+    private static State computeSplit(
+        CommentsHelper commentsHelper, int maxWidth, List<Doc> docs, State state) {
+      for (Doc doc : docs) {
+        state = doc.computeBreaks(commentsHelper, maxWidth, state);
+      }
+      return state;
+    }
+
+    @Override
+    public void write(Output output) {
+      if (oneLine) {
+        output.append(getFlat(), range()); // This is defined because width is finite.
+      } else {
+        writeFilled(output);
+      }
+    }
+
+    private void writeFilled(Output output) {
+      // Handle first split.
+      for (Doc doc : splits.get(0)) {
+        doc.write(output);
+      }
+      // Handle following breaks and split.
+      for (int i = 0; i < breaks.size(); i++) {
+        breaks.get(i).write(output);
+        for (Doc doc : splits.get(i + 1)) {
+          doc.write(output);
+        }
+      }
+    }
+
+    /**
+     * Get the width of a sequence of {@link Doc}s.
+     *
+     * @param docs the {@link Doc}s
+     * @return the width, or {@code Float.POSITIVE_INFINITY} if any {@link Doc} must be broken
+     */
+    static float getWidth(List<Doc> docs) {
+      float width = 0.0F;
+      for (Doc doc : docs) {
+        width += doc.getWidth();
+      }
+      return width;
+    }
+
+    private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
+      return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(INTEGERS);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("plusIndent", plusIndent)
+          .add("docs", docs)
+          .toString();
+    }
+  }
+
+  /** A leaf {@link Doc} for a token. */
+  public static final class Token extends Doc implements Op {
+    /** Is a Token a real token, or imaginary (e.g., a token generated incorrectly, or an EOF)? */
+    public enum RealOrImaginary {
+      REAL,
+      IMAGINARY;
+
+      boolean isReal() {
+        return this == REAL;
+      }
+    }
+
+    private final Input.Token token;
+    private final RealOrImaginary realOrImaginary;
+    private final Indent plusIndentCommentsBefore;
+    private final Optional<Indent> breakAndIndentTrailingComment;
+
+    private Token(
+        Input.Token token,
+        RealOrImaginary realOrImaginary,
+        Indent plusIndentCommentsBefore,
+        Optional<Indent> breakAndIndentTrailingComment) {
+      this.token = token;
+      this.realOrImaginary = realOrImaginary;
+      this.plusIndentCommentsBefore = plusIndentCommentsBefore;
+      this.breakAndIndentTrailingComment = breakAndIndentTrailingComment;
+    }
+
+    /**
+     * How much extra to indent comments before the {@code Token}.
+     *
+     * @return the extra indent
+     */
+    Indent getPlusIndentCommentsBefore() {
+      return plusIndentCommentsBefore;
+    }
+
+    /** Force a line break and indent trailing javadoc or block comments. */
+    Optional<Indent> breakAndIndentTrailingComment() {
+      return breakAndIndentTrailingComment;
+    }
+
+    /**
+     * Make a {@code Token}.
+     *
+     * @param token the {@link Input.Token} to wrap
+     * @param realOrImaginary did this {@link Input.Token} appear in the input, or was it generated
+     *     incorrectly?
+     * @param plusIndentCommentsBefore extra {@code plusIndent} for comments just before this token
+     * @return the new {@code Token}
+     */
+    static Op make(
+        Input.Token token,
+        Doc.Token.RealOrImaginary realOrImaginary,
+        Indent plusIndentCommentsBefore,
+        Optional<Indent> breakAndIndentTrailingComment) {
+      return new Token(
+          token, realOrImaginary, plusIndentCommentsBefore, breakAndIndentTrailingComment);
+    }
+
+    /**
+     * Return the wrapped {@link Input.Token}.
+     *
+     * @return the {@link Input.Token}
+     */
+    Input.Token getToken() {
+      return token;
+    }
+
+    /**
+     * Is the token good? That is, does it match an {@link Input.Token}?
+     *
+     * @return whether the @code Token} is good
+     */
+    RealOrImaginary realOrImaginary() {
+      return realOrImaginary;
+    }
+
+    @Override
+    public void add(DocBuilder builder) {
+      builder.add(this);
+    }
+
+    @Override
+    float computeWidth() {
+      return token.getTok().length();
+    }
+
+    @Override
+    String computeFlat() {
+      return token.getTok().getOriginalText();
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      return Range.singleton(token.getTok().getIndex()).canonical(INTEGERS);
+    }
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      String text = token.getTok().getOriginalText();
+      return state.withColumn(state.column + text.length());
+    }
+
+    @Override
+    public void write(Output output) {
+      String text = token.getTok().getOriginalText();
+      output.append(text, range());
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("token", token)
+          .add("realOrImaginary", realOrImaginary)
+          .add("plusIndentCommentsBefore", plusIndentCommentsBefore)
+          .toString();
+    }
+  }
+
+  /** A Leaf node in a {@link Doc} for a non-breaking space. */
+  static final class Space extends Doc implements Op {
+    private static final Space SPACE = new Space();
+
+    private Space() {}
+
+    /**
+     * Factor method for {@code Space}.
+     *
+     * @return the new {@code Space}
+     */
+    static Space make() {
+      return SPACE;
+    }
+
+    @Override
+    public void add(DocBuilder builder) {
+      builder.add(this);
+    }
+
+    @Override
+    float computeWidth() {
+      return 1.0F;
+    }
+
+    @Override
+    String computeFlat() {
+      return " ";
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      return EMPTY_RANGE;
+    }
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      return state.withColumn(state.column + 1);
+    }
+
+    @Override
+    public void write(Output output) {
+      output.append(" ", range());
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).toString();
+    }
+  }
+
+  /** A leaf node in a {@link Doc} for an optional break. */
+  public static final class Break extends Doc implements Op {
+    private final FillMode fillMode;
+    private final String flat;
+    private final Indent plusIndent;
+    private final Optional<BreakTag> optTag;
+
+    private Break(FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
+      this.fillMode = fillMode;
+      this.flat = flat;
+      this.plusIndent = plusIndent;
+      this.optTag = optTag;
+    }
+
+    /**
+     * Make a {@code Break}.
+     *
+     * @param fillMode the {@link FillMode}
+     * @param flat the text when not broken
+     * @param plusIndent extra indent if taken
+     * @return the new {@code Break}
+     */
+    public static Break make(FillMode fillMode, String flat, Indent plusIndent) {
+      return new Break(fillMode, flat, plusIndent, /* optTag= */ Optional.empty());
+    }
+
+    /**
+     * Make a {@code Break}.
+     *
+     * @param fillMode the {@link FillMode}
+     * @param flat the text when not broken
+     * @param plusIndent extra indent if taken
+     * @param optTag an optional tag for remembering whether the break was taken
+     * @return the new {@code Break}
+     */
+    public static Break make(
+        FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optTag) {
+      return new Break(fillMode, flat, plusIndent, optTag);
+    }
+
+    /**
+     * Make a forced {@code Break}.
+     *
+     * @return the new forced {@code Break}
+     */
+    public static Break makeForced() {
+      return make(FillMode.FORCED, "", Indent.Const.ZERO);
+    }
+
+    /**
+     * Return the {@code Break}'s extra indent.
+     *
+     * @return the extra indent
+     */
+    int getPlusIndent() {
+      return plusIndent.eval();
+    }
+
+    /**
+     * Is the {@code Break} forced?
+     *
+     * @return whether the {@code Break} is forced
+     */
+    boolean isForced() {
+      return fillMode == FillMode.FORCED;
+    }
+
+    @Override
+    public void add(DocBuilder builder) {
+      builder.breakDoc(this);
+    }
+
+    @Override
+    float computeWidth() {
+      return isForced() ? Float.POSITIVE_INFINITY : (float) flat.length();
+    }
+
+    @Override
+    String computeFlat() {
+      return flat;
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      return EMPTY_RANGE;
+    }
+
+    /** Was this break taken? */
+    boolean broken;
+
+    /** New indent after this break. */
+    int newIndent;
+
+    public State computeBreaks(State state, int lastIndent, boolean broken) {
+      if (optTag.isPresent()) {
+        optTag.get().recordBroken(broken);
+      }
+
+      if (broken) {
+        this.broken = true;
+        this.newIndent = Math.max(lastIndent + plusIndent.eval(), 0);
+        return state.withColumn(newIndent);
+      } else {
+        this.broken = false;
+        this.newIndent = -1;
+        return state.withColumn(state.column + flat.length());
+      }
+    }
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      // Updating the state for {@link Break}s requires deciding if the break
+      // should be taken.
+      // TODO(cushon): this hierarchy is wrong, create a separate interface
+      // for unbreakable Docs?
+      throw new UnsupportedOperationException("Did you mean computeBreaks(State, int, boolean)?");
+    }
+
+    @Override
+    public void write(Output output) {
+      if (broken) {
+        output.append("\n", EMPTY_RANGE);
+        output.indent(newIndent);
+      } else {
+        output.append(flat, range());
+      }
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("fillMode", fillMode)
+          .add("flat", flat)
+          .add("plusIndent", plusIndent)
+          .add("optTag", optTag)
+          .toString();
+    }
+  }
+
+  /** A leaf node in a {@link Doc} for a non-token. */
+  static final class Tok extends Doc implements Op {
+    private final Input.Tok tok;
+
+    private Tok(Input.Tok tok) {
+      this.tok = tok;
+    }
+
+    /**
+     * Factory method for a {@code Tok}.
+     *
+     * @param tok the {@link Input.Tok} to wrap
+     * @return the new {@code Tok}
+     */
+    static Tok make(Input.Tok tok) {
+      return new Tok(tok);
+    }
+
+    @Override
+    public void add(DocBuilder builder) {
+      builder.add(this);
+    }
+
+    @Override
+    float computeWidth() {
+      int idx = Newlines.firstBreak(tok.getOriginalText());
+      // only count the first line of multi-line block comments
+      if (tok.isComment()) {
+        if (idx > 0) {
+          return idx;
+        } else if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) {
+          // Account for line comments with missing spaces, see computeFlat.
+          return tok.length() + 1;
+        } else {
+          return tok.length();
+        }
+      }
+      return idx != -1 ? Float.POSITIVE_INFINITY : (float) tok.length();
+    }
+
+    @Override
+    String computeFlat() {
+      // TODO(cushon): commentsHelper.rewrite doesn't get called for spans that fit in a single
+      // line. That's fine for multi-line comment reflowing, but problematic for adding missing
+      // spaces in line comments.
+      if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) {
+        return "// " + tok.getOriginalText().substring("//".length());
+      }
+      return tok.getOriginalText();
+    }
+
+    @Override
+    Range<Integer> computeRange() {
+      return Range.singleton(tok.getIndex()).canonical(INTEGERS);
+    }
+
+    String text;
+
+    @Override
+    public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) {
+      text = commentsHelper.rewrite(tok, maxWidth, state.column);
+      int firstLineLength = text.length() - Iterators.getLast(Newlines.lineOffsetIterator(text));
+      return state.withColumn(state.column + firstLineLength);
+    }
+
+    @Override
+    public void write(Output output) {
+      output.append(text, range());
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("tok", tok).toString();
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/DocBuilder.java b/core/src/main/java/com/google/googlejavaformat/DocBuilder.java
new file mode 100644
index 0000000..31cc798
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/DocBuilder.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.MoreObjects;
+import java.util.ArrayDeque;
+import java.util.List;
+
+/** A {@code DocBuilder} converts a sequence of {@link Op}s into a {@link Doc}. */
+public final class DocBuilder {
+  private final Doc.Level base = Doc.Level.make(Indent.Const.ZERO);
+  private final ArrayDeque<Doc.Level> stack = new ArrayDeque<>();
+
+  /**
+   * A possibly earlier {@link Doc.Level} for appending text, à la Philip Wadler.
+   *
+   * <p>Processing {@link Doc}s presents a subtle problem. Suppose we have a {@link Doc} for to an
+   * assignment node, {@code a = b}, with an optional {@link Doc.Break} following the {@code =}.
+   * Suppose we have 5 characters to write it, so that we think we don't need the break.
+   * Unfortunately, this {@link Doc} lies in an expression statement {@link Doc} for the statement
+   * {@code a = b;} and this statement does not fit in 3 characters. This is why many formatters
+   * sometimes emit lines that are too long, or cheat by using a narrower line length to avoid such
+   * problems.
+   *
+   * <p>One solution to this problem is not to decide whether a {@link Doc.Level} should be broken
+   * until later (in this case, after the semicolon has been seen). A simpler approach is to rewrite
+   * the {@link Doc} as here, so that the semicolon moves inside the inner {@link Doc}, and we can
+   * decide whether to break that {@link Doc} without seeing later text.
+   */
+  private Doc.Level appendLevel = base;
+
+  /** Start to build a {@code DocBuilder}. */
+  public DocBuilder() {
+    stack.addLast(base);
+  }
+
+  /**
+   * Add a list of {@link Op}s to the {@link OpsBuilder}.
+   *
+   * @param ops the {@link Op}s
+   * @return the {@link OpsBuilder}
+   */
+  public DocBuilder withOps(List<Op> ops) {
+    for (Op op : ops) {
+      op.add(this); // These operations call the operations below to build the doc.
+    }
+    return this;
+  }
+
+  /**
+   * Open a new {@link Doc.Level}.
+   *
+   * @param plusIndent the extra indent for the {@link Doc.Level}
+   */
+  void open(Indent plusIndent) {
+    Doc.Level level = Doc.Level.make(plusIndent);
+    stack.addLast(level);
+  }
+
+  /** Close the current {@link Doc.Level}. */
+  void close() {
+    Doc.Level top = stack.removeLast();
+    stack.peekLast().add(top);
+  }
+
+  /**
+   * Add a {@link Doc} to the current {@link Doc.Level}.
+   *
+   * @param doc the {@link Doc}
+   */
+  void add(Doc doc) {
+    appendLevel.add(doc);
+  }
+
+  /**
+   * Add a {@link Doc.Break} to the current {@link Doc.Level}.
+   *
+   * @param breakDoc the {@link Doc.Break}
+   */
+  void breakDoc(Doc.Break breakDoc) {
+    appendLevel = stack.peekLast();
+    appendLevel.add(breakDoc);
+  }
+
+  /**
+   * Return the {@link Doc}.
+   *
+   * @return the {@link Doc}
+   */
+  public Doc build() {
+    return base;
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("base", base)
+        .add("stack", stack)
+        .add("appendLevel", appendLevel)
+        .toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java b/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java
new file mode 100644
index 0000000..be7f8a6
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/** An error that prevented formatting from succeeding. */
+public class FormatterDiagnostic {
+  private final int lineNumber;
+  private final String message;
+  private final int column;
+
+  public static FormatterDiagnostic create(String message) {
+    return new FormatterDiagnostic(-1, -1, message);
+  }
+
+  public static FormatterDiagnostic create(int lineNumber, int column, String message) {
+    checkArgument(lineNumber >= 0);
+    checkArgument(column >= 0);
+    checkNotNull(message);
+    return new FormatterDiagnostic(lineNumber, column, message);
+  }
+
+  private FormatterDiagnostic(int lineNumber, int column, String message) {
+    this.lineNumber = lineNumber;
+    this.column = column;
+    this.message = message;
+  }
+
+  /**
+   * Returns the line number on which the error occurred, or {@code -1} if the error does not have a
+   * line number.
+   */
+  public int line() {
+    return lineNumber;
+  }
+
+  /**
+   * Returns the 0-indexed column number on which the error occurred, or {@code -1} if the error
+   * does not have a column.
+   */
+  public int column() {
+    return column;
+  }
+
+  /** Returns a description of the problem that prevented formatting from succeeding. */
+  public String message() {
+    return message;
+  }
+
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    if (lineNumber >= 0) {
+      sb.append(lineNumber).append(':');
+    }
+    if (column >= 0) {
+      // internal column numbers are 0-based, but diagnostics use 1-based indexing by convention
+      sb.append(column + 1).append(':');
+    }
+    if (lineNumber >= 0 || column >= 0) {
+      sb.append(' ');
+    }
+    sb.append("error: ").append(message);
+    return sb.toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/FormattingError.java b/core/src/main/java/com/google/googlejavaformat/FormattingError.java
new file mode 100644
index 0000000..50381d0
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/FormattingError.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+
+/** An unchecked formatting error. */
+public class FormattingError extends Error {
+
+  private final ImmutableList<FormatterDiagnostic> diagnostics;
+
+  public FormattingError(FormatterDiagnostic diagnostic) {
+    this(ImmutableList.of(diagnostic));
+  }
+
+  public FormattingError(Iterable<FormatterDiagnostic> diagnostics) {
+    super(Joiner.on("\n").join(diagnostics) + "\n");
+    this.diagnostics = ImmutableList.copyOf(diagnostics);
+  }
+
+  public ImmutableList<FormatterDiagnostic> diagnostics() {
+    return diagnostics;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Indent.java b/core/src/main/java/com/google/googlejavaformat/Indent.java
new file mode 100644
index 0000000..44b56cd
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Indent.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.MoreObjects;
+import com.google.googlejavaformat.Output.BreakTag;
+
+/**
+ * An indent for a {@link Doc.Level} or {@link Doc.Break}. The indent is either a constant {@code
+ * int}, or a conditional expression whose value depends on whether or not a {@link Doc.Break} has
+ * been broken.
+ */
+public abstract class Indent {
+
+  abstract int eval();
+
+  /** A constant function, returning a constant indent. */
+  public static final class Const extends Indent {
+    private final int n;
+
+    public static final Const ZERO = new Const(+0);
+
+    private Const(int n) {
+      this.n = n;
+    }
+
+    public static Const make(int n, int indentMultiplier) {
+      return new Const(n * indentMultiplier);
+    }
+
+    @Override
+    int eval() {
+      return n;
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("n", n).toString();
+    }
+  }
+
+  /** A conditional function, whose value depends on whether a break was taken. */
+  public static final class If extends Indent {
+    private final BreakTag condition;
+    private final Indent thenIndent;
+    private final Indent elseIndent;
+
+    private If(BreakTag condition, Indent thenIndent, Indent elseIndent) {
+      this.condition = condition;
+      this.thenIndent = thenIndent;
+      this.elseIndent = elseIndent;
+    }
+
+    public static If make(BreakTag condition, Indent thenIndent, Indent elseIndent) {
+      return new If(condition, thenIndent, elseIndent);
+    }
+
+    @Override
+    int eval() {
+      return (condition.wasBreakTaken() ? thenIndent : elseIndent).eval();
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("condition", condition)
+          .add("thenIndent", thenIndent)
+          .add("elseIndent", elseIndent)
+          .toString();
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Input.java b/core/src/main/java/com/google/googlejavaformat/Input.java
new file mode 100644
index 0000000..9e17c2b
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Input.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableRangeMap;
+
+/** An input to the formatter. */
+public abstract class Input extends InputOutput {
+  /**
+   * A {@code Tok} ("tock") is a token, or a comment, or a newline, or a maximal string of blanks. A
+   * token {@code Tok} underlies a {@link Token}, and each other {@code Tok} is attached to a single
+   * {@code Token}. Tokens and comments have indices; white space {@code Tok}s do not.
+   */
+  public interface Tok {
+    /**
+     * Return the {@code Tok}'s index.
+     *
+     * @return its index
+     */
+    int getIndex();
+
+    /**
+     * Return the {@code Tok}'s {@code 0}-based position.
+     *
+     * @return its position
+     */
+    int getPosition();
+
+    /**
+     * Return the {@code Tok}'s {@code 0}-based column number.
+     *
+     * @return its column number
+     */
+    int getColumn();
+
+    /** The {@code Tok}'s text. */
+    String getText();
+
+    /** The {@code Tok}'s original text (before processing escapes). */
+    String getOriginalText();
+
+    /** The length of the {@code Tok}'s original text. */
+    int length();
+
+    /** Is the {@code Tok} a newline? */
+    boolean isNewline();
+
+    /** Is the {@code Tok} a "//" comment? */
+    boolean isSlashSlashComment();
+
+    /** Is the {@code Tok} a "//" comment? */
+    boolean isSlashStarComment();
+
+    /** Is the {@code Tok} a javadoc comment? */
+    boolean isJavadocComment();
+
+    /** Is the {@code Tok} a comment? */
+    boolean isComment();
+  }
+
+  /** A {@code Token} is a language-level token. */
+  public interface Token {
+    /**
+     * Get the token's {@link Tok}.
+     *
+     * @return the token's {@link Tok}
+     */
+    Tok getTok();
+
+    /**
+     * Get the earlier {@link Tok}s assigned to this {@code Token}.
+     *
+     * @return the earlier {@link Tok}s assigned to this {@code Token}
+     */
+    ImmutableList<? extends Tok> getToksBefore();
+
+    /**
+     * Get the later {@link Tok}s assigned to this {@code Token}.
+     *
+     * @return the later {@link Tok}s assigned to this {@code Token}
+     */
+    ImmutableList<? extends Tok> getToksAfter();
+  }
+
+  /**
+   * Get the input tokens.
+   *
+   * @return the input tokens
+   */
+  public abstract ImmutableList<? extends Token> getTokens();
+
+  /** A map from [start, end] position ranges to {@link Token}s. */
+  public abstract ImmutableRangeMap<Integer, ? extends Token> getPositionTokenMap();
+
+  public abstract ImmutableMap<Integer, Integer> getPositionToColumnMap();
+
+  public abstract String getText();
+
+  /**
+   * Get the number of toks.
+   *
+   * @return the number of toks, including the EOF tok
+   */
+  public abstract int getkN();
+
+  /**
+   * Get the Token by index.
+   *
+   * @param k the token index
+   */
+  public abstract Token getToken(int k);
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("super", super.toString()).toString();
+  }
+
+  /** Converts a character offset in the input to a line number. */
+  public abstract int getLineNumber(int inputPosition);
+
+  /** Converts a character offset in the input to a 0-based column number. */
+  public abstract int getColumnNumber(int inputPosition);
+
+  /**
+   * Construct a diagnostic. Populates the input filename, and converts character offsets to
+   * numbers.
+   */
+  public FormatterDiagnostic createDiagnostic(int inputPosition, String message) {
+    return FormatterDiagnostic.create(
+        getLineNumber(inputPosition), getColumnNumber(inputPosition), message);
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/InputOutput.java b/core/src/main/java/com/google/googlejavaformat/InputOutput.java
new file mode 100644
index 0000000..46dc70b
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/InputOutput.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Range;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** This interface defines methods common to an {@link Input} or an {@link Output}. */
+public abstract class InputOutput {
+  private ImmutableList<String> lines = ImmutableList.of();
+
+  protected static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
+  private static final DiscreteDomain<Integer> INTEGERS = DiscreteDomain.integers();
+
+  /** Set the lines. */
+  protected final void setLines(ImmutableList<String> lines) {
+    this.lines = lines;
+  }
+
+  /**
+   * Get the line count.
+   *
+   * @return the line count
+   */
+  public final int getLineCount() {
+    return lines.size();
+  }
+
+  /**
+   * Get a line.
+   *
+   * @param lineI the line number
+   * @return the line
+   */
+  public final String getLine(int lineI) {
+    return lines.get(lineI);
+  }
+
+  /** The {@link Range}s of the tokens or comments lying on each line, in any part. */
+  protected final List<Range<Integer>> ranges = new ArrayList<>();
+
+  private static void addToRanges(List<Range<Integer>> ranges, int i, int k) {
+    while (ranges.size() <= i) {
+      ranges.add(EMPTY_RANGE);
+    }
+    Range<Integer> oldValue = ranges.get(i);
+    ranges.set(i, Range.closedOpen(oldValue.isEmpty() ? k : oldValue.lowerEndpoint(), k + 1));
+  }
+
+  protected final void computeRanges(List<? extends Input.Tok> toks) {
+    int lineI = 0;
+    for (Input.Tok tok : toks) {
+      String txt = tok.getOriginalText();
+      int lineI0 = lineI;
+      lineI += Newlines.count(txt);
+      int k = tok.getIndex();
+      if (k >= 0) {
+        for (int i = lineI0; i <= lineI; i++) {
+          addToRanges(ranges, i, k);
+        }
+      }
+    }
+  }
+
+  /**
+   * Given an {@code InputOutput}, compute the map from tok indices to line ranges.
+   *
+   * @param put the {@code InputOutput}
+   * @return the map from {@code com.google.googlejavaformat.java.JavaInput.Tok} indices to line
+   *     ranges in this {@code put}
+   */
+  public static Map<Integer, Range<Integer>> makeKToIJ(InputOutput put) {
+    Map<Integer, Range<Integer>> map = new HashMap<>();
+    int ijN = put.getLineCount();
+    for (int ij = 0; ij <= ijN; ij++) {
+      Range<Integer> range = put.getRanges(ij).canonical(INTEGERS);
+      for (int k = range.lowerEndpoint(); k < range.upperEndpoint(); k++) {
+        if (map.containsKey(k)) {
+          map.put(k, Range.closedOpen(map.get(k).lowerEndpoint(), ij + 1));
+        } else {
+          map.put(k, Range.closedOpen(ij, ij + 1));
+        }
+      }
+    }
+    return map;
+  }
+
+  /**
+   * Get the {@link Range} of {@link Input.Tok}s lying in any part on a line.
+   *
+   * @param lineI the line number
+   * @return the {@link Range} of {@link Input.Tok}s on the specified line
+   */
+  public final Range<Integer> getRanges(int lineI) {
+    return 0 <= lineI && lineI < ranges.size() ? ranges.get(lineI) : EMPTY_RANGE;
+  }
+
+  @Override
+  public String toString() {
+    return "InputOutput{" + "lines=" + lines + ", ranges=" + ranges + '}';
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Newlines.java b/core/src/main/java/com/google/googlejavaformat/Newlines.java
new file mode 100644
index 0000000..dbb82d3
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Newlines.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/** Platform-independent newline handling. */
+public class Newlines {
+
+  /** Returns the number of line breaks in the input. */
+  public static int count(String input) {
+    return Iterators.size(lineOffsetIterator(input)) - 1;
+  }
+
+  /** Returns the index of the first break in the input, or {@code -1}. */
+  public static int firstBreak(String input) {
+    Iterator<Integer> it = lineOffsetIterator(input);
+    it.next();
+    return it.hasNext() ? it.next() : -1;
+  }
+
+  private static final ImmutableSet<String> BREAKS = ImmutableSet.of("\r\n", "\n", "\r");
+
+  /** Returns true if the entire input string is a recognized line break. */
+  public static boolean isNewline(String input) {
+    return BREAKS.contains(input);
+  }
+
+  /** Returns the length of the newline sequence at the current offset, or {@code -1}. */
+  public static int hasNewlineAt(String input, int idx) {
+    for (String b : BREAKS) {
+      if (input.startsWith(b, idx)) {
+        return b.length();
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Returns the terminating line break in the input, or {@code null} if the input does not end in a
+   * break.
+   */
+  public static String getLineEnding(String input) {
+    for (String b : BREAKS) {
+      if (input.endsWith(b)) {
+        return b;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns the first line separator in the text, or {@code "\n"} if the text does not contain a
+   * single line separator.
+   */
+  public static String guessLineSeparator(String text) {
+    for (int i = 0; i < text.length(); i++) {
+      char c = text.charAt(i);
+      switch (c) {
+        case '\r':
+          if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
+            return "\r\n";
+          }
+          return "\r";
+        case '\n':
+          return "\n";
+        default:
+          break;
+      }
+    }
+    return "\n";
+  }
+
+  /** Returns true if the input contains any line breaks. */
+  public static boolean containsBreaks(String text) {
+    return CharMatcher.anyOf("\n\r").matchesAnyOf(text);
+  }
+
+  /** Returns an iterator over the start offsets of lines in the input. */
+  public static Iterator<Integer> lineOffsetIterator(String input) {
+    return new LineOffsetIterator(input);
+  }
+
+  /** Returns an iterator over lines in the input, including trailing whitespace. */
+  public static Iterator<String> lineIterator(String input) {
+    return new LineIterator(input);
+  }
+
+  private static class LineOffsetIterator implements Iterator<Integer> {
+
+    private int curr = 0;
+    private int idx = 0;
+    private final String input;
+
+    private LineOffsetIterator(String input) {
+      this.input = input;
+    }
+
+    @Override
+    public boolean hasNext() {
+      return curr != -1;
+    }
+
+    @Override
+    public Integer next() {
+      if (curr == -1) {
+        throw new NoSuchElementException();
+      }
+      int result = curr;
+      advance();
+      return result;
+    }
+
+    private void advance() {
+      for (; idx < input.length(); idx++) {
+        char c = input.charAt(idx);
+        switch (c) {
+          case '\r':
+            if (idx + 1 < input.length() && input.charAt(idx + 1) == '\n') {
+              idx++;
+            }
+            // falls through
+          case '\n':
+            idx++;
+            curr = idx;
+            return;
+          default:
+            break;
+        }
+      }
+      curr = -1;
+    }
+
+    @Override
+    public void remove() {
+      throw new UnsupportedOperationException("remove");
+    }
+  }
+
+  private static class LineIterator implements Iterator<String> {
+
+    int idx;
+    String curr;
+
+    private final String input;
+    private final Iterator<Integer> indices;
+
+    private LineIterator(String input) {
+      this.input = input;
+      this.indices = lineOffsetIterator(input);
+      idx = indices.next(); // read leading 0
+    }
+
+    private void advance() {
+      int last = idx;
+      if (indices.hasNext()) {
+        idx = indices.next();
+      } else if (hasNext()) {
+        // no terminal line break
+        idx = input.length();
+      } else {
+        throw new NoSuchElementException();
+      }
+      curr = input.substring(last, idx);
+    }
+
+    @Override
+    public boolean hasNext() {
+      return idx < input.length();
+    }
+
+    @Override
+    public String next() {
+      advance();
+      return curr;
+    }
+
+    @Override
+    public void remove() {
+      throw new UnsupportedOperationException("remove");
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Op.java b/core/src/main/java/com/google/googlejavaformat/Op.java
new file mode 100644
index 0000000..a484541
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Op.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+/**
+ * An {@code Op} is a member of the sequence of formatting operations emitted by {@link OpsBuilder}
+ * and transformed by {@link DocBuilder} into a {@link Doc}. Leaf subclasses of {@link Doc}
+ * implement {@code Op}; {@link Doc.Level} is the only non-leaf, and is represented by paired {@link
+ * OpenOp}-{@link CloseOp} {@code Op}s.
+ */
+public interface Op {
+  /**
+   * Add an {@code Op} to a {@link DocBuilder}.
+   *
+   * @param builder the {@link DocBuilder}
+   */
+  void add(DocBuilder builder);
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/OpenOp.java b/core/src/main/java/com/google/googlejavaformat/OpenOp.java
new file mode 100644
index 0000000..6498201
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/OpenOp.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * An {@code OpenOp} opens a level. It is an {@link Op} in the sequence of {@link Op}s generated by
+ * {@link OpsBuilder}. When the sequence is turned into a {@link Doc} by {@link DocBuilder}, {@link
+ * Input.Tok}s delimited by {@code OpenOp}-{@link CloseOp} pairs turn into nested {@link
+ * Doc.Level}s.
+ */
+public final class OpenOp implements Op {
+  private final Indent plusIndent;
+
+  private OpenOp(Indent plusIndent) {
+    this.plusIndent = plusIndent;
+  }
+
+  /**
+   * Make an ordinary {@code OpenOp}.
+   *
+   * @param plusIndent the indent for breaks at this level
+   * @return the {@code OpenOp}
+   */
+  public static Op make(Indent plusIndent) {
+    return new OpenOp(plusIndent);
+  }
+
+  @Override
+  public void add(DocBuilder builder) {
+    builder.open(plusIndent);
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("plusIndent", plusIndent).toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java
new file mode 100644
index 0000000..e8e100f
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java
@@ -0,0 +1,631 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.googlejavaformat.Indent.Const;
+import com.google.googlejavaformat.Input.Tok;
+import com.google.googlejavaformat.Input.Token;
+import com.google.googlejavaformat.Output.BreakTag;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * An {@code OpsBuilder} creates a list of {@link Op}s, which is turned into a {@link Doc} by {@link
+ * DocBuilder}.
+ */
+public final class OpsBuilder {
+
+  /** @return the actual size of the AST node at position, including comments. */
+  public int actualSize(int position, int length) {
+    Token startToken = input.getPositionTokenMap().get(position);
+    int start = startToken.getTok().getPosition();
+    for (Tok tok : startToken.getToksBefore()) {
+      if (tok.isComment()) {
+        start = Math.min(start, tok.getPosition());
+      }
+    }
+    Token endToken = input.getPositionTokenMap().get(position + length - 1);
+    int end = endToken.getTok().getPosition() + endToken.getTok().length();
+    for (Tok tok : endToken.getToksAfter()) {
+      if (tok.isComment()) {
+        end = Math.max(end, tok.getPosition() + tok.length());
+      }
+    }
+    return end - start;
+  }
+
+  /** @return the start column of the token at {@code position}, including leading comments. */
+  public Integer actualStartColumn(int position) {
+    Token startToken = input.getPositionTokenMap().get(position);
+    int start = startToken.getTok().getPosition();
+    int line0 = input.getLineNumber(start);
+    for (Tok tok : startToken.getToksBefore()) {
+      if (line0 != input.getLineNumber(tok.getPosition())) {
+        return start;
+      }
+      if (tok.isComment()) {
+        start = Math.min(start, tok.getPosition());
+      }
+    }
+    return start;
+  }
+
+  /** A request to add or remove a blank line in the output. */
+  public abstract static class BlankLineWanted {
+
+    /** Always emit a blank line. */
+    public static final BlankLineWanted YES = new SimpleBlankLine(Optional.of(true));
+
+    /** Never emit a blank line. */
+    public static final BlankLineWanted NO = new SimpleBlankLine(Optional.of(false));
+
+    /**
+     * Explicitly preserve blank lines from the input (e.g. before the first member in a class
+     * declaration). Overrides conditional blank lines.
+     */
+    public static final BlankLineWanted PRESERVE =
+        new SimpleBlankLine(/* wanted= */ Optional.empty());
+
+    /** Is the blank line wanted? */
+    public abstract Optional<Boolean> wanted();
+
+    /** Merge this blank line request with another. */
+    public abstract BlankLineWanted merge(BlankLineWanted wanted);
+
+    /** Emit a blank line if the given break is taken. */
+    public static BlankLineWanted conditional(BreakTag breakTag) {
+      return new ConditionalBlankLine(ImmutableList.of(breakTag));
+    }
+
+    private static final class SimpleBlankLine extends BlankLineWanted {
+      private final Optional<Boolean> wanted;
+
+      SimpleBlankLine(Optional<Boolean> wanted) {
+        this.wanted = wanted;
+      }
+
+      @Override
+      public Optional<Boolean> wanted() {
+        return wanted;
+      }
+
+      @Override
+      public BlankLineWanted merge(BlankLineWanted other) {
+        return this;
+      }
+    }
+
+    private static final class ConditionalBlankLine extends BlankLineWanted {
+
+      private final ImmutableList<BreakTag> tags;
+
+      ConditionalBlankLine(Iterable<BreakTag> tags) {
+        this.tags = ImmutableList.copyOf(tags);
+      }
+
+      @Override
+      public Optional<Boolean> wanted() {
+        for (BreakTag tag : tags) {
+          if (tag.wasBreakTaken()) {
+            return Optional.of(true);
+          }
+        }
+        return Optional.empty();
+      }
+
+      @Override
+      public BlankLineWanted merge(BlankLineWanted other) {
+        if (!(other instanceof ConditionalBlankLine)) {
+          return other;
+        }
+        return new ConditionalBlankLine(
+            Iterables.concat(this.tags, ((ConditionalBlankLine) other).tags));
+      }
+    }
+  }
+
+  private final Input input;
+  private final List<Op> ops = new ArrayList<>();
+  private final Output output;
+  private static final Indent.Const ZERO = Indent.Const.ZERO;
+
+  private int tokenI = 0;
+  private int inputPosition = Integer.MIN_VALUE;
+
+  /** The number of unclosed open ops in the input stream. */
+  int depth = 0;
+
+  /** Add an {@link Op}, and record open/close ops for later validation of unclosed levels. */
+  private void add(Op op) {
+    if (op instanceof OpenOp) {
+      depth++;
+    } else if (op instanceof CloseOp) {
+      depth--;
+      if (depth < 0) {
+        throw new AssertionError();
+      }
+    }
+    ops.add(op);
+  }
+
+  /** Add a list of {@link Op}s. */
+  public final void addAll(List<Op> ops) {
+    for (Op op : ops) {
+      add(op);
+    }
+  }
+
+  /**
+   * The {@code OpsBuilder} constructor.
+   *
+   * @param input the {@link Input}, used for retrieve information from the AST
+   * @param output the {@link Output}, used here only to record blank-line information
+   */
+  public OpsBuilder(Input input, Output output) {
+    this.input = input;
+    this.output = output;
+  }
+
+  /** Get the {@code OpsBuilder}'s {@link Input}. */
+  public final Input getInput() {
+    return input;
+  }
+
+  /** Returns the number of unclosed open ops in the input stream. */
+  public int depth() {
+    return depth;
+  }
+
+  /**
+   * Checks that all open ops in the op stream have matching close ops.
+   *
+   * @throws FormattingError if any ops were unclosed
+   */
+  public void checkClosed(int previous) {
+    if (depth != previous) {
+      throw new FormattingError(diagnostic(String.format("saw %d unclosed ops", depth)));
+    }
+  }
+
+  /** Create a {@link FormatterDiagnostic} at the current position. */
+  public FormatterDiagnostic diagnostic(String message) {
+    return input.createDiagnostic(inputPosition, message);
+  }
+
+  /**
+   * Sync to position in the input. If we've skipped outputting any tokens that were present in the
+   * input tokens, output them here and optionally complain.
+   *
+   * @param inputPosition the {@code 0}-based input position
+   */
+  public final void sync(int inputPosition) {
+    if (inputPosition > this.inputPosition) {
+      ImmutableList<? extends Input.Token> tokens = input.getTokens();
+      int tokensN = tokens.size();
+      this.inputPosition = inputPosition;
+      if (tokenI < tokensN && inputPosition > tokens.get(tokenI).getTok().getPosition()) {
+        // Found a missing input token. Insert it and mark it missing (usually not good).
+        Input.Token token = tokens.get(tokenI++);
+        throw new FormattingError(
+            diagnostic(String.format("did not generate token \"%s\"", token.getTok().getText())));
+      }
+    }
+  }
+
+  /** Output any remaining tokens from the input stream (e.g. terminal whitespace). */
+  public final void drain() {
+    int inputPosition = input.getText().length() + 1;
+    if (inputPosition > this.inputPosition) {
+      ImmutableList<? extends Input.Token> tokens = input.getTokens();
+      int tokensN = tokens.size();
+      while (tokenI < tokensN && inputPosition > tokens.get(tokenI).getTok().getPosition()) {
+        Input.Token token = tokens.get(tokenI++);
+        add(
+            Doc.Token.make(
+                token,
+                Doc.Token.RealOrImaginary.IMAGINARY,
+                ZERO,
+                /* breakAndIndentTrailingComment= */ Optional.empty()));
+      }
+    }
+    this.inputPosition = inputPosition;
+    checkClosed(0);
+  }
+
+  /**
+   * Open a new level by emitting an {@link OpenOp}.
+   *
+   * @param plusIndent the extra indent for the new level
+   */
+  public final void open(Indent plusIndent) {
+    add(OpenOp.make(plusIndent));
+  }
+
+  /** Close the current level, by emitting a {@link CloseOp}. */
+  public final void close() {
+    add(CloseOp.make());
+  }
+
+  /** Return the text of the next {@link Input.Token}, or absent if there is none. */
+  public final Optional<String> peekToken() {
+    return peekToken(0);
+  }
+
+  /** Return the text of an upcoming {@link Input.Token}, or absent if there is none. */
+  public final Optional<String> peekToken(int skip) {
+    ImmutableList<? extends Input.Token> tokens = input.getTokens();
+    int idx = tokenI + skip;
+    return idx < tokens.size()
+        ? Optional.of(tokens.get(idx).getTok().getOriginalText())
+        : Optional.empty();
+  }
+
+  /**
+   * Emit an optional token iff it exists on the input. This is used to emit tokens whose existence
+   * has been lost in the AST.
+   *
+   * @param token the optional token
+   */
+  public final void guessToken(String token) {
+    token(
+        token,
+        Doc.Token.RealOrImaginary.IMAGINARY,
+        ZERO,
+        /* breakAndIndentTrailingComment=  */ Optional.empty());
+  }
+
+  public final void token(
+      String token,
+      Doc.Token.RealOrImaginary realOrImaginary,
+      Indent plusIndentCommentsBefore,
+      Optional<Indent> breakAndIndentTrailingComment) {
+    ImmutableList<? extends Input.Token> tokens = input.getTokens();
+    if (token.equals(peekToken().orElse(null))) { // Found the input token. Output it.
+      add(
+          Doc.Token.make(
+              tokens.get(tokenI++),
+              Doc.Token.RealOrImaginary.REAL,
+              plusIndentCommentsBefore,
+              breakAndIndentTrailingComment));
+    } else {
+      /*
+       * Generated a "bad" token, which doesn't exist on the input. Drop it, and complain unless
+       * (for example) we're guessing at an optional token.
+       */
+      if (realOrImaginary.isReal()) {
+        throw new FormattingError(
+            diagnostic(
+                String.format(
+                    "expected token: '%s'; generated %s instead",
+                    peekToken().orElse(null), token)));
+      }
+    }
+  }
+
+  /**
+   * Emit a single- or multi-character op by breaking it into single-character {@link Doc.Token}s.
+   *
+   * @param op the operator to emit
+   */
+  public final void op(String op) {
+    int opN = op.length();
+    for (int i = 0; i < opN; i++) {
+      token(
+          op.substring(i, i + 1),
+          Doc.Token.RealOrImaginary.REAL,
+          ZERO,
+          /* breakAndIndentTrailingComment=  */ Optional.empty());
+    }
+  }
+
+  /** Emit a {@link Doc.Space}. */
+  public final void space() {
+    add(Doc.Space.make());
+  }
+
+  /** Emit a {@link Doc.Break}. */
+  public final void breakOp() {
+    breakOp(Doc.FillMode.UNIFIED, "", ZERO);
+  }
+
+  /**
+   * Emit a {@link Doc.Break}.
+   *
+   * @param plusIndent extra indent if taken
+   */
+  public final void breakOp(Indent plusIndent) {
+    breakOp(Doc.FillMode.UNIFIED, "", plusIndent);
+  }
+
+  /** Emit a filled {@link Doc.Break}. */
+  public final void breakToFill() {
+    breakOp(Doc.FillMode.INDEPENDENT, "", ZERO);
+  }
+
+  /** Emit a forced {@link Doc.Break}. */
+  public final void forcedBreak() {
+    breakOp(Doc.FillMode.FORCED, "", ZERO);
+  }
+
+  /**
+   * Emit a forced {@link Doc.Break}.
+   *
+   * @param plusIndent extra indent if taken
+   */
+  public final void forcedBreak(Indent plusIndent) {
+    breakOp(Doc.FillMode.FORCED, "", plusIndent);
+  }
+
+  /**
+   * Emit a {@link Doc.Break}, with a specified {@code flat} value (e.g., {@code " "}).
+   *
+   * @param flat the {@link Doc.Break} when not broken
+   */
+  public final void breakOp(String flat) {
+    breakOp(Doc.FillMode.UNIFIED, flat, ZERO);
+  }
+
+  /**
+   * Emit a {@link Doc.Break}, with a specified {@code flat} value (e.g., {@code " "}).
+   *
+   * @param flat the {@link Doc.Break} when not broken
+   */
+  public final void breakToFill(String flat) {
+    breakOp(Doc.FillMode.INDEPENDENT, flat, ZERO);
+  }
+
+  /**
+   * Emit a generic {@link Doc.Break}.
+   *
+   * @param fillMode the {@link Doc.FillMode}
+   * @param flat the {@link Doc.Break} when not broken
+   * @param plusIndent extra indent if taken
+   */
+  public final void breakOp(Doc.FillMode fillMode, String flat, Indent plusIndent) {
+    breakOp(fillMode, flat, plusIndent, /* optionalTag=  */ Optional.empty());
+  }
+
+  /**
+   * Emit a generic {@link Doc.Break}.
+   *
+   * @param fillMode the {@link Doc.FillMode}
+   * @param flat the {@link Doc.Break} when not broken
+   * @param plusIndent extra indent if taken
+   * @param optionalTag an optional tag for remembering whether the break was taken
+   */
+  public final void breakOp(
+      Doc.FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optionalTag) {
+    add(Doc.Break.make(fillMode, flat, plusIndent, optionalTag));
+  }
+
+  private int lastPartialFormatBoundary = -1;
+
+  /**
+   * Make the boundary of a region that can be partially formatted. The boundary will be included in
+   * the following region, e.g.: [[boundary0, boundary1), [boundary1, boundary2), ...].
+   */
+  public void markForPartialFormat() {
+    if (lastPartialFormatBoundary == -1) {
+      lastPartialFormatBoundary = tokenI;
+      return;
+    }
+    if (tokenI == lastPartialFormatBoundary) {
+      return;
+    }
+    Token start = input.getTokens().get(lastPartialFormatBoundary);
+    Token end = input.getTokens().get(tokenI - 1);
+    output.markForPartialFormat(start, end);
+    lastPartialFormatBoundary = tokenI;
+  }
+
+  /**
+   * Force or suppress a blank line here in the output.
+   *
+   * @param wanted whether to force ({@code true}) or suppress {@code false}) the blank line
+   */
+  public final void blankLineWanted(BlankLineWanted wanted) {
+    output.blankLine(getI(input.getTokens().get(tokenI)), wanted);
+  }
+
+  private static int getI(Input.Token token) {
+    for (Input.Tok tok : token.getToksBefore()) {
+      if (tok.getIndex() >= 0) {
+        return tok.getIndex();
+      }
+    }
+    return token.getTok().getIndex();
+  }
+
+  private static final Doc.Space SPACE = Doc.Space.make();
+
+  /**
+   * Build a list of {@link Op}s from the {@code OpsBuilder}.
+   *
+   * @return the list of {@link Op}s
+   */
+  public final ImmutableList<Op> build() {
+    markForPartialFormat();
+    // Rewrite the ops to insert comments.
+    Multimap<Integer, Op> tokOps = ArrayListMultimap.create();
+    int opsN = ops.size();
+    for (int i = 0; i < opsN; i++) {
+      Op op = ops.get(i);
+      if (op instanceof Doc.Token) {
+        /*
+         * Token ops can have associated non-tokens, including comments, which we need to insert.
+         * They can also cause line breaks, so we insert them before or after the current level,
+         * when possible.
+         */
+        Doc.Token tokenOp = (Doc.Token) op;
+        Input.Token token = tokenOp.getToken();
+        int j = i; // Where to insert toksBefore before.
+        while (0 < j && ops.get(j - 1) instanceof OpenOp) {
+          --j;
+        }
+        int k = i; // Where to insert toksAfter after.
+        while (k + 1 < opsN && ops.get(k + 1) instanceof CloseOp) {
+          ++k;
+        }
+        if (tokenOp.realOrImaginary().isReal()) {
+          /*
+           * Regular input token. Copy out toksBefore before token, and toksAfter after it. Insert
+           * this token's toksBefore at position j.
+           */
+          int newlines = 0; // Count of newlines in a row.
+          boolean space = false; // Do we need an extra space after a previous "/*" comment?
+          boolean lastWasComment = false; // Was the last thing we output a comment?
+          boolean allowBlankAfterLastComment = false;
+          for (Input.Tok tokBefore : token.getToksBefore()) {
+            if (tokBefore.isNewline()) {
+              newlines++;
+            } else if (tokBefore.isComment()) {
+              tokOps.put(
+                  j,
+                  Doc.Break.make(
+                      tokBefore.isSlashSlashComment() ? Doc.FillMode.FORCED : Doc.FillMode.UNIFIED,
+                      "",
+                      tokenOp.getPlusIndentCommentsBefore()));
+              tokOps.putAll(j, makeComment(tokBefore));
+              space = tokBefore.isSlashStarComment();
+              newlines = 0;
+              lastWasComment = true;
+              if (tokBefore.isJavadocComment()) {
+                tokOps.put(j, Doc.Break.makeForced());
+              }
+              allowBlankAfterLastComment =
+                  tokBefore.isSlashSlashComment()
+                      || (tokBefore.isSlashStarComment() && !tokBefore.isJavadocComment());
+            }
+          }
+          if (allowBlankAfterLastComment && newlines > 1) {
+            // Force a line break after two newlines in a row following a line or block comment
+            output.blankLine(token.getTok().getIndex(), BlankLineWanted.YES);
+          }
+          if (lastWasComment && newlines > 0) {
+            tokOps.put(j, Doc.Break.makeForced());
+          } else if (space) {
+            tokOps.put(j, SPACE);
+          }
+          // Now we've seen the Token; output the toksAfter.
+          for (Input.Tok tokAfter : token.getToksAfter()) {
+            if (tokAfter.isComment()) {
+              boolean breakAfter =
+                  tokAfter.isJavadocComment()
+                      || (tokAfter.isSlashStarComment()
+                          && tokenOp.breakAndIndentTrailingComment().isPresent());
+              if (breakAfter) {
+                tokOps.put(
+                    k + 1,
+                    Doc.Break.make(
+                        Doc.FillMode.FORCED,
+                        "",
+                        tokenOp.breakAndIndentTrailingComment().orElse(Const.ZERO)));
+              } else {
+                tokOps.put(k + 1, SPACE);
+              }
+              tokOps.putAll(k + 1, makeComment(tokAfter));
+              if (breakAfter) {
+                tokOps.put(k + 1, Doc.Break.make(Doc.FillMode.FORCED, "", ZERO));
+              }
+            }
+          }
+        } else {
+          /*
+           * This input token was mistakenly not generated for output. As no whitespace or comments
+           * were generated (presumably), copy all input non-tokens literally, even spaces and
+           * newlines.
+           */
+          int newlines = 0;
+          boolean lastWasComment = false;
+          for (Input.Tok tokBefore : token.getToksBefore()) {
+            if (tokBefore.isNewline()) {
+              newlines++;
+            } else if (tokBefore.isComment()) {
+              newlines = 0;
+              lastWasComment = tokBefore.isComment();
+            }
+            if (lastWasComment && newlines > 0) {
+              tokOps.put(j, Doc.Break.makeForced());
+            }
+            tokOps.put(j, Doc.Tok.make(tokBefore));
+          }
+          for (Input.Tok tokAfter : token.getToksAfter()) {
+            tokOps.put(k + 1, Doc.Tok.make(tokAfter));
+          }
+        }
+      }
+    }
+    /*
+     * Construct new list of ops, splicing in the comments. If a comment is inserted immediately
+     * before a space, suppress the space.
+     */
+    ImmutableList.Builder<Op> newOps = ImmutableList.builder();
+    boolean afterForcedBreak = false; // Was the last Op a forced break? If so, suppress spaces.
+    for (int i = 0; i < opsN; i++) {
+      for (Op op : tokOps.get(i)) {
+        if (!(afterForcedBreak && op instanceof Doc.Space)) {
+          newOps.add(op);
+          afterForcedBreak = isForcedBreak(op);
+        }
+      }
+      Op op = ops.get(i);
+      if (afterForcedBreak
+          && (op instanceof Doc.Space
+              || (op instanceof Doc.Break
+                  && ((Doc.Break) op).getPlusIndent() == 0
+                  && " ".equals(((Doc) op).getFlat())))) {
+        continue;
+      }
+      newOps.add(op);
+      if (!(op instanceof OpenOp)) {
+        afterForcedBreak = isForcedBreak(op);
+      }
+    }
+    for (Op op : tokOps.get(opsN)) {
+      if (!(afterForcedBreak && op instanceof Doc.Space)) {
+        newOps.add(op);
+        afterForcedBreak = isForcedBreak(op);
+      }
+    }
+    return newOps.build();
+  }
+
+  private static boolean isForcedBreak(Op op) {
+    return op instanceof Doc.Break && ((Doc.Break) op).isForced();
+  }
+
+  private static List<Op> makeComment(Input.Tok comment) {
+    return comment.isSlashStarComment()
+        ? ImmutableList.of(Doc.Tok.make(comment))
+        : ImmutableList.of(Doc.Tok.make(comment), Doc.Break.makeForced());
+  }
+
+  @Override
+  public final String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("input", input)
+        .add("ops", ops)
+        .add("output", output)
+        .add("tokenI", tokenI)
+        .add("inputPosition", inputPosition)
+        .toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/Output.java b/core/src/main/java/com/google/googlejavaformat/Output.java
new file mode 100644
index 0000000..ea039fa
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/Output.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Range;
+import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
+import java.util.Optional;
+
+/** An output from the formatter. */
+public abstract class Output extends InputOutput {
+  /** Unique identifier for a break. */
+  public static final class BreakTag {
+
+    Optional<Boolean> taken = Optional.empty();
+
+    public void recordBroken(boolean broken) {
+      // TODO(cushon): enforce invariants.
+      // Currently we rely on setting Breaks multiple times, e.g. when deciding
+      // whether a Level should be flowed. Using separate data structures
+      // instead of mutation or adding an explicit 'reset' step would allow
+      // a useful invariant to be enforced here.
+      taken = Optional.of(broken);
+    }
+
+    public boolean wasBreakTaken() {
+      return taken.orElse(false);
+    }
+  }
+
+  /**
+   * Indent by outputting {@code indent} spaces.
+   *
+   * @param indent the current indent
+   */
+  public abstract void indent(int indent);
+
+  /**
+   * Output a string.
+   *
+   * @param text the string
+   * @param range the {@link Range} corresponding to the string
+   */
+  public abstract void append(String text, Range<Integer> range);
+
+  /**
+   * A blank line is or is not wanted here.
+   *
+   * @param k the {@link Input.Tok} index
+   * @param wanted whether a blank line is wanted here
+   */
+  public abstract void blankLine(int k, BlankLineWanted wanted);
+
+  /** Marks a region that can be partially formatted. */
+  public abstract void markForPartialFormat(Input.Token start, Input.Token end);
+
+  /**
+   * Get the {@link CommentsHelper}.
+   *
+   * @return the {@link CommentsHelper}
+   */
+  public abstract CommentsHelper getCommentsHelper();
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("super", super.toString()).toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java
new file mode 100644
index 0000000..5a23328
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableRangeSet;
+import java.util.Optional;
+
+/**
+ * Command line options for google-java-format.
+ *
+ * <p>google-java-format doesn't depend on AutoValue, to allow AutoValue to depend on
+ * google-java-format.
+ */
+final class CommandLineOptions {
+
+  private final ImmutableList<String> files;
+  private final boolean inPlace;
+  private final ImmutableRangeSet<Integer> lines;
+  private final ImmutableList<Integer> offsets;
+  private final ImmutableList<Integer> lengths;
+  private final boolean aosp;
+  private final boolean version;
+  private final boolean help;
+  private final boolean stdin;
+  private final boolean fixImportsOnly;
+  private final boolean sortImports;
+  private final boolean removeUnusedImports;
+  private final boolean dryRun;
+  private final boolean setExitIfChanged;
+  private final Optional<String> assumeFilename;
+  private final boolean reflowLongStrings;
+  private final boolean formatJavadoc;
+
+  CommandLineOptions(
+      ImmutableList<String> files,
+      boolean inPlace,
+      ImmutableRangeSet<Integer> lines,
+      ImmutableList<Integer> offsets,
+      ImmutableList<Integer> lengths,
+      boolean aosp,
+      boolean version,
+      boolean help,
+      boolean stdin,
+      boolean fixImportsOnly,
+      boolean sortImports,
+      boolean removeUnusedImports,
+      boolean dryRun,
+      boolean setExitIfChanged,
+      Optional<String> assumeFilename,
+      boolean reflowLongStrings,
+      boolean formatJavadoc) {
+    this.files = files;
+    this.inPlace = inPlace;
+    this.lines = lines;
+    this.offsets = offsets;
+    this.lengths = lengths;
+    this.aosp = aosp;
+    this.version = version;
+    this.help = help;
+    this.stdin = stdin;
+    this.fixImportsOnly = fixImportsOnly;
+    this.sortImports = sortImports;
+    this.removeUnusedImports = removeUnusedImports;
+    this.dryRun = dryRun;
+    this.setExitIfChanged = setExitIfChanged;
+    this.assumeFilename = assumeFilename;
+    this.reflowLongStrings = reflowLongStrings;
+    this.formatJavadoc = formatJavadoc;
+  }
+
+  /** The files to format. */
+  ImmutableList<String> files() {
+    return files;
+  }
+
+  /** Format files in place. */
+  boolean inPlace() {
+    return inPlace;
+  }
+
+  /** Line ranges to format. */
+  ImmutableRangeSet<Integer> lines() {
+    return lines;
+  }
+
+  /** Character offsets for partial formatting, paired with {@code lengths}. */
+  ImmutableList<Integer> offsets() {
+    return offsets;
+  }
+
+  /** Partial formatting region lengths, paired with {@code offsets}. */
+  ImmutableList<Integer> lengths() {
+    return lengths;
+  }
+
+  /** Use AOSP style instead of Google Style (4-space indentation). */
+  boolean aosp() {
+    return aosp;
+  }
+
+  /** Print the version. */
+  boolean version() {
+    return version;
+  }
+
+  /** Print usage information. */
+  boolean help() {
+    return help;
+  }
+
+  /** Format input from stdin. */
+  boolean stdin() {
+    return stdin;
+  }
+
+  /** Fix imports, but do no formatting. */
+  boolean fixImportsOnly() {
+    return fixImportsOnly;
+  }
+
+  /** Sort imports. */
+  boolean sortImports() {
+    return sortImports;
+  }
+
+  /** Remove unused imports. */
+  boolean removeUnusedImports() {
+    return removeUnusedImports;
+  }
+
+  /**
+   * Print the paths of the files whose contents would change if the formatter were run normally.
+   */
+  boolean dryRun() {
+    return dryRun;
+  }
+
+  /** Return exit code 1 if there are any formatting changes. */
+  boolean setExitIfChanged() {
+    return setExitIfChanged;
+  }
+
+  /** Return the name to use for diagnostics when formatting standard input. */
+  Optional<String> assumeFilename() {
+    return assumeFilename;
+  }
+
+  boolean reflowLongStrings() {
+    return reflowLongStrings;
+  }
+
+  /** Returns true if partial formatting was selected. */
+  boolean isSelection() {
+    return !lines().isEmpty() || !offsets().isEmpty() || !lengths().isEmpty();
+  }
+
+  boolean formatJavadoc() {
+    return formatJavadoc;
+  }
+
+  static Builder builder() {
+    return new Builder();
+  }
+
+  static class Builder {
+
+    private final ImmutableList.Builder<String> files = ImmutableList.builder();
+    private final ImmutableRangeSet.Builder<Integer> lines = ImmutableRangeSet.builder();
+    private final ImmutableList.Builder<Integer> offsets = ImmutableList.builder();
+    private final ImmutableList.Builder<Integer> lengths = ImmutableList.builder();
+    private boolean inPlace = false;
+    private boolean aosp = false;
+    private boolean version = false;
+    private boolean help = false;
+    private boolean stdin = false;
+    private boolean fixImportsOnly = false;
+    private boolean sortImports = true;
+    private boolean removeUnusedImports = true;
+    private boolean dryRun = false;
+    private boolean setExitIfChanged = false;
+    private Optional<String> assumeFilename = Optional.empty();
+    private boolean reflowLongStrings = true;
+    private boolean formatJavadoc = true;
+
+    ImmutableList.Builder<String> filesBuilder() {
+      return files;
+    }
+
+    Builder inPlace(boolean inPlace) {
+      this.inPlace = inPlace;
+      return this;
+    }
+
+    ImmutableRangeSet.Builder<Integer> linesBuilder() {
+      return lines;
+    }
+
+    Builder addOffset(Integer offset) {
+      offsets.add(offset);
+      return this;
+    }
+
+    Builder addLength(Integer length) {
+      lengths.add(length);
+      return this;
+    }
+
+    Builder aosp(boolean aosp) {
+      this.aosp = aosp;
+      return this;
+    }
+
+    Builder version(boolean version) {
+      this.version = version;
+      return this;
+    }
+
+    Builder help(boolean help) {
+      this.help = help;
+      return this;
+    }
+
+    Builder stdin(boolean stdin) {
+      this.stdin = stdin;
+      return this;
+    }
+
+    Builder fixImportsOnly(boolean fixImportsOnly) {
+      this.fixImportsOnly = fixImportsOnly;
+      return this;
+    }
+
+    Builder sortImports(boolean sortImports) {
+      this.sortImports = sortImports;
+      return this;
+    }
+
+    Builder removeUnusedImports(boolean removeUnusedImports) {
+      this.removeUnusedImports = removeUnusedImports;
+      return this;
+    }
+
+    Builder dryRun(boolean dryRun) {
+      this.dryRun = dryRun;
+      return this;
+    }
+
+    Builder setExitIfChanged(boolean setExitIfChanged) {
+      this.setExitIfChanged = setExitIfChanged;
+      return this;
+    }
+
+    Builder assumeFilename(String assumeFilename) {
+      this.assumeFilename = Optional.of(assumeFilename);
+      return this;
+    }
+
+    Builder reflowLongStrings(boolean reflowLongStrings) {
+      this.reflowLongStrings = reflowLongStrings;
+      return this;
+    }
+
+    Builder formatJavadoc(boolean formatJavadoc) {
+      this.formatJavadoc = formatJavadoc;
+      return this;
+    }
+
+    CommandLineOptions build() {
+      return new CommandLineOptions(
+          files.build(),
+          inPlace,
+          lines.build(),
+          offsets.build(),
+          lengths.build(),
+          aosp,
+          version,
+          help,
+          stdin,
+          fixImportsOnly,
+          sortImports,
+          removeUnusedImports,
+          dryRun,
+          setExitIfChanged,
+          assumeFilename,
+          reflowLongStrings,
+          formatJavadoc);
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
new file mode 100644
index 0000000..2023826
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableRangeSet;
+import com.google.common.collect.Range;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/** A parser for {@link CommandLineOptions}. */
+final class CommandLineOptionsParser {
+
+  private static final Splitter COMMA_SPLITTER = Splitter.on(',');
+  private static final Splitter COLON_SPLITTER = Splitter.on(':');
+  private static final Splitter ARG_SPLITTER =
+      Splitter.on(CharMatcher.breakingWhitespace()).omitEmptyStrings().trimResults();
+
+  /** Parses {@link CommandLineOptions}. */
+  static CommandLineOptions parse(Iterable<String> options) {
+    CommandLineOptions.Builder optionsBuilder = CommandLineOptions.builder();
+    List<String> expandedOptions = new ArrayList<>();
+    expandParamsFiles(options, expandedOptions);
+    Iterator<String> it = expandedOptions.iterator();
+    while (it.hasNext()) {
+      String option = it.next();
+      if (!option.startsWith("-")) {
+        optionsBuilder.filesBuilder().add(option).addAll(it);
+        break;
+      }
+      String flag;
+      String value;
+      int idx = option.indexOf('=');
+      if (idx >= 0) {
+        flag = option.substring(0, idx);
+        value = option.substring(idx + 1, option.length());
+      } else {
+        flag = option;
+        value = null;
+      }
+      // NOTE: update usage information in UsageException when new flags are added
+      switch (flag) {
+        case "-i":
+        case "-r":
+        case "-replace":
+        case "--replace":
+          optionsBuilder.inPlace(true);
+          break;
+        case "--lines":
+        case "-lines":
+        case "--line":
+        case "-line":
+          parseRangeSet(optionsBuilder.linesBuilder(), getValue(flag, it, value));
+          break;
+        case "--offset":
+        case "-offset":
+          optionsBuilder.addOffset(parseInteger(it, flag, value));
+          break;
+        case "--length":
+        case "-length":
+          optionsBuilder.addLength(parseInteger(it, flag, value));
+          break;
+        case "--aosp":
+        case "-aosp":
+        case "-a":
+          optionsBuilder.aosp(true);
+          break;
+        case "--version":
+        case "-version":
+        case "-v":
+          optionsBuilder.version(true);
+          break;
+        case "--help":
+        case "-help":
+        case "-h":
+          optionsBuilder.help(true);
+          break;
+        case "--fix-imports-only":
+          optionsBuilder.fixImportsOnly(true);
+          break;
+        case "--skip-sorting-imports":
+          optionsBuilder.sortImports(false);
+          break;
+        case "--skip-removing-unused-imports":
+          optionsBuilder.removeUnusedImports(false);
+          break;
+        case "--skip-reflowing-long-strings":
+          optionsBuilder.reflowLongStrings(false);
+          break;
+        case "--skip-javadoc-formatting":
+          optionsBuilder.formatJavadoc(false);
+          break;
+        case "-":
+          optionsBuilder.stdin(true);
+          break;
+        case "-n":
+        case "--dry-run":
+          optionsBuilder.dryRun(true);
+          break;
+        case "--set-exit-if-changed":
+          optionsBuilder.setExitIfChanged(true);
+          break;
+        case "-assume-filename":
+        case "--assume-filename":
+          optionsBuilder.assumeFilename(getValue(flag, it, value));
+          break;
+        default:
+          throw new IllegalArgumentException("unexpected flag: " + flag);
+      }
+    }
+    return optionsBuilder.build();
+  }
+
+  private static Integer parseInteger(Iterator<String> it, String flag, String value) {
+    try {
+      return Integer.valueOf(getValue(flag, it, value));
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException(
+          String.format("invalid integer value for %s: %s", flag, value), e);
+    }
+  }
+
+  private static String getValue(String flag, Iterator<String> it, String value) {
+    if (value != null) {
+      return value;
+    }
+    if (!it.hasNext()) {
+      throw new IllegalArgumentException("required value was not provided for: " + flag);
+    }
+    return it.next();
+  }
+
+  /**
+   * Parse multiple --lines flags, like {"1:12,14,20:36", "40:45,50"}. Multiple ranges can be given
+   * with multiple --lines flags or separated by commas. A single line can be set by a single
+   * number. Line numbers are {@code 1}-based, but are converted to the {@code 0}-based numbering
+   * used internally by google-java-format.
+   */
+  private static void parseRangeSet(ImmutableRangeSet.Builder<Integer> result, String ranges) {
+    for (String range : COMMA_SPLITTER.split(ranges)) {
+      result.add(parseRange(range));
+    }
+  }
+
+  /**
+   * Parse a range, as in "1:12" or "42". Line numbers provided are {@code 1}-based, but are
+   * converted here to {@code 0}-based.
+   */
+  private static Range<Integer> parseRange(String arg) {
+    List<String> args = COLON_SPLITTER.splitToList(arg);
+    switch (args.size()) {
+      case 1:
+        int line = Integer.parseInt(args.get(0)) - 1;
+        return Range.closedOpen(line, line + 1);
+      case 2:
+        int line0 = Integer.parseInt(args.get(0)) - 1;
+        int line1 = Integer.parseInt(args.get(1)) - 1;
+        return Range.closedOpen(line0, line1 + 1);
+      default:
+        throw new IllegalArgumentException(arg);
+    }
+  }
+
+  /**
+   * Pre-processes an argument list, expanding arguments of the form {@code @filename} by reading
+   * the content of the file and appending whitespace-delimited options to {@code arguments}.
+   */
+  private static void expandParamsFiles(Iterable<String> args, List<String> expanded) {
+    for (String arg : args) {
+      if (arg.isEmpty()) {
+        continue;
+      }
+      if (!arg.startsWith("@")) {
+        expanded.add(arg);
+      } else if (arg.startsWith("@@")) {
+        expanded.add(arg.substring(1));
+      } else {
+        Path path = Paths.get(arg.substring(1));
+        try {
+          String sequence = new String(Files.readAllBytes(path), UTF_8);
+          expandParamsFiles(ARG_SPLITTER.split(sequence), expanded);
+        } catch (IOException e) {
+          throw new UncheckedIOException(path + ": could not read file: " + e.getMessage(), e);
+        }
+      }
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java b/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java
new file mode 100644
index 0000000..4bd19be
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.google.common.collect.ImmutableList;
+import com.sun.source.tree.AnnotatedTypeTree;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ArrayTypeTree;
+import com.sun.source.tree.Tree;
+import com.sun.tools.javac.tree.JCTree;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Utilities for working with array dimensions.
+ *
+ * <p>javac's parser does not preserve concrete syntax for mixed-notation arrays, so we have to
+ * re-lex the input to extra it.
+ *
+ * <p>For example, {@code int [] a;} cannot be distinguished from {@code int [] a [];} in the AST.
+ */
+class DimensionHelpers {
+
+  /** The array dimension specifiers (including any type annotations) associated with a type. */
+  static class TypeWithDims {
+    final Tree node;
+    final ImmutableList<List<AnnotationTree>> dims;
+
+    public TypeWithDims(Tree node, ImmutableList<List<AnnotationTree>> dims) {
+      this.node = node;
+      this.dims = dims;
+    }
+  }
+
+  enum SortedDims {
+    YES,
+    NO
+  }
+
+  /** Returns a (possibly re-ordered) {@link TypeWithDims} for the given type. */
+  static TypeWithDims extractDims(Tree node, SortedDims sorted) {
+    Deque<List<AnnotationTree>> builder = new ArrayDeque<>();
+    node = extractDims(builder, node);
+    Iterable<List<AnnotationTree>> dims;
+    if (sorted == SortedDims.YES) {
+      dims = reorderBySourcePosition(builder);
+    } else {
+      dims = builder;
+    }
+    return new TypeWithDims(node, ImmutableList.copyOf(dims));
+  }
+
+  /**
+   * Rotate the list of dimension specifiers until all dimensions with type annotations appear in
+   * source order.
+   *
+   * <p>javac reorders dimension specifiers in method declarations with mixed-array notation, which
+   * means that any type annotations don't appear in source order.
+   *
+   * <p>For example, the type of {@code int @A [] f() @B [] {}} is parsed as {@code @B [] @A []}.
+   *
+   * <p>This doesn't handle cases with un-annotated dimension specifiers, so the formatting logic
+   * checks the token stream to figure out which side of the method name they appear on.
+   */
+  private static Iterable<List<AnnotationTree>> reorderBySourcePosition(
+      Deque<List<AnnotationTree>> dims) {
+    int lastAnnotation = -1;
+    int lastPos = -1;
+    int idx = 0;
+    for (List<AnnotationTree> dim : dims) {
+      if (!dim.isEmpty()) {
+        int pos = ((JCTree) dim.get(0)).getStartPosition();
+        if (pos < lastPos) {
+          List<List<AnnotationTree>> list = new ArrayList<>(dims);
+          Collections.rotate(list, -(lastAnnotation + 1));
+          return list;
+        }
+        lastPos = pos;
+        lastAnnotation = idx;
+      }
+      idx++;
+    }
+    return dims;
+  }
+
+  /**
+   * Accumulates a flattened list of array dimensions specifiers with type annotations, and returns
+   * the base type.
+   *
+   * <p>Given {@code int @A @B [][] @C []}, adds {@code [[@A, @B], [@C]]} to dims and returns {@code
+   * int}.
+   */
+  private static Tree extractDims(Deque<List<AnnotationTree>> dims, Tree node) {
+    switch (node.getKind()) {
+      case ARRAY_TYPE:
+        return extractDims(dims, ((ArrayTypeTree) node).getType());
+      case ANNOTATED_TYPE:
+        AnnotatedTypeTree annotatedTypeTree = (AnnotatedTypeTree) node;
+        if (annotatedTypeTree.getUnderlyingType().getKind() != Tree.Kind.ARRAY_TYPE) {
+          return node;
+        }
+        node = extractDims(dims, annotatedTypeTree.getUnderlyingType());
+        dims.addFirst(ImmutableList.copyOf(annotatedTypeTree.getAnnotations()));
+        return node;
+      default:
+        return node;
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
new file mode 100644
index 0000000..9d8ae41
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import java.util.concurrent.Callable;
+
+/**
+ * Encapsulates information about a file to be formatted, including which parts of the file to
+ * format.
+ */
+class FormatFileCallable implements Callable<String> {
+  private final String input;
+  private final CommandLineOptions parameters;
+  private final JavaFormatterOptions options;
+
+  public FormatFileCallable(
+      CommandLineOptions parameters, String input, JavaFormatterOptions options) {
+    this.input = input;
+    this.parameters = parameters;
+    this.options = options;
+  }
+
+  @Override
+  public String call() throws FormatterException {
+    if (parameters.fixImportsOnly()) {
+      return fixImports(input);
+    }
+
+    Formatter formatter = new Formatter(options);
+    String formatted = formatter.formatSource(input, characterRanges(input).asRanges());
+    formatted = fixImports(formatted);
+    if (parameters.reflowLongStrings()) {
+      formatted = StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, formatted, formatter);
+    }
+    return formatted;
+  }
+
+  private String fixImports(String input) throws FormatterException {
+    if (parameters.removeUnusedImports()) {
+      input = RemoveUnusedImports.removeUnusedImports(input);
+    }
+    if (parameters.sortImports()) {
+      input = ImportOrderer.reorderImports(input, options.style());
+    }
+    return input;
+  }
+
+  private RangeSet<Integer> characterRanges(String input) {
+    final RangeSet<Integer> characterRanges = TreeRangeSet.create();
+
+    if (parameters.lines().isEmpty() && parameters.offsets().isEmpty()) {
+      characterRanges.add(Range.closedOpen(0, input.length()));
+      return characterRanges;
+    }
+
+    characterRanges.addAll(Formatter.lineRangesToCharRanges(input, parameters.lines()));
+
+    for (int i = 0; i < parameters.offsets().size(); i++) {
+      Integer length = parameters.lengths().get(i);
+      if (length == 0) {
+        // 0 stands for "format the line under the cursor"
+        length = 1;
+      }
+      characterRanges.add(
+          Range.closedOpen(parameters.offsets().get(i), parameters.offsets().get(i) + length));
+    }
+
+    return characterRanges;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
new file mode 100644
index 0000000..3e97395
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_VERSION;
+import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import com.google.common.io.CharSink;
+import com.google.common.io.CharSource;
+import com.google.errorprone.annotations.Immutable;
+import com.google.googlejavaformat.Doc;
+import com.google.googlejavaformat.DocBuilder;
+import com.google.googlejavaformat.FormattingError;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.Op;
+import com.google.googlejavaformat.OpsBuilder;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.parser.JavacParser;
+import com.sun.tools.javac.parser.ParserFactory;
+import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Options;
+import java.io.IOError;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DiagnosticListener;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * This is google-java-format, a new Java formatter that follows the Google Java Style Guide quite
+ * precisely---to the letter and to the spirit.
+ *
+ * <p>This formatter uses the javac parser to generate an AST. Because the AST loses information
+ * about the non-tokens in the input (including newlines, comments, etc.), and even some tokens
+ * (e.g., optional commas or semicolons), this formatter lexes the input again and follows along in
+ * the resulting list of tokens. Its lexer splits all multi-character operators (like "&gt;&gt;")
+ * into multiple single-character operators. Each non-token is assigned to a token---non-tokens
+ * following a token on the same line go with that token; those following go with the next token---
+ * and there is a final EOF token to hold final comments.
+ *
+ * <p>The formatter walks the AST to generate a Greg Nelson/Derek Oppen-style list of formatting
+ * {@link Op}s [1--2] that then generates a structured {@link Doc}. Each AST node type has a visitor
+ * to emit a sequence of {@link Op}s for the node.
+ *
+ * <p>Some data-structure operations are easier in the list of {@link Op}s, while others become
+ * easier in the {@link Doc}. The {@link Op}s are walked to attach the comments. As the {@link Op}s
+ * are generated, missing input tokens are inserted and incorrect output tokens are dropped,
+ * ensuring that the output matches the input even in the face of formatter errors. Finally, the
+ * formatter walks the {@link Doc} to format it in the given width.
+ *
+ * <p>This formatter also produces data structures of which tokens and comments appear where on the
+ * input, and on the output, to help output a partial reformatting of a slightly edited input.
+ *
+ * <p>Instances of the formatter are immutable and thread-safe.
+ *
+ * <p>[1] Nelson, Greg, and John DeTreville. Personal communication.
+ *
+ * <p>[2] Oppen, Derek C. "Prettyprinting". ACM Transactions on Programming Languages and Systems,
+ * Volume 2 Issue 4, Oct. 1980, pp. 465–483.
+ */
+@Immutable
+public final class Formatter {
+
+  public static final int MAX_LINE_LENGTH = 100;
+
+  static final Range<Integer> EMPTY_RANGE = Range.closedOpen(-1, -1);
+
+  private final JavaFormatterOptions options;
+
+  /** A new Formatter instance with default options. */
+  public Formatter() {
+    this(JavaFormatterOptions.defaultOptions());
+  }
+
+  public Formatter(JavaFormatterOptions options) {
+    this.options = options;
+  }
+
+  /**
+   * Construct a {@code Formatter} given a Java compilation unit. Parses the code; builds a {@link
+   * JavaInput} and the corresponding {@link JavaOutput}.
+   *
+   * @param javaInput the input, a Java compilation unit
+   * @param javaOutput the {@link JavaOutput}
+   * @param options the {@link JavaFormatterOptions}
+   */
+  static void format(final JavaInput javaInput, JavaOutput javaOutput, JavaFormatterOptions options)
+      throws FormatterException {
+    Context context = new Context();
+    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+    context.put(DiagnosticListener.class, diagnostics);
+    Options.instance(context).put("allowStringFolding", "false");
+    Options.instance(context).put("--enable-preview", "true");
+    JCCompilationUnit unit;
+    JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
+    try {
+      fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
+    } catch (IOException e) {
+      // impossible
+      throw new IOError(e);
+    }
+    SimpleJavaFileObject source =
+        new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
+          @Override
+          public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+            return javaInput.getText();
+          }
+        };
+    Log.instance(context).useSource(source);
+    ParserFactory parserFactory = ParserFactory.instance(context);
+    JavacParser parser =
+        parserFactory.newParser(
+            javaInput.getText(),
+            /*keepDocComments=*/ true,
+            /*keepEndPos=*/ true,
+            /*keepLineMap=*/ true);
+    unit = parser.parseCompilationUnit();
+    unit.sourcefile = source;
+
+    javaInput.setCompilationUnit(unit);
+    Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
+        Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
+    if (!Iterables.isEmpty(errorDiagnostics)) {
+      throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
+    }
+    OpsBuilder builder = new OpsBuilder(javaInput, javaOutput);
+    // Output the compilation unit.
+    JavaInputAstVisitor visitor;
+    if (getMajor() >= 14) {
+      try {
+        visitor =
+            Class.forName("com.google.googlejavaformat.java.java14.Java14InputAstVisitor")
+                .asSubclass(JavaInputAstVisitor.class)
+                .getConstructor(OpsBuilder.class, int.class)
+                .newInstance(builder, options.indentationMultiplier());
+      } catch (ReflectiveOperationException e) {
+        throw new LinkageError(e.getMessage(), e);
+      }
+    } else {
+      visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier());
+    }
+    visitor.scan(unit, null);
+    builder.sync(javaInput.getText().length());
+    builder.drain();
+    Doc doc = new DocBuilder().withOps(builder.build()).build();
+    doc.computeBreaks(javaOutput.getCommentsHelper(), MAX_LINE_LENGTH, new Doc.State(+0, 0));
+    doc.write(javaOutput);
+    javaOutput.flush();
+  }
+
+  // Runtime.Version was added in JDK 9, so use reflection to access it to preserve source
+  // compatibility with Java 8.
+  private static int getMajor() {
+    try {
+      Method versionMethod = Runtime.class.getMethod("version");
+      Object version = versionMethod.invoke(null);
+      return (int) version.getClass().getMethod("major").invoke(version);
+    } catch (Exception e) {
+      // continue below
+    }
+    int version = (int) Double.parseDouble(JAVA_CLASS_VERSION.value());
+    if (49 <= version && version <= 52) {
+      return version - (49 - 5);
+    }
+    throw new IllegalStateException("Unknown Java version: " + JAVA_SPECIFICATION_VERSION.value());
+  }
+
+  static boolean errorDiagnostic(Diagnostic<?> input) {
+    if (input.getKind() != Diagnostic.Kind.ERROR) {
+      return false;
+    }
+    switch (input.getCode()) {
+      case "compiler.err.invalid.meth.decl.ret.type.req":
+        // accept constructor-like method declarations that don't match the name of their
+        // enclosing class
+        return false;
+      default:
+        break;
+    }
+    return true;
+  }
+
+  /**
+   * Format the given input (a Java compilation unit) into the output stream.
+   *
+   * @throws FormatterException if the input cannot be parsed
+   */
+  public void formatSource(CharSource input, CharSink output)
+      throws FormatterException, IOException {
+    // TODO(cushon): proper support for streaming input/output. Input may
+    // not be feasible (parsing) but output should be easier.
+    output.write(formatSource(input.read()));
+  }
+
+  /**
+   * Format an input string (a Java compilation unit) into an output string.
+   *
+   * <p>Leaves import statements untouched.
+   *
+   * @param input the input string
+   * @return the output string
+   * @throws FormatterException if the input string cannot be parsed
+   */
+  public String formatSource(String input) throws FormatterException {
+    return formatSource(input, ImmutableList.of(Range.closedOpen(0, input.length())));
+  }
+
+  /**
+   * Formats an input string (a Java compilation unit) and fixes imports.
+   *
+   * <p>Fixing imports includes ordering, spacing, and removal of unused import statements.
+   *
+   * @param input the input string
+   * @return the output string
+   * @throws FormatterException if the input string cannot be parsed
+   * @see <a
+   *     href="https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing">
+   *     Google Java Style Guide - 3.3.3 Import ordering and spacing</a>
+   */
+  public String formatSourceAndFixImports(String input) throws FormatterException {
+    input = ImportOrderer.reorderImports(input, options.style());
+    input = RemoveUnusedImports.removeUnusedImports(input);
+    String formatted = formatSource(input);
+    formatted = StringWrapper.wrap(formatted, this);
+    return formatted;
+  }
+
+  /**
+   * Format an input string (a Java compilation unit), for only the specified character ranges.
+   * These ranges are extended as necessary (e.g., to encompass whole lines).
+   *
+   * @param input the input string
+   * @param characterRanges the character ranges to be reformatted
+   * @return the output string
+   * @throws FormatterException if the input string cannot be parsed
+   */
+  public String formatSource(String input, Collection<Range<Integer>> characterRanges)
+      throws FormatterException {
+    return JavaOutput.applyReplacements(input, getFormatReplacements(input, characterRanges));
+  }
+
+  /**
+   * Emit a list of {@link Replacement}s to convert from input to output.
+   *
+   * @param input the input compilation unit
+   * @param characterRanges the character ranges to reformat
+   * @return a list of {@link Replacement}s, sorted from low index to high index, without overlaps
+   * @throws FormatterException if the input string cannot be parsed
+   */
+  public ImmutableList<Replacement> getFormatReplacements(
+      String input, Collection<Range<Integer>> characterRanges) throws FormatterException {
+    JavaInput javaInput = new JavaInput(input);
+
+    // TODO(cushon): this is only safe because the modifier ordering doesn't affect whitespace,
+    // and doesn't change the replacements that are output. This is not true in general for
+    // 'de-linting' changes (e.g. import ordering).
+    javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges);
+
+    String lineSeparator = Newlines.guessLineSeparator(input);
+    JavaOutput javaOutput =
+        new JavaOutput(lineSeparator, javaInput, new JavaCommentsHelper(lineSeparator, options));
+    try {
+      format(javaInput, javaOutput, options);
+    } catch (FormattingError e) {
+      throw new FormatterException(e.diagnostics());
+    }
+    RangeSet<Integer> tokenRangeSet = javaInput.characterRangesToTokenRanges(characterRanges);
+    return javaOutput.getFormatReplacements(tokenRangeSet);
+  }
+
+  /**
+   * Converts zero-indexed, [closed, open) line ranges in the given source file to character ranges.
+   */
+  public static RangeSet<Integer> lineRangesToCharRanges(
+      String input, RangeSet<Integer> lineRanges) {
+    List<Integer> lines = new ArrayList<>();
+    Iterators.addAll(lines, Newlines.lineOffsetIterator(input));
+    lines.add(input.length() + 1);
+
+    final RangeSet<Integer> characterRanges = TreeRangeSet.create();
+    for (Range<Integer> lineRange :
+        lineRanges.subRangeSet(Range.closedOpen(0, lines.size() - 1)).asRanges()) {
+      int lineStart = lines.get(lineRange.lowerEndpoint());
+      // Exclude the trailing newline. This isn't strictly necessary, but handling blank lines
+      // as empty ranges is convenient.
+      int lineEnd = lines.get(lineRange.upperEndpoint()) - 1;
+      Range<Integer> range = Range.closedOpen(lineStart, lineEnd);
+      characterRanges.add(range);
+    }
+    return characterRanges;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java
new file mode 100644
index 0000000..3ccb44a
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static java.util.Locale.ENGLISH;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.googlejavaformat.FormatterDiagnostic;
+import java.util.List;
+import javax.tools.Diagnostic;
+import javax.tools.JavaFileObject;
+
+/** Checked exception class for formatter errors. */
+public final class FormatterException extends Exception {
+
+  private ImmutableList<FormatterDiagnostic> diagnostics;
+
+  public FormatterException(String message) {
+    this(FormatterDiagnostic.create(message));
+  }
+
+  public FormatterException(FormatterDiagnostic diagnostic) {
+    this(ImmutableList.of(diagnostic));
+  }
+
+  public FormatterException(Iterable<FormatterDiagnostic> diagnostics) {
+    super(diagnostics.iterator().next().toString());
+    this.diagnostics = ImmutableList.copyOf(diagnostics);
+  }
+
+  public List<FormatterDiagnostic> diagnostics() {
+    return diagnostics;
+  }
+
+  public static FormatterException fromJavacDiagnostics(
+      Iterable<Diagnostic<? extends JavaFileObject>> diagnostics) {
+    return new FormatterException(Iterables.transform(diagnostics, d -> toFormatterDiagnostic(d)));
+  }
+
+  private static FormatterDiagnostic toFormatterDiagnostic(Diagnostic<?> input) {
+    return FormatterDiagnostic.create(
+        (int) input.getLineNumber(), (int) input.getColumnNumber(), input.getMessage(ENGLISH));
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template
new file mode 100644
index 0000000..88706fb
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+class GoogleJavaFormatVersion {
+
+  static String version() {
+    return "%VERSION%";
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java
new file mode 100644
index 0000000..a82715e
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.googlejavaformat.java;
+
+import static com.google.common.collect.Iterables.getLast;
+import static com.google.common.primitives.Booleans.trueFirst;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import com.google.googlejavaformat.java.JavaInput.Tok;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.stream.Stream;
+
+/** Orders imports in Java source code. */
+public class ImportOrderer {
+
+  private static final Splitter DOT_SPLITTER = Splitter.on('.');
+
+  /**
+   * Reorder the inputs in {@code text}, a complete Java program. On success, another complete Java
+   * program is returned, which is the same as the original except the imports are in order.
+   *
+   * @throws FormatterException if the input could not be parsed.
+   */
+  public static String reorderImports(String text, Style style) throws FormatterException {
+    ImmutableList<Tok> toks = JavaInput.buildToks(text, CLASS_START);
+    return new ImportOrderer(text, toks, style).reorderImports();
+  }
+
+  /**
+   * Reorder the inputs in {@code text}, a complete Java program, in Google style. On success,
+   * another complete Java program is returned, which is the same as the original except the imports
+   * are in order.
+   *
+   * @deprecated Use {@link #reorderImports(String, Style)} instead
+   * @throws FormatterException if the input could not be parsed.
+   */
+  @Deprecated
+  public static String reorderImports(String text) throws FormatterException {
+    return reorderImports(text, Style.GOOGLE);
+  }
+
+  private String reorderImports() throws FormatterException {
+    int firstImportStart;
+    Optional<Integer> maybeFirstImport = findIdentifier(0, IMPORT_OR_CLASS_START);
+    if (!maybeFirstImport.isPresent() || !tokenAt(maybeFirstImport.get()).equals("import")) {
+      // No imports, so nothing to do.
+      return text;
+    }
+    firstImportStart = maybeFirstImport.get();
+    int unindentedFirstImportStart = unindent(firstImportStart);
+
+    ImportsAndIndex imports = scanImports(firstImportStart);
+    int afterLastImport = imports.index;
+
+    // Make sure there are no more imports before the next class (etc) definition.
+    Optional<Integer> maybeLaterImport = findIdentifier(afterLastImport, IMPORT_OR_CLASS_START);
+    if (maybeLaterImport.isPresent() && tokenAt(maybeLaterImport.get()).equals("import")) {
+      throw new FormatterException("Imports not contiguous (perhaps a comment separates them?)");
+    }
+
+    StringBuilder result = new StringBuilder();
+    String prefix = tokString(0, unindentedFirstImportStart);
+    result.append(prefix);
+    if (!prefix.isEmpty() && Newlines.getLineEnding(prefix) == null) {
+      result.append(lineSeparator).append(lineSeparator);
+    }
+    result.append(reorderedImportsString(imports.imports));
+
+    List<String> tail = new ArrayList<>();
+    tail.add(CharMatcher.whitespace().trimLeadingFrom(tokString(afterLastImport, toks.size())));
+    if (!toks.isEmpty()) {
+      Tok lastTok = getLast(toks);
+      int tailStart = lastTok.getPosition() + lastTok.length();
+      tail.add(text.substring(tailStart));
+    }
+    if (tail.stream().anyMatch(s -> !s.isEmpty())) {
+      result.append(lineSeparator);
+      tail.forEach(result::append);
+    }
+
+    return result.toString();
+  }
+
+  /**
+   * {@link TokenKind}s that indicate the start of a type definition. We use this to avoid scanning
+   * the whole file, since we know that imports must precede any type definition.
+   */
+  private static final ImmutableSet<TokenKind> CLASS_START =
+      ImmutableSet.of(TokenKind.CLASS, TokenKind.INTERFACE, TokenKind.ENUM);
+
+  /**
+   * We use this set to find the first import, and again to check that there are no imports after
+   * the place we stopped gathering them. An annotation definition ({@code @interface}) is two
+   * tokens, the second which is {@code interface}, so we don't need a separate entry for that.
+   */
+  private static final ImmutableSet<String> IMPORT_OR_CLASS_START =
+      ImmutableSet.of("import", "class", "interface", "enum");
+
+  /**
+   * A {@link Comparator} that orders {@link Import}s by Google Style, defined at
+   * https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing.
+   */
+  private static final Comparator<Import> GOOGLE_IMPORT_COMPARATOR =
+      Comparator.comparing(Import::isStatic, trueFirst()).thenComparing(Import::imported);
+
+  /**
+   * A {@link Comparator} that orders {@link Import}s by AOSP Style, defined at
+   * https://source.android.com/setup/contribute/code-style#order-import-statements and implemented
+   * in IntelliJ at
+   * https://android.googlesource.com/platform/development/+/master/ide/intellij/codestyles/AndroidStyle.xml.
+   */
+  private static final Comparator<Import> AOSP_IMPORT_COMPARATOR =
+      Comparator.comparing(Import::isStatic, trueFirst())
+          .thenComparing(Import::isAndroid, trueFirst())
+          .thenComparing(Import::isThirdParty, trueFirst())
+          .thenComparing(Import::isJava, trueFirst())
+          .thenComparing(Import::imported);
+
+  /**
+   * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
+   * Import}s based on Google style.
+   */
+  private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) {
+    return prev.isStatic() && !curr.isStatic();
+  }
+
+  /**
+   * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link
+   * Import}s based on AOSP style.
+   */
+  private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) {
+    if (prev.isStatic() && !curr.isStatic()) {
+      return true;
+    }
+    // insert blank line between "com.android" from "com.anythingelse"
+    if (prev.isAndroid() && !curr.isAndroid()) {
+      return true;
+    }
+    return !prev.topLevel().equals(curr.topLevel());
+  }
+
+  private final String text;
+  private final ImmutableList<Tok> toks;
+  private final String lineSeparator;
+  private final Comparator<Import> importComparator;
+  private final BiFunction<Import, Import, Boolean> shouldInsertBlankLineFn;
+
+  private ImportOrderer(String text, ImmutableList<Tok> toks, Style style) {
+    this.text = text;
+    this.toks = toks;
+    this.lineSeparator = Newlines.guessLineSeparator(text);
+    if (style.equals(Style.GOOGLE)) {
+      this.importComparator = GOOGLE_IMPORT_COMPARATOR;
+      this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineGoogle;
+    } else if (style.equals(Style.AOSP)) {
+      this.importComparator = AOSP_IMPORT_COMPARATOR;
+      this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineAosp;
+    } else {
+      throw new IllegalArgumentException("Unsupported code style: " + style);
+    }
+  }
+
+  /** An import statement. */
+  class Import {
+    private final String imported;
+    private final boolean isStatic;
+    private final String trailing;
+
+    Import(String imported, String trailing, boolean isStatic) {
+      this.imported = imported;
+      this.trailing = trailing;
+      this.isStatic = isStatic;
+    }
+
+    /** The name being imported, for example {@code java.util.List}. */
+    String imported() {
+      return imported;
+    }
+
+    /** True if this is {@code import static}. */
+    boolean isStatic() {
+      return isStatic;
+    }
+
+    /** The top-level package of the import. */
+    String topLevel() {
+      return DOT_SPLITTER.split(imported()).iterator().next();
+    }
+
+    /** True if this is an Android import per AOSP style. */
+    boolean isAndroid() {
+      return Stream.of("android.", "androidx.", "dalvik.", "libcore.", "com.android.")
+          .anyMatch(imported::startsWith);
+    }
+
+    /** True if this is a Java import per AOSP style. */
+    boolean isJava() {
+      switch (topLevel()) {
+        case "java":
+        case "javax":
+          return true;
+        default:
+          return false;
+      }
+    }
+
+    /**
+     * The {@code //} comment lines after the final {@code ;}, up to and including the line
+     * terminator of the last one. Note: In case two imports were separated by a space (which is
+     * disallowed by the style guide), the trailing whitespace of the first import does not include
+     * a line terminator.
+     */
+    String trailing() {
+      return trailing;
+    }
+
+    /** True if this is a third-party import per AOSP style. */
+    public boolean isThirdParty() {
+      return !(isAndroid() || isJava());
+    }
+
+    // One or multiple lines, the import itself and following comments, including the line
+    // terminator.
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("import ");
+      if (isStatic()) {
+        sb.append("static ");
+      }
+      sb.append(imported()).append(';');
+      if (trailing().trim().isEmpty()) {
+        sb.append(lineSeparator);
+      } else {
+        sb.append(trailing());
+      }
+      return sb.toString();
+    }
+  }
+
+  private String tokString(int start, int end) {
+    StringBuilder sb = new StringBuilder();
+    for (int i = start; i < end; i++) {
+      sb.append(toks.get(i).getOriginalText());
+    }
+    return sb.toString();
+  }
+
+  private static class ImportsAndIndex {
+    final ImmutableSortedSet<Import> imports;
+    final int index;
+
+    ImportsAndIndex(ImmutableSortedSet<Import> imports, int index) {
+      this.imports = imports;
+      this.index = index;
+    }
+  }
+
+  /**
+   * Scans a sequence of import lines. The parsing uses this approximate grammar:
+   *
+   * <pre>{@code
+   * <imports> -> (<end-of-line> | <import>)*
+   * <import> -> "import" <whitespace> ("static" <whitespace>)?
+   *    <identifier> ("." <identifier>)* ("." "*")? <whitespace>? ";"
+   *    <whitespace>? <end-of-line>? (<line-comment> <end-of-line>)*
+   * }</pre>
+   *
+   * @param i the index to start parsing at.
+   * @return the result of parsing the imports.
+   * @throws FormatterException if imports could not parsed according to the grammar.
+   */
+  private ImportsAndIndex scanImports(int i) throws FormatterException {
+    int afterLastImport = i;
+    ImmutableSortedSet.Builder<Import> imports = ImmutableSortedSet.orderedBy(importComparator);
+    // JavaInput.buildToks appends a zero-width EOF token after all tokens. It won't match any
+    // of our tests here and protects us from running off the end of the toks list. Since it is
+    // zero-width it doesn't matter if we include it in our string concatenation at the end.
+    while (i < toks.size() && tokenAt(i).equals("import")) {
+      i++;
+      if (isSpaceToken(i)) {
+        i++;
+      }
+      boolean isStatic = tokenAt(i).equals("static");
+      if (isStatic) {
+        i++;
+        if (isSpaceToken(i)) {
+          i++;
+        }
+      }
+      if (!isIdentifierToken(i)) {
+        throw new FormatterException("Unexpected token after import: " + tokenAt(i));
+      }
+      StringAndIndex imported = scanImported(i);
+      String importedName = imported.string;
+      i = imported.index;
+      if (isSpaceToken(i)) {
+        i++;
+      }
+      if (!tokenAt(i).equals(";")) {
+        throw new FormatterException("Expected ; after import");
+      }
+      while (tokenAt(i).equals(";")) {
+        // Extra semicolons are not allowed by the JLS but are accepted by javac.
+        i++;
+      }
+      StringBuilder trailing = new StringBuilder();
+      if (isSpaceToken(i)) {
+        trailing.append(tokenAt(i));
+        i++;
+      }
+      if (isNewlineToken(i)) {
+        trailing.append(tokenAt(i));
+        i++;
+      }
+      // Gather (if any) all single line comments and accompanied line terminators following this
+      // import
+      while (isSlashSlashCommentToken(i)) {
+        trailing.append(tokenAt(i));
+        i++;
+        if (isNewlineToken(i)) {
+          trailing.append(tokenAt(i));
+          i++;
+        }
+      }
+      imports.add(new Import(importedName, trailing.toString(), isStatic));
+      // Remember the position just after the import we just saw, before skipping blank lines.
+      // If the next thing after the blank lines is not another import then we don't want to
+      // include those blank lines in the text to be replaced.
+      afterLastImport = i;
+      while (isNewlineToken(i) || isSpaceToken(i)) {
+        i++;
+      }
+    }
+    return new ImportsAndIndex(imports.build(), afterLastImport);
+  }
+
+  // Produces the sorted output based on the imports we have scanned.
+  private String reorderedImportsString(ImmutableSortedSet<Import> imports) {
+    Preconditions.checkArgument(!imports.isEmpty(), "imports");
+
+    // Pretend that the first import was preceded by another import of the same kind, so we don't
+    // insert a newline there.
+    Import prevImport = imports.iterator().next();
+
+    StringBuilder sb = new StringBuilder();
+    for (Import currImport : imports) {
+      if (shouldInsertBlankLineFn.apply(prevImport, currImport)) {
+        // Blank line between static and non-static imports.
+        sb.append(lineSeparator);
+      }
+      sb.append(currImport);
+      prevImport = currImport;
+    }
+    return sb.toString();
+  }
+
+  private static class StringAndIndex {
+    private final String string;
+    private final int index;
+
+    StringAndIndex(String string, int index) {
+      this.string = string;
+      this.index = index;
+    }
+  }
+
+  /**
+   * Scans the imported thing, the dot-separated name that comes after import [static] and before
+   * the semicolon. We don't allow spaces inside the dot-separated name. Wildcard imports are
+   * supported: if the input is {@code import java.util.*;} then the returned string will be {@code
+   * java.util.*}.
+   *
+   * @param start the index of the start of the identifier. If the import is {@code import
+   *     java.util.List;} then this index points to the token {@code java}.
+   * @return the parsed import ({@code java.util.List} in the example) and the index of the first
+   *     token after the imported thing ({@code ;} in the example).
+   * @throws FormatterException if the imported name could not be parsed.
+   */
+  private StringAndIndex scanImported(int start) throws FormatterException {
+    int i = start;
+    StringBuilder imported = new StringBuilder();
+    // At the start of each iteration of this loop, i points to an identifier.
+    // On exit from the loop, i points to a token after an identifier or after *.
+    while (true) {
+      Preconditions.checkState(isIdentifierToken(i));
+      imported.append(tokenAt(i));
+      i++;
+      if (!tokenAt(i).equals(".")) {
+        return new StringAndIndex(imported.toString(), i);
+      }
+      imported.append('.');
+      i++;
+      if (tokenAt(i).equals("*")) {
+        imported.append('*');
+        return new StringAndIndex(imported.toString(), i + 1);
+      } else if (!isIdentifierToken(i)) {
+        throw new FormatterException("Could not parse imported name, at: " + tokenAt(i));
+      }
+    }
+  }
+
+  /**
+   * Returns the index of the first place where one of the given identifiers occurs, or {@code
+   * Optional.empty()} if there is none.
+   *
+   * @param start the index to start looking at
+   * @param identifiers the identifiers to look for
+   */
+  private Optional<Integer> findIdentifier(int start, ImmutableSet<String> identifiers) {
+    for (int i = start; i < toks.size(); i++) {
+      if (isIdentifierToken(i)) {
+        String id = tokenAt(i);
+        if (identifiers.contains(id)) {
+          return Optional.of(i);
+        }
+      }
+    }
+    return Optional.empty();
+  }
+
+  /** Returns the given token, or the preceding token if it is a whitespace token. */
+  private int unindent(int i) {
+    if (i > 0 && isSpaceToken(i - 1)) {
+      return i - 1;
+    } else {
+      return i;
+    }
+  }
+
+  private String tokenAt(int i) {
+    return toks.get(i).getOriginalText();
+  }
+
+  private boolean isIdentifierToken(int i) {
+    String s = tokenAt(i);
+    return !s.isEmpty() && Character.isJavaIdentifierStart(s.codePointAt(0));
+  }
+
+  private boolean isSpaceToken(int i) {
+    String s = tokenAt(i);
+    if (s.isEmpty()) {
+      return false;
+    } else {
+      return " \t\f".indexOf(s.codePointAt(0)) >= 0;
+    }
+  }
+
+  private boolean isSlashSlashCommentToken(int i) {
+    return toks.get(i).isSlashSlashComment();
+  }
+
+  private boolean isNewlineToken(int i) {
+    return toks.get(i).isNewline();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java
new file mode 100644
index 0000000..346324a
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.googlejavaformat.CommentsHelper;
+import com.google.googlejavaformat.Input.Tok;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.java.javadoc.JavadocFormatter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** {@code JavaCommentsHelper} extends {@link CommentsHelper} to rewrite Java comments. */
+public final class JavaCommentsHelper implements CommentsHelper {
+
+  private final String lineSeparator;
+  private final JavaFormatterOptions options;
+
+  public JavaCommentsHelper(String lineSeparator, JavaFormatterOptions options) {
+    this.lineSeparator = lineSeparator;
+    this.options = options;
+  }
+
+  @Override
+  public String rewrite(Tok tok, int maxWidth, int column0) {
+    if (!tok.isComment()) {
+      return tok.getOriginalText();
+    }
+    String text = tok.getOriginalText();
+    if (tok.isJavadocComment() && options.formatJavadoc()) {
+      text = JavadocFormatter.formatJavadoc(text, column0);
+    }
+    List<String> lines = new ArrayList<>();
+    Iterator<String> it = Newlines.lineIterator(text);
+    while (it.hasNext()) {
+      lines.add(CharMatcher.whitespace().trimTrailingFrom(it.next()));
+    }
+    if (tok.isSlashSlashComment()) {
+      return indentLineComments(lines, column0);
+    } else if (javadocShaped(lines)) {
+      return indentJavadoc(lines, column0);
+    } else {
+      return preserveIndentation(lines, column0);
+    }
+  }
+
+  // For non-javadoc-shaped block comments, shift the entire block to the correct
+  // column, but do not adjust relative indentation.
+  private String preserveIndentation(List<String> lines, int column0) {
+    StringBuilder builder = new StringBuilder();
+
+    // find the leftmost non-whitespace character in all trailing lines
+    int startCol = -1;
+    for (int i = 1; i < lines.size(); i++) {
+      int lineIdx = CharMatcher.whitespace().negate().indexIn(lines.get(i));
+      if (lineIdx >= 0 && (startCol == -1 || lineIdx < startCol)) {
+        startCol = lineIdx;
+      }
+    }
+
+    // output the first line at the current column
+    builder.append(lines.get(0));
+
+    // output all trailing lines with plausible indentation
+    for (int i = 1; i < lines.size(); ++i) {
+      builder.append(lineSeparator).append(Strings.repeat(" ", column0));
+      // check that startCol is valid index, e.g. for blank lines
+      if (lines.get(i).length() >= startCol) {
+        builder.append(lines.get(i).substring(startCol));
+      } else {
+        builder.append(lines.get(i));
+      }
+    }
+    return builder.toString();
+  }
+
+  // Wraps and re-indents line comments.
+  private String indentLineComments(List<String> lines, int column0) {
+    lines = wrapLineComments(lines, column0);
+    StringBuilder builder = new StringBuilder();
+    builder.append(lines.get(0).trim());
+    String indentString = Strings.repeat(" ", column0);
+    for (int i = 1; i < lines.size(); ++i) {
+      builder.append(lineSeparator).append(indentString).append(lines.get(i).trim());
+    }
+    return builder.toString();
+  }
+
+  // Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot
+  // contain leading spaces.
+  private static final Pattern LINE_COMMENT_MISSING_SPACE_PREFIX =
+      Pattern.compile("^(//+)(?!noinspection|\\$NON-NLS-\\d+\\$)[^\\s/]");
+
+  private List<String> wrapLineComments(List<String> lines, int column0) {
+    List<String> result = new ArrayList<>();
+    for (String line : lines) {
+      // Add missing leading spaces to line comments: `//foo` -> `// foo`.
+      Matcher matcher = LINE_COMMENT_MISSING_SPACE_PREFIX.matcher(line);
+      if (matcher.find()) {
+        int length = matcher.group(1).length();
+        line = Strings.repeat("/", length) + " " + line.substring(length);
+      }
+      if (line.startsWith("// MOE:")) {
+        // don't wrap comments for https://github.com/google/MOE
+        result.add(line);
+        continue;
+      }
+      while (line.length() + column0 > Formatter.MAX_LINE_LENGTH) {
+        int idx = Formatter.MAX_LINE_LENGTH - column0;
+        // only break on whitespace characters, and ignore the leading `// `
+        while (idx >= 2 && !CharMatcher.whitespace().matches(line.charAt(idx))) {
+          idx--;
+        }
+        if (idx <= 2) {
+          break;
+        }
+        result.add(line.substring(0, idx));
+        line = "//" + line.substring(idx);
+      }
+      result.add(line);
+    }
+    return result;
+  }
+
+  // Remove leading whitespace (trailing was already removed), and re-indent.
+  // Add a +1 indent before '*', and add the '*' if necessary.
+  private String indentJavadoc(List<String> lines, int column0) {
+    StringBuilder builder = new StringBuilder();
+    builder.append(lines.get(0).trim());
+    int indent = column0 + 1;
+    String indentString = Strings.repeat(" ", indent);
+    for (int i = 1; i < lines.size(); ++i) {
+      builder.append(lineSeparator).append(indentString);
+      String line = lines.get(i).trim();
+      if (!line.startsWith("*")) {
+        builder.append("* ");
+      }
+      builder.append(line);
+    }
+    return builder.toString();
+  }
+
+  // Returns true if the comment looks like javadoc
+  private static boolean javadocShaped(List<String> lines) {
+    Iterator<String> it = lines.iterator();
+    if (!it.hasNext()) {
+      return false;
+    }
+    String first = it.next().trim();
+    // if it's actually javadoc, we're done
+    if (first.startsWith("/**")) {
+      return true;
+    }
+    // if it's a block comment, check all trailing lines for '*'
+    if (!first.startsWith("/*")) {
+      return false;
+    }
+    while (it.hasNext()) {
+      if (!it.next().trim().startsWith("*")) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
+
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java
new file mode 100644
index 0000000..4d3d30d
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.google.errorprone.annotations.Immutable;
+
+/**
+ * Options for a google-java-format invocation.
+ *
+ * <p>Like gofmt, the google-java-format CLI exposes <em>no</em> configuration options (aside from
+ * {@code --aosp}).
+ *
+ * <p>The goal of google-java-format is to provide consistent formatting, and to free developers
+ * from arguments over style choices. It is an explicit non-goal to support developers' individual
+ * preferences, and in fact it would work directly against our primary goals.
+ */
+@Immutable
+public class JavaFormatterOptions {
+
+  public enum Style {
+    /** The default Google Java Style configuration. */
+    GOOGLE(1),
+
+    /** The AOSP-compliant configuration. */
+    AOSP(2);
+
+    private final int indentationMultiplier;
+
+    Style(int indentationMultiplier) {
+      this.indentationMultiplier = indentationMultiplier;
+    }
+
+    int indentationMultiplier() {
+      return indentationMultiplier;
+    }
+  }
+
+  private final Style style;
+  private final boolean formatJavadoc;
+
+  private JavaFormatterOptions(Style style, boolean formatJavadoc) {
+    this.style = style;
+    this.formatJavadoc = formatJavadoc;
+  }
+
+  /** Returns the multiplier for the unit of indent. */
+  public int indentationMultiplier() {
+    return style.indentationMultiplier();
+  }
+
+  boolean formatJavadoc() {
+    return formatJavadoc;
+  }
+
+  /** Returns the code style. */
+  public Style style() {
+    return style;
+  }
+
+  /** Returns the default formatting options. */
+  public static JavaFormatterOptions defaultOptions() {
+    return builder().build();
+  }
+
+  /** Returns a builder for {@link JavaFormatterOptions}. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** A builder for {@link JavaFormatterOptions}. */
+  public static class Builder {
+    private Style style = Style.GOOGLE;
+    private boolean formatJavadoc = true;
+
+    private Builder() {}
+
+    public Builder style(Style style) {
+      this.style = style;
+      return this;
+    }
+
+    Builder formatJavadoc(boolean formatJavadoc) {
+      this.formatJavadoc = formatJavadoc;
+      return this;
+    }
+
+    public JavaFormatterOptions build() {
+      return new JavaFormatterOptions(style, formatJavadoc);
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
new file mode 100644
index 0000000..999c8fb
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
@@ -0,0 +1,676 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.getLast;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Verify;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableRangeMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import com.google.googlejavaformat.Input;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.java.JavacTokens.RawTok;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Log.DeferredDiagnosticHandler;
+import com.sun.tools.javac.util.Options;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DiagnosticListener;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.SimpleJavaFileObject;
+
+/** {@code JavaInput} extends {@link Input} to represent a Java input document. */
+public final class JavaInput extends Input {
+  /**
+   * A {@code JavaInput} is a sequence of {@link Tok}s that cover the Java input. A {@link Tok} is
+   * either a token (if {@code isToken()}), or a non-token, which is a comment (if {@code
+   * isComment()}) or a newline (if {@code isNewline()}) or a maximal sequence of other whitespace
+   * characters (if {@code isSpaces()}). Each {@link Tok} contains a sequence of characters, an
+   * index (sequential starting at {@code 0} for tokens and comments, else {@code -1}), and a
+   * ({@code 0}-origin) position in the input. The concatenation of the texts of all the {@link
+   * Tok}s equals the input. Each Input ends with a token EOF {@link Tok}, with empty text.
+   *
+   * <p>A {@code /*} comment possibly contains newlines; a {@code //} comment does not contain the
+   * terminating newline character, but is followed by a newline {@link Tok}.
+   */
+  static final class Tok implements Input.Tok {
+    private final int index;
+    private final String originalText;
+    private final String text;
+    private final int position;
+    private final int columnI;
+    private final boolean isToken;
+    private final TokenKind kind;
+
+    /**
+     * The {@code Tok} constructor.
+     *
+     * @param index its index
+     * @param originalText its original text, before removing Unicode escapes
+     * @param text its text after removing Unicode escapes
+     * @param position its {@code 0}-origin position in the input
+     * @param columnI its {@code 0}-origin column number in the input
+     * @param isToken whether the {@code Tok} is a token
+     * @param kind the token kind
+     */
+    Tok(
+        int index,
+        String originalText,
+        String text,
+        int position,
+        int columnI,
+        boolean isToken,
+        TokenKind kind) {
+      this.index = index;
+      this.originalText = originalText;
+      this.text = text;
+      this.position = position;
+      this.columnI = columnI;
+      this.isToken = isToken;
+      this.kind = kind;
+    }
+
+    @Override
+    public int getIndex() {
+      return index;
+    }
+
+    @Override
+    public String getText() {
+      return text;
+    }
+
+    @Override
+    public String getOriginalText() {
+      return originalText;
+    }
+
+    @Override
+    public int length() {
+      return originalText.length();
+    }
+
+    @Override
+    public int getPosition() {
+      return position;
+    }
+
+    @Override
+    public int getColumn() {
+      return columnI;
+    }
+
+    boolean isToken() {
+      return isToken;
+    }
+
+    @Override
+    public boolean isNewline() {
+      return Newlines.isNewline(text);
+    }
+
+    @Override
+    public boolean isSlashSlashComment() {
+      return text.startsWith("//");
+    }
+
+    @Override
+    public boolean isSlashStarComment() {
+      return text.startsWith("/*");
+    }
+
+    @Override
+    public boolean isJavadocComment() {
+      // comments like `/***` are also javadoc, but their formatting probably won't be improved
+      // by the javadoc formatter
+      return text.startsWith("/**") && text.charAt("/**".length()) != '*' && text.length() > 4;
+    }
+
+    @Override
+    public boolean isComment() {
+      return isSlashSlashComment() || isSlashStarComment();
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("index", index)
+          .add("text", text)
+          .add("position", position)
+          .add("columnI", columnI)
+          .add("isToken", isToken)
+          .toString();
+    }
+
+    public TokenKind kind() {
+      return kind;
+    }
+  }
+
+  /**
+   * A {@link Token} contains a token {@link Tok} and its associated non-tokens; each non-token
+   * {@link Tok} belongs to one {@link Token}. Each {@link Token} has an immutable list of its
+   * non-tokens that appear before it, and another list of its non-tokens that appear after it. The
+   * concatenation of the texts of all the {@link Token}s' {@link Tok}s, each preceded by the texts
+   * of its {@code toksBefore} and followed by the texts of its {@code toksAfter}, equals the input.
+   */
+  static final class Token implements Input.Token {
+    private final Tok tok;
+    private final ImmutableList<Tok> toksBefore;
+    private final ImmutableList<Tok> toksAfter;
+
+    /**
+     * Token constructor.
+     *
+     * @param toksBefore the earlier non-token {link Tok}s assigned to this {@code Token}
+     * @param tok this token {@link Tok}
+     * @param toksAfter the later non-token {link Tok}s assigned to this {@code Token}
+     */
+    Token(List<Tok> toksBefore, Tok tok, List<Tok> toksAfter) {
+      this.toksBefore = ImmutableList.copyOf(toksBefore);
+      this.tok = tok;
+      this.toksAfter = ImmutableList.copyOf(toksAfter);
+    }
+
+    /**
+     * Get the token's {@link Tok}.
+     *
+     * @return the token's {@link Tok}
+     */
+    @Override
+    public Tok getTok() {
+      return tok;
+    }
+
+    /**
+     * Get the earlier {@link Tok}s assigned to this {@code Token}.
+     *
+     * @return the earlier {@link Tok}s assigned to this {@code Token}
+     */
+    @Override
+    public ImmutableList<? extends Input.Tok> getToksBefore() {
+      return toksBefore;
+    }
+
+    /**
+     * Get the later {@link Tok}s assigned to this {@code Token}.
+     *
+     * @return the later {@link Tok}s assigned to this {@code Token}
+     */
+    @Override
+    public ImmutableList<? extends Input.Tok> getToksAfter() {
+      return toksAfter;
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("tok", tok)
+          .add("toksBefore", toksBefore)
+          .add("toksAfter", toksAfter)
+          .toString();
+    }
+  }
+
+  private final String text; // The input.
+  private int kN; // The number of numbered toks (tokens or comments), excluding the EOF.
+
+  /*
+   * The following lists record the sequential indices of the {@code Tok}s on each input line. (Only
+   * tokens and comments have sequential indices.) Tokens and {@code //} comments lie on just one
+   * line; {@code /*} comments can lie on multiple lines. These data structures (along with
+   * equivalent ones for the formatted output) let us compute correspondences between the input and
+   * output.
+   */
+
+  private final ImmutableMap<Integer, Integer> positionToColumnMap; // Map Tok position to column.
+  private final ImmutableList<Token> tokens; // The Tokens for this input.
+  private final ImmutableRangeMap<Integer, Token> positionTokenMap; // Map position to Token.
+
+  /** Map from Tok index to the associated Token. */
+  private final Token[] kToToken;
+
+  /**
+   * Input constructor.
+   *
+   * @param text the input text
+   * @throws FormatterException if the input cannot be parsed
+   */
+  public JavaInput(String text) throws FormatterException {
+    this.text = checkNotNull(text);
+    setLines(ImmutableList.copyOf(Newlines.lineIterator(text)));
+    ImmutableList<Tok> toks = buildToks(text);
+    positionToColumnMap = makePositionToColumnMap(toks);
+    tokens = buildTokens(toks);
+    ImmutableRangeMap.Builder<Integer, Token> tokenLocations = ImmutableRangeMap.builder();
+    for (Token token : tokens) {
+      Input.Tok end = JavaOutput.endTok(token);
+      int upper = end.getPosition();
+      if (!end.getText().isEmpty()) {
+        upper += end.length() - 1;
+      }
+      tokenLocations.put(Range.closed(JavaOutput.startTok(token).getPosition(), upper), token);
+    }
+    positionTokenMap = tokenLocations.build();
+
+    // adjust kN for EOF
+    kToToken = new Token[kN + 1];
+    for (Token token : tokens) {
+      for (Input.Tok tok : token.getToksBefore()) {
+        if (tok.getIndex() < 0) {
+          continue;
+        }
+        kToToken[tok.getIndex()] = token;
+      }
+      kToToken[token.getTok().getIndex()] = token;
+      for (Input.Tok tok : token.getToksAfter()) {
+        if (tok.getIndex() < 0) {
+          continue;
+        }
+        kToToken[tok.getIndex()] = token;
+      }
+    }
+  }
+
+  private static ImmutableMap<Integer, Integer> makePositionToColumnMap(List<Tok> toks) {
+    ImmutableMap.Builder<Integer, Integer> builder = ImmutableMap.builder();
+    for (Tok tok : toks) {
+      builder.put(tok.getPosition(), tok.getColumn());
+    }
+    return builder.build();
+  }
+
+  /**
+   * Get the input text.
+   *
+   * @return the input text
+   */
+  @Override
+  public String getText() {
+    return text;
+  }
+
+  @Override
+  public ImmutableMap<Integer, Integer> getPositionToColumnMap() {
+    return positionToColumnMap;
+  }
+
+  /** Lex the input and build the list of toks. */
+  private ImmutableList<Tok> buildToks(String text) throws FormatterException {
+    ImmutableList<Tok> toks = buildToks(text, ImmutableSet.of());
+    kN = getLast(toks).getIndex();
+    computeRanges(toks);
+    return toks;
+  }
+
+  /**
+   * Lex the input and build the list of toks.
+   *
+   * @param text the text to be lexed.
+   * @param stopTokens a set of tokens which should cause lexing to stop. If one of these is found,
+   *     the returned list will include tokens up to but not including that token.
+   */
+  static ImmutableList<Tok> buildToks(String text, ImmutableSet<TokenKind> stopTokens)
+      throws FormatterException {
+    stopTokens = ImmutableSet.<TokenKind>builder().addAll(stopTokens).add(TokenKind.EOF).build();
+    Context context = new Context();
+    Options.instance(context).put("--enable-preview", "true");
+    new JavacFileManager(context, true, UTF_8);
+    DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
+    context.put(DiagnosticListener.class, diagnosticCollector);
+    Log log = Log.instance(context);
+    log.useSource(
+        new SimpleJavaFileObject(URI.create("Source.java"), Kind.SOURCE) {
+          @Override
+          public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+            return text;
+          }
+        });
+    DeferredDiagnosticHandler diagnostics = new DeferredDiagnosticHandler(log);
+    ImmutableList<RawTok> rawToks = JavacTokens.getTokens(text, context, stopTokens);
+    if (diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) {
+      return ImmutableList.of(new Tok(0, "", "", 0, 0, true, null)); // EOF
+    }
+    int kN = 0;
+    List<Tok> toks = new ArrayList<>();
+    int charI = 0;
+    int columnI = 0;
+    for (RawTok t : rawToks) {
+      if (stopTokens.contains(t.kind())) {
+        break;
+      }
+      int charI0 = t.pos();
+      // Get string, possibly with Unicode escapes.
+      String originalTokText = text.substring(charI0, t.endPos());
+      String tokText =
+          t.kind() == TokenKind.STRINGLITERAL
+              ? t.stringVal() // Unicode escapes removed.
+              : originalTokText;
+      char tokText0 = tokText.charAt(0); // The token's first character.
+      final boolean isToken; // Is this tok a token?
+      final boolean isNumbered; // Is this tok numbered? (tokens and comments)
+      String extraNewline = null; // Extra newline at end?
+      List<String> strings = new ArrayList<>();
+      if (Character.isWhitespace(tokText0)) {
+        isToken = false;
+        isNumbered = false;
+        Iterator<String> it = Newlines.lineIterator(originalTokText);
+        while (it.hasNext()) {
+          String line = it.next();
+          String newline = Newlines.getLineEnding(line);
+          if (newline != null) {
+            String spaces = line.substring(0, line.length() - newline.length());
+            if (!spaces.isEmpty()) {
+              strings.add(spaces);
+            }
+            strings.add(newline);
+          } else if (!line.isEmpty()) {
+            strings.add(line);
+          }
+        }
+      } else if (tokText.startsWith("'") || tokText.startsWith("\"")) {
+        isToken = true;
+        isNumbered = true;
+        strings.add(originalTokText);
+      } else if (tokText.startsWith("//") || tokText.startsWith("/*")) {
+        // For compatibility with an earlier lexer, the newline after a // comment is its own tok.
+        if (tokText.startsWith("//")
+            && (originalTokText.endsWith("\n") || originalTokText.endsWith("\r"))) {
+          extraNewline = Newlines.getLineEnding(originalTokText);
+          tokText = tokText.substring(0, tokText.length() - extraNewline.length());
+          originalTokText =
+              originalTokText.substring(0, originalTokText.length() - extraNewline.length());
+        }
+        isToken = false;
+        isNumbered = true;
+        strings.add(originalTokText);
+      } else if (Character.isJavaIdentifierStart(tokText0)
+          || Character.isDigit(tokText0)
+          || (tokText0 == '.' && tokText.length() > 1 && Character.isDigit(tokText.charAt(1)))) {
+        // Identifier, keyword, or numeric literal (a dot may begin a number, as in .2D).
+        isToken = true;
+        isNumbered = true;
+        strings.add(tokText);
+      } else {
+        // Other tokens ("+" or "++" or ">>" are broken into one-character toks, because ">>"
+        // cannot be lexed without syntactic knowledge. This implementation fails if the token
+        // contains Unicode escapes.
+        isToken = true;
+        isNumbered = true;
+        for (char c : tokText.toCharArray()) {
+          strings.add(String.valueOf(c));
+        }
+      }
+      if (strings.size() == 1) {
+        toks.add(
+            new Tok(
+                isNumbered ? kN++ : -1,
+                originalTokText,
+                tokText,
+                charI,
+                columnI,
+                isToken,
+                t.kind()));
+        charI += originalTokText.length();
+        columnI = updateColumn(columnI, originalTokText);
+
+      } else {
+        if (strings.size() != 1 && !tokText.equals(originalTokText)) {
+          throw new FormatterException(
+              "Unicode escapes not allowed in whitespace or multi-character operators");
+        }
+        for (String str : strings) {
+          toks.add(new Tok(isNumbered ? kN++ : -1, str, str, charI, columnI, isToken, null));
+          charI += str.length();
+          columnI = updateColumn(columnI, originalTokText);
+        }
+      }
+      if (extraNewline != null) {
+        toks.add(new Tok(-1, extraNewline, extraNewline, charI, columnI, false, null));
+        columnI = 0;
+        charI += extraNewline.length();
+      }
+    }
+    toks.add(new Tok(kN, "", "", charI, columnI, true, null)); // EOF tok.
+    return ImmutableList.copyOf(toks);
+  }
+
+  private static int updateColumn(int columnI, String originalTokText) {
+    Integer last = Iterators.getLast(Newlines.lineOffsetIterator(originalTokText));
+    if (last > 0) {
+      columnI = originalTokText.length() - last;
+    } else {
+      columnI += originalTokText.length();
+    }
+    return columnI;
+  }
+
+  private static ImmutableList<Token> buildTokens(List<Tok> toks) {
+    ImmutableList.Builder<Token> tokens = ImmutableList.builder();
+    int k = 0;
+    int kN = toks.size();
+
+    // Remaining non-tokens before the token go here.
+    ImmutableList.Builder<Tok> toksBefore = ImmutableList.builder();
+
+    OUTERMOST:
+    while (k < kN) {
+      while (!toks.get(k).isToken()) {
+        Tok tok = toks.get(k++);
+        toksBefore.add(tok);
+        if (isParamComment(tok)) {
+          while (toks.get(k).isNewline()) {
+            // drop newlines after parameter comments
+            k++;
+          }
+        }
+      }
+      Tok tok = toks.get(k++);
+
+      // Non-tokens starting on the same line go here too.
+      ImmutableList.Builder<Tok> toksAfter = ImmutableList.builder();
+      OUTER:
+      while (k < kN && !toks.get(k).isToken()) {
+        // Don't attach inline comments to certain leading tokens, e.g. for `f(/*flag1=*/true).
+        //
+        // Attaching inline comments to the right token is hard, and this barely
+        // scratches the surface. But it's enough to do a better job with parameter
+        // name comments.
+        //
+        // TODO(cushon): find a better strategy.
+        if (toks.get(k).isSlashStarComment()) {
+          switch (tok.getText()) {
+            case "(":
+            case "<":
+            case ".":
+              break OUTER;
+            default:
+              break;
+          }
+        }
+        if (toks.get(k).isJavadocComment()) {
+          switch (tok.getText()) {
+            case ";":
+              break OUTER;
+            default:
+              break;
+          }
+        }
+        if (isParamComment(toks.get(k))) {
+          tokens.add(new Token(toksBefore.build(), tok, toksAfter.build()));
+          toksBefore = ImmutableList.<Tok>builder().add(toks.get(k++));
+          // drop newlines after parameter comments
+          while (toks.get(k).isNewline()) {
+            k++;
+          }
+          continue OUTERMOST;
+        }
+        Tok nonTokenAfter = toks.get(k++);
+        toksAfter.add(nonTokenAfter);
+        if (Newlines.containsBreaks(nonTokenAfter.getText())) {
+          break;
+        }
+      }
+      tokens.add(new Token(toksBefore.build(), tok, toksAfter.build()));
+      toksBefore = ImmutableList.builder();
+    }
+    return tokens.build();
+  }
+
+  private static boolean isParamComment(Tok tok) {
+    return tok.isSlashStarComment()
+        && tok.getText().matches("\\/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*\\/");
+  }
+
+  /**
+   * Convert from an offset and length flag pair to a token range.
+   *
+   * @param offset the {@code 0}-based offset in characters
+   * @param length the length in characters
+   * @return the {@code 0}-based {@link Range} of tokens
+   * @throws FormatterException if offset + length is outside the file
+   */
+  Range<Integer> characterRangeToTokenRange(int offset, int length) throws FormatterException {
+    int requiredLength = offset + length;
+    if (requiredLength > text.length()) {
+      throw new FormatterException(
+          String.format(
+              "error: invalid length %d, offset + length (%d) is outside the file",
+              length, requiredLength));
+    }
+    if (length < 0) {
+      return EMPTY_RANGE;
+    }
+    if (length == 0) {
+      // 0 stands for "format the line under the cursor"
+      length = 1;
+    }
+    ImmutableCollection<Token> enclosed =
+        getPositionTokenMap()
+            .subRangeMap(Range.closedOpen(offset, offset + length))
+            .asMapOfRanges()
+            .values();
+    if (enclosed.isEmpty()) {
+      return EMPTY_RANGE;
+    }
+    return Range.closedOpen(
+        enclosed.iterator().next().getTok().getIndex(), getLast(enclosed).getTok().getIndex() + 1);
+  }
+
+  /**
+   * Get the number of toks.
+   *
+   * @return the number of toks, including the EOF tok
+   */
+  @Override
+  public int getkN() {
+    return kN;
+  }
+
+  /**
+   * Get the Token by index.
+   *
+   * @param k the token index
+   */
+  @Override
+  public Token getToken(int k) {
+    return kToToken[k];
+  }
+
+  /**
+   * Get the input tokens.
+   *
+   * @return the input tokens
+   */
+  @Override
+  public ImmutableList<? extends Input.Token> getTokens() {
+    return tokens;
+  }
+
+  /**
+   * Get the navigable map from position to {@link Token}. Used to look for tokens following a given
+   * one, and to implement the --offset and --length flags to reformat a character range in the
+   * input file.
+   *
+   * @return the navigable map from position to {@link Token}
+   */
+  @Override
+  public ImmutableRangeMap<Integer, Token> getPositionTokenMap() {
+    return positionTokenMap;
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("tokens", tokens)
+        .add("super", super.toString())
+        .toString();
+  }
+
+  private JCCompilationUnit unit;
+
+  @Override
+  public int getLineNumber(int inputPosition) {
+    Verify.verifyNotNull(unit, "Expected compilation unit to be set.");
+    return unit.getLineMap().getLineNumber(inputPosition);
+  }
+
+  @Override
+  public int getColumnNumber(int inputPosition) {
+    Verify.verifyNotNull(unit, "Expected compilation unit to be set.");
+    return unit.getLineMap().getColumnNumber(inputPosition);
+  }
+
+  // TODO(cushon): refactor JavaInput so the CompilationUnit can be passed into
+  // the constructor.
+  public void setCompilationUnit(JCCompilationUnit unit) {
+    this.unit = unit;
+  }
+
+  public RangeSet<Integer> characterRangesToTokenRanges(Collection<Range<Integer>> characterRanges)
+      throws FormatterException {
+    RangeSet<Integer> tokenRangeSet = TreeRangeSet.create();
+    for (Range<Integer> characterRange0 : characterRanges) {
+      Range<Integer> characterRange = characterRange0.canonical(DiscreteDomain.integers());
+      tokenRangeSet.add(
+          characterRangeToTokenRange(
+              characterRange.lowerEndpoint(),
+              characterRange.upperEndpoint() - characterRange.lowerEndpoint()));
+    }
+    return tokenRangeSet;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java
new file mode 100644
index 0000000..6ce0f66
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java
@@ -0,0 +1,3676 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.collect.Iterables.getLast;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.googlejavaformat.Doc.FillMode.INDEPENDENT;
+import static com.google.googlejavaformat.Doc.FillMode.UNIFIED;
+import static com.google.googlejavaformat.Indent.If.make;
+import static com.google.googlejavaformat.OpsBuilder.BlankLineWanted.PRESERVE;
+import static com.google.googlejavaformat.OpsBuilder.BlankLineWanted.YES;
+import static com.google.googlejavaformat.java.Trees.getEndPosition;
+import static com.google.googlejavaformat.java.Trees.getLength;
+import static com.google.googlejavaformat.java.Trees.getMethodName;
+import static com.google.googlejavaformat.java.Trees.getSourceForNode;
+import static com.google.googlejavaformat.java.Trees.getStartPosition;
+import static com.google.googlejavaformat.java.Trees.operatorName;
+import static com.google.googlejavaformat.java.Trees.precedence;
+import static com.google.googlejavaformat.java.Trees.skipParen;
+import static com.sun.source.tree.Tree.Kind.ANNOTATION;
+import static com.sun.source.tree.Tree.Kind.ARRAY_ACCESS;
+import static com.sun.source.tree.Tree.Kind.ASSIGNMENT;
+import static com.sun.source.tree.Tree.Kind.BLOCK;
+import static com.sun.source.tree.Tree.Kind.EXTENDS_WILDCARD;
+import static com.sun.source.tree.Tree.Kind.IF;
+import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION;
+import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
+import static com.sun.source.tree.Tree.Kind.NEW_CLASS;
+import static com.sun.source.tree.Tree.Kind.STRING_LITERAL;
+import static com.sun.source.tree.Tree.Kind.UNION_TYPE;
+import static com.sun.source.tree.Tree.Kind.VARIABLE;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Predicate;
+import com.google.common.base.Throwables;
+import com.google.common.base.Verify;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.PeekingIterator;
+import com.google.common.collect.Streams;
+import com.google.googlejavaformat.CloseOp;
+import com.google.googlejavaformat.Doc;
+import com.google.googlejavaformat.Doc.FillMode;
+import com.google.googlejavaformat.FormattingError;
+import com.google.googlejavaformat.Indent;
+import com.google.googlejavaformat.Input;
+import com.google.googlejavaformat.Op;
+import com.google.googlejavaformat.OpenOp;
+import com.google.googlejavaformat.OpsBuilder;
+import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
+import com.google.googlejavaformat.Output.BreakTag;
+import com.google.googlejavaformat.java.DimensionHelpers.SortedDims;
+import com.google.googlejavaformat.java.DimensionHelpers.TypeWithDims;
+import com.sun.source.tree.AnnotatedTypeTree;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ArrayAccessTree;
+import com.sun.source.tree.ArrayTypeTree;
+import com.sun.source.tree.AssertTree;
+import com.sun.source.tree.AssignmentTree;
+import com.sun.source.tree.BinaryTree;
+import com.sun.source.tree.BlockTree;
+import com.sun.source.tree.BreakTree;
+import com.sun.source.tree.CaseTree;
+import com.sun.source.tree.CatchTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.CompoundAssignmentTree;
+import com.sun.source.tree.ConditionalExpressionTree;
+import com.sun.source.tree.ContinueTree;
+import com.sun.source.tree.DirectiveTree;
+import com.sun.source.tree.DoWhileLoopTree;
+import com.sun.source.tree.EmptyStatementTree;
+import com.sun.source.tree.EnhancedForLoopTree;
+import com.sun.source.tree.ExportsTree;
+import com.sun.source.tree.ExpressionStatementTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.ForLoopTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.IfTree;
+import com.sun.source.tree.ImportTree;
+import com.sun.source.tree.InstanceOfTree;
+import com.sun.source.tree.IntersectionTypeTree;
+import com.sun.source.tree.LabeledStatementTree;
+import com.sun.source.tree.LambdaExpressionTree;
+import com.sun.source.tree.LiteralTree;
+import com.sun.source.tree.MemberReferenceTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.ModifiersTree;
+import com.sun.source.tree.ModuleTree;
+import com.sun.source.tree.NewArrayTree;
+import com.sun.source.tree.NewClassTree;
+import com.sun.source.tree.OpensTree;
+import com.sun.source.tree.ParameterizedTypeTree;
+import com.sun.source.tree.ParenthesizedTree;
+import com.sun.source.tree.PrimitiveTypeTree;
+import com.sun.source.tree.ProvidesTree;
+import com.sun.source.tree.RequiresTree;
+import com.sun.source.tree.ReturnTree;
+import com.sun.source.tree.StatementTree;
+import com.sun.source.tree.SwitchTree;
+import com.sun.source.tree.SynchronizedTree;
+import com.sun.source.tree.ThrowTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.TryTree;
+import com.sun.source.tree.TypeCastTree;
+import com.sun.source.tree.TypeParameterTree;
+import com.sun.source.tree.UnaryTree;
+import com.sun.source.tree.UnionTypeTree;
+import com.sun.source.tree.UsesTree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.tree.WhileLoopTree;
+import com.sun.source.tree.WildcardTree;
+import com.sun.source.util.TreePath;
+import com.sun.source.util.TreePathScanner;
+import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
+import com.sun.tools.javac.tree.TreeScanner;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import javax.lang.model.element.Name;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * An AST visitor that builds a stream of {@link Op}s to format from the given {@link
+ * CompilationUnitTree}.
+ */
+public class JavaInputAstVisitor extends TreePathScanner<Void, Void> {
+
+  /** Direction for Annotations (usually VERTICAL). */
+  protected enum Direction {
+    VERTICAL,
+    HORIZONTAL;
+
+    boolean isVertical() {
+      return this == VERTICAL;
+    }
+  }
+
+  /** Whether to break or not. */
+  enum BreakOrNot {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether to collapse empty blocks. */
+  enum CollapseEmptyOrNot {
+    YES,
+    NO;
+
+    static CollapseEmptyOrNot valueOf(boolean b) {
+      return b ? YES : NO;
+    }
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether to allow leading blank lines in blocks. */
+  enum AllowLeadingBlankLine {
+    YES,
+    NO;
+
+    static AllowLeadingBlankLine valueOf(boolean b) {
+      return b ? YES : NO;
+    }
+  }
+
+  /** Whether to allow trailing blank lines in blocks. */
+  enum AllowTrailingBlankLine {
+    YES,
+    NO;
+
+    static AllowTrailingBlankLine valueOf(boolean b) {
+      return b ? YES : NO;
+    }
+  }
+
+  /** Whether to include braces. */
+  protected enum BracesOrNot {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether or not to include dimensions. */
+  enum DimensionsOrNot {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether or not the declaration is Varargs. */
+  enum VarArgsOrNot {
+    YES,
+    NO;
+
+    static VarArgsOrNot valueOf(boolean b) {
+      return b ? YES : NO;
+    }
+
+    boolean isYes() {
+      return this == YES;
+    }
+
+    static VarArgsOrNot fromVariable(VariableTree node) {
+      return valueOf((((JCTree.JCVariableDecl) node).mods.flags & Flags.VARARGS) == Flags.VARARGS);
+    }
+  }
+
+  /** Whether the formal parameter declaration is a receiver. */
+  enum ReceiverParameter {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  /** Whether these declarations are the first in the block. */
+  protected enum FirstDeclarationsOrNot {
+    YES,
+    NO;
+
+    boolean isYes() {
+      return this == YES;
+    }
+  }
+
+  protected final OpsBuilder builder;
+
+  protected static final Indent.Const ZERO = Indent.Const.ZERO;
+  protected final int indentMultiplier;
+  protected final Indent.Const minusTwo;
+  protected final Indent.Const minusFour;
+  protected final Indent.Const plusTwo;
+  protected final Indent.Const plusFour;
+
+  private static final ImmutableList<Op> breakList(Optional<BreakTag> breakTag) {
+    return ImmutableList.of(Doc.Break.make(Doc.FillMode.UNIFIED, " ", ZERO, breakTag));
+  }
+
+  private static final ImmutableList<Op> breakFillList(Optional<BreakTag> breakTag) {
+    return ImmutableList.of(
+        OpenOp.make(ZERO),
+        Doc.Break.make(Doc.FillMode.INDEPENDENT, " ", ZERO, breakTag),
+        CloseOp.make());
+  }
+
+  private static final ImmutableList<Op> forceBreakList(Optional<BreakTag> breakTag) {
+    return ImmutableList.of(Doc.Break.make(FillMode.FORCED, "", Indent.Const.ZERO, breakTag));
+  }
+
+  private static final ImmutableList<Op> EMPTY_LIST = ImmutableList.of();
+
+  /**
+   * Allow multi-line filling (of array initializers, argument lists, and boolean expressions) for
+   * items with length less than or equal to this threshold.
+   */
+  private static final int MAX_ITEM_LENGTH_FOR_FILLING = 10;
+
+  /**
+   * The {@code Visitor} constructor.
+   *
+   * @param builder the {@link OpsBuilder}
+   */
+  public JavaInputAstVisitor(OpsBuilder builder, int indentMultiplier) {
+    this.builder = builder;
+    this.indentMultiplier = indentMultiplier;
+    minusTwo = Indent.Const.make(-2, indentMultiplier);
+    minusFour = Indent.Const.make(-4, indentMultiplier);
+    plusTwo = Indent.Const.make(+2, indentMultiplier);
+    plusFour = Indent.Const.make(+4, indentMultiplier);
+  }
+
+  /** A record of whether we have visited into an expression. */
+  private final Deque<Boolean> inExpression = new ArrayDeque<>(ImmutableList.of(false));
+
+  private boolean inExpression() {
+    return inExpression.peekLast();
+  }
+
+  @Override
+  public Void scan(Tree tree, Void unused) {
+    inExpression.addLast(tree instanceof ExpressionTree || inExpression.peekLast());
+    int previous = builder.depth();
+    try {
+      super.scan(tree, null);
+    } catch (FormattingError e) {
+      throw e;
+    } catch (Throwable t) {
+      throw new FormattingError(builder.diagnostic(Throwables.getStackTraceAsString(t)));
+    } finally {
+      inExpression.removeLast();
+    }
+    builder.checkClosed(previous);
+    return null;
+  }
+
+  @Override
+  public Void visitCompilationUnit(CompilationUnitTree node, Void unused) {
+    boolean first = true;
+    if (node.getPackageName() != null) {
+      markForPartialFormat();
+      visitPackage(node.getPackageName(), node.getPackageAnnotations());
+      builder.forcedBreak();
+      first = false;
+    }
+    dropEmptyDeclarations();
+    if (!node.getImports().isEmpty()) {
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.YES);
+      }
+      for (ImportTree importDeclaration : node.getImports()) {
+        markForPartialFormat();
+        builder.blankLineWanted(PRESERVE);
+        scan(importDeclaration, null);
+        builder.forcedBreak();
+      }
+      first = false;
+    }
+    dropEmptyDeclarations();
+    for (Tree type : node.getTypeDecls()) {
+      if (type.getKind() == Tree.Kind.IMPORT) {
+        // javac treats extra semicolons in the import list as type declarations
+        // TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed
+        continue;
+      }
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.YES);
+      }
+      markForPartialFormat();
+      scan(type, null);
+      builder.forcedBreak();
+      first = false;
+      dropEmptyDeclarations();
+    }
+    // set a partial format marker at EOF to make sure we can format the entire file
+    markForPartialFormat();
+    return null;
+  }
+
+  /** Skips over extra semi-colons at the top-level, or in a class member declaration lists. */
+  protected void dropEmptyDeclarations() {
+    if (builder.peekToken().equals(Optional.of(";"))) {
+      while (builder.peekToken().equals(Optional.of(";"))) {
+        builder.forcedBreak();
+        markForPartialFormat();
+        token(";");
+      }
+    }
+  }
+
+  @Override
+  public Void visitClass(ClassTree tree, Void unused) {
+    switch (tree.getKind()) {
+      case ANNOTATION_TYPE:
+        visitAnnotationType(tree);
+        break;
+      case CLASS:
+      case INTERFACE:
+        visitClassDeclaration(tree);
+        break;
+      case ENUM:
+        visitEnumDeclaration(tree);
+        break;
+      default:
+        throw new AssertionError(tree.getKind());
+    }
+    return null;
+  }
+
+  public void visitAnnotationType(ClassTree node) {
+    sync(node);
+    builder.open(ZERO);
+    visitAndBreakModifiers(
+        node.getModifiers(),
+        Direction.VERTICAL,
+        /* declarationAnnotationBreak= */ Optional.empty());
+    builder.open(ZERO);
+    token("@");
+    token("interface");
+    builder.breakOp(" ");
+    visit(node.getSimpleName());
+    builder.close();
+    builder.close();
+    if (node.getMembers() == null) {
+      builder.open(plusFour);
+      token(";");
+      builder.close();
+    } else {
+      addBodyDeclarations(node.getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+    builder.guessToken(";");
+  }
+
+  @Override
+  public Void visitArrayAccess(ArrayAccessTree node, Void unused) {
+    sync(node);
+    visitDot(node);
+    return null;
+  }
+
+  @Override
+  public Void visitNewArray(NewArrayTree node, Void unused) {
+    if (node.getType() != null) {
+      builder.open(plusFour);
+      token("new");
+      builder.space();
+
+      TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getType(), SortedDims.YES);
+      Tree base = extractedDims.node;
+
+      Deque<ExpressionTree> dimExpressions = new ArrayDeque<>(node.getDimensions());
+
+      Deque<List<? extends AnnotationTree>> annotations = new ArrayDeque<>();
+      annotations.add(ImmutableList.copyOf(node.getAnnotations()));
+      annotations.addAll(node.getDimAnnotations());
+      annotations.addAll(extractedDims.dims);
+
+      scan(base, null);
+      builder.open(ZERO);
+      maybeAddDims(dimExpressions, annotations);
+      builder.close();
+      builder.close();
+    }
+    if (node.getInitializers() != null) {
+      if (node.getType() != null) {
+        builder.space();
+      }
+      visitArrayInitializer(node.getInitializers());
+    }
+    return null;
+  }
+
+  public boolean visitArrayInitializer(List<? extends ExpressionTree> expressions) {
+    int cols;
+    if (expressions.isEmpty()) {
+      tokenBreakTrailingComment("{", plusTwo);
+      if (builder.peekToken().equals(Optional.of(","))) {
+        token(",");
+      }
+      token("}", plusTwo);
+    } else if ((cols = argumentsAreTabular(expressions)) != -1) {
+      builder.open(plusTwo);
+      token("{");
+      builder.forcedBreak();
+      boolean first = true;
+      for (Iterable<? extends ExpressionTree> row : Iterables.partition(expressions, cols)) {
+        if (!first) {
+          builder.forcedBreak();
+        }
+        builder.open(row.iterator().next().getKind() == NEW_ARRAY || cols == 1 ? ZERO : plusFour);
+        boolean firstInRow = true;
+        for (ExpressionTree item : row) {
+          if (!firstInRow) {
+            token(",");
+            builder.breakToFill(" ");
+          }
+          scan(item, null);
+          firstInRow = false;
+        }
+        builder.guessToken(",");
+        builder.close();
+        first = false;
+      }
+      builder.breakOp(minusTwo);
+      builder.close();
+      token("}", plusTwo);
+    } else {
+      // Special-case the formatting of array initializers inside annotations
+      // to more eagerly use a one-per-line layout.
+      boolean inMemberValuePair = false;
+      // walk up past the enclosing NewArrayTree (and maybe an enclosing AssignmentTree)
+      TreePath path = getCurrentPath();
+      for (int i = 0; i < 2; i++) {
+        if (path == null) {
+          break;
+        }
+        if (path.getLeaf().getKind() == ANNOTATION) {
+          inMemberValuePair = true;
+          break;
+        }
+        path = path.getParentPath();
+      }
+      boolean shortItems = hasOnlyShortItems(expressions);
+      boolean allowFilledElementsOnOwnLine = shortItems || !inMemberValuePair;
+
+      builder.open(plusTwo);
+      tokenBreakTrailingComment("{", plusTwo);
+      boolean hasTrailingComma = hasTrailingToken(builder.getInput(), expressions, ",");
+      builder.breakOp(hasTrailingComma ? FillMode.FORCED : FillMode.UNIFIED, "", ZERO);
+      if (allowFilledElementsOnOwnLine) {
+        builder.open(ZERO);
+      }
+      boolean first = true;
+      FillMode fillMode = shortItems ? FillMode.INDEPENDENT : FillMode.UNIFIED;
+      for (ExpressionTree expression : expressions) {
+        if (!first) {
+          token(",");
+          builder.breakOp(fillMode, " ", ZERO);
+        }
+        scan(expression, null);
+        first = false;
+      }
+      builder.guessToken(",");
+      if (allowFilledElementsOnOwnLine) {
+        builder.close();
+      }
+      builder.breakOp(minusTwo);
+      builder.close();
+      token("}", plusTwo);
+    }
+    return false;
+  }
+
+  private boolean hasOnlyShortItems(List<? extends ExpressionTree> expressions) {
+    for (ExpressionTree expression : expressions) {
+      int startPosition = getStartPosition(expression);
+      if (builder.actualSize(
+              startPosition, getEndPosition(expression, getCurrentPath()) - startPosition)
+          >= MAX_ITEM_LENGTH_FOR_FILLING) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public Void visitArrayType(ArrayTypeTree node, Void unused) {
+    sync(node);
+    visitAnnotatedArrayType(node);
+    return null;
+  }
+
+  private void visitAnnotatedArrayType(Tree node) {
+    TypeWithDims extractedDims = DimensionHelpers.extractDims(node, SortedDims.YES);
+    builder.open(plusFour);
+    scan(extractedDims.node, null);
+    Deque<List<? extends AnnotationTree>> dims = new ArrayDeque<>(extractedDims.dims);
+    maybeAddDims(dims);
+    Verify.verify(dims.isEmpty());
+    builder.close();
+  }
+
+  @Override
+  public Void visitAssert(AssertTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    token("assert");
+    builder.space();
+    builder.open(node.getDetail() == null ? ZERO : plusFour);
+    scan(node.getCondition(), null);
+    if (node.getDetail() != null) {
+      builder.breakOp(" ");
+      token(":");
+      builder.space();
+      scan(node.getDetail(), null);
+    }
+    builder.close();
+    builder.close();
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitAssignment(AssignmentTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getVariable(), null);
+    builder.space();
+    splitToken(operatorName(node));
+    builder.breakOp(" ");
+    scan(node.getExpression(), null);
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitBlock(BlockTree node, Void unused) {
+    visitBlock(node, CollapseEmptyOrNot.NO, AllowLeadingBlankLine.NO, AllowTrailingBlankLine.NO);
+    return null;
+  }
+
+  @Override
+  public Void visitCompoundAssignment(CompoundAssignmentTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getVariable(), null);
+    builder.space();
+    splitToken(operatorName(node));
+    builder.breakOp(" ");
+    scan(node.getExpression(), null);
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitBreak(BreakTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    token("break");
+    if (node.getLabel() != null) {
+      builder.breakOp(" ");
+      visit(node.getLabel());
+    }
+    builder.close();
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitTypeCast(TypeCastTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    token("(");
+    scan(node.getType(), null);
+    token(")");
+    builder.breakOp(" ");
+    scan(node.getExpression(), null);
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitNewClass(NewClassTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    if (node.getEnclosingExpression() != null) {
+      scan(node.getEnclosingExpression(), null);
+      builder.breakOp();
+      token(".");
+    }
+    token("new");
+    builder.space();
+    addTypeArguments(node.getTypeArguments(), plusFour);
+    if (node.getClassBody() != null) {
+      builder.addAll(
+          visitModifiers(
+              node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.empty()));
+    }
+    scan(node.getIdentifier(), null);
+    addArguments(node.getArguments(), plusFour);
+    builder.close();
+    if (node.getClassBody() != null) {
+      addBodyDeclarations(
+          node.getClassBody().getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+    return null;
+  }
+
+  @Override
+  public Void visitConditionalExpression(ConditionalExpressionTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getCondition(), null);
+    builder.breakOp(" ");
+    token("?");
+    builder.space();
+    scan(node.getTrueExpression(), null);
+    builder.breakOp(" ");
+    token(":");
+    builder.space();
+    scan(node.getFalseExpression(), null);
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitContinue(ContinueTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    token("continue");
+    if (node.getLabel() != null) {
+      builder.breakOp(" ");
+      visit(node.getLabel());
+    }
+    token(";");
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitDoWhileLoop(DoWhileLoopTree node, Void unused) {
+    sync(node);
+    token("do");
+    visitStatement(
+        node.getStatement(),
+        CollapseEmptyOrNot.YES,
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.YES);
+    if (node.getStatement().getKind() == BLOCK) {
+      builder.space();
+    } else {
+      builder.breakOp(" ");
+    }
+    token("while");
+    builder.space();
+    token("(");
+    scan(skipParen(node.getCondition()), null);
+    token(")");
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitEmptyStatement(EmptyStatementTree node, Void unused) {
+    sync(node);
+    dropEmptyDeclarations();
+    return null;
+  }
+
+  @Override
+  public Void visitEnhancedForLoop(EnhancedForLoopTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    token("for");
+    builder.space();
+    token("(");
+    builder.open(ZERO);
+    visitToDeclare(
+        DeclarationKind.NONE,
+        Direction.HORIZONTAL,
+        node.getVariable(),
+        Optional.of(node.getExpression()),
+        ":",
+        /* trailing= */ Optional.empty());
+    builder.close();
+    token(")");
+    builder.close();
+    visitStatement(
+        node.getStatement(),
+        CollapseEmptyOrNot.YES,
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.NO);
+    return null;
+  }
+
+  private void visitEnumConstantDeclaration(VariableTree enumConstant) {
+    for (AnnotationTree annotation : enumConstant.getModifiers().getAnnotations()) {
+      scan(annotation, null);
+      builder.forcedBreak();
+    }
+    visit(enumConstant.getName());
+    NewClassTree init = ((NewClassTree) enumConstant.getInitializer());
+    if (init.getArguments().isEmpty()) {
+      builder.guessToken("(");
+      builder.guessToken(")");
+    } else {
+      addArguments(init.getArguments(), plusFour);
+    }
+    if (init.getClassBody() != null) {
+      addBodyDeclarations(
+          init.getClassBody().getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+  }
+
+  public boolean visitEnumDeclaration(ClassTree node) {
+    sync(node);
+    builder.open(ZERO);
+    visitAndBreakModifiers(
+        node.getModifiers(),
+        Direction.VERTICAL,
+        /* declarationAnnotationBreak= */ Optional.empty());
+    builder.open(plusFour);
+    token("enum");
+    builder.breakOp(" ");
+    visit(node.getSimpleName());
+    builder.close();
+    builder.close();
+    if (!node.getImplementsClause().isEmpty()) {
+      builder.open(plusFour);
+      builder.breakOp(" ");
+      builder.open(plusFour);
+      token("implements");
+      builder.breakOp(" ");
+      builder.open(ZERO);
+      boolean first = true;
+      for (Tree superInterfaceType : node.getImplementsClause()) {
+        if (!first) {
+          token(",");
+          builder.breakToFill(" ");
+        }
+        scan(superInterfaceType, null);
+        first = false;
+      }
+      builder.close();
+      builder.close();
+      builder.close();
+    }
+    builder.space();
+    tokenBreakTrailingComment("{", plusTwo);
+    ArrayList<VariableTree> enumConstants = new ArrayList<>();
+    ArrayList<Tree> members = new ArrayList<>();
+    for (Tree member : node.getMembers()) {
+      if (member instanceof JCTree.JCVariableDecl) {
+        JCTree.JCVariableDecl variableDecl = (JCTree.JCVariableDecl) member;
+        if ((variableDecl.mods.flags & Flags.ENUM) == Flags.ENUM) {
+          enumConstants.add(variableDecl);
+          continue;
+        }
+      }
+      members.add(member);
+    }
+    if (enumConstants.isEmpty() && members.isEmpty()) {
+      if (builder.peekToken().equals(Optional.of(";"))) {
+        builder.open(plusTwo);
+        builder.forcedBreak();
+        token(";");
+        builder.forcedBreak();
+        dropEmptyDeclarations();
+        builder.close();
+        builder.open(ZERO);
+        builder.forcedBreak();
+        builder.blankLineWanted(BlankLineWanted.NO);
+        token("}", plusTwo);
+        builder.close();
+      } else {
+        builder.open(ZERO);
+        builder.blankLineWanted(BlankLineWanted.NO);
+        token("}");
+        builder.close();
+      }
+    } else {
+      builder.open(plusTwo);
+      builder.blankLineWanted(BlankLineWanted.NO);
+      builder.forcedBreak();
+      builder.open(ZERO);
+      boolean first = true;
+      for (VariableTree enumConstant : enumConstants) {
+        if (!first) {
+          token(",");
+          builder.forcedBreak();
+          builder.blankLineWanted(BlankLineWanted.PRESERVE);
+        }
+        markForPartialFormat();
+        visitEnumConstantDeclaration(enumConstant);
+        first = false;
+      }
+      if (builder.peekToken().orElse("").equals(",")) {
+        token(",");
+        builder.forcedBreak(); // The ";" goes on its own line.
+      }
+      builder.close();
+      builder.close();
+      if (builder.peekToken().equals(Optional.of(";"))) {
+        builder.open(plusTwo);
+        token(";");
+        builder.forcedBreak();
+        dropEmptyDeclarations();
+        builder.close();
+      }
+      builder.open(ZERO);
+      addBodyDeclarations(members, BracesOrNot.NO, FirstDeclarationsOrNot.NO);
+      builder.forcedBreak();
+      builder.blankLineWanted(BlankLineWanted.NO);
+      token("}", plusTwo);
+      builder.close();
+    }
+    builder.guessToken(";");
+    return false;
+  }
+
+  @Override
+  public Void visitMemberReference(MemberReferenceTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getQualifierExpression(), null);
+    builder.breakOp();
+    builder.op("::");
+    addTypeArguments(node.getTypeArguments(), plusFour);
+    switch (node.getMode()) {
+      case INVOKE:
+        visit(node.getName());
+        break;
+      case NEW:
+        token("new");
+        break;
+      default:
+        throw new AssertionError(node.getMode());
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitExpressionStatement(ExpressionStatementTree node, Void unused) {
+    sync(node);
+    scan(node.getExpression(), null);
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitVariable(VariableTree node, Void unused) {
+    sync(node);
+    visitVariables(
+        ImmutableList.of(node),
+        DeclarationKind.NONE,
+        fieldAnnotationDirection(node.getModifiers()));
+    return null;
+  }
+
+  void visitVariables(
+      List<VariableTree> fragments,
+      DeclarationKind declarationKind,
+      Direction annotationDirection) {
+    if (fragments.size() == 1) {
+      VariableTree fragment = fragments.get(0);
+      declareOne(
+          declarationKind,
+          annotationDirection,
+          Optional.of(fragment.getModifiers()),
+          fragment.getType(),
+          /* name= */ fragment.getName(),
+          "",
+          "=",
+          Optional.ofNullable(fragment.getInitializer()),
+          Optional.of(";"),
+          /* receiverExpression= */ Optional.empty(),
+          Optional.ofNullable(variableFragmentDims(true, 0, fragment.getType())));
+
+    } else {
+      declareMany(fragments, annotationDirection);
+    }
+  }
+
+  private TypeWithDims variableFragmentDims(boolean first, int leadingDims, Tree type) {
+    if (type == null) {
+      return null;
+    }
+    if (first) {
+      return DimensionHelpers.extractDims(type, SortedDims.YES);
+    }
+    TypeWithDims dims = DimensionHelpers.extractDims(type, SortedDims.NO);
+    return new TypeWithDims(
+        null, leadingDims > 0 ? dims.dims.subList(0, dims.dims.size() - leadingDims) : dims.dims);
+  }
+
+  @Override
+  public Void visitForLoop(ForLoopTree node, Void unused) {
+    sync(node);
+    token("for");
+    builder.space();
+    token("(");
+    builder.open(plusFour);
+    builder.open(
+        node.getInitializer().size() > 1
+                && node.getInitializer().get(0).getKind() == Tree.Kind.EXPRESSION_STATEMENT
+            ? plusFour
+            : ZERO);
+    if (!node.getInitializer().isEmpty()) {
+      if (node.getInitializer().get(0).getKind() == VARIABLE) {
+        PeekingIterator<StatementTree> it =
+            Iterators.peekingIterator(node.getInitializer().iterator());
+        visitVariables(
+            variableFragments(it, it.next()), DeclarationKind.NONE, Direction.HORIZONTAL);
+      } else {
+        boolean first = true;
+        builder.open(ZERO);
+        for (StatementTree t : node.getInitializer()) {
+          if (!first) {
+            token(",");
+            builder.breakOp(" ");
+          }
+          scan(((ExpressionStatementTree) t).getExpression(), null);
+          first = false;
+        }
+        token(";");
+        builder.close();
+      }
+    } else {
+      token(";");
+    }
+    builder.close();
+    builder.breakOp(" ");
+    if (node.getCondition() != null) {
+      scan(node.getCondition(), null);
+    }
+    token(";");
+    if (!node.getUpdate().isEmpty()) {
+      builder.breakOp(" ");
+      builder.open(node.getUpdate().size() <= 1 ? ZERO : plusFour);
+      boolean firstUpdater = true;
+      for (ExpressionStatementTree updater : node.getUpdate()) {
+        if (!firstUpdater) {
+          token(",");
+          builder.breakToFill(" ");
+        }
+        scan(updater.getExpression(), null);
+        firstUpdater = false;
+      }
+      builder.guessToken(";");
+      builder.close();
+    } else {
+      builder.space();
+    }
+    builder.close();
+    token(")");
+    visitStatement(
+        node.getStatement(),
+        CollapseEmptyOrNot.YES,
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.NO);
+    return null;
+  }
+
+  @Override
+  public Void visitIf(IfTree node, Void unused) {
+    sync(node);
+    // Collapse chains of else-ifs.
+    List<ExpressionTree> expressions = new ArrayList<>();
+    List<StatementTree> statements = new ArrayList<>();
+    while (true) {
+      expressions.add(node.getCondition());
+      statements.add(node.getThenStatement());
+      if (node.getElseStatement() != null && node.getElseStatement().getKind() == IF) {
+        node = (IfTree) node.getElseStatement();
+      } else {
+        break;
+      }
+    }
+    builder.open(ZERO);
+    boolean first = true;
+    boolean followingBlock = false;
+    int expressionsN = expressions.size();
+    for (int i = 0; i < expressionsN; i++) {
+      if (!first) {
+        if (followingBlock) {
+          builder.space();
+        } else {
+          builder.forcedBreak();
+        }
+        token("else");
+        builder.space();
+      }
+      token("if");
+      builder.space();
+      token("(");
+      scan(skipParen(expressions.get(i)), null);
+      token(")");
+      // An empty block can collapse to "{}" if there are no if/else or else clauses
+      boolean onlyClause = expressionsN == 1 && node.getElseStatement() == null;
+      // Trailing blank lines are permitted if this isn't the last clause
+      boolean trailingClauses = i < expressionsN - 1 || node.getElseStatement() != null;
+      visitStatement(
+          statements.get(i),
+          CollapseEmptyOrNot.valueOf(onlyClause),
+          AllowLeadingBlankLine.YES,
+          AllowTrailingBlankLine.valueOf(trailingClauses));
+      followingBlock = statements.get(i).getKind() == BLOCK;
+      first = false;
+    }
+    if (node.getElseStatement() != null) {
+      if (followingBlock) {
+        builder.space();
+      } else {
+        builder.forcedBreak();
+      }
+      token("else");
+      visitStatement(
+          node.getElseStatement(),
+          CollapseEmptyOrNot.NO,
+          AllowLeadingBlankLine.YES,
+          AllowTrailingBlankLine.NO);
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitImport(ImportTree node, Void unused) {
+    sync(node);
+    token("import");
+    builder.space();
+    if (node.isStatic()) {
+      token("static");
+      builder.space();
+    }
+    visitName(node.getQualifiedIdentifier());
+    token(";");
+    // TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed
+    dropEmptyDeclarations();
+    return null;
+  }
+
+  @Override
+  public Void visitBinary(BinaryTree node, Void unused) {
+    sync(node);
+    /*
+     * Collect together all operators with same precedence to clean up indentation.
+     */
+    List<ExpressionTree> operands = new ArrayList<>();
+    List<String> operators = new ArrayList<>();
+    walkInfix(precedence(node), node, operands, operators);
+    FillMode fillMode = hasOnlyShortItems(operands) ? INDEPENDENT : UNIFIED;
+    builder.open(plusFour);
+    scan(operands.get(0), null);
+    int operatorsN = operators.size();
+    for (int i = 0; i < operatorsN; i++) {
+      builder.breakOp(fillMode, " ", ZERO);
+      builder.op(operators.get(i));
+      builder.space();
+      scan(operands.get(i + 1), null);
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitInstanceOf(InstanceOfTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getExpression(), null);
+    builder.breakOp(" ");
+    builder.open(ZERO);
+    token("instanceof");
+    builder.breakOp(" ");
+    scan(node.getType(), null);
+    builder.close();
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitIntersectionType(IntersectionTypeTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    boolean first = true;
+    for (Tree type : node.getBounds()) {
+      if (!first) {
+        builder.breakToFill(" ");
+        token("&");
+        builder.space();
+      }
+      scan(type, null);
+      first = false;
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitLabeledStatement(LabeledStatementTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    visit(node.getLabel());
+    token(":");
+    builder.forcedBreak();
+    builder.close();
+    scan(node.getStatement(), null);
+    return null;
+  }
+
+  @Override
+  public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) {
+    sync(node);
+    boolean statementBody = node.getBodyKind() == LambdaExpressionTree.BodyKind.STATEMENT;
+    boolean parens = builder.peekToken().equals(Optional.of("("));
+    builder.open(parens ? plusFour : ZERO);
+    if (parens) {
+      token("(");
+    }
+    boolean first = true;
+    for (VariableTree parameter : node.getParameters()) {
+      if (!first) {
+        token(",");
+        builder.breakOp(" ");
+      }
+      scan(parameter, null);
+      first = false;
+    }
+    if (parens) {
+      token(")");
+    }
+    builder.close();
+    builder.space();
+    builder.op("->");
+    builder.open(statementBody ? ZERO : plusFour);
+    if (statementBody) {
+      builder.space();
+    } else {
+      builder.breakOp(" ");
+    }
+    if (node.getBody().getKind() == Tree.Kind.BLOCK) {
+      visitBlock(
+          (BlockTree) node.getBody(),
+          CollapseEmptyOrNot.YES,
+          AllowLeadingBlankLine.NO,
+          AllowTrailingBlankLine.NO);
+    } else {
+      scan(node.getBody(), null);
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitAnnotation(AnnotationTree node, Void unused) {
+    sync(node);
+
+    if (visitSingleMemberAnnotation(node)) {
+      return null;
+    }
+
+    builder.open(ZERO);
+    token("@");
+    scan(node.getAnnotationType(), null);
+    if (!node.getArguments().isEmpty()) {
+      builder.open(plusFour);
+      token("(");
+      builder.breakOp();
+      boolean first = true;
+
+      // Format the member value pairs one-per-line if any of them are
+      // initialized with arrays.
+      boolean hasArrayInitializer =
+          Iterables.any(node.getArguments(), JavaInputAstVisitor::isArrayValue);
+      for (ExpressionTree argument : node.getArguments()) {
+        if (!first) {
+          token(",");
+          if (hasArrayInitializer) {
+            builder.forcedBreak();
+          } else {
+            builder.breakOp(" ");
+          }
+        }
+        if (argument instanceof AssignmentTree) {
+          visitAnnotationArgument((AssignmentTree) argument);
+        } else {
+          scan(argument, null);
+        }
+        first = false;
+      }
+      token(")");
+      builder.close();
+      builder.close();
+      return null;
+
+    } else if (builder.peekToken().equals(Optional.of("("))) {
+      token("(");
+      token(")");
+    }
+    builder.close();
+    return null;
+  }
+
+  private static boolean isArrayValue(ExpressionTree argument) {
+    if (!(argument instanceof AssignmentTree)) {
+      return false;
+    }
+    ExpressionTree expression = ((AssignmentTree) argument).getExpression();
+    return expression instanceof NewArrayTree && ((NewArrayTree) expression).getType() == null;
+  }
+
+  public void visitAnnotationArgument(AssignmentTree node) {
+    boolean isArrayInitializer = node.getExpression().getKind() == NEW_ARRAY;
+    sync(node);
+    builder.open(isArrayInitializer ? ZERO : plusFour);
+    scan(node.getVariable(), null);
+    builder.space();
+    token("=");
+    if (isArrayInitializer) {
+      builder.space();
+    } else {
+      builder.breakOp(" ");
+    }
+    scan(node.getExpression(), null);
+    builder.close();
+  }
+
+  @Override
+  public Void visitAnnotatedType(AnnotatedTypeTree node, Void unused) {
+    sync(node);
+    ExpressionTree base = node.getUnderlyingType();
+    if (base instanceof MemberSelectTree) {
+      MemberSelectTree selectTree = (MemberSelectTree) base;
+      scan(selectTree.getExpression(), null);
+      token(".");
+      visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO);
+      builder.breakToFill(" ");
+      visit(selectTree.getIdentifier());
+    } else if (base instanceof ArrayTypeTree) {
+      visitAnnotatedArrayType(node);
+    } else {
+      visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO);
+      builder.breakToFill(" ");
+      scan(base, null);
+    }
+    return null;
+  }
+
+  // TODO(cushon): Use Flags if/when we drop support for Java 11
+
+  protected static final long COMPACT_RECORD_CONSTRUCTOR = 1L << 51;
+
+  protected static final long RECORD = 1L << 61;
+
+  @Override
+  public Void visitMethod(MethodTree node, Void unused) {
+    sync(node);
+    List<? extends AnnotationTree> annotations = node.getModifiers().getAnnotations();
+    List<? extends AnnotationTree> returnTypeAnnotations = ImmutableList.of();
+
+    boolean isRecordConstructor =
+        (((JCMethodDecl) node).mods.flags & COMPACT_RECORD_CONSTRUCTOR)
+            == COMPACT_RECORD_CONSTRUCTOR;
+
+    if (!node.getTypeParameters().isEmpty() && !annotations.isEmpty()) {
+      int typeParameterStart = getStartPosition(node.getTypeParameters().get(0));
+      for (int i = 0; i < annotations.size(); i++) {
+        if (getStartPosition(annotations.get(i)) > typeParameterStart) {
+          returnTypeAnnotations = annotations.subList(i, annotations.size());
+          annotations = annotations.subList(0, i);
+          break;
+        }
+      }
+    }
+    builder.addAll(
+        visitModifiers(
+            annotations, Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty()));
+
+    Tree baseReturnType = null;
+    Deque<List<? extends AnnotationTree>> dims = null;
+    if (node.getReturnType() != null) {
+      TypeWithDims extractedDims =
+          DimensionHelpers.extractDims(node.getReturnType(), SortedDims.YES);
+      baseReturnType = extractedDims.node;
+      dims = new ArrayDeque<>(extractedDims.dims);
+    }
+
+    builder.open(plusFour);
+    BreakTag breakBeforeName = genSym();
+    BreakTag breakBeforeType = genSym();
+    builder.open(ZERO);
+    {
+      boolean first = true;
+      if (!node.getTypeParameters().isEmpty()) {
+        token("<");
+        typeParametersRest(node.getTypeParameters(), plusFour);
+        if (!returnTypeAnnotations.isEmpty()) {
+          builder.breakToFill(" ");
+          visitAnnotations(returnTypeAnnotations, BreakOrNot.NO, BreakOrNot.NO);
+        }
+        first = false;
+      }
+
+      boolean openedNameAndTypeScope = false;
+      // constructor-like declarations that don't match the name of the enclosing class are
+      // parsed as method declarations with a null return type
+      if (baseReturnType != null) {
+        if (!first) {
+          builder.breakOp(INDEPENDENT, " ", ZERO, Optional.of(breakBeforeType));
+        } else {
+          first = false;
+        }
+        if (!openedNameAndTypeScope) {
+          builder.open(make(breakBeforeType, plusFour, ZERO));
+          openedNameAndTypeScope = true;
+        }
+        scan(baseReturnType, null);
+        maybeAddDims(dims);
+      }
+      if (!first) {
+        builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO, Optional.of(breakBeforeName));
+      } else {
+        first = false;
+      }
+      if (!openedNameAndTypeScope) {
+        builder.open(ZERO);
+        openedNameAndTypeScope = true;
+      }
+      String name = node.getName().toString();
+      if (name.equals("<init>")) {
+        name = builder.peekToken().get();
+      }
+      token(name);
+      if (!isRecordConstructor) {
+        token("(");
+      }
+      // end of name and type scope
+      builder.close();
+    }
+    builder.close();
+
+    builder.open(Indent.If.make(breakBeforeName, plusFour, ZERO));
+    builder.open(Indent.If.make(breakBeforeType, plusFour, ZERO));
+    builder.open(ZERO);
+    {
+      if (!isRecordConstructor) {
+        if (!node.getParameters().isEmpty() || node.getReceiverParameter() != null) {
+          // Break before args.
+          builder.breakToFill("");
+          visitFormals(Optional.ofNullable(node.getReceiverParameter()), node.getParameters());
+        }
+        token(")");
+      }
+      if (dims != null) {
+        maybeAddDims(dims);
+      }
+      if (!node.getThrows().isEmpty()) {
+        builder.breakToFill(" ");
+        builder.open(plusFour);
+        {
+          visitThrowsClause(node.getThrows());
+        }
+        builder.close();
+      }
+      if (node.getDefaultValue() != null) {
+        builder.space();
+        token("default");
+        if (node.getDefaultValue().getKind() == Tree.Kind.NEW_ARRAY) {
+          builder.open(minusFour);
+          {
+            builder.space();
+            scan(node.getDefaultValue(), null);
+          }
+          builder.close();
+        } else {
+          builder.open(ZERO);
+          {
+            builder.breakToFill(" ");
+            scan(node.getDefaultValue(), null);
+          }
+          builder.close();
+        }
+      }
+    }
+    builder.close();
+    builder.close();
+    builder.close();
+    if (node.getBody() == null) {
+      token(";");
+    } else {
+      builder.space();
+      builder.token("{", Doc.Token.RealOrImaginary.REAL, plusTwo, Optional.of(plusTwo));
+    }
+    builder.close();
+
+    if (node.getBody() != null) {
+      methodBody(node);
+    }
+
+    return null;
+  }
+
+  private void methodBody(MethodTree node) {
+    if (node.getBody().getStatements().isEmpty()) {
+      builder.blankLineWanted(BlankLineWanted.NO);
+    } else {
+      builder.open(plusTwo);
+      builder.forcedBreak();
+      builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      visitStatements(node.getBody().getStatements());
+      builder.close();
+      builder.forcedBreak();
+      builder.blankLineWanted(BlankLineWanted.NO);
+      markForPartialFormat();
+    }
+    token("}", plusTwo);
+  }
+
+  @Override
+  public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
+    sync(node);
+    if (handleLogStatement(node)) {
+      return null;
+    }
+    visitDot(node);
+    return null;
+  }
+
+  /**
+   * Special-cases log statements, to output:
+   *
+   * <pre>{@code
+   * logger.atInfo().log(
+   *     "Number of foos: %d, foos.size());
+   * }</pre>
+   *
+   * <p>Instead of:
+   *
+   * <pre>{@code
+   * logger
+   *     .atInfo()
+   *     .log(
+   *         "Number of foos: %d, foos.size());
+   * }</pre>
+   */
+  private boolean handleLogStatement(MethodInvocationTree node) {
+    if (!getMethodName(node).contentEquals("log")) {
+      return false;
+    }
+    Deque<ExpressionTree> parts = new ArrayDeque<>();
+    ExpressionTree curr = node;
+    while (curr instanceof MethodInvocationTree) {
+      MethodInvocationTree method = (MethodInvocationTree) curr;
+      parts.addFirst(method);
+      if (!LOG_METHODS.contains(getMethodName(method).toString())) {
+        return false;
+      }
+      curr = Trees.getMethodReceiver(method);
+    }
+    if (!(curr instanceof IdentifierTree)) {
+      return false;
+    }
+    parts.addFirst(curr);
+    visitDotWithPrefix(
+        ImmutableList.copyOf(parts), false, ImmutableList.of(parts.size() - 1), INDEPENDENT);
+    return true;
+  }
+
+  static final ImmutableSet<String> LOG_METHODS =
+      ImmutableSet.of(
+          "at",
+          "atConfig",
+          "atDebug",
+          "atFine",
+          "atFiner",
+          "atFinest",
+          "atInfo",
+          "atMostEvery",
+          "atSevere",
+          "atWarning",
+          "every",
+          "log",
+          "logVarargs",
+          "perUnique",
+          "withCause",
+          "withStackTrace");
+
+  private static List<Long> handleStream(List<ExpressionTree> parts) {
+    return indexes(
+            parts.stream(),
+            p -> {
+              if (!(p instanceof MethodInvocationTree)) {
+                return false;
+              }
+              Name name = getMethodName((MethodInvocationTree) p);
+              return Stream.of("stream", "parallelStream", "toBuilder")
+                  .anyMatch(name::contentEquals);
+            })
+        .collect(toList());
+  }
+
+  private static <T> Stream<Long> indexes(Stream<T> stream, Predicate<T> predicate) {
+    return Streams.mapWithIndex(stream, (x, i) -> predicate.apply(x) ? i : -1).filter(x -> x != -1);
+  }
+
+  @Override
+  public Void visitMemberSelect(MemberSelectTree node, Void unused) {
+    sync(node);
+    visitDot(node);
+    return null;
+  }
+
+  @Override
+  public Void visitLiteral(LiteralTree node, Void unused) {
+    sync(node);
+    String sourceForNode = getSourceForNode(node, getCurrentPath());
+    // A negative numeric literal -n is usually represented as unary minus on n,
+    // but that doesn't work for integer or long MIN_VALUE. The parser works
+    // around that by representing it directly as a signed literal (with no
+    // unary minus), but the lexer still expects two tokens.
+    if (sourceForNode.startsWith("-")) {
+      token("-");
+      sourceForNode = sourceForNode.substring(1).trim();
+    }
+    token(sourceForNode);
+    return null;
+  }
+
+  private void visitPackage(
+      ExpressionTree packageName, List<? extends AnnotationTree> packageAnnotations) {
+    if (!packageAnnotations.isEmpty()) {
+      for (AnnotationTree annotation : packageAnnotations) {
+        builder.forcedBreak();
+        scan(annotation, null);
+      }
+      builder.forcedBreak();
+    }
+    builder.open(plusFour);
+    token("package");
+    builder.space();
+    visitName(packageName);
+    builder.close();
+    token(";");
+  }
+
+  @Override
+  public Void visitParameterizedType(ParameterizedTypeTree node, Void unused) {
+    sync(node);
+    if (node.getTypeArguments().isEmpty()) {
+      scan(node.getType(), null);
+      token("<");
+      token(">");
+    } else {
+      builder.open(plusFour);
+      scan(node.getType(), null);
+      token("<");
+      builder.breakOp();
+      builder.open(ZERO);
+      boolean first = true;
+      for (Tree typeArgument : node.getTypeArguments()) {
+        if (!first) {
+          token(",");
+          builder.breakOp(" ");
+        }
+        scan(typeArgument, null);
+        first = false;
+      }
+      builder.close();
+      builder.close();
+      token(">");
+    }
+    return null;
+  }
+
+  @Override
+  public Void visitParenthesized(ParenthesizedTree node, Void unused) {
+    token("(");
+    scan(node.getExpression(), null);
+    token(")");
+    return null;
+  }
+
+  @Override
+  public Void visitUnary(UnaryTree node, Void unused) {
+    sync(node);
+    String operatorName = operatorName(node);
+    if (((JCTree) node).getTag().isPostUnaryOp()) {
+      scan(node.getExpression(), null);
+      splitToken(operatorName);
+    } else {
+      splitToken(operatorName);
+      if (ambiguousUnaryOperator(node, operatorName)) {
+        builder.space();
+      }
+      scan(node.getExpression(), null);
+    }
+    return null;
+  }
+
+  private void splitToken(String operatorName) {
+    for (int i = 0; i < operatorName.length(); i++) {
+      token(String.valueOf(operatorName.charAt(i)));
+    }
+  }
+
+  private boolean ambiguousUnaryOperator(UnaryTree node, String operatorName) {
+    switch (node.getKind()) {
+      case UNARY_MINUS:
+      case UNARY_PLUS:
+        break;
+      default:
+        return false;
+    }
+    if (!(node.getExpression() instanceof UnaryTree)) {
+      return false;
+    }
+    JCTree.Tag tag = ((JCTree) node.getExpression()).getTag();
+    if (tag.isPostUnaryOp()) {
+      return false;
+    }
+    if (!operatorName(node).startsWith(operatorName)) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public Void visitPrimitiveType(PrimitiveTypeTree node, Void unused) {
+    sync(node);
+    switch (node.getPrimitiveTypeKind()) {
+      case BOOLEAN:
+        token("boolean");
+        break;
+      case BYTE:
+        token("byte");
+        break;
+      case SHORT:
+        token("short");
+        break;
+      case INT:
+        token("int");
+        break;
+      case LONG:
+        token("long");
+        break;
+      case CHAR:
+        token("char");
+        break;
+      case FLOAT:
+        token("float");
+        break;
+      case DOUBLE:
+        token("double");
+        break;
+      case VOID:
+        token("void");
+        break;
+      default:
+        throw new AssertionError(node.getPrimitiveTypeKind());
+    }
+    return null;
+  }
+
+  public boolean visit(Name name) {
+    token(name.toString());
+    return false;
+  }
+
+  @Override
+  public Void visitReturn(ReturnTree node, Void unused) {
+    sync(node);
+    token("return");
+    if (node.getExpression() != null) {
+      builder.space();
+      scan(node.getExpression(), null);
+    }
+    token(";");
+    return null;
+  }
+
+  // TODO(cushon): is this worth special-casing?
+  boolean visitSingleMemberAnnotation(AnnotationTree node) {
+    if (node.getArguments().size() != 1) {
+      return false;
+    }
+    ExpressionTree value = getOnlyElement(node.getArguments());
+    if (value.getKind() == ASSIGNMENT) {
+      return false;
+    }
+    boolean isArrayInitializer = value.getKind() == NEW_ARRAY;
+    builder.open(isArrayInitializer ? ZERO : plusFour);
+    token("@");
+    scan(node.getAnnotationType(), null);
+    token("(");
+    if (!isArrayInitializer) {
+      builder.breakOp();
+    }
+    scan(value, null);
+    builder.close();
+    token(")");
+    return true;
+  }
+
+  @Override
+  public Void visitCase(CaseTree node, Void unused) {
+    sync(node);
+    markForPartialFormat();
+    builder.forcedBreak();
+    if (node.getExpression() == null) {
+      token("default", plusTwo);
+      token(":");
+    } else {
+      token("case", plusTwo);
+      builder.space();
+      scan(node.getExpression(), null);
+      token(":");
+    }
+    builder.open(plusTwo);
+    visitStatements(node.getStatements());
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitSwitch(SwitchTree node, Void unused) {
+    sync(node);
+    visitSwitch(node.getExpression(), node.getCases());
+    return null;
+  }
+
+  protected void visitSwitch(ExpressionTree expression, List<? extends CaseTree> cases) {
+    token("switch");
+    builder.space();
+    token("(");
+    scan(skipParen(expression), null);
+    token(")");
+    builder.space();
+    tokenBreakTrailingComment("{", plusTwo);
+    builder.blankLineWanted(BlankLineWanted.NO);
+    builder.open(plusTwo);
+    boolean first = true;
+    for (CaseTree caseTree : cases) {
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      }
+      scan(caseTree, null);
+      first = false;
+    }
+    builder.close();
+    builder.forcedBreak();
+    builder.blankLineWanted(BlankLineWanted.NO);
+    token("}", plusFour);
+  }
+
+  @Override
+  public Void visitSynchronized(SynchronizedTree node, Void unused) {
+    sync(node);
+    token("synchronized");
+    builder.space();
+    token("(");
+    builder.open(plusFour);
+    builder.breakOp();
+    scan(skipParen(node.getExpression()), null);
+    builder.close();
+    token(")");
+    builder.space();
+    scan(node.getBlock(), null);
+    return null;
+  }
+
+  @Override
+  public Void visitThrow(ThrowTree node, Void unused) {
+    sync(node);
+    token("throw");
+    builder.space();
+    scan(node.getExpression(), null);
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitTry(TryTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    token("try");
+    builder.space();
+    if (!node.getResources().isEmpty()) {
+      token("(");
+      builder.open(node.getResources().size() > 1 ? plusFour : ZERO);
+      boolean first = true;
+      for (Tree resource : node.getResources()) {
+        if (!first) {
+          builder.forcedBreak();
+        }
+        if (resource instanceof VariableTree) {
+          VariableTree variableTree = (VariableTree) resource;
+          declareOne(
+              DeclarationKind.PARAMETER,
+              fieldAnnotationDirection(variableTree.getModifiers()),
+              Optional.of(variableTree.getModifiers()),
+              variableTree.getType(),
+              /* name= */ variableTree.getName(),
+              "",
+              "=",
+              Optional.ofNullable(variableTree.getInitializer()),
+              /* trailing= */ Optional.empty(),
+              /* receiverExpression= */ Optional.empty(),
+              /* typeWithDims= */ Optional.empty());
+        } else {
+          // TODO(cushon): think harder about what to do with `try (resource1; resource2) {}`
+          scan(resource, null);
+        }
+        if (builder.peekToken().equals(Optional.of(";"))) {
+          token(";");
+          builder.space();
+        }
+        first = false;
+      }
+      if (builder.peekToken().equals(Optional.of(";"))) {
+        token(";");
+        builder.space();
+      }
+      token(")");
+      builder.close();
+      builder.space();
+    }
+    // An empty try-with-resources body can collapse to "{}" if there are no trailing catch or
+    // finally blocks.
+    boolean trailingClauses = !node.getCatches().isEmpty() || node.getFinallyBlock() != null;
+    visitBlock(
+        node.getBlock(),
+        CollapseEmptyOrNot.valueOf(!trailingClauses),
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.valueOf(trailingClauses));
+    for (int i = 0; i < node.getCatches().size(); i++) {
+      CatchTree catchClause = node.getCatches().get(i);
+      trailingClauses = i < node.getCatches().size() - 1 || node.getFinallyBlock() != null;
+      visitCatchClause(catchClause, AllowTrailingBlankLine.valueOf(trailingClauses));
+    }
+    if (node.getFinallyBlock() != null) {
+      builder.space();
+      token("finally");
+      builder.space();
+      visitBlock(
+          node.getFinallyBlock(),
+          CollapseEmptyOrNot.NO,
+          AllowLeadingBlankLine.YES,
+          AllowTrailingBlankLine.NO);
+    }
+    builder.close();
+    return null;
+  }
+
+  public void visitClassDeclaration(ClassTree node) {
+    sync(node);
+    List<Op> breaks =
+        visitModifiers(
+            node.getModifiers(),
+            Direction.VERTICAL,
+            /* declarationAnnotationBreak= */ Optional.empty());
+    boolean hasSuperclassType = node.getExtendsClause() != null;
+    boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty();
+    builder.addAll(breaks);
+    token(node.getKind() == Tree.Kind.INTERFACE ? "interface" : "class");
+    builder.space();
+    visit(node.getSimpleName());
+    if (!node.getTypeParameters().isEmpty()) {
+      token("<");
+    }
+    builder.open(plusFour);
+    {
+      if (!node.getTypeParameters().isEmpty()) {
+        typeParametersRest(
+            node.getTypeParameters(),
+            hasSuperclassType || hasSuperInterfaceTypes ? plusFour : ZERO);
+      }
+      if (hasSuperclassType) {
+        builder.breakToFill(" ");
+        token("extends");
+        builder.space();
+        scan(node.getExtendsClause(), null);
+      }
+      if (hasSuperInterfaceTypes) {
+        builder.breakToFill(" ");
+        builder.open(node.getImplementsClause().size() > 1 ? plusFour : ZERO);
+        token(node.getKind() == Tree.Kind.INTERFACE ? "extends" : "implements");
+        builder.space();
+        boolean first = true;
+        for (Tree superInterfaceType : node.getImplementsClause()) {
+          if (!first) {
+            token(",");
+            builder.breakOp(" ");
+          }
+          scan(superInterfaceType, null);
+          first = false;
+        }
+        builder.close();
+      }
+    }
+    builder.close();
+    if (node.getMembers() == null) {
+      token(";");
+    } else {
+      addBodyDeclarations(node.getMembers(), BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+    dropEmptyDeclarations();
+  }
+
+  @Override
+  public Void visitTypeParameter(TypeParameterTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.YES);
+    visit(node.getName());
+    if (!node.getBounds().isEmpty()) {
+      builder.space();
+      token("extends");
+      builder.open(plusFour);
+      builder.breakOp(" ");
+      builder.open(plusFour);
+      boolean first = true;
+      for (Tree typeBound : node.getBounds()) {
+        if (!first) {
+          builder.breakToFill(" ");
+          token("&");
+          builder.space();
+        }
+        scan(typeBound, null);
+        first = false;
+      }
+      builder.close();
+      builder.close();
+    }
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitUnionType(UnionTypeTree node, Void unused) {
+    throw new IllegalStateException("expected manual descent into union types");
+  }
+
+  @Override
+  public Void visitWhileLoop(WhileLoopTree node, Void unused) {
+    sync(node);
+    token("while");
+    builder.space();
+    token("(");
+    scan(skipParen(node.getCondition()), null);
+    token(")");
+    visitStatement(
+        node.getStatement(),
+        CollapseEmptyOrNot.YES,
+        AllowLeadingBlankLine.YES,
+        AllowTrailingBlankLine.NO);
+    return null;
+  }
+
+  @Override
+  public Void visitWildcard(WildcardTree node, Void unused) {
+    sync(node);
+    builder.open(ZERO);
+    token("?");
+    if (node.getBound() != null) {
+      builder.open(plusFour);
+      builder.space();
+      token(node.getKind() == EXTENDS_WILDCARD ? "extends" : "super");
+      builder.breakOp(" ");
+      scan(node.getBound(), null);
+      builder.close();
+    }
+    builder.close();
+    return null;
+  }
+
+  // Helper methods.
+
+  /** Helper method for annotations. */
+  void visitAnnotations(
+      List<? extends AnnotationTree> annotations, BreakOrNot breakBefore, BreakOrNot breakAfter) {
+    if (!annotations.isEmpty()) {
+      if (breakBefore.isYes()) {
+        builder.breakToFill(" ");
+      }
+      boolean first = true;
+      for (AnnotationTree annotation : annotations) {
+        if (!first) {
+          builder.breakToFill(" ");
+        }
+        scan(annotation, null);
+        first = false;
+      }
+      if (breakAfter.isYes()) {
+        builder.breakToFill(" ");
+      }
+    }
+  }
+
+  /** Helper method for blocks. */
+  private void visitBlock(
+      BlockTree node,
+      CollapseEmptyOrNot collapseEmptyOrNot,
+      AllowLeadingBlankLine allowLeadingBlankLine,
+      AllowTrailingBlankLine allowTrailingBlankLine) {
+    sync(node);
+    if (node.isStatic()) {
+      token("static");
+      builder.space();
+    }
+    if (collapseEmptyOrNot.isYes() && node.getStatements().isEmpty()) {
+      if (builder.peekToken().equals(Optional.of(";"))) {
+        // TODO(cushon): is this needed?
+        token(";");
+      } else {
+        tokenBreakTrailingComment("{", plusTwo);
+        builder.blankLineWanted(BlankLineWanted.NO);
+        token("}", plusTwo);
+      }
+    } else {
+      builder.open(ZERO);
+      builder.open(plusTwo);
+      tokenBreakTrailingComment("{", plusTwo);
+      if (allowLeadingBlankLine == AllowLeadingBlankLine.NO) {
+        builder.blankLineWanted(BlankLineWanted.NO);
+      } else {
+        builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      }
+      visitStatements(node.getStatements());
+      builder.close();
+      builder.forcedBreak();
+      builder.close();
+      if (allowTrailingBlankLine == AllowTrailingBlankLine.NO) {
+        builder.blankLineWanted(BlankLineWanted.NO);
+      } else {
+        builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      }
+      markForPartialFormat();
+      token("}", plusTwo);
+    }
+  }
+
+  /** Helper method for statements. */
+  private void visitStatement(
+      StatementTree node,
+      CollapseEmptyOrNot collapseEmptyOrNot,
+      AllowLeadingBlankLine allowLeadingBlank,
+      AllowTrailingBlankLine allowTrailingBlank) {
+    sync(node);
+    switch (node.getKind()) {
+      case BLOCK:
+        builder.space();
+        visitBlock((BlockTree) node, collapseEmptyOrNot, allowLeadingBlank, allowTrailingBlank);
+        break;
+      default:
+        builder.open(plusTwo);
+        builder.breakOp(" ");
+        scan(node, null);
+        builder.close();
+    }
+  }
+
+  protected void visitStatements(List<? extends StatementTree> statements) {
+    boolean first = true;
+    PeekingIterator<StatementTree> it = Iterators.peekingIterator(statements.iterator());
+    dropEmptyDeclarations();
+    while (it.hasNext()) {
+      StatementTree tree = it.next();
+      builder.forcedBreak();
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.PRESERVE);
+      }
+      markForPartialFormat();
+      first = false;
+      List<VariableTree> fragments = variableFragments(it, tree);
+      if (!fragments.isEmpty()) {
+        visitVariables(
+            fragments,
+            DeclarationKind.NONE,
+            canLocalHaveHorizontalAnnotations(fragments.get(0).getModifiers()));
+      } else {
+        scan(tree, null);
+      }
+    }
+  }
+
+  /** Output combined modifiers and annotations and the trailing break. */
+  void visitAndBreakModifiers(
+      ModifiersTree modifiers,
+      Direction annotationDirection,
+      Optional<BreakTag> declarationAnnotationBreak) {
+    builder.addAll(visitModifiers(modifiers, annotationDirection, declarationAnnotationBreak));
+  }
+
+  @Override
+  public Void visitModifiers(ModifiersTree node, Void unused) {
+    throw new IllegalStateException("expected manual descent into modifiers");
+  }
+
+  /** Output combined modifiers and annotations and returns the trailing break. */
+  protected List<Op> visitModifiers(
+      ModifiersTree modifiersTree,
+      Direction annotationsDirection,
+      Optional<BreakTag> declarationAnnotationBreak) {
+    return visitModifiers(
+        modifiersTree.getAnnotations(), annotationsDirection, declarationAnnotationBreak);
+  }
+
+  protected List<Op> visitModifiers(
+      List<? extends AnnotationTree> annotationTrees,
+      Direction annotationsDirection,
+      Optional<BreakTag> declarationAnnotationBreak) {
+    if (annotationTrees.isEmpty() && !nextIsModifier()) {
+      return EMPTY_LIST;
+    }
+    Deque<AnnotationTree> annotations = new ArrayDeque<>(annotationTrees);
+    builder.open(ZERO);
+    boolean first = true;
+    boolean lastWasAnnotation = false;
+    while (!annotations.isEmpty()) {
+      if (nextIsModifier()) {
+        break;
+      }
+      if (!first) {
+        builder.addAll(
+            annotationsDirection.isVertical()
+                ? forceBreakList(declarationAnnotationBreak)
+                : breakList(declarationAnnotationBreak));
+      }
+      scan(annotations.removeFirst(), null);
+      first = false;
+      lastWasAnnotation = true;
+    }
+    builder.close();
+    ImmutableList<Op> trailingBreak =
+        annotationsDirection.isVertical()
+            ? forceBreakList(declarationAnnotationBreak)
+            : breakList(declarationAnnotationBreak);
+    if (annotations.isEmpty() && !nextIsModifier()) {
+      return trailingBreak;
+    }
+    if (lastWasAnnotation) {
+      builder.addAll(trailingBreak);
+    }
+
+    builder.open(ZERO);
+    first = true;
+    while (nextIsModifier() || !annotations.isEmpty()) {
+      if (!first) {
+        builder.addAll(breakFillList(Optional.empty()));
+      }
+      if (nextIsModifier()) {
+        token(builder.peekToken().get());
+      } else {
+        scan(annotations.removeFirst(), null);
+        lastWasAnnotation = true;
+      }
+      first = false;
+    }
+    builder.close();
+    return breakFillList(Optional.empty());
+  }
+
+  boolean nextIsModifier() {
+    switch (builder.peekToken().get()) {
+      case "public":
+      case "protected":
+      case "private":
+      case "abstract":
+      case "static":
+      case "final":
+      case "transient":
+      case "volatile":
+      case "synchronized":
+      case "native":
+      case "strictfp":
+      case "default":
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  @Override
+  public Void visitCatch(CatchTree node, Void unused) {
+    throw new IllegalStateException("expected manual descent into catch trees");
+  }
+
+  /** Helper method for {@link CatchTree}s. */
+  private void visitCatchClause(CatchTree node, AllowTrailingBlankLine allowTrailingBlankLine) {
+    sync(node);
+    builder.space();
+    token("catch");
+    builder.space();
+    token("(");
+    builder.open(plusFour);
+    VariableTree ex = node.getParameter();
+    if (ex.getType().getKind() == UNION_TYPE) {
+      builder.open(ZERO);
+      visitUnionType(ex);
+      builder.close();
+    } else {
+      // TODO(cushon): don't break after here for consistency with for, while, etc.
+      builder.breakToFill();
+      builder.open(ZERO);
+      scan(ex, null);
+      builder.close();
+    }
+    builder.close();
+    token(")");
+    builder.space();
+    visitBlock(
+        node.getBlock(), CollapseEmptyOrNot.NO, AllowLeadingBlankLine.YES, allowTrailingBlankLine);
+  }
+
+  /** Formats a union type declaration in a catch clause. */
+  private void visitUnionType(VariableTree declaration) {
+    UnionTypeTree type = (UnionTypeTree) declaration.getType();
+    builder.open(ZERO);
+    sync(declaration);
+    visitAndBreakModifiers(
+        declaration.getModifiers(),
+        Direction.HORIZONTAL,
+        /* declarationAnnotationBreak= */ Optional.empty());
+    List<? extends Tree> union = type.getTypeAlternatives();
+    boolean first = true;
+    for (int i = 0; i < union.size() - 1; i++) {
+      if (!first) {
+        builder.breakOp(" ");
+        token("|");
+        builder.space();
+      } else {
+        first = false;
+      }
+      scan(union.get(i), null);
+    }
+    builder.breakOp(" ");
+    token("|");
+    builder.space();
+    Tree last = union.get(union.size() - 1);
+    declareOne(
+        DeclarationKind.NONE,
+        Direction.HORIZONTAL,
+        /* modifiers= */ Optional.empty(),
+        last,
+        /* name= */ declaration.getName(),
+        /* op= */ "",
+        "=",
+        Optional.ofNullable(declaration.getInitializer()),
+        /* trailing= */ Optional.empty(),
+        /* receiverExpression= */ Optional.empty(),
+        /* typeWithDims= */ Optional.empty());
+    builder.close();
+  }
+
+  /** Accumulate the operands and operators. */
+  private static void walkInfix(
+      int precedence,
+      ExpressionTree expression,
+      List<ExpressionTree> operands,
+      List<String> operators) {
+    if (expression instanceof BinaryTree) {
+      BinaryTree binaryTree = (BinaryTree) expression;
+      if (precedence(binaryTree) == precedence) {
+        walkInfix(precedence, binaryTree.getLeftOperand(), operands, operators);
+        operators.add(operatorName(expression));
+        walkInfix(precedence, binaryTree.getRightOperand(), operands, operators);
+      } else {
+        operands.add(expression);
+      }
+    } else {
+      operands.add(expression);
+    }
+  }
+
+  protected void visitFormals(
+      Optional<VariableTree> receiver, List<? extends VariableTree> parameters) {
+    if (!receiver.isPresent() && parameters.isEmpty()) {
+      return;
+    }
+    builder.open(ZERO);
+    boolean first = true;
+    if (receiver.isPresent()) {
+      // TODO(jdd): Use builders.
+      declareOne(
+          DeclarationKind.PARAMETER,
+          Direction.HORIZONTAL,
+          Optional.of(receiver.get().getModifiers()),
+          receiver.get().getType(),
+          /* name= */ receiver.get().getName(),
+          "",
+          "",
+          /* initializer= */ Optional.empty(),
+          !parameters.isEmpty() ? Optional.of(",") : Optional.empty(),
+          Optional.of(receiver.get().getNameExpression()),
+          /* typeWithDims= */ Optional.empty());
+      first = false;
+    }
+    for (int i = 0; i < parameters.size(); i++) {
+      VariableTree parameter = parameters.get(i);
+      if (!first) {
+        builder.breakOp(" ");
+      }
+      visitToDeclare(
+          DeclarationKind.PARAMETER,
+          Direction.HORIZONTAL,
+          parameter,
+          /* initializer= */ Optional.empty(),
+          "=",
+          i < parameters.size() - 1 ? Optional.of(",") : /* a= */ Optional.empty());
+      first = false;
+    }
+    builder.close();
+  }
+
+  //  /** Helper method for {@link MethodDeclaration}s. */
+  private void visitThrowsClause(List<? extends ExpressionTree> thrownExceptionTypes) {
+    token("throws");
+    builder.breakToFill(" ");
+    boolean first = true;
+    for (ExpressionTree thrownExceptionType : thrownExceptionTypes) {
+      if (!first) {
+        token(",");
+        builder.breakToFill(" ");
+      }
+      scan(thrownExceptionType, null);
+      first = false;
+    }
+  }
+
+  @Override
+  public Void visitIdentifier(IdentifierTree node, Void unused) {
+    sync(node);
+    token(node.getName().toString());
+    return null;
+  }
+
+  @Override
+  public Void visitModule(ModuleTree node, Void unused) {
+    for (AnnotationTree annotation : node.getAnnotations()) {
+      scan(annotation, null);
+      builder.forcedBreak();
+    }
+    if (node.getModuleType() == ModuleTree.ModuleKind.OPEN) {
+      token("open");
+      builder.space();
+    }
+    token("module");
+    builder.space();
+    scan(node.getName(), null);
+    builder.space();
+    if (node.getDirectives().isEmpty()) {
+      tokenBreakTrailingComment("{", plusTwo);
+      builder.blankLineWanted(BlankLineWanted.NO);
+      token("}", plusTwo);
+    } else {
+      builder.open(plusTwo);
+      token("{");
+      builder.forcedBreak();
+      Optional<Tree.Kind> previousDirective = Optional.empty();
+      for (DirectiveTree directiveTree : node.getDirectives()) {
+        markForPartialFormat();
+        builder.blankLineWanted(
+            previousDirective.map(k -> !k.equals(directiveTree.getKind())).orElse(false)
+                ? BlankLineWanted.YES
+                : BlankLineWanted.NO);
+        builder.forcedBreak();
+        scan(directiveTree, null);
+        previousDirective = Optional.of(directiveTree.getKind());
+      }
+      builder.close();
+      builder.forcedBreak();
+      token("}");
+    }
+    return null;
+  }
+
+  private void visitDirective(
+      String name,
+      String separator,
+      ExpressionTree nameExpression,
+      @Nullable List<? extends ExpressionTree> items) {
+    token(name);
+    builder.space();
+    scan(nameExpression, null);
+    if (items != null) {
+      builder.open(plusFour);
+      builder.space();
+      token(separator);
+      builder.forcedBreak();
+      boolean first = true;
+      for (ExpressionTree item : items) {
+        if (!first) {
+          token(",");
+          builder.forcedBreak();
+        }
+        scan(item, null);
+        first = false;
+      }
+      token(";");
+      builder.close();
+    } else {
+      token(";");
+    }
+  }
+
+  @Override
+  public Void visitExports(ExportsTree node, Void unused) {
+    visitDirective("exports", "to", node.getPackageName(), node.getModuleNames());
+    return null;
+  }
+
+  @Override
+  public Void visitOpens(OpensTree node, Void unused) {
+    visitDirective("opens", "to", node.getPackageName(), node.getModuleNames());
+    return null;
+  }
+
+  @Override
+  public Void visitProvides(ProvidesTree node, Void unused) {
+    visitDirective("provides", "with", node.getServiceName(), node.getImplementationNames());
+    return null;
+  }
+
+  @Override
+  public Void visitRequires(RequiresTree node, Void unused) {
+    token("requires");
+    builder.space();
+    while (true) {
+      if (builder.peekToken().equals(Optional.of("static"))) {
+        token("static");
+        builder.space();
+      } else if (builder.peekToken().equals(Optional.of("transitive"))) {
+        token("transitive");
+        builder.space();
+      } else {
+        break;
+      }
+    }
+    scan(node.getModuleName(), null);
+    token(";");
+    return null;
+  }
+
+  @Override
+  public Void visitUses(UsesTree node, Void unused) {
+    token("uses");
+    builder.space();
+    scan(node.getServiceName(), null);
+    token(";");
+    return null;
+  }
+
+  /** Helper method for import declarations, names, and qualified names. */
+  private void visitName(Tree node) {
+    Deque<Name> stack = new ArrayDeque<>();
+    for (; node instanceof MemberSelectTree; node = ((MemberSelectTree) node).getExpression()) {
+      stack.addFirst(((MemberSelectTree) node).getIdentifier());
+    }
+    stack.addFirst(((IdentifierTree) node).getName());
+    boolean first = true;
+    for (Name name : stack) {
+      if (!first) {
+        token(".");
+      }
+      token(name.toString());
+      first = false;
+    }
+  }
+
+  private void visitToDeclare(
+      DeclarationKind kind,
+      Direction annotationsDirection,
+      VariableTree node,
+      Optional<ExpressionTree> initializer,
+      String equals,
+      Optional<String> trailing) {
+    sync(node);
+    Optional<TypeWithDims> typeWithDims;
+    Tree type;
+    if (node.getType() != null) {
+      TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getType(), SortedDims.YES);
+      typeWithDims = Optional.of(extractedDims);
+      type = extractedDims.node;
+    } else {
+      typeWithDims = Optional.empty();
+      type = null;
+    }
+    declareOne(
+        kind,
+        annotationsDirection,
+        Optional.of(node.getModifiers()),
+        type,
+        node.getName(),
+        "",
+        equals,
+        initializer,
+        trailing,
+        /* receiverExpression= */ Optional.empty(),
+        typeWithDims);
+  }
+
+  /** Does not omit the leading {@code "<"}, which should be associated with the type name. */
+  protected void typeParametersRest(
+      List<? extends TypeParameterTree> typeParameters, Indent plusIndent) {
+    builder.open(plusIndent);
+    builder.breakOp();
+    builder.open(ZERO);
+    boolean first = true;
+    for (TypeParameterTree typeParameter : typeParameters) {
+      if (!first) {
+        token(",");
+        builder.breakOp(" ");
+      }
+      scan(typeParameter, null);
+      first = false;
+    }
+    token(">");
+    builder.close();
+    builder.close();
+  }
+
+  /** Collapse chains of {@code .} operators, across multiple {@link ASTNode} types. */
+
+  /**
+   * Output a "." node.
+   *
+   * @param node0 the "." node
+   */
+  void visitDot(ExpressionTree node0) {
+    ExpressionTree node = node0;
+
+    // collect a flattened list of "."-separated items
+    // e.g. ImmutableList.builder().add(1).build() -> [ImmutableList, builder(), add(1), build()]
+    Deque<ExpressionTree> stack = new ArrayDeque<>();
+    LOOP:
+    do {
+      stack.addFirst(node);
+      if (node.getKind() == ARRAY_ACCESS) {
+        node = getArrayBase(node);
+      }
+      switch (node.getKind()) {
+        case MEMBER_SELECT:
+          node = ((MemberSelectTree) node).getExpression();
+          break;
+        case METHOD_INVOCATION:
+          node = getMethodReceiver((MethodInvocationTree) node);
+          break;
+        case IDENTIFIER:
+          node = null;
+          break LOOP;
+        default:
+          // If the dot chain starts with a primary expression
+          // (e.g. a class instance creation, or a conditional expression)
+          // then remove it from the list and deal with it first.
+          node = stack.removeFirst();
+          break LOOP;
+      }
+    } while (node != null);
+    List<ExpressionTree> items = new ArrayList<>(stack);
+
+    boolean needDot = false;
+
+    // The dot chain started with a primary expression: output it normally, and indent
+    // the rest of the chain +4.
+    if (node != null) {
+      // Exception: if it's an anonymous class declaration, we don't need to
+      // break and indent after the trailing '}'.
+      if (node.getKind() == NEW_CLASS && ((NewClassTree) node).getClassBody() != null) {
+        builder.open(ZERO);
+        scan(getArrayBase(node), null);
+        token(".");
+      } else {
+        builder.open(plusFour);
+        scan(getArrayBase(node), null);
+        builder.breakOp();
+        needDot = true;
+      }
+      formatArrayIndices(getArrayIndices(node));
+      if (stack.isEmpty()) {
+        builder.close();
+        return;
+      }
+    }
+
+    Set<Integer> prefixes = new LinkedHashSet<>();
+
+    // Check if the dot chain has a prefix that looks like a type name, so we can
+    // treat the type name-shaped part as a single syntactic unit.
+    TypeNameClassifier.typePrefixLength(simpleNames(stack)).ifPresent(prefixes::add);
+
+    int invocationCount = 0;
+    int firstInvocationIndex = -1;
+    {
+      for (int i = 0; i < items.size(); i++) {
+        ExpressionTree expression = items.get(i);
+        if (expression.getKind() == METHOD_INVOCATION) {
+          if (i > 0 || node != null) {
+            // we only want dereference invocations
+            invocationCount++;
+          }
+          if (firstInvocationIndex < 0) {
+            firstInvocationIndex = i;
+          }
+        }
+      }
+    }
+
+    // If there's only one invocation, treat leading field accesses as a single
+    // unit. In the normal case we want to preserve the alignment of subsequent
+    // method calls, and would emit e.g.:
+    //
+    // myField
+    //     .foo()
+    //     .bar();
+    //
+    // But if there's no 'bar()' to worry about the alignment of we prefer:
+    //
+    // myField.foo();
+    //
+    // to:
+    //
+    // myField
+    //     .foo();
+    //
+    if (invocationCount == 1 && firstInvocationIndex > 0) {
+      prefixes.add(firstInvocationIndex);
+    }
+
+    if (prefixes.isEmpty() && items.get(0) instanceof IdentifierTree) {
+      switch (((IdentifierTree) items.get(0)).getName().toString()) {
+        case "this":
+        case "super":
+          prefixes.add(1);
+          break;
+        default:
+          break;
+      }
+    }
+
+    List<Long> streamPrefixes = handleStream(items);
+    streamPrefixes.forEach(x -> prefixes.add(x.intValue()));
+    if (!prefixes.isEmpty()) {
+      visitDotWithPrefix(
+          items, needDot, prefixes, streamPrefixes.isEmpty() ? INDEPENDENT : UNIFIED);
+    } else {
+      visitRegularDot(items, needDot);
+    }
+
+    if (node != null) {
+      builder.close();
+    }
+  }
+
+  /**
+   * Output a "regular" chain of dereferences, possibly in builder-style. Break before every dot.
+   *
+   * @param items in the chain
+   * @param needDot whether a leading dot is needed
+   */
+  private void visitRegularDot(List<ExpressionTree> items, boolean needDot) {
+    boolean trailingDereferences = items.size() > 1;
+    boolean needDot0 = needDot;
+    if (!needDot0) {
+      builder.open(plusFour);
+    }
+    // don't break after the first element if it is every small, unless the
+    // chain starts with another expression
+    int minLength = indentMultiplier * 4;
+    int length = needDot0 ? minLength : 0;
+    for (ExpressionTree e : items) {
+      if (needDot) {
+        if (length > minLength) {
+          builder.breakOp(FillMode.UNIFIED, "", ZERO);
+        }
+        token(".");
+        length++;
+      }
+      if (!fillFirstArgument(e, items, trailingDereferences ? ZERO : minusFour)) {
+        BreakTag tyargTag = genSym();
+        dotExpressionUpToArgs(e, Optional.of(tyargTag));
+        Indent tyargIndent = Indent.If.make(tyargTag, plusFour, ZERO);
+        dotExpressionArgsAndParen(
+            e, tyargIndent, (trailingDereferences || needDot) ? plusFour : ZERO);
+      }
+      length += getLength(e, getCurrentPath());
+      needDot = true;
+    }
+    if (!needDot0) {
+      builder.close();
+    }
+  }
+
+  // avoid formattings like:
+  //
+  // when(
+  //         something
+  //             .happens())
+  //     .thenReturn(result);
+  //
+  private boolean fillFirstArgument(ExpressionTree e, List<ExpressionTree> items, Indent indent) {
+    // is there a trailing dereference?
+    if (items.size() < 2) {
+      return false;
+    }
+    // don't special-case calls nested inside expressions
+    if (e.getKind() != METHOD_INVOCATION) {
+      return false;
+    }
+    MethodInvocationTree methodInvocation = (MethodInvocationTree) e;
+    Name name = getMethodName(methodInvocation);
+    if (!(methodInvocation.getMethodSelect() instanceof IdentifierTree)
+        || name.length() > 4
+        || !methodInvocation.getTypeArguments().isEmpty()
+        || methodInvocation.getArguments().size() != 1) {
+      return false;
+    }
+    builder.open(ZERO);
+    builder.open(indent);
+    visit(name);
+    token("(");
+    ExpressionTree arg = getOnlyElement(methodInvocation.getArguments());
+    scan(arg, null);
+    builder.close();
+    token(")");
+    builder.close();
+    return true;
+  }
+
+  /**
+   * Output a chain of dereferences where some prefix should be treated as a single syntactic unit,
+   * either because it looks like a type name or because there is only a single method invocation in
+   * the chain.
+   *
+   * @param items in the chain
+   * @param needDot whether a leading dot is needed
+   * @param prefixes the terminal indices of 'prefixes' of the expression that should be treated as
+   *     a syntactic unit
+   */
+  private void visitDotWithPrefix(
+      List<ExpressionTree> items,
+      boolean needDot,
+      Collection<Integer> prefixes,
+      FillMode prefixFillMode) {
+    // Are there method invocations or field accesses after the prefix?
+    boolean trailingDereferences = !prefixes.isEmpty() && getLast(prefixes) < items.size() - 1;
+
+    builder.open(plusFour);
+    for (int times = 0; times < prefixes.size(); times++) {
+      builder.open(ZERO);
+    }
+
+    Deque<Integer> unconsumedPrefixes = new ArrayDeque<>(ImmutableSortedSet.copyOf(prefixes));
+    BreakTag nameTag = genSym();
+    for (int i = 0; i < items.size(); i++) {
+      ExpressionTree e = items.get(i);
+      if (needDot) {
+        FillMode fillMode;
+        if (!unconsumedPrefixes.isEmpty() && i <= unconsumedPrefixes.peekFirst()) {
+          fillMode = prefixFillMode;
+        } else {
+          fillMode = FillMode.UNIFIED;
+        }
+
+        builder.breakOp(fillMode, "", ZERO, Optional.of(nameTag));
+        token(".");
+      }
+      BreakTag tyargTag = genSym();
+      dotExpressionUpToArgs(e, Optional.of(tyargTag));
+      if (!unconsumedPrefixes.isEmpty() && i == unconsumedPrefixes.peekFirst()) {
+        builder.close();
+        unconsumedPrefixes.removeFirst();
+      }
+
+      Indent tyargIndent = Indent.If.make(tyargTag, plusFour, ZERO);
+      Indent argsIndent = Indent.If.make(nameTag, plusFour, trailingDereferences ? plusFour : ZERO);
+      dotExpressionArgsAndParen(e, tyargIndent, argsIndent);
+
+      needDot = true;
+    }
+
+    builder.close();
+  }
+
+  /** Returns the simple names of expressions in a "." chain. */
+  private List<String> simpleNames(Deque<ExpressionTree> stack) {
+    ImmutableList.Builder<String> simpleNames = ImmutableList.builder();
+    OUTER:
+    for (ExpressionTree expression : stack) {
+      boolean isArray = expression.getKind() == ARRAY_ACCESS;
+      expression = getArrayBase(expression);
+      switch (expression.getKind()) {
+        case MEMBER_SELECT:
+          simpleNames.add(((MemberSelectTree) expression).getIdentifier().toString());
+          break;
+        case IDENTIFIER:
+          simpleNames.add(((IdentifierTree) expression).getName().toString());
+          break;
+        case METHOD_INVOCATION:
+          simpleNames.add(getMethodName((MethodInvocationTree) expression).toString());
+          break OUTER;
+        default:
+          break OUTER;
+      }
+      if (isArray) {
+        break OUTER;
+      }
+    }
+    return simpleNames.build();
+  }
+
+  private void dotExpressionUpToArgs(ExpressionTree expression, Optional<BreakTag> tyargTag) {
+    expression = getArrayBase(expression);
+    switch (expression.getKind()) {
+      case MEMBER_SELECT:
+        MemberSelectTree fieldAccess = (MemberSelectTree) expression;
+        visit(fieldAccess.getIdentifier());
+        break;
+      case METHOD_INVOCATION:
+        MethodInvocationTree methodInvocation = (MethodInvocationTree) expression;
+        if (!methodInvocation.getTypeArguments().isEmpty()) {
+          builder.open(plusFour);
+          addTypeArguments(methodInvocation.getTypeArguments(), ZERO);
+          // TODO(jdd): Should indent the name -4.
+          builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, tyargTag);
+          builder.close();
+        }
+        visit(getMethodName(methodInvocation));
+        break;
+      case IDENTIFIER:
+        visit(((IdentifierTree) expression).getName());
+        break;
+      default:
+        scan(expression, null);
+        break;
+    }
+  }
+
+  /**
+   * Returns the base expression of an erray access, e.g. given {@code foo[0][0]} returns {@code
+   * foo}.
+   */
+  private ExpressionTree getArrayBase(ExpressionTree node) {
+    while (node instanceof ArrayAccessTree) {
+      node = ((ArrayAccessTree) node).getExpression();
+    }
+    return node;
+  }
+
+  private ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) {
+    ExpressionTree select = methodInvocation.getMethodSelect();
+    return select instanceof MemberSelectTree ? ((MemberSelectTree) select).getExpression() : null;
+  }
+
+  private void dotExpressionArgsAndParen(
+      ExpressionTree expression, Indent tyargIndent, Indent indent) {
+    Deque<ExpressionTree> indices = getArrayIndices(expression);
+    expression = getArrayBase(expression);
+    switch (expression.getKind()) {
+      case METHOD_INVOCATION:
+        builder.open(tyargIndent);
+        MethodInvocationTree methodInvocation = (MethodInvocationTree) expression;
+        addArguments(methodInvocation.getArguments(), indent);
+        builder.close();
+        break;
+      default:
+        break;
+    }
+    formatArrayIndices(indices);
+  }
+
+  /** Lays out one or more array indices. Does not output the expression for the array itself. */
+  private void formatArrayIndices(Deque<ExpressionTree> indices) {
+    if (indices.isEmpty()) {
+      return;
+    }
+    builder.open(ZERO);
+    do {
+      token("[");
+      builder.breakToFill();
+      scan(indices.removeLast(), null);
+      token("]");
+    } while (!indices.isEmpty());
+    builder.close();
+  }
+
+  /**
+   * Returns all array indices for the given expression, e.g. given {@code foo[0][0]} returns the
+   * expressions for {@code [0][0]}.
+   */
+  private Deque<ExpressionTree> getArrayIndices(ExpressionTree expression) {
+    Deque<ExpressionTree> indices = new ArrayDeque<>();
+    while (expression instanceof ArrayAccessTree) {
+      ArrayAccessTree array = (ArrayAccessTree) expression;
+      indices.addLast(array.getIndex());
+      expression = array.getExpression();
+    }
+    return indices;
+  }
+
+  /** Helper methods for method invocations. */
+  void addTypeArguments(List<? extends Tree> typeArguments, Indent plusIndent) {
+    if (typeArguments == null || typeArguments.isEmpty()) {
+      return;
+    }
+    token("<");
+    builder.open(plusIndent);
+    boolean first = true;
+    for (Tree typeArgument : typeArguments) {
+      if (!first) {
+        token(",");
+        builder.breakToFill(" ");
+      }
+      scan(typeArgument, null);
+      first = false;
+    }
+    builder.close();
+    token(">");
+  }
+
+  /**
+   * Add arguments to a method invocation, etc. The arguments indented {@code plusFour}, filled,
+   * from the current indent. The arguments may be output two at a time if they seem to be arguments
+   * to a map constructor, etc.
+   *
+   * @param arguments the arguments
+   * @param plusIndent the extra indent for the arguments
+   */
+  void addArguments(List<? extends ExpressionTree> arguments, Indent plusIndent) {
+    builder.open(plusIndent);
+    token("(");
+    if (!arguments.isEmpty()) {
+      if (arguments.size() % 2 == 0 && argumentsAreTabular(arguments) == 2) {
+        builder.forcedBreak();
+        builder.open(ZERO);
+        boolean first = true;
+        for (int i = 0; i < arguments.size() - 1; i += 2) {
+          ExpressionTree argument0 = arguments.get(i);
+          ExpressionTree argument1 = arguments.get(i + 1);
+          if (!first) {
+            token(",");
+            builder.forcedBreak();
+          }
+          builder.open(plusFour);
+          scan(argument0, null);
+          token(",");
+          builder.breakOp(" ");
+          scan(argument1, null);
+          builder.close();
+          first = false;
+        }
+        builder.close();
+      } else if (isFormatMethod(arguments)) {
+        builder.breakOp();
+        builder.open(ZERO);
+        scan(arguments.get(0), null);
+        token(",");
+        builder.breakOp(" ");
+        builder.open(ZERO);
+        argList(arguments.subList(1, arguments.size()));
+        builder.close();
+        builder.close();
+      } else {
+        builder.breakOp();
+        argList(arguments);
+      }
+    }
+    token(")");
+    builder.close();
+  }
+
+  private void argList(List<? extends ExpressionTree> arguments) {
+    builder.open(ZERO);
+    boolean first = true;
+    FillMode fillMode = hasOnlyShortItems(arguments) ? FillMode.INDEPENDENT : FillMode.UNIFIED;
+    for (ExpressionTree argument : arguments) {
+      if (!first) {
+        token(",");
+        builder.breakOp(fillMode, " ", ZERO);
+      }
+      scan(argument, null);
+      first = false;
+    }
+    builder.close();
+  }
+
+  /**
+   * Identifies String formatting methods like {@link String#format} which we prefer to format as:
+   *
+   * <pre>{@code
+   * String.format(
+   *     "the format string: %s %s %s",
+   *     arg, arg, arg);
+   * }</pre>
+   *
+   * <p>And not:
+   *
+   * <pre>{@code
+   * String.format(
+   *     "the format string: %s %s %s",
+   *     arg,
+   *     arg,
+   *     arg);
+   * }</pre>
+   */
+  private boolean isFormatMethod(List<? extends ExpressionTree> arguments) {
+    if (arguments.size() < 2) {
+      return false;
+    }
+    return isStringConcat(arguments.get(0));
+  }
+
+  private static final Pattern FORMAT_SPECIFIER = Pattern.compile("%|\\{[0-9]\\}");
+
+  private boolean isStringConcat(ExpressionTree first) {
+    final boolean[] stringLiteral = {true};
+    final boolean[] formatString = {false};
+    new TreeScanner() {
+      @Override
+      public void scan(JCTree tree) {
+        if (tree == null) {
+          return;
+        }
+        switch (tree.getKind()) {
+          case STRING_LITERAL:
+            break;
+          case PLUS:
+            super.scan(tree);
+            break;
+          default:
+            stringLiteral[0] = false;
+            break;
+        }
+        if (tree.getKind() == STRING_LITERAL) {
+          Object value = ((LiteralTree) tree).getValue();
+          if (value instanceof String && FORMAT_SPECIFIER.matcher(value.toString()).find()) {
+            formatString[0] = true;
+          }
+        }
+      }
+    }.scan((JCTree) first);
+    return stringLiteral[0] && formatString[0];
+  }
+
+  /** Returns the number of columns if the arguments arg laid out in a grid, or else {@code -1}. */
+  private int argumentsAreTabular(List<? extends ExpressionTree> arguments) {
+    if (arguments.isEmpty()) {
+      return -1;
+    }
+    List<List<ExpressionTree>> rows = new ArrayList<>();
+    PeekingIterator<ExpressionTree> it = Iterators.peekingIterator(arguments.iterator());
+    int start0 = actualColumn(it.peek());
+    {
+      List<ExpressionTree> row = new ArrayList<>();
+      row.add(it.next());
+      while (it.hasNext() && actualColumn(it.peek()) > start0) {
+        row.add(it.next());
+      }
+      if (!it.hasNext()) {
+        return -1;
+      }
+      if (rowLength(row) <= 1) {
+        return -1;
+      }
+      rows.add(row);
+    }
+    while (it.hasNext()) {
+      List<ExpressionTree> row = new ArrayList<>();
+      int start = actualColumn(it.peek());
+      if (start != start0) {
+        return -1;
+      }
+      row.add(it.next());
+      while (it.hasNext() && actualColumn(it.peek()) > start0) {
+        row.add(it.next());
+      }
+      rows.add(row);
+    }
+    int size0 = rows.get(0).size();
+    if (!expressionsAreParallel(rows, 0, rows.size())) {
+      return -1;
+    }
+    for (int i = 1; i < size0; i++) {
+      if (!expressionsAreParallel(rows, i, rows.size() / 2 + 1)) {
+        return -1;
+      }
+    }
+    // if there are only two rows, they must be the same length
+    if (rows.size() == 2) {
+      if (size0 == rows.get(1).size()) {
+        return size0;
+      }
+      return -1;
+    }
+    // allow a ragged trailing row for >= 3 columns
+    for (int i = 1; i < rows.size() - 1; i++) {
+      if (size0 != rows.get(i).size()) {
+        return -1;
+      }
+    }
+    if (size0 < getLast(rows).size()) {
+      return -1;
+    }
+    return size0;
+  }
+
+  static int rowLength(List<? extends ExpressionTree> row) {
+    int size = 0;
+    for (ExpressionTree tree : row) {
+      if (tree.getKind() != NEW_ARRAY) {
+        size++;
+        continue;
+      }
+      NewArrayTree array = (NewArrayTree) tree;
+      if (array.getInitializers() == null) {
+        size++;
+        continue;
+      }
+      size += rowLength(array.getInitializers());
+    }
+    return size;
+  }
+
+  private Integer actualColumn(ExpressionTree expression) {
+    Map<Integer, Integer> positionToColumnMap = builder.getInput().getPositionToColumnMap();
+    return positionToColumnMap.get(builder.actualStartColumn(getStartPosition(expression)));
+  }
+
+  /** Returns true if {@code atLeastM} of the expressions in the given column are the same kind. */
+  private static boolean expressionsAreParallel(
+      List<List<ExpressionTree>> rows, int column, int atLeastM) {
+    Multiset<Tree.Kind> nodeTypes = HashMultiset.create();
+    for (List<? extends ExpressionTree> row : rows) {
+      if (column >= row.size()) {
+        continue;
+      }
+      // Treat UnaryTree expressions as their underlying type for the comparison (so, for example
+      // -ve and +ve numeric literals are considered the same).
+      if (row.get(column) instanceof UnaryTree) {
+        nodeTypes.add(((UnaryTree) row.get(column)).getExpression().getKind());
+      } else {
+        nodeTypes.add(row.get(column).getKind());
+      }
+    }
+    for (Multiset.Entry<Tree.Kind> nodeType : nodeTypes.entrySet()) {
+      if (nodeType.getCount() >= atLeastM) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // General helper functions.
+
+  enum DeclarationKind {
+    NONE,
+    FIELD,
+    PARAMETER
+  }
+
+  /** Declare one variable or variable-like thing. */
+  int declareOne(
+      DeclarationKind kind,
+      Direction annotationsDirection,
+      Optional<ModifiersTree> modifiers,
+      Tree type,
+      Name name,
+      String op,
+      String equals,
+      Optional<ExpressionTree> initializer,
+      Optional<String> trailing,
+      Optional<ExpressionTree> receiverExpression,
+      Optional<TypeWithDims> typeWithDims) {
+
+    BreakTag typeBreak = genSym();
+    BreakTag verticalAnnotationBreak = genSym();
+
+    // If the node is a field declaration, try to output any declaration
+    // annotations in-line. If the entire declaration doesn't fit on a single
+    // line, fall back to one-per-line.
+    boolean isField = kind == DeclarationKind.FIELD;
+
+    if (isField) {
+      builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak));
+    }
+
+    Deque<List<? extends AnnotationTree>> dims =
+        new ArrayDeque<>(
+            typeWithDims.isPresent() ? typeWithDims.get().dims : Collections.emptyList());
+    int baseDims = 0;
+
+    builder.open(
+        kind == DeclarationKind.PARAMETER
+                && (modifiers.isPresent() && !modifiers.get().getAnnotations().isEmpty())
+            ? plusFour
+            : ZERO);
+    {
+      if (modifiers.isPresent()) {
+        visitAndBreakModifiers(
+            modifiers.get(), annotationsDirection, Optional.of(verticalAnnotationBreak));
+      }
+      boolean isVar =
+          builder.peekToken().get().equals("var")
+              && (!name.contentEquals("var") || builder.peekToken(1).get().equals("var"));
+      boolean hasType = type != null || isVar;
+      builder.open(hasType ? plusFour : ZERO);
+      {
+        builder.open(ZERO);
+        {
+          builder.open(ZERO);
+          {
+            if (typeWithDims.isPresent() && typeWithDims.get().node != null) {
+              scan(typeWithDims.get().node, null);
+              int totalDims = dims.size();
+              builder.open(plusFour);
+              maybeAddDims(dims);
+              builder.close();
+              baseDims = totalDims - dims.size();
+            } else if (isVar) {
+              token("var");
+            } else {
+              scan(type, null);
+            }
+          }
+          builder.close();
+
+          if (hasType) {
+            builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO, Optional.of(typeBreak));
+          }
+
+          // conditionally ident the name and initializer +4 if the type spans
+          // multiple lines
+          builder.open(Indent.If.make(typeBreak, plusFour, ZERO));
+          if (receiverExpression.isPresent()) {
+            scan(receiverExpression.get(), null);
+          } else {
+            visit(name);
+          }
+          builder.op(op);
+        }
+        maybeAddDims(dims);
+        builder.close();
+      }
+      builder.close();
+
+      if (initializer.isPresent()) {
+        builder.space();
+        token(equals);
+        if (initializer.get().getKind() == Tree.Kind.NEW_ARRAY
+            && ((NewArrayTree) initializer.get()).getType() == null) {
+          builder.open(minusFour);
+          builder.space();
+          initializer.get().accept(this, null);
+          builder.close();
+        } else {
+          builder.open(Indent.If.make(typeBreak, plusFour, ZERO));
+          {
+            builder.breakToFill(" ");
+            scan(initializer.get(), null);
+          }
+          builder.close();
+        }
+      }
+      if (trailing.isPresent() && builder.peekToken().equals(trailing)) {
+        builder.guessToken(trailing.get());
+      }
+
+      // end of conditional name and initializer indent
+      builder.close();
+    }
+    builder.close();
+
+    if (isField) {
+      builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak));
+    }
+
+    return baseDims;
+  }
+
+  private void maybeAddDims(Deque<List<? extends AnnotationTree>> annotations) {
+    maybeAddDims(new ArrayDeque<>(), annotations);
+  }
+
+  /**
+   * The compiler does not always preserve the concrete syntax of annotated array dimensions, and
+   * mixed-notation array dimensions. Use look-ahead to preserve the original syntax.
+   *
+   * <p>It is assumed that any number of regular dimension specifiers ({@code []} with no
+   * annotations) may be present in the input.
+   *
+   * @param dimExpressions an ordered list of dimension expressions (e.g. the {@code 0} in {@code
+   *     new int[0]}
+   * @param annotations an ordered list of type annotations grouped by dimension (e.g. {@code
+   *     [[@A, @B], [@C]]} for {@code int @A [] @B @C []}
+   */
+  private void maybeAddDims(
+      Deque<ExpressionTree> dimExpressions, Deque<List<? extends AnnotationTree>> annotations) {
+    boolean lastWasAnnotation = false;
+    while (builder.peekToken().isPresent()) {
+      switch (builder.peekToken().get()) {
+        case "@":
+          if (annotations.isEmpty()) {
+            return;
+          }
+          List<? extends AnnotationTree> dimAnnotations = annotations.removeFirst();
+          if (dimAnnotations.isEmpty()) {
+            continue;
+          }
+          builder.breakToFill(" ");
+          visitAnnotations(dimAnnotations, BreakOrNot.NO, BreakOrNot.NO);
+          lastWasAnnotation = true;
+          break;
+        case "[":
+          if (lastWasAnnotation) {
+            builder.breakToFill(" ");
+          } else {
+            builder.breakToFill();
+          }
+          token("[");
+          if (!builder.peekToken().get().equals("]")) {
+            scan(dimExpressions.removeFirst(), null);
+          }
+          token("]");
+          lastWasAnnotation = false;
+          break;
+        case ".":
+          if (!builder.peekToken().get().equals(".") || !builder.peekToken(1).get().equals(".")) {
+            return;
+          }
+          if (lastWasAnnotation) {
+            builder.breakToFill(" ");
+          } else {
+            builder.breakToFill();
+          }
+          builder.op("...");
+          lastWasAnnotation = false;
+          break;
+        default:
+          return;
+      }
+    }
+  }
+
+  private void declareMany(List<VariableTree> fragments, Direction annotationDirection) {
+    builder.open(ZERO);
+
+    ModifiersTree modifiers = fragments.get(0).getModifiers();
+    Tree type = fragments.get(0).getType();
+
+    visitAndBreakModifiers(
+        modifiers, annotationDirection, /* declarationAnnotationBreak= */ Optional.empty());
+    builder.open(plusFour);
+    builder.open(ZERO);
+    TypeWithDims extractedDims = DimensionHelpers.extractDims(type, SortedDims.YES);
+    Deque<List<? extends AnnotationTree>> dims = new ArrayDeque<>(extractedDims.dims);
+    scan(extractedDims.node, null);
+    int baseDims = dims.size();
+    maybeAddDims(dims);
+    baseDims = baseDims - dims.size();
+    boolean first = true;
+    for (VariableTree fragment : fragments) {
+      if (!first) {
+        token(",");
+      }
+      TypeWithDims fragmentDims = variableFragmentDims(first, baseDims, fragment.getType());
+      dims = new ArrayDeque<>(fragmentDims.dims);
+      builder.breakOp(" ");
+      builder.open(ZERO);
+      maybeAddDims(dims);
+      visit(fragment.getName());
+      maybeAddDims(dims);
+      ExpressionTree initializer = fragment.getInitializer();
+      if (initializer != null) {
+        builder.space();
+        token("=");
+        builder.open(plusFour);
+        builder.breakOp(" ");
+        scan(initializer, null);
+        builder.close();
+      }
+      builder.close();
+      if (first) {
+        builder.close();
+      }
+      first = false;
+    }
+    builder.close();
+    token(";");
+    builder.close();
+  }
+
+  /** Add a list of declarations. */
+  protected void addBodyDeclarations(
+      List<? extends Tree> bodyDeclarations, BracesOrNot braces, FirstDeclarationsOrNot first0) {
+    if (bodyDeclarations.isEmpty()) {
+      if (braces.isYes()) {
+        builder.space();
+        tokenBreakTrailingComment("{", plusTwo);
+        builder.blankLineWanted(BlankLineWanted.NO);
+        builder.open(ZERO);
+        token("}", plusTwo);
+        builder.close();
+      }
+    } else {
+      if (braces.isYes()) {
+        builder.space();
+        tokenBreakTrailingComment("{", plusTwo);
+        builder.open(ZERO);
+      }
+      builder.open(plusTwo);
+      boolean first = first0.isYes();
+      boolean lastOneGotBlankLineBefore = false;
+      PeekingIterator<Tree> it = Iterators.peekingIterator(bodyDeclarations.iterator());
+      while (it.hasNext()) {
+        Tree bodyDeclaration = it.next();
+        dropEmptyDeclarations();
+        builder.forcedBreak();
+        boolean thisOneGetsBlankLineBefore =
+            bodyDeclaration.getKind() != VARIABLE || hasJavaDoc(bodyDeclaration);
+        if (first) {
+          builder.blankLineWanted(PRESERVE);
+        } else if (!first && (thisOneGetsBlankLineBefore || lastOneGotBlankLineBefore)) {
+          builder.blankLineWanted(YES);
+        }
+        markForPartialFormat();
+
+        if (bodyDeclaration.getKind() == VARIABLE) {
+          visitVariables(
+              variableFragments(it, bodyDeclaration),
+              DeclarationKind.FIELD,
+              fieldAnnotationDirection(((VariableTree) bodyDeclaration).getModifiers()));
+        } else {
+          scan(bodyDeclaration, null);
+        }
+        first = false;
+        lastOneGotBlankLineBefore = thisOneGetsBlankLineBefore;
+      }
+      dropEmptyDeclarations();
+      builder.forcedBreak();
+      builder.close();
+      builder.forcedBreak();
+      markForPartialFormat();
+      if (braces.isYes()) {
+        builder.blankLineWanted(BlankLineWanted.NO);
+        token("}", plusTwo);
+        builder.close();
+      }
+    }
+  }
+
+  /**
+   * The parser expands multi-variable declarations into separate single-variable declarations. All
+   * of the fragments in the original declaration have the same start position, so we use that as a
+   * signal to collect them and preserve the multi-variable declaration in the output.
+   *
+   * <p>e.g. {@code int x, y;} is parsed as {@code int x; int y;}.
+   */
+  private List<VariableTree> variableFragments(PeekingIterator<? extends Tree> it, Tree first) {
+    List<VariableTree> fragments = new ArrayList<>();
+    if (first.getKind() == VARIABLE) {
+      int start = getStartPosition(first);
+      fragments.add((VariableTree) first);
+      while (it.hasNext()
+          && it.peek().getKind() == VARIABLE
+          && getStartPosition(it.peek()) == start) {
+        fragments.add((VariableTree) it.next());
+      }
+    }
+    return fragments;
+  }
+
+  /** Does this declaration have javadoc preceding it? */
+  private boolean hasJavaDoc(Tree bodyDeclaration) {
+    int position = ((JCTree) bodyDeclaration).getStartPosition();
+    Input.Token token = builder.getInput().getPositionTokenMap().get(position);
+    if (token != null) {
+      for (Input.Tok tok : token.getToksBefore()) {
+        if (tok.getText().startsWith("/**")) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private static Optional<? extends Input.Token> getNextToken(Input input, int position) {
+    return Optional.ofNullable(input.getPositionTokenMap().get(position));
+  }
+
+  /** Does this list of trees end with the specified token? */
+  private boolean hasTrailingToken(Input input, List<? extends Tree> nodes, String token) {
+    if (nodes.isEmpty()) {
+      return false;
+    }
+    Tree lastNode = getLast(nodes);
+    Optional<? extends Input.Token> nextToken =
+        getNextToken(input, getEndPosition(lastNode, getCurrentPath()));
+    return nextToken.isPresent() && nextToken.get().getTok().getText().equals(token);
+  }
+
+  /**
+   * Can a local with a set of modifiers be declared with horizontal annotations? This is currently
+   * true if there is at most one parameterless annotation, and no others.
+   *
+   * @param modifiers the list of {@link ModifiersTree}s
+   * @return whether the local can be declared with horizontal annotations
+   */
+  private Direction canLocalHaveHorizontalAnnotations(ModifiersTree modifiers) {
+    int parameterlessAnnotations = 0;
+    for (AnnotationTree annotation : modifiers.getAnnotations()) {
+      if (annotation.getArguments().isEmpty()) {
+        parameterlessAnnotations++;
+      }
+    }
+    return parameterlessAnnotations <= 1
+            && parameterlessAnnotations == modifiers.getAnnotations().size()
+        ? Direction.HORIZONTAL
+        : Direction.VERTICAL;
+  }
+
+  /**
+   * Should a field with a set of modifiers be declared with horizontal annotations? This is
+   * currently true if all annotations are parameterless annotations.
+   */
+  private Direction fieldAnnotationDirection(ModifiersTree modifiers) {
+    for (AnnotationTree annotation : modifiers.getAnnotations()) {
+      if (!annotation.getArguments().isEmpty()) {
+        return Direction.VERTICAL;
+      }
+    }
+    return Direction.HORIZONTAL;
+  }
+
+  /**
+   * Emit a {@link Doc.Token}.
+   *
+   * @param token the {@link String} to wrap in a {@link Doc.Token}
+   */
+  protected final void token(String token) {
+    builder.token(
+        token,
+        Doc.Token.RealOrImaginary.REAL,
+        ZERO,
+        /* breakAndIndentTrailingComment= */ Optional.empty());
+  }
+
+  /**
+   * Emit a {@link Doc.Token}.
+   *
+   * @param token the {@link String} to wrap in a {@link Doc.Token}
+   * @param plusIndentCommentsBefore extra indent for comments before this token
+   */
+  protected final void token(String token, Indent plusIndentCommentsBefore) {
+    builder.token(
+        token,
+        Doc.Token.RealOrImaginary.REAL,
+        plusIndentCommentsBefore,
+        /* breakAndIndentTrailingComment= */ Optional.empty());
+  }
+
+  /** Emit a {@link Doc.Token}, and breaks and indents trailing javadoc or block comments. */
+  final void tokenBreakTrailingComment(String token, Indent breakAndIndentTrailingComment) {
+    builder.token(
+        token, Doc.Token.RealOrImaginary.REAL, ZERO, Optional.of(breakAndIndentTrailingComment));
+  }
+
+  protected void markForPartialFormat() {
+    if (!inExpression()) {
+      builder.markForPartialFormat();
+    }
+  }
+
+  /**
+   * Sync to position in the input. If we've skipped outputting any tokens that were present in the
+   * input tokens, output them here and complain.
+   *
+   * @param node the ASTNode holding the input position
+   */
+  protected final void sync(Tree node) {
+    builder.sync(((JCTree) node).getStartPosition());
+  }
+
+  final BreakTag genSym() {
+    return new BreakTag();
+  }
+
+  @Override
+  public final String toString() {
+    return MoreObjects.toStringHelper(this).add("builder", builder).toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
new file mode 100644
index 0000000..c059318
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static java.util.Comparator.comparing;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import com.google.googlejavaformat.CommentsHelper;
+import com.google.googlejavaformat.Input;
+import com.google.googlejavaformat.Input.Token;
+import com.google.googlejavaformat.Newlines;
+import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
+import com.google.googlejavaformat.Output;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/*
+ * Throughout this file, {@code i} is an index for input lines, {@code j} is an index for output
+ * lines, {@code ij} is an index into either input or output lines, and {@code k} is an index for
+ * toks.
+ */
+
+/**
+ * {@code JavaOutput} extends {@link Output Output} to represent a Java output document. It includes
+ * methods to emit the output document.
+ */
+public final class JavaOutput extends Output {
+  private final String lineSeparator;
+  private final Input javaInput; // Used to follow along while emitting the output.
+  private final CommentsHelper commentsHelper; // Used to re-flow comments.
+  private final Map<Integer, BlankLineWanted> blankLines = new HashMap<>(); // Info on blank lines.
+  private final RangeSet<Integer> partialFormatRanges = TreeRangeSet.create();
+
+  private final List<String> mutableLines = new ArrayList<>();
+  private final int kN; // The number of tokens or comments in the input, excluding the EOF.
+  private int iLine = 0; // Closest corresponding line number on input.
+  private int lastK = -1; // Last {@link Tok} index output.
+  private int newlinesPending = 0;
+  private StringBuilder lineBuilder = new StringBuilder();
+  private StringBuilder spacesPending = new StringBuilder();
+
+  /**
+   * {@code JavaOutput} constructor.
+   *
+   * @param javaInput the {@link Input}, used to match up blank lines in the output
+   * @param commentsHelper the {@link CommentsHelper}, used to rewrite comments
+   */
+  public JavaOutput(String lineSeparator, Input javaInput, CommentsHelper commentsHelper) {
+    this.lineSeparator = lineSeparator;
+    this.javaInput = javaInput;
+    this.commentsHelper = commentsHelper;
+    kN = javaInput.getkN();
+  }
+
+  @Override
+  public void blankLine(int k, BlankLineWanted wanted) {
+    if (blankLines.containsKey(k)) {
+      blankLines.put(k, blankLines.get(k).merge(wanted));
+    } else {
+      blankLines.put(k, wanted);
+    }
+  }
+
+  @Override
+  public void markForPartialFormat(Token start, Token end) {
+    int lo = JavaOutput.startTok(start).getIndex();
+    int hi = JavaOutput.endTok(end).getIndex();
+    partialFormatRanges.add(Range.closed(lo, hi));
+  }
+
+  // TODO(jdd): Add invariant.
+  @Override
+  public void append(String text, Range<Integer> range) {
+    if (!range.isEmpty()) {
+      boolean sawNewlines = false;
+      // Skip over input line we've passed.
+      int iN = javaInput.getLineCount();
+      while (iLine < iN
+          && (javaInput.getRanges(iLine).isEmpty()
+              || javaInput.getRanges(iLine).upperEndpoint() <= range.lowerEndpoint())) {
+        if (javaInput.getRanges(iLine).isEmpty()) {
+          // Skipped over a blank line.
+          sawNewlines = true;
+        }
+        ++iLine;
+      }
+      /*
+       * Output blank line if we've called {@link OpsBuilder#blankLine}{@code (true)} here, or if
+       * there's a blank line here and it's a comment.
+       */
+      BlankLineWanted wanted = blankLines.getOrDefault(lastK, BlankLineWanted.NO);
+      if (isComment(text) ? sawNewlines : wanted.wanted().orElse(sawNewlines)) {
+        ++newlinesPending;
+      }
+    }
+    if (Newlines.isNewline(text)) {
+      /*
+       * Don't update range information, and swallow extra newlines. The case below for '\n' is for
+       * block comments.
+       */
+      if (newlinesPending == 0) {
+        ++newlinesPending;
+      }
+      spacesPending = new StringBuilder();
+    } else {
+      boolean rangesSet = false;
+      int textN = text.length();
+      for (int i = 0; i < textN; i++) {
+        char c = text.charAt(i);
+        switch (c) {
+          case ' ':
+            spacesPending.append(' ');
+            break;
+          case '\t':
+            spacesPending.append('\t');
+            break;
+          case '\r':
+            if (i + 1 < text.length() && text.charAt(i + 1) == '\n') {
+              i++;
+            }
+            // falls through
+          case '\n':
+            spacesPending = new StringBuilder();
+            ++newlinesPending;
+            break;
+          default:
+            while (newlinesPending > 0) {
+              // drop leading blank lines
+              if (!mutableLines.isEmpty() || lineBuilder.length() > 0) {
+                mutableLines.add(lineBuilder.toString());
+              }
+              lineBuilder = new StringBuilder();
+              rangesSet = false;
+              --newlinesPending;
+            }
+            if (spacesPending.length() > 0) {
+              lineBuilder.append(spacesPending);
+              spacesPending = new StringBuilder();
+            }
+            lineBuilder.append(c);
+            if (!range.isEmpty()) {
+              if (!rangesSet) {
+                while (ranges.size() <= mutableLines.size()) {
+                  ranges.add(Formatter.EMPTY_RANGE);
+                }
+                ranges.set(mutableLines.size(), union(ranges.get(mutableLines.size()), range));
+                rangesSet = true;
+              }
+            }
+        }
+      }
+    }
+    if (!range.isEmpty()) {
+      lastK = range.upperEndpoint();
+    }
+  }
+
+  @Override
+  public void indent(int indent) {
+    spacesPending.append(Strings.repeat(" ", indent));
+  }
+
+  /** Flush any incomplete last line, then add the EOF token into our data structures. */
+  public void flush() {
+    String lastLine = lineBuilder.toString();
+    if (!CharMatcher.whitespace().matchesAllOf(lastLine)) {
+      mutableLines.add(lastLine);
+    }
+    int jN = mutableLines.size();
+    Range<Integer> eofRange = Range.closedOpen(kN, kN + 1);
+    while (ranges.size() < jN) {
+      ranges.add(Formatter.EMPTY_RANGE);
+    }
+    ranges.add(eofRange);
+    setLines(ImmutableList.copyOf(mutableLines));
+  }
+
+  // The following methods can be used after the Output has been built.
+
+  @Override
+  public CommentsHelper getCommentsHelper() {
+    return commentsHelper;
+  }
+
+  /**
+   * Emit a list of {@link Replacement}s to convert from input to output.
+   *
+   * @return a list of {@link Replacement}s, sorted by start index, without overlaps
+   */
+  public ImmutableList<Replacement> getFormatReplacements(RangeSet<Integer> iRangeSet0) {
+    ImmutableList.Builder<Replacement> result = ImmutableList.builder();
+    Map<Integer, Range<Integer>> kToJ = JavaOutput.makeKToIJ(this);
+
+    // Expand the token ranges to align with re-formattable boundaries.
+    RangeSet<Integer> breakableRanges = TreeRangeSet.create();
+    RangeSet<Integer> iRangeSet = iRangeSet0.subRangeSet(Range.closed(0, javaInput.getkN()));
+    for (Range<Integer> iRange : iRangeSet.asRanges()) {
+      Range<Integer> range = expandToBreakableRegions(iRange.canonical(DiscreteDomain.integers()));
+      if (range.equals(EMPTY_RANGE)) {
+        // the range contains only whitespace
+        continue;
+      }
+      breakableRanges.add(range);
+    }
+
+    // Construct replacements for each reformatted region.
+    for (Range<Integer> range : breakableRanges.asRanges()) {
+
+      Input.Tok startTok = startTok(javaInput.getToken(range.lowerEndpoint()));
+      Input.Tok endTok = endTok(javaInput.getToken(range.upperEndpoint() - 1));
+
+      // Add all output lines in the given token range to the replacement.
+      StringBuilder replacement = new StringBuilder();
+
+      int replaceFrom = startTok.getPosition();
+      // Replace leading whitespace in the input with the whitespace from the formatted file
+      while (replaceFrom > 0) {
+        char previous = javaInput.getText().charAt(replaceFrom - 1);
+        if (!CharMatcher.whitespace().matches(previous)) {
+          break;
+        }
+        replaceFrom--;
+      }
+
+      int i = kToJ.get(startTok.getIndex()).lowerEndpoint();
+      // Include leading blank lines from the formatted output, unless the formatted range
+      // starts at the beginning of the file.
+      while (i > 0 && getLine(i - 1).isEmpty()) {
+        i--;
+      }
+      // Write out the formatted range.
+      for (; i < kToJ.get(endTok.getIndex()).upperEndpoint(); i++) {
+        // It's possible to run out of output lines (e.g. if the input ended with
+        // multiple trailing newlines).
+        if (i < getLineCount()) {
+          if (i > 0) {
+            replacement.append(lineSeparator);
+          }
+          replacement.append(getLine(i));
+        }
+      }
+
+      int replaceTo =
+          Math.min(endTok.getPosition() + endTok.length(), javaInput.getText().length());
+      // If the formatted ranged ended in the trailing trivia of the last token before EOF,
+      // format all the way up to EOF to deal with trailing whitespace correctly.
+      if (endTok.getIndex() == javaInput.getkN() - 1) {
+        replaceTo = javaInput.getText().length();
+      }
+      // Replace trailing whitespace in the input with the whitespace from the formatted file.
+      // If the trailing whitespace in the input includes one or more line breaks, preserve the
+      // whitespace after the last newline to avoid re-indenting the line following the formatted
+      // line.
+      int newline = -1;
+      while (replaceTo < javaInput.getText().length()) {
+        char next = javaInput.getText().charAt(replaceTo);
+        if (!CharMatcher.whitespace().matches(next)) {
+          break;
+        }
+        int newlineLength = Newlines.hasNewlineAt(javaInput.getText(), replaceTo);
+        if (newlineLength != -1) {
+          newline = replaceTo;
+          // Skip over the entire newline; don't count the second character of \r\n as a newline.
+          replaceTo += newlineLength;
+        } else {
+          replaceTo++;
+        }
+      }
+      if (newline != -1) {
+        replaceTo = newline;
+      }
+
+      if (newline == -1) {
+        // There wasn't an existing trailing newline; add one.
+        replacement.append(lineSeparator);
+      }
+      for (; i < getLineCount(); i++) {
+        String after = getLine(i);
+        int idx = CharMatcher.whitespace().negate().indexIn(after);
+        if (idx == -1) {
+          // Write out trailing empty lines from the formatted output.
+          replacement.append(lineSeparator);
+        } else {
+          if (newline == -1) {
+            // If there wasn't a trailing newline in the input, indent the next line.
+            replacement.append(after.substring(0, idx));
+          }
+          break;
+        }
+      }
+
+      result.add(Replacement.create(replaceFrom, replaceTo, replacement.toString()));
+    }
+    return result.build();
+  }
+
+  /**
+   * Expand a token range to start and end on acceptable boundaries for re-formatting.
+   *
+   * @param iRange the {@link Range} of tokens
+   * @return the expanded token range
+   */
+  private Range<Integer> expandToBreakableRegions(Range<Integer> iRange) {
+    // The original line range.
+    int loTok = iRange.lowerEndpoint();
+    int hiTok = iRange.upperEndpoint() - 1;
+
+    // Expand the token indices to formattable boundaries (e.g. edges of statements).
+    if (!partialFormatRanges.contains(loTok) || !partialFormatRanges.contains(hiTok)) {
+      return EMPTY_RANGE;
+    }
+    loTok = partialFormatRanges.rangeContaining(loTok).lowerEndpoint();
+    hiTok = partialFormatRanges.rangeContaining(hiTok).upperEndpoint();
+    return Range.closedOpen(loTok, hiTok + 1);
+  }
+
+  public static String applyReplacements(String input, List<Replacement> replacements) {
+    replacements = new ArrayList<>(replacements);
+    replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()).reversed());
+    StringBuilder writer = new StringBuilder(input);
+    for (Replacement replacement : replacements) {
+      writer.replace(
+          replacement.getReplaceRange().lowerEndpoint(),
+          replacement.getReplaceRange().upperEndpoint(),
+          replacement.getReplacementString());
+    }
+    return writer.toString();
+  }
+
+  /** The earliest position of any Tok in the Token, including leading whitespace. */
+  public static int startPosition(Token token) {
+    int min = token.getTok().getPosition();
+    for (Input.Tok tok : token.getToksBefore()) {
+      min = Math.min(min, tok.getPosition());
+    }
+    return min;
+  }
+
+  /** The earliest non-whitespace Tok in the Token. */
+  public static Input.Tok startTok(Token token) {
+    for (Input.Tok tok : token.getToksBefore()) {
+      if (tok.getIndex() >= 0) {
+        return tok;
+      }
+    }
+    return token.getTok();
+  }
+
+  /** The last non-whitespace Tok in the Token. */
+  public static Input.Tok endTok(Token token) {
+    for (int i = token.getToksAfter().size() - 1; i >= 0; i--) {
+      Input.Tok tok = token.getToksAfter().get(i);
+      if (tok.getIndex() >= 0) {
+        return tok;
+      }
+    }
+    return token.getTok();
+  }
+
+  private boolean isComment(String text) {
+    return text.startsWith("//") || text.startsWith("/*");
+  }
+
+  private static Range<Integer> union(Range<Integer> x, Range<Integer> y) {
+    return x.isEmpty() ? y : y.isEmpty() ? x : x.span(y).canonical(DiscreteDomain.integers());
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this)
+        .add("iLine", iLine)
+        .add("lastK", lastK)
+        .add("spacesPending", spacesPending.toString().replace("\t", "\\t"))
+        .add("newlinesPending", newlinesPending)
+        .add("blankLines", blankLines)
+        .add("super", super.toString())
+        .toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java
new file mode 100644
index 0000000..a8c9efd
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.sun.tools.javac.parser.JavaTokenizer;
+import com.sun.tools.javac.parser.Scanner;
+import com.sun.tools.javac.parser.ScannerFactory;
+import com.sun.tools.javac.parser.Tokens.Comment;
+import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle;
+import com.sun.tools.javac.parser.Tokens.Token;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import com.sun.tools.javac.parser.UnicodeReader;
+import com.sun.tools.javac.util.Context;
+import java.util.Set;
+
+/** A wrapper around javac's lexer. */
+class JavacTokens {
+
+  /** The lexer eats terminal comments, so feed it one we don't care about. */
+  // TODO(b/33103797): fix javac and remove the work-around
+  private static final CharSequence EOF_COMMENT = "\n//EOF";
+
+  /** An unprocessed input token, including whitespace and comments. */
+  static class RawTok {
+    private final String stringVal;
+    private final TokenKind kind;
+    private final int pos;
+    private final int endPos;
+
+    RawTok(String stringVal, TokenKind kind, int pos, int endPos) {
+      this.stringVal = stringVal;
+      this.kind = kind;
+      this.pos = pos;
+      this.endPos = endPos;
+    }
+
+    /** The token kind, or {@code null} for whitespace and comments. */
+    public TokenKind kind() {
+      return kind;
+    }
+
+    /** The start position. */
+    public int pos() {
+      return pos;
+    }
+
+    /** The end position. */
+    public int endPos() {
+      return endPos;
+    }
+
+    /** The escaped string value of a literal, or {@code null} for other tokens. */
+    public String stringVal() {
+      return stringVal;
+    }
+  }
+
+  /** Lex the input and return a list of {@link RawTok}s. */
+  public static ImmutableList<RawTok> getTokens(
+      String source, Context context, Set<TokenKind> stopTokens) {
+    if (source == null) {
+      return ImmutableList.of();
+    }
+    ScannerFactory fac = ScannerFactory.instance(context);
+    char[] buffer = (source + EOF_COMMENT).toCharArray();
+    Scanner scanner =
+        new AccessibleScanner(fac, new CommentSavingTokenizer(fac, buffer, buffer.length));
+    ImmutableList.Builder<RawTok> tokens = ImmutableList.builder();
+    int end = source.length();
+    int last = 0;
+    do {
+      scanner.nextToken();
+      Token t = scanner.token();
+      if (t.comments != null) {
+        for (Comment c : Lists.reverse(t.comments)) {
+          if (last < c.getSourcePos(0)) {
+            tokens.add(new RawTok(null, null, last, c.getSourcePos(0)));
+          }
+          tokens.add(
+              new RawTok(null, null, c.getSourcePos(0), c.getSourcePos(0) + c.getText().length()));
+          last = c.getSourcePos(0) + c.getText().length();
+        }
+      }
+      if (stopTokens.contains(t.kind)) {
+        if (t.kind != TokenKind.EOF) {
+          end = t.pos;
+        }
+        break;
+      }
+      if (last < t.pos) {
+        tokens.add(new RawTok(null, null, last, t.pos));
+      }
+      tokens.add(
+          new RawTok(
+              t.kind == TokenKind.STRINGLITERAL ? "\"" + t.stringVal() + "\"" : null,
+              t.kind,
+              t.pos,
+              t.endPos));
+      last = t.endPos;
+    } while (scanner.token().kind != TokenKind.EOF);
+    if (last < end) {
+      tokens.add(new RawTok(null, null, last, end));
+    }
+    return tokens.build();
+  }
+
+  /** A {@link JavaTokenizer} that saves comments. */
+  static class CommentSavingTokenizer extends JavaTokenizer {
+    CommentSavingTokenizer(ScannerFactory fac, char[] buffer, int length) {
+      super(fac, buffer, length);
+    }
+
+    @Override
+    protected Comment processComment(int pos, int endPos, CommentStyle style) {
+      char[] buf = reader.getRawCharacters(pos, endPos);
+      return new CommentWithTextAndPosition(
+          pos, endPos, new AccessibleReader(fac, buf, buf.length), style);
+    }
+  }
+
+  /** A {@link Comment} that saves its text and start position. */
+  static class CommentWithTextAndPosition implements Comment {
+
+    private final int pos;
+    private final int endPos;
+    private final AccessibleReader reader;
+    private final CommentStyle style;
+
+    private String text = null;
+
+    public CommentWithTextAndPosition(
+        int pos, int endPos, AccessibleReader reader, CommentStyle style) {
+      this.pos = pos;
+      this.endPos = endPos;
+      this.reader = reader;
+      this.style = style;
+    }
+
+    /**
+     * Returns the source position of the character at index {@code index} in the comment text.
+     *
+     * <p>The handling of javadoc comments in javac has more logic to skip over leading whitespace
+     * and '*' characters when indexing into doc comments, but we don't need any of that.
+     */
+    @Override
+    public int getSourcePos(int index) {
+      checkArgument(
+          0 <= index && index < (endPos - pos),
+          "Expected %s in the range [0, %s)",
+          index,
+          endPos - pos);
+      return pos + index;
+    }
+
+    @Override
+    public CommentStyle getStyle() {
+      return style;
+    }
+
+    @Override
+    public String getText() {
+      String text = this.text;
+      if (text == null) {
+        this.text = text = new String(reader.getRawCharacters());
+      }
+      return text;
+    }
+
+    /**
+     * We don't care about {@code @deprecated} javadoc tags (see the DepAnn check).
+     *
+     * @return false
+     */
+    @Override
+    public boolean isDeprecated() {
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return String.format("Comment: '%s'", getText());
+    }
+  }
+
+  // Scanner(ScannerFactory, JavaTokenizer) is package-private
+  static class AccessibleScanner extends Scanner {
+    protected AccessibleScanner(ScannerFactory fac, JavaTokenizer tokenizer) {
+      super(fac, tokenizer);
+    }
+  }
+
+  // UnicodeReader(ScannerFactory, char[], int) is package-private
+  static class AccessibleReader extends UnicodeReader {
+    protected AccessibleReader(ScannerFactory fac, char[] buffer, int length) {
+      super(fac, buffer, length);
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Main.java b/core/src/main/java/com/google/googlejavaformat/java/Main.java
new file mode 100644
index 0000000..9231bda
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/Main.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.ByteStreams;
+import com.google.googlejavaformat.FormatterDiagnostic;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/** The main class for the Java formatter CLI. */
+public final class Main {
+  private static final int MAX_THREADS = 20;
+  private static final String STDIN_FILENAME = "<stdin>";
+
+  static final String versionString() {
+    return "google-java-format: Version " + GoogleJavaFormatVersion.version();
+  }
+
+  private final PrintWriter outWriter;
+  private final PrintWriter errWriter;
+  private final InputStream inStream;
+
+  public Main(PrintWriter outWriter, PrintWriter errWriter, InputStream inStream) {
+    this.outWriter = outWriter;
+    this.errWriter = errWriter;
+    this.inStream = inStream;
+  }
+
+  /**
+   * The main method for the formatter, with some number of file names to format. We process them in
+   * parallel, but we must be careful; if multiple file names refer to the same file (which is hard
+   * to determine), we must serialize their updates.
+   *
+   * @param args the command-line arguments
+   */
+  public static void main(String[] args) {
+    int result;
+    PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out, UTF_8));
+    PrintWriter err = new PrintWriter(new OutputStreamWriter(System.err, UTF_8));
+    try {
+      Main formatter = new Main(out, err, System.in);
+      result = formatter.format(args);
+    } catch (UsageException e) {
+      err.print(e.getMessage());
+      result = 0;
+    } finally {
+      err.flush();
+      out.flush();
+    }
+    System.exit(result);
+  }
+
+  /**
+   * The main entry point for the formatter, with some number of file names to format. We process
+   * them in parallel, but we must be careful; if multiple file names refer to the same file (which
+   * is hard to determine), we must serialize their update.
+   *
+   * @param args the command-line arguments
+   */
+  public int format(String... args) throws UsageException {
+    CommandLineOptions parameters = processArgs(args);
+    if (parameters.version()) {
+      errWriter.println(versionString());
+      return 0;
+    }
+    if (parameters.help()) {
+      throw new UsageException();
+    }
+
+    JavaFormatterOptions options =
+        JavaFormatterOptions.builder()
+            .style(parameters.aosp() ? Style.AOSP : Style.GOOGLE)
+            .formatJavadoc(parameters.formatJavadoc())
+            .build();
+
+    if (parameters.stdin()) {
+      return formatStdin(parameters, options);
+    } else {
+      return formatFiles(parameters, options);
+    }
+  }
+
+  private int formatFiles(CommandLineOptions parameters, JavaFormatterOptions options) {
+    int numThreads = Math.min(MAX_THREADS, parameters.files().size());
+    ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
+
+    Map<Path, String> inputs = new LinkedHashMap<>();
+    Map<Path, Future<String>> results = new LinkedHashMap<>();
+    boolean allOk = true;
+
+    for (String fileName : parameters.files()) {
+      if (!fileName.endsWith(".java")) {
+        errWriter.println("Skipping non-Java file: " + fileName);
+        continue;
+      }
+      Path path = Paths.get(fileName);
+      String input;
+      try {
+        input = new String(Files.readAllBytes(path), UTF_8);
+        inputs.put(path, input);
+        results.put(
+            path, executorService.submit(new FormatFileCallable(parameters, input, options)));
+      } catch (IOException e) {
+        errWriter.println(fileName + ": could not read file: " + e.getMessage());
+        allOk = false;
+      }
+    }
+
+    for (Map.Entry<Path, Future<String>> result : results.entrySet()) {
+      Path path = result.getKey();
+      String formatted;
+      try {
+        formatted = result.getValue().get();
+      } catch (InterruptedException e) {
+        errWriter.println(e.getMessage());
+        allOk = false;
+        continue;
+      } catch (ExecutionException e) {
+        if (e.getCause() instanceof FormatterException) {
+          for (FormatterDiagnostic diagnostic : ((FormatterException) e.getCause()).diagnostics()) {
+            errWriter.println(path + ":" + diagnostic.toString());
+          }
+        } else {
+          errWriter.println(path + ": error: " + e.getCause().getMessage());
+          e.getCause().printStackTrace(errWriter);
+        }
+        allOk = false;
+        continue;
+      }
+      boolean changed = !formatted.equals(inputs.get(path));
+      if (changed && parameters.setExitIfChanged()) {
+        allOk = false;
+      }
+      if (parameters.inPlace()) {
+        if (!changed) {
+          continue; // preserve original file
+        }
+        try {
+          Files.write(path, formatted.getBytes(UTF_8));
+        } catch (IOException e) {
+          errWriter.println(path + ": could not write file: " + e.getMessage());
+          allOk = false;
+          continue;
+        }
+      } else if (parameters.dryRun()) {
+        if (changed) {
+          outWriter.println(path);
+        }
+      } else {
+        outWriter.write(formatted);
+      }
+    }
+    return allOk ? 0 : 1;
+  }
+
+  private int formatStdin(CommandLineOptions parameters, JavaFormatterOptions options) {
+    String input;
+    try {
+      input = new String(ByteStreams.toByteArray(inStream), UTF_8);
+    } catch (IOException e) {
+      throw new IOError(e);
+    }
+    String stdinFilename = parameters.assumeFilename().orElse(STDIN_FILENAME);
+    boolean ok = true;
+    try {
+      String output = new FormatFileCallable(parameters, input, options).call();
+      boolean changed = !input.equals(output);
+      if (changed && parameters.setExitIfChanged()) {
+        ok = false;
+      }
+      if (parameters.dryRun()) {
+        if (changed) {
+          outWriter.println(stdinFilename);
+        }
+      } else {
+        outWriter.write(output);
+      }
+    } catch (FormatterException e) {
+      for (FormatterDiagnostic diagnostic : e.diagnostics()) {
+        errWriter.println(stdinFilename + ":" + diagnostic.toString());
+      }
+      ok = false;
+      // TODO(cpovirk): Catch other types of exception (as we do in the formatFiles case).
+    }
+    return ok ? 0 : 1;
+  }
+
+  /** Parses and validates command-line flags. */
+  public static CommandLineOptions processArgs(String... args) throws UsageException {
+    CommandLineOptions parameters;
+    try {
+      parameters = CommandLineOptionsParser.parse(Arrays.asList(args));
+    } catch (IllegalArgumentException e) {
+      throw new UsageException(e.getMessage());
+    } catch (Throwable t) {
+      t.printStackTrace();
+      throw new UsageException(t.getMessage());
+    }
+    int filesToFormat = parameters.files().size();
+    if (parameters.stdin()) {
+      filesToFormat++;
+    }
+
+    if (parameters.inPlace() && parameters.files().isEmpty()) {
+      throw new UsageException("in-place formatting was requested but no files were provided");
+    }
+    if (parameters.isSelection() && filesToFormat != 1) {
+      throw new UsageException("partial formatting is only support for a single file");
+    }
+    if (parameters.offsets().size() != parameters.lengths().size()) {
+      throw new UsageException("-offsets and -lengths flags must be provided in matching pairs");
+    }
+    if (filesToFormat <= 0 && !parameters.version() && !parameters.help()) {
+      throw new UsageException("no files were provided");
+    }
+    if (parameters.stdin() && !parameters.files().isEmpty()) {
+      throw new UsageException("cannot format from standard input and files simultaneously");
+    }
+    if (parameters.assumeFilename().isPresent() && !parameters.stdin()) {
+      throw new UsageException(
+          "--assume-filename is only supported when formatting standard input");
+    }
+    if (parameters.dryRun() && parameters.inPlace()) {
+      throw new UsageException("cannot use --dry-run and --in-place at the same time");
+    }
+    return parameters;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java
new file mode 100644
index 0000000..f7f610b
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeMap;
+import com.google.googlejavaformat.Input.Tok;
+import com.google.googlejavaformat.Input.Token;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.lang.model.element.Modifier;
+
+/** Fixes sequences of modifiers to be in JLS order. */
+final class ModifierOrderer {
+
+  /**
+   * Returns the {@link javax.lang.model.element.Modifier} for the given token kind, or {@code
+   * null}.
+   */
+  private static Modifier getModifier(TokenKind kind) {
+    if (kind == null) {
+      return null;
+    }
+    switch (kind) {
+      case PUBLIC:
+        return Modifier.PUBLIC;
+      case PROTECTED:
+        return Modifier.PROTECTED;
+      case PRIVATE:
+        return Modifier.PRIVATE;
+      case ABSTRACT:
+        return Modifier.ABSTRACT;
+      case STATIC:
+        return Modifier.STATIC;
+      case DEFAULT:
+        return Modifier.DEFAULT;
+      case FINAL:
+        return Modifier.FINAL;
+      case TRANSIENT:
+        return Modifier.TRANSIENT;
+      case VOLATILE:
+        return Modifier.VOLATILE;
+      case SYNCHRONIZED:
+        return Modifier.SYNCHRONIZED;
+      case NATIVE:
+        return Modifier.NATIVE;
+      case STRICTFP:
+        return Modifier.STRICTFP;
+      default:
+        return null;
+    }
+  }
+
+  /** Reorders all modifiers in the given text to be in JLS order. */
+  static JavaInput reorderModifiers(String text) throws FormatterException {
+    return reorderModifiers(
+        new JavaInput(text), ImmutableList.of(Range.closedOpen(0, text.length())));
+  }
+
+  /**
+   * Reorders all modifiers in the given text and within the given character ranges to be in JLS
+   * order.
+   */
+  static JavaInput reorderModifiers(JavaInput javaInput, Collection<Range<Integer>> characterRanges)
+      throws FormatterException {
+    if (javaInput.getTokens().isEmpty()) {
+      // There weren't any tokens, possible because of a lexing error.
+      // Errors about invalid input will be reported later after parsing.
+      return javaInput;
+    }
+    RangeSet<Integer> tokenRanges = javaInput.characterRangesToTokenRanges(characterRanges);
+    Iterator<? extends Token> it = javaInput.getTokens().iterator();
+    TreeRangeMap<Integer, String> replacements = TreeRangeMap.create();
+    while (it.hasNext()) {
+      Token token = it.next();
+      if (!tokenRanges.contains(token.getTok().getIndex())) {
+        continue;
+      }
+      Modifier mod = asModifier(token);
+      if (mod == null) {
+        continue;
+      }
+
+      List<Token> modifierTokens = new ArrayList<>();
+      List<Modifier> mods = new ArrayList<>();
+
+      int begin = token.getTok().getPosition();
+      mods.add(mod);
+      modifierTokens.add(token);
+
+      int end = -1;
+      while (it.hasNext()) {
+        token = it.next();
+        mod = asModifier(token);
+        if (mod == null) {
+          break;
+        }
+        mods.add(mod);
+        modifierTokens.add(token);
+        end = token.getTok().getPosition() + token.getTok().length();
+      }
+
+      if (!Ordering.natural().isOrdered(mods)) {
+        Collections.sort(mods);
+        StringBuilder replacement = new StringBuilder();
+        for (int i = 0; i < mods.size(); i++) {
+          if (i > 0) {
+            addTrivia(replacement, modifierTokens.get(i).getToksBefore());
+          }
+          replacement.append(mods.get(i).toString());
+          if (i < (modifierTokens.size() - 1)) {
+            addTrivia(replacement, modifierTokens.get(i).getToksAfter());
+          }
+        }
+        replacements.put(Range.closedOpen(begin, end), replacement.toString());
+      }
+    }
+    return applyReplacements(javaInput, replacements);
+  }
+
+  private static void addTrivia(StringBuilder replacement, ImmutableList<? extends Tok> toks) {
+    for (Tok tok : toks) {
+      replacement.append(tok.getText());
+    }
+  }
+
+  /**
+   * Returns the given token as a {@link javax.lang.model.element.Modifier}, or {@code null} if it
+   * is not a modifier.
+   */
+  private static Modifier asModifier(Token token) {
+    return getModifier(((JavaInput.Tok) token.getTok()).kind());
+  }
+
+  /** Applies replacements to the given string. */
+  private static JavaInput applyReplacements(
+      JavaInput javaInput, TreeRangeMap<Integer, String> replacementMap) throws FormatterException {
+    // process in descending order so the replacement ranges aren't perturbed if any replacements
+    // differ in size from the input
+    Map<Range<Integer>, String> ranges = replacementMap.asDescendingMapOfRanges();
+    if (ranges.isEmpty()) {
+      return javaInput;
+    }
+    StringBuilder sb = new StringBuilder(javaInput.getText());
+    for (Entry<Range<Integer>, String> entry : ranges.entrySet()) {
+      Range<Integer> range = entry.getKey();
+      sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
+    }
+    return new JavaInput(sb.toString());
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java
new file mode 100644
index 0000000..d939480
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeMap;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeMap;
+import com.google.common.collect.TreeRangeSet;
+import com.google.googlejavaformat.Newlines;
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.ReferenceTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.ImportTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.util.DocTreePath;
+import com.sun.source.util.DocTreePathScanner;
+import com.sun.source.util.TreePathScanner;
+import com.sun.source.util.TreeScanner;
+import com.sun.tools.javac.api.JavacTrees;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.parser.JavacParser;
+import com.sun.tools.javac.parser.ParserFactory;
+import com.sun.tools.javac.tree.DCTree;
+import com.sun.tools.javac.tree.DCTree.DCReference;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
+import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
+import com.sun.tools.javac.tree.JCTree.JCIdent;
+import com.sun.tools.javac.tree.JCTree.JCImport;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Options;
+import java.io.IOError;
+import java.io.IOException;
+import java.net.URI;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DiagnosticListener;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Removes unused imports from a source file. Imports that are only used in javadoc are also
+ * removed, and the references in javadoc are replaced with fully qualified names.
+ */
+public class RemoveUnusedImports {
+
+  // Visits an AST, recording all simple names that could refer to imported
+  // types and also any javadoc references that could refer to imported
+  // types (`@link`, `@see`, `@throws`, etc.)
+  //
+  // No attempt is made to determine whether simple names occur in contexts
+  // where they are type names, so there will be false positives. For example,
+  // `List` is not identified as unused import below:
+  //
+  // ```
+  // import java.util.List;
+  // class List {}
+  // ```
+  //
+  // This is still reasonably effective in practice because type names differ
+  // from other kinds of names in casing convention, and simple name
+  // clashes between imported and declared types are rare.
+  private static class UnusedImportScanner extends TreePathScanner<Void, Void> {
+
+    private final Set<String> usedNames = new LinkedHashSet<>();
+    private final Multimap<String, Range<Integer>> usedInJavadoc = HashMultimap.create();
+    final JavacTrees trees;
+    final DocTreeScanner docTreeSymbolScanner;
+
+    private UnusedImportScanner(JavacTrees trees) {
+      this.trees = trees;
+      docTreeSymbolScanner = new DocTreeScanner();
+    }
+
+    /** Skip the imports themselves when checking for usage. */
+    @Override
+    public Void visitImport(ImportTree importTree, Void usedSymbols) {
+      return null;
+    }
+
+    @Override
+    public Void visitIdentifier(IdentifierTree tree, Void unused) {
+      if (tree == null) {
+        return null;
+      }
+      usedNames.add(tree.getName().toString());
+      return null;
+    }
+
+    @Override
+    public Void scan(Tree tree, Void unused) {
+      if (tree == null) {
+        return null;
+      }
+      scanJavadoc();
+      return super.scan(tree, unused);
+    }
+
+    private void scanJavadoc() {
+      if (getCurrentPath() == null) {
+        return;
+      }
+      DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath());
+      if (commentTree == null) {
+        return;
+      }
+      docTreeSymbolScanner.scan(new DocTreePath(getCurrentPath(), commentTree), null);
+    }
+
+    // scan javadoc comments, checking for references to imported types
+    class DocTreeScanner extends DocTreePathScanner<Void, Void> {
+      @Override
+      public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVoid) {
+        return null;
+      }
+
+      @Override
+      public Void visitReference(ReferenceTree referenceTree, Void unused) {
+        DCReference reference = (DCReference) referenceTree;
+        long basePos =
+            reference.getSourcePosition((DCTree.DCDocComment) getCurrentPath().getDocComment());
+        // the position of trees inside the reference node aren't stored, but the qualifier's
+        // start position is the beginning of the reference node
+        if (reference.qualifierExpression != null) {
+          new ReferenceScanner(basePos).scan(reference.qualifierExpression, null);
+        }
+        // Record uses inside method parameters. The javadoc tool doesn't use these, but
+        // IntelliJ does.
+        if (reference.paramTypes != null) {
+          for (JCTree param : reference.paramTypes) {
+            // TODO(cushon): get start positions for the parameters
+            new ReferenceScanner(-1).scan(param, null);
+          }
+        }
+        return null;
+      }
+
+      // scans the qualifier and parameters of a javadoc reference for possible type names
+      private class ReferenceScanner extends TreeScanner<Void, Void> {
+        private final long basePos;
+
+        public ReferenceScanner(long basePos) {
+          this.basePos = basePos;
+        }
+
+        @Override
+        public Void visitIdentifier(IdentifierTree node, Void aVoid) {
+          usedInJavadoc.put(
+              node.getName().toString(),
+              basePos != -1
+                  ? Range.closedOpen((int) basePos, (int) basePos + node.getName().length())
+                  : null);
+          return super.visitIdentifier(node, aVoid);
+        }
+      }
+    }
+  }
+
+  public static String removeUnusedImports(final String contents) throws FormatterException {
+    Context context = new Context();
+    JCCompilationUnit unit = parse(context, contents);
+    if (unit == null) {
+      // error handling is done during formatting
+      return contents;
+    }
+    UnusedImportScanner scanner = new UnusedImportScanner(JavacTrees.instance(context));
+    scanner.scan(unit, null);
+    return applyReplacements(
+        contents, buildReplacements(contents, unit, scanner.usedNames, scanner.usedInJavadoc));
+  }
+
+  private static JCCompilationUnit parse(Context context, String javaInput)
+      throws FormatterException {
+    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+    context.put(DiagnosticListener.class, diagnostics);
+    Options.instance(context).put("--enable-preview", "true");
+    Options.instance(context).put("allowStringFolding", "false");
+    JCCompilationUnit unit;
+    JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
+    try {
+      fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
+    } catch (IOException e) {
+      // impossible
+      throw new IOError(e);
+    }
+    SimpleJavaFileObject source =
+        new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
+          @Override
+          public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+            return javaInput;
+          }
+        };
+    Log.instance(context).useSource(source);
+    ParserFactory parserFactory = ParserFactory.instance(context);
+    JavacParser parser =
+        parserFactory.newParser(
+            javaInput, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);
+    unit = parser.parseCompilationUnit();
+    unit.sourcefile = source;
+    Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
+        Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
+    if (!Iterables.isEmpty(errorDiagnostics)) {
+      // error handling is done during formatting
+      throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
+    }
+    return unit;
+  }
+
+  /** Construct replacements to fix unused imports. */
+  private static RangeMap<Integer, String> buildReplacements(
+      String contents,
+      JCCompilationUnit unit,
+      Set<String> usedNames,
+      Multimap<String, Range<Integer>> usedInJavadoc) {
+    RangeMap<Integer, String> replacements = TreeRangeMap.create();
+    for (JCImport importTree : unit.getImports()) {
+      String simpleName = getSimpleName(importTree);
+      if (!isUnused(unit, usedNames, usedInJavadoc, importTree, simpleName)) {
+        continue;
+      }
+      // delete the import
+      int endPosition = importTree.getEndPosition(unit.endPositions);
+      endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
+      String sep = Newlines.guessLineSeparator(contents);
+      if (endPosition + sep.length() < contents.length()
+          && contents.subSequence(endPosition, endPosition + sep.length()).toString().equals(sep)) {
+        endPosition += sep.length();
+      }
+      replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
+    }
+    return replacements;
+  }
+
+  private static String getSimpleName(JCImport importTree) {
+    return importTree.getQualifiedIdentifier() instanceof JCIdent
+        ? ((JCIdent) importTree.getQualifiedIdentifier()).getName().toString()
+        : ((JCFieldAccess) importTree.getQualifiedIdentifier()).getIdentifier().toString();
+  }
+
+  private static boolean isUnused(
+      JCCompilationUnit unit,
+      Set<String> usedNames,
+      Multimap<String, Range<Integer>> usedInJavadoc,
+      JCImport importTree,
+      String simpleName) {
+    String qualifier =
+        ((JCFieldAccess) importTree.getQualifiedIdentifier()).getExpression().toString();
+    if (qualifier.equals("java.lang")) {
+      return true;
+    }
+    if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) {
+      return true;
+    }
+    if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess
+        && ((JCFieldAccess) importTree.getQualifiedIdentifier())
+            .getIdentifier()
+            .contentEquals("*")) {
+      return false;
+    }
+
+    if (usedNames.contains(simpleName)) {
+      return false;
+    }
+    if (usedInJavadoc.containsKey(simpleName)) {
+      return false;
+    }
+    return true;
+  }
+
+  /** Applies the replacements to the given source, and re-format any edited javadoc. */
+  private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
+    // save non-empty fixed ranges for reformatting after fixes are applied
+    RangeSet<Integer> fixedRanges = TreeRangeSet.create();
+
+    // Apply the fixes in increasing order, adjusting ranges to account for
+    // earlier fixes that change the length of the source. The output ranges are
+    // needed so we can reformat fixed regions, otherwise the fixes could just
+    // be applied in descending order without adjusting offsets.
+    StringBuilder sb = new StringBuilder(source);
+    int offset = 0;
+    for (Map.Entry<Range<Integer>, String> replacement : replacements.asMapOfRanges().entrySet()) {
+      Range<Integer> range = replacement.getKey();
+      String replaceWith = replacement.getValue();
+      int start = offset + range.lowerEndpoint();
+      int end = offset + range.upperEndpoint();
+      sb.replace(start, end, replaceWith);
+      if (!replaceWith.isEmpty()) {
+        fixedRanges.add(Range.closedOpen(start, end));
+      }
+      offset += replaceWith.length() - (range.upperEndpoint() - range.lowerEndpoint());
+    }
+    return sb.toString();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Replacement.java b/core/src/main/java/com/google/googlejavaformat/java/Replacement.java
new file mode 100644
index 0000000..5df0991
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/Replacement.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Range;
+import java.util.Objects;
+
+/**
+ * Represents a range in the original source and replacement text for that range.
+ *
+ * <p>google-java-format doesn't depend on AutoValue, to allow AutoValue to depend on
+ * google-java-format.
+ */
+public final class Replacement {
+
+  public static Replacement create(int startPosition, int endPosition, String replaceWith) {
+    checkArgument(startPosition >= 0, "startPosition must be non-negative");
+    checkArgument(startPosition <= endPosition, "startPosition cannot be after endPosition");
+    return new Replacement(Range.closedOpen(startPosition, endPosition), replaceWith);
+  }
+
+  private final Range<Integer> replaceRange;
+  private final String replacementString;
+
+  private Replacement(Range<Integer> replaceRange, String replacementString) {
+    this.replaceRange = checkNotNull(replaceRange, "Null replaceRange");
+    this.replacementString = checkNotNull(replacementString, "Null replacementString");
+  }
+
+  /** The range of characters in the original source to replace. */
+  public Range<Integer> getReplaceRange() {
+    return replaceRange;
+  }
+
+  /** The string to replace the range of characters with. */
+  public String getReplacementString() {
+    return replacementString;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (o instanceof Replacement) {
+      Replacement that = (Replacement) o;
+      return replaceRange.equals(that.getReplaceRange())
+          && replacementString.equals(that.getReplacementString());
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(replaceRange, replacementString);
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java
new file mode 100644
index 0000000..8d426b6
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Formats a subset of a compilation unit. */
+public class SnippetFormatter {
+
+  /** The kind of snippet to format. */
+  public enum SnippetKind {
+    COMPILATION_UNIT,
+    CLASS_BODY_DECLARATIONS,
+    STATEMENTS,
+    EXPRESSION
+  }
+
+  private class SnippetWrapper {
+    int offset;
+    final StringBuilder contents = new StringBuilder();
+
+    public SnippetWrapper append(String str) {
+      contents.append(str);
+      return this;
+    }
+
+    public SnippetWrapper appendSource(String source) {
+      this.offset = contents.length();
+      contents.append(source);
+      return this;
+    }
+
+    public void closeBraces(int initialIndent) {
+      for (int i = initialIndent; --i >= 0; ) {
+        contents.append("\n").append(createIndentationString(i)).append("}");
+      }
+    }
+  }
+
+  private static final int INDENTATION_SIZE = 2;
+  private final Formatter formatter = new Formatter();
+  private static final CharMatcher NOT_WHITESPACE = CharMatcher.whitespace().negate();
+
+  public String createIndentationString(int indentationLevel) {
+    Preconditions.checkArgument(
+        indentationLevel >= 0,
+        "Indentation level cannot be less than zero. Given: %s",
+        indentationLevel);
+    int spaces = indentationLevel * INDENTATION_SIZE;
+    StringBuilder buf = new StringBuilder(spaces);
+    for (int i = 0; i < spaces; i++) {
+      buf.append(' ');
+    }
+    return buf.toString();
+  }
+
+  private static Range<Integer> offsetRange(Range<Integer> range, int offset) {
+    range = range.canonical(DiscreteDomain.integers());
+    return Range.closedOpen(range.lowerEndpoint() + offset, range.upperEndpoint() + offset);
+  }
+
+  private static List<Range<Integer>> offsetRanges(List<Range<Integer>> ranges, int offset) {
+    List<Range<Integer>> result = new ArrayList<>();
+    for (Range<Integer> range : ranges) {
+      result.add(offsetRange(range, offset));
+    }
+    return result;
+  }
+
+  /** Runs the Google Java formatter on the given source, with only the given ranges specified. */
+  public ImmutableList<Replacement> format(
+      SnippetKind kind,
+      String source,
+      List<Range<Integer>> ranges,
+      int initialIndent,
+      boolean includeComments)
+      throws FormatterException {
+    RangeSet<Integer> rangeSet = TreeRangeSet.create();
+    for (Range<Integer> range : ranges) {
+      rangeSet.add(range);
+    }
+    if (includeComments) {
+      if (kind != SnippetKind.COMPILATION_UNIT) {
+        throw new IllegalArgumentException(
+            "comment formatting is only supported for compilation units");
+      }
+      return formatter.getFormatReplacements(source, ranges);
+    }
+    SnippetWrapper wrapper = snippetWrapper(kind, source, initialIndent);
+    ranges = offsetRanges(ranges, wrapper.offset);
+
+    String replacement = formatter.formatSource(wrapper.contents.toString(), ranges);
+    replacement =
+        replacement.substring(
+            wrapper.offset,
+            replacement.length() - (wrapper.contents.length() - wrapper.offset - source.length()));
+
+    return toReplacements(source, replacement).stream()
+        .filter(r -> rangeSet.encloses(r.getReplaceRange()))
+        .collect(toImmutableList());
+  }
+
+  /**
+   * Generates {@code Replacement}s rewriting {@code source} to {@code replacement}, under the
+   * assumption that they differ in whitespace alone.
+   */
+  private static List<Replacement> toReplacements(String source, String replacement) {
+    if (!NOT_WHITESPACE.retainFrom(source).equals(NOT_WHITESPACE.retainFrom(replacement))) {
+      throw new IllegalArgumentException(
+          "source = \"" + source + "\", replacement = \"" + replacement + "\"");
+    }
+    /*
+     * In the past we seemed to have problems touching non-whitespace text in the formatter, even
+     * just replacing some code with itself.  Retrospective attempts to reproduce this have failed,
+     * but this may be an issue for future changes.
+     */
+    List<Replacement> replacements = new ArrayList<>();
+    int i = NOT_WHITESPACE.indexIn(source);
+    int j = NOT_WHITESPACE.indexIn(replacement);
+    if (i != 0 || j != 0) {
+      replacements.add(Replacement.create(0, i, replacement.substring(0, j)));
+    }
+    while (i != -1 && j != -1) {
+      int i2 = NOT_WHITESPACE.indexIn(source, i + 1);
+      int j2 = NOT_WHITESPACE.indexIn(replacement, j + 1);
+      if (i2 == -1 || j2 == -1) {
+        break;
+      }
+      if ((i2 - i) != (j2 - j)
+          || !source.substring(i + 1, i2).equals(replacement.substring(j + 1, j2))) {
+        replacements.add(Replacement.create(i + 1, i2, replacement.substring(j + 1, j2)));
+      }
+      i = i2;
+      j = j2;
+    }
+    return replacements;
+  }
+
+  private SnippetWrapper snippetWrapper(SnippetKind kind, String source, int initialIndent) {
+    /*
+     * Synthesize a dummy class around the code snippet provided by Eclipse.  The dummy class is
+     * correctly formatted -- the blocks use correct indentation, etc.
+     */
+    switch (kind) {
+      case COMPILATION_UNIT:
+        {
+          SnippetWrapper wrapper = new SnippetWrapper();
+          for (int i = 1; i <= initialIndent; i++) {
+            wrapper.append("class Dummy {\n").append(createIndentationString(i));
+          }
+          wrapper.appendSource(source);
+          wrapper.closeBraces(initialIndent);
+          return wrapper;
+        }
+      case CLASS_BODY_DECLARATIONS:
+        {
+          SnippetWrapper wrapper = new SnippetWrapper();
+          for (int i = 1; i <= initialIndent; i++) {
+            wrapper.append("class Dummy {\n").append(createIndentationString(i));
+          }
+          wrapper.appendSource(source);
+          wrapper.closeBraces(initialIndent);
+          return wrapper;
+        }
+      case STATEMENTS:
+        {
+          SnippetWrapper wrapper = new SnippetWrapper();
+          wrapper.append("class Dummy {\n").append(createIndentationString(1));
+          for (int i = 2; i <= initialIndent; i++) {
+            wrapper.append("{\n").append(createIndentationString(i));
+          }
+          wrapper.appendSource(source);
+          wrapper.closeBraces(initialIndent);
+          return wrapper;
+        }
+      case EXPRESSION:
+        {
+          SnippetWrapper wrapper = new SnippetWrapper();
+          wrapper.append("class Dummy {\n").append(createIndentationString(1));
+          for (int i = 2; i <= initialIndent; i++) {
+            wrapper.append("{\n").append(createIndentationString(i));
+          }
+          wrapper.append("Object o = ");
+          wrapper.appendSource(source);
+          wrapper.append(";");
+          wrapper.closeBraces(initialIndent);
+          return wrapper;
+        }
+      default:
+        throw new IllegalArgumentException("Unknown snippet kind: " + kind);
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
new file mode 100644
index 0000000..e41bb66
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.collect.Iterables.getLast;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Range;
+import com.google.common.collect.TreeRangeMap;
+import com.google.googlejavaformat.Newlines;
+import com.sun.source.tree.BinaryTree;
+import com.sun.source.tree.LiteralTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.Tree.Kind;
+import com.sun.source.util.TreePath;
+import com.sun.source.util.TreePathScanner;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.parser.JavacParser;
+import com.sun.tools.javac.parser.ParserFactory;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+import com.sun.tools.javac.util.Options;
+import com.sun.tools.javac.util.Position;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DiagnosticListener;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+
+/** Wraps string literals that exceed the column limit. */
+public final class StringWrapper {
+  /** Reflows long string literals in the given Java source code. */
+  public static String wrap(String input, Formatter formatter) throws FormatterException {
+    return StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, input, formatter);
+  }
+
+  /**
+   * Reflows string literals in the given Java source code that extend past the given column limit.
+   */
+  static String wrap(final int columnLimit, String input, Formatter formatter)
+      throws FormatterException {
+    if (!longLines(columnLimit, input)) {
+      // fast path
+      return input;
+    }
+
+    TreeRangeMap<Integer, String> replacements = getReflowReplacements(columnLimit, input);
+    String firstPass = formatter.formatSource(input, replacements.asMapOfRanges().keySet());
+
+    if (!firstPass.equals(input)) {
+      // If formatting the replacement ranges resulted in a change, recalculate the replacements on
+      // the updated input.
+      input = firstPass;
+      replacements = getReflowReplacements(columnLimit, input);
+    }
+
+    String result = applyReplacements(input, replacements);
+
+    {
+      // We really don't want bugs in this pass to change the behaviour of programs we're
+      // formatting, so check that the pretty-printed AST is the same before and after reformatting.
+      String expected = parse(input, /* allowStringFolding= */ true).toString();
+      String actual = parse(result, /* allowStringFolding= */ true).toString();
+      if (!expected.equals(actual)) {
+        throw new FormatterException(
+            String.format(
+                "Something has gone terribly wrong. Please file a bug: "
+                    + "https://github.com/google/google-java-format/issues/new"
+                    + "\n\n=== Actual: ===\n%s\n=== Expected: ===\n%s\n",
+                actual, expected));
+      }
+    }
+
+    return result;
+  }
+
+  private static TreeRangeMap<Integer, String> getReflowReplacements(
+      int columnLimit, final String input) throws FormatterException {
+    JCTree.JCCompilationUnit unit = parse(input, /* allowStringFolding= */ false);
+    String separator = Newlines.guessLineSeparator(input);
+
+    // Paths to string literals that extend past the column limit.
+    List<TreePath> toFix = new ArrayList<>();
+    final Position.LineMap lineMap = unit.getLineMap();
+    new TreePathScanner<Void, Void>() {
+      @Override
+      public Void visitLiteral(LiteralTree literalTree, Void aVoid) {
+        if (literalTree.getKind() != Kind.STRING_LITERAL) {
+          return null;
+        }
+        Tree parent = getCurrentPath().getParentPath().getLeaf();
+        if (parent instanceof MemberSelectTree
+            && ((MemberSelectTree) parent).getExpression().equals(literalTree)) {
+          return null;
+        }
+        int endPosition = getEndPosition(unit, literalTree);
+        int lineEnd = endPosition;
+        while (Newlines.hasNewlineAt(input, lineEnd) == -1) {
+          lineEnd++;
+        }
+        if (lineMap.getColumnNumber(lineEnd) - 1 <= columnLimit) {
+          return null;
+        }
+        toFix.add(getCurrentPath());
+        return null;
+      }
+    }.scan(new TreePath(unit), null);
+
+    TreeRangeMap<Integer, String> replacements = TreeRangeMap.create();
+    for (TreePath path : toFix) {
+      // Find the outermost contiguous enclosing concatenation expression
+      TreePath enclosing = path;
+      while (enclosing.getParentPath().getLeaf().getKind() == Tree.Kind.PLUS) {
+        enclosing = enclosing.getParentPath();
+      }
+      // Is the literal being wrapped the first in a chain of concatenation expressions?
+      // i.e. `ONE + TWO + THREE`
+      // We need this information to handle continuation indents.
+      AtomicBoolean first = new AtomicBoolean(false);
+      // Finds the set of string literals in the concat expression that includes the one that needs
+      // to be wrapped.
+      List<Tree> flat = flatten(input, unit, path, enclosing, first);
+      // Zero-indexed start column
+      int startColumn = lineMap.getColumnNumber(getStartPosition(flat.get(0))) - 1;
+
+      // Handling leaving trailing non-string tokens at the end of the literal,
+      // e.g. the trailing `);` in `foo("...");`.
+      int end = getEndPosition(unit, getLast(flat));
+      int lineEnd = end;
+      while (Newlines.hasNewlineAt(input, lineEnd) == -1) {
+        lineEnd++;
+      }
+      int trailing = lineEnd - end;
+
+      // Get the original source text of the string literals, excluding `"` and `+`.
+      ImmutableList<String> components = stringComponents(input, unit, flat);
+      replacements.put(
+          Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(unit, getLast(flat))),
+          reflow(separator, columnLimit, startColumn, trailing, components, first.get()));
+    }
+    return replacements;
+  }
+
+  /**
+   * Returns the source text of the given string literal trees, excluding the leading and trailing
+   * double-quotes and the `+` operator.
+   */
+  private static ImmutableList<String> stringComponents(
+      String input, JCTree.JCCompilationUnit unit, List<Tree> flat) {
+    ImmutableList.Builder<String> result = ImmutableList.builder();
+    StringBuilder piece = new StringBuilder();
+    for (Tree tree : flat) {
+      // adjust for leading and trailing double quotes
+      String text = input.substring(getStartPosition(tree) + 1, getEndPosition(unit, tree) - 1);
+      int start = 0;
+      for (int idx = 0; idx < text.length(); idx++) {
+        if (CharMatcher.whitespace().matches(text.charAt(idx))) {
+          // continue below
+        } else if (hasEscapedWhitespaceAt(text, idx) != -1) {
+          // continue below
+        } else if (hasEscapedNewlineAt(text, idx) != -1) {
+          int length;
+          while ((length = hasEscapedNewlineAt(text, idx)) != -1) {
+            idx += length;
+          }
+        } else {
+          continue;
+        }
+        piece.append(text, start, idx);
+        result.add(piece.toString());
+        piece = new StringBuilder();
+        start = idx;
+      }
+      if (piece.length() > 0) {
+        result.add(piece.toString());
+        piece = new StringBuilder();
+      }
+      if (start < text.length()) {
+        piece.append(text, start, text.length());
+      }
+    }
+    if (piece.length() > 0) {
+      result.add(piece.toString());
+    }
+    return result.build();
+  }
+
+  static int hasEscapedWhitespaceAt(String input, int idx) {
+    return Stream.of("\\t")
+        .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1)
+        .filter(x -> x != -1)
+        .findFirst()
+        .orElse(-1);
+  }
+
+  static int hasEscapedNewlineAt(String input, int idx) {
+    return Stream.of("\\r\\n", "\\r", "\\n")
+        .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1)
+        .filter(x -> x != -1)
+        .findFirst()
+        .orElse(-1);
+  }
+
+  /**
+   * Reflows the given source text, trying to split on word boundaries.
+   *
+   * @param separator the line separator
+   * @param columnLimit the number of columns to wrap at
+   * @param startColumn the column position of the beginning of the original text
+   * @param trailing extra space to leave after the last line
+   * @param components the text to reflow
+   * @param first0 true if the text includes the beginning of its enclosing concat chain, i.e. a
+   */
+  private static String reflow(
+      String separator,
+      int columnLimit,
+      int startColumn,
+      int trailing,
+      ImmutableList<String> components,
+      boolean first0) {
+    // We have space between the start column and the limit to output the first line.
+    // Reserve two spaces for the quotes.
+    int width = columnLimit - startColumn - 2;
+    Deque<String> input = new ArrayDeque<>(components);
+    List<String> lines = new ArrayList<>();
+    boolean first = first0;
+    while (!input.isEmpty()) {
+      int length = 0;
+      List<String> line = new ArrayList<>();
+      if (input.stream().mapToInt(x -> x.length()).sum() <= width) {
+        width -= trailing;
+      }
+      while (!input.isEmpty() && (length <= 4 || (length + input.peekFirst().length()) < width)) {
+        String text = input.removeFirst();
+        line.add(text);
+        length += text.length();
+        if (text.endsWith("\\n") || text.endsWith("\\r")) {
+          break;
+        }
+      }
+      if (line.isEmpty()) {
+        line.add(input.removeFirst());
+      }
+      // add the split line to the output, and process whatever's left
+      lines.add(String.join("", line));
+      if (first) {
+        width -= 6; // subsequent lines have a four-space continuation indent and a `+ `
+        first = false;
+      }
+    }
+
+    return lines.stream()
+        .collect(
+            joining(
+                "\"" + separator + Strings.repeat(" ", startColumn + (first0 ? 4 : -2)) + "+ \"",
+                "\"",
+                "\""));
+  }
+
+  /**
+   * Flattens the given binary expression tree, and extracts the subset that contains the given path
+   * and any adjacent nodes that are also string literals.
+   */
+  private static List<Tree> flatten(
+      String input,
+      JCTree.JCCompilationUnit unit,
+      TreePath path,
+      TreePath parent,
+      AtomicBoolean firstInChain) {
+    List<Tree> flat = new ArrayList<>();
+
+    // flatten the expression tree with a pre-order traversal
+    ArrayDeque<Tree> todo = new ArrayDeque<>();
+    todo.add(parent.getLeaf());
+    while (!todo.isEmpty()) {
+      Tree first = todo.removeFirst();
+      if (first.getKind() == Tree.Kind.PLUS) {
+        BinaryTree bt = (BinaryTree) first;
+        todo.addFirst(bt.getRightOperand());
+        todo.addFirst(bt.getLeftOperand());
+      } else {
+        flat.add(first);
+      }
+    }
+
+    int idx = flat.indexOf(path.getLeaf());
+    Verify.verify(idx != -1);
+
+    // walk outwards from the leaf for adjacent string literals to also reflow
+    int startIdx = idx;
+    int endIdx = idx + 1;
+    while (startIdx > 0
+        && flat.get(startIdx - 1).getKind() == Tree.Kind.STRING_LITERAL
+        && noComments(input, unit, flat.get(startIdx - 1), flat.get(startIdx))) {
+      startIdx--;
+    }
+    while (endIdx < flat.size()
+        && flat.get(endIdx).getKind() == Tree.Kind.STRING_LITERAL
+        && noComments(input, unit, flat.get(endIdx - 1), flat.get(endIdx))) {
+      endIdx++;
+    }
+
+    firstInChain.set(startIdx == 0);
+    return ImmutableList.copyOf(flat.subList(startIdx, endIdx));
+  }
+
+  private static boolean noComments(
+      String input, JCTree.JCCompilationUnit unit, Tree one, Tree two) {
+    return STRING_CONCAT_DELIMITER.matchesAllOf(
+        input.subSequence(getEndPosition(unit, one), getStartPosition(two)));
+  }
+
+  public static final CharMatcher STRING_CONCAT_DELIMITER =
+      CharMatcher.whitespace().or(CharMatcher.anyOf("\"+"));
+
+  private static int getEndPosition(JCTree.JCCompilationUnit unit, Tree tree) {
+    return ((JCTree) tree).getEndPosition(unit.endPositions);
+  }
+
+  private static int getStartPosition(Tree tree) {
+    return ((JCTree) tree).getStartPosition();
+  }
+
+  /** Returns true if any lines in the given Java source exceed the column limit. */
+  private static boolean longLines(int columnLimit, String input) {
+    // TODO(cushon): consider adding Newlines.lineIterable?
+    Iterator<String> it = Newlines.lineIterator(input);
+    while (it.hasNext()) {
+      String line = it.next();
+      if (line.length() > columnLimit) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /** Parses the given Java source. */
+  private static JCTree.JCCompilationUnit parse(String source, boolean allowStringFolding)
+      throws FormatterException {
+    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+    Context context = new Context();
+    context.put(DiagnosticListener.class, diagnostics);
+    Options.instance(context).put("--enable-preview", "true");
+    Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding));
+    JCTree.JCCompilationUnit unit;
+    JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
+    try {
+      fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+    SimpleJavaFileObject sjfo =
+        new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
+          @Override
+          public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+            return source;
+          }
+        };
+    Log.instance(context).useSource(sjfo);
+    ParserFactory parserFactory = ParserFactory.instance(context);
+    JavacParser parser =
+        parserFactory.newParser(
+            source, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);
+    unit = parser.parseCompilationUnit();
+    unit.sourcefile = sjfo;
+    Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
+        Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
+    if (!Iterables.isEmpty(errorDiagnostics)) {
+      // error handling is done during formatting
+      throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
+    }
+    return unit;
+  }
+
+  /** Applies replacements to the given string. */
+  private static String applyReplacements(
+      String javaInput, TreeRangeMap<Integer, String> replacementMap) throws FormatterException {
+    // process in descending order so the replacement ranges aren't perturbed if any replacements
+    // differ in size from the input
+    Map<Range<Integer>, String> ranges = replacementMap.asDescendingMapOfRanges();
+    if (ranges.isEmpty()) {
+      return javaInput;
+    }
+    StringBuilder sb = new StringBuilder(javaInput);
+    for (Map.Entry<Range<Integer>, String> entry : ranges.entrySet()) {
+      Range<Integer> range = entry.getKey();
+      sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
+    }
+    return sb.toString();
+  }
+
+  private StringWrapper() {}
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Trees.java b/core/src/main/java/com/google/googlejavaformat/java/Trees.java
new file mode 100644
index 0000000..397daca
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/Trees.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompoundAssignmentTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.ParenthesizedTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.Pretty;
+import com.sun.tools.javac.tree.TreeInfo;
+import java.io.IOError;
+import java.io.IOException;
+import javax.lang.model.element.Name;
+
+/** Utilities for working with {@link Tree}s. */
+class Trees {
+  /** Returns the length of the source for the node. */
+  static int getLength(Tree tree, TreePath path) {
+    return getEndPosition(tree, path) - getStartPosition(tree);
+  }
+
+  /** Returns the source start position of the node. */
+  static int getStartPosition(Tree expression) {
+    return ((JCTree) expression).getStartPosition();
+  }
+
+  /** Returns the source end position of the node. */
+  static int getEndPosition(Tree expression, TreePath path) {
+    return ((JCTree) expression)
+        .getEndPosition(((JCTree.JCCompilationUnit) path.getCompilationUnit()).endPositions);
+  }
+
+  /** Returns the source text for the node. */
+  static String getSourceForNode(Tree node, TreePath path) {
+    CharSequence source;
+    try {
+      source = path.getCompilationUnit().getSourceFile().getCharContent(false);
+    } catch (IOException e) {
+      throw new IOError(e);
+    }
+    return source.subSequence(getStartPosition(node), getEndPosition(node, path)).toString();
+  }
+
+  /** Returns the simple name of a (possibly qualified) method invocation expression. */
+  static Name getMethodName(MethodInvocationTree methodInvocation) {
+    ExpressionTree select = methodInvocation.getMethodSelect();
+    return select instanceof MemberSelectTree
+        ? ((MemberSelectTree) select).getIdentifier()
+        : ((IdentifierTree) select).getName();
+  }
+
+  /** Returns the receiver of a qualified method invocation expression, or {@code null}. */
+  static ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) {
+    ExpressionTree select = methodInvocation.getMethodSelect();
+    return select instanceof MemberSelectTree ? ((MemberSelectTree) select).getExpression() : null;
+  }
+
+  /** Returns the string name of an operator, including assignment and compound assignment. */
+  static String operatorName(ExpressionTree expression) {
+    JCTree.Tag tag = ((JCTree) expression).getTag();
+    if (tag == JCTree.Tag.ASSIGN) {
+      return "=";
+    }
+    boolean assignOp = expression instanceof CompoundAssignmentTree;
+    if (assignOp) {
+      tag = tag.noAssignOp();
+    }
+    String name = new Pretty(/*writer*/ null, /*sourceOutput*/ true).operatorName(tag);
+    return assignOp ? name + "=" : name;
+  }
+
+  /** Returns the precedence of an expression's operator. */
+  static int precedence(ExpressionTree expression) {
+    return TreeInfo.opPrec(((JCTree) expression).getTag());
+  }
+
+  /**
+   * Returns the enclosing type declaration (class, enum, interface, or annotation) for the given
+   * path.
+   */
+  static ClassTree getEnclosingTypeDeclaration(TreePath path) {
+    for (; path != null; path = path.getParentPath()) {
+      switch (path.getLeaf().getKind()) {
+        case CLASS:
+        case ENUM:
+        case INTERFACE:
+        case ANNOTATED_TYPE:
+          return (ClassTree) path.getLeaf();
+        default:
+          break;
+      }
+    }
+    throw new AssertionError();
+  }
+
+  /** Skips a single parenthesized tree. */
+  static ExpressionTree skipParen(ExpressionTree node) {
+    return ((ParenthesizedTree) node).getExpression();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java
new file mode 100644
index 0000000..4e871a6
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.google.common.base.Verify;
+import java.util.List;
+import java.util.Optional;
+
+/** Heuristics for classifying qualified names as types. */
+public final class TypeNameClassifier {
+
+  private TypeNameClassifier() {}
+
+  /** A state machine for classifying qualified names. */
+  private enum TyParseState {
+
+    /** The start state. */
+    START(false) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        switch (n) {
+          case UPPERCASE:
+            // if we see an UpperCamel later, assume this was a class
+            // e.g. com.google.FOO.Bar
+            return TyParseState.AMBIGUOUS;
+          case LOWER_CAMEL:
+            return TyParseState.REJECT;
+          case LOWERCASE:
+            // could be a package
+            return TyParseState.START;
+          case UPPER_CAMEL:
+            return TyParseState.TYPE;
+        }
+        throw new AssertionError();
+      }
+    },
+
+    /** The current prefix is a type. */
+    TYPE(true) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        switch (n) {
+          case UPPERCASE:
+          case LOWER_CAMEL:
+          case LOWERCASE:
+            return TyParseState.FIRST_STATIC_MEMBER;
+          case UPPER_CAMEL:
+            return TyParseState.TYPE;
+        }
+        throw new AssertionError();
+      }
+    },
+
+    /** The current prefix is a type, followed by a single static member access. */
+    FIRST_STATIC_MEMBER(true) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        return TyParseState.REJECT;
+      }
+    },
+
+    /** Anything not represented by one of the other states. */
+    REJECT(false) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        return TyParseState.REJECT;
+      }
+    },
+
+    /** An ambiguous type prefix. */
+    AMBIGUOUS(false) {
+      @Override
+      public TyParseState next(JavaCaseFormat n) {
+        switch (n) {
+          case UPPERCASE:
+            return AMBIGUOUS;
+          case LOWER_CAMEL:
+          case LOWERCASE:
+            return TyParseState.REJECT;
+          case UPPER_CAMEL:
+            return TyParseState.TYPE;
+        }
+        throw new AssertionError();
+      }
+    };
+
+    private final boolean isSingleUnit;
+
+    TyParseState(boolean isSingleUnit) {
+      this.isSingleUnit = isSingleUnit;
+    }
+
+    public boolean isSingleUnit() {
+      return isSingleUnit;
+    }
+
+    /** Transition function. */
+    public abstract TyParseState next(JavaCaseFormat n);
+  }
+
+  /**
+   * Returns the end index (inclusive) of the longest prefix that matches the naming conventions of
+   * a type or static field access, or -1 if no such prefix was found.
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   *   <li>ClassName
+   *   <li>ClassName.staticMemberName
+   *   <li>com.google.ClassName.InnerClass.staticMemberName
+   * </ul>
+   */
+  static Optional<Integer> typePrefixLength(List<String> nameParts) {
+    TyParseState state = TyParseState.START;
+    Optional<Integer> typeLength = Optional.empty();
+    for (int i = 0; i < nameParts.size(); i++) {
+      state = state.next(JavaCaseFormat.from(nameParts.get(i)));
+      if (state == TyParseState.REJECT) {
+        break;
+      }
+      if (state.isSingleUnit()) {
+        typeLength = Optional.of(i);
+      }
+    }
+    return typeLength;
+  }
+
+  /** Case formats used in Java identifiers. */
+  public enum JavaCaseFormat {
+    UPPERCASE,
+    LOWERCASE,
+    UPPER_CAMEL,
+    LOWER_CAMEL;
+
+    /** Classifies an identifier's case format. */
+    static JavaCaseFormat from(String name) {
+      Verify.verify(!name.isEmpty());
+      boolean firstUppercase = false;
+      boolean hasUppercase = false;
+      boolean hasLowercase = false;
+      boolean first = true;
+      for (int i = 0; i < name.length(); i++) {
+        char c = name.charAt(i);
+        if (!Character.isAlphabetic(c)) {
+          continue;
+        }
+        if (first) {
+          firstUppercase = Character.isUpperCase(c);
+          first = false;
+        }
+        hasUppercase |= Character.isUpperCase(c);
+        hasLowercase |= Character.isLowerCase(c);
+      }
+      if (firstUppercase) {
+        return hasLowercase ? UPPER_CAMEL : UPPERCASE;
+      } else {
+        return hasUppercase ? LOWER_CAMEL : LOWERCASE;
+      }
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
new file mode 100644
index 0000000..82c0843
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Joiner;
+
+/** Checked exception class for formatter command-line usage errors. */
+final class UsageException extends Exception {
+
+  private static final Joiner NEWLINE_JOINER = Joiner.on(System.lineSeparator());
+
+  private static final String[] DOCS_LINK = {
+    "https://github.com/google/google-java-format",
+  };
+
+  private static final String[] USAGE = {
+    "",
+    "Usage: google-java-format [options] file(s)",
+    "",
+    "Options:",
+    "  -i, -r, -replace, --replace",
+    "    Send formatted output back to files, not stdout.",
+    "  -",
+    "    Format stdin -> stdout",
+    "  --assume-filename, -assume-filename",
+    "    File name to use for diagnostics when formatting standard input (default is <stdin>).",
+    "  --aosp, -aosp, -a",
+    "    Use AOSP style instead of Google Style (4-space indentation).",
+    "  --fix-imports-only",
+    "    Fix import order and remove any unused imports, but do no other formatting.",
+    "  --skip-sorting-imports",
+    "    Do not fix the import order. Unused imports will still be removed.",
+    "  --skip-removing-unused-imports",
+    "    Do not remove unused imports. Imports will still be sorted.",
+    " . --skip-reflowing-long-strings",
+    "    Do not reflow string literals that exceed the column limit.",
+    " . --skip-javadoc-formatting",
+    "    Do not reformat javadoc.",
+    "  --dry-run, -n",
+    "    Prints the paths of the files whose contents would change if the formatter were run"
+        + " normally.",
+    "  --set-exit-if-changed",
+    "    Return exit code 1 if there are any formatting changes.",
+    "  --lines, -lines, --line, -line",
+    "    Line range(s) to format, like 5:10 (1-based; default is all).",
+    "  --offset, -offset",
+    "    Character offset to format (0-based; default is all).",
+    "  --length, -length",
+    "    Character length to format.",
+    "  --help, -help, -h",
+    "    Print this usage statement.",
+    "  --version, -version, -v",
+    "    Print the version.",
+    "  @<filename>",
+    "    Read options and filenames from file.",
+    "",
+  };
+
+  private static final String[] ADDITIONAL_USAGE = {
+    "If -i is given with -, the result is sent to stdout.",
+    "The --lines, --offset, and --length flags may be given more than once.",
+    "The --offset and --length flags must be given an equal number of times.",
+    "If --lines, --offset, or --length are given, only one file (or -) may be given."
+  };
+
+  UsageException() {
+    super(buildMessage(null));
+  }
+
+  UsageException(String message) {
+    super(buildMessage(checkNotNull(message)));
+  }
+
+  private static String buildMessage(String message) {
+    StringBuilder builder = new StringBuilder();
+    if (message != null) {
+      builder.append(message).append('\n');
+    }
+    appendLines(builder, USAGE);
+    appendLines(builder, ADDITIONAL_USAGE);
+    appendLines(builder, new String[] {""});
+    appendLine(builder, Main.versionString());
+    appendLines(builder, DOCS_LINK);
+    return builder.toString();
+  }
+
+  private static void appendLine(StringBuilder builder, String line) {
+    builder.append(line).append(System.lineSeparator());
+  }
+
+  private static void appendLines(StringBuilder builder, String[] lines) {
+    NEWLINE_JOINER.appendTo(builder, lines).append(System.lineSeparator());
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java
new file mode 100644
index 0000000..d38d84e
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.filer;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.googlejavaformat.java.Formatter;
+import java.io.IOException;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.Messager;
+import javax.lang.model.element.Element;
+import javax.tools.FileObject;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A decorating {@link Filer} implementation which formats Java source files with a {@link
+ * Formatter}.
+ */
+public final class FormattingFiler implements Filer {
+
+  private final Filer delegate;
+  // TODO(ronshapiro): consider allowing users to create their own Formatter instance
+  private final Formatter formatter = new Formatter();
+  private final Messager messager;
+
+  /** @param delegate filer to decorate */
+  public FormattingFiler(Filer delegate) {
+    this(delegate, null);
+  }
+
+  /**
+   * Create a new {@link FormattingFiler}. An optional {@link Messager} may be specified to make
+   * logs more visible.
+   *
+   * @param delegate filer to decorate
+   * @param messager to log warnings to
+   */
+  public FormattingFiler(Filer delegate, @Nullable Messager messager) {
+    this.delegate = checkNotNull(delegate);
+    this.messager = messager;
+  }
+
+  @Override
+  public JavaFileObject createSourceFile(CharSequence name, Element... originatingElements)
+      throws IOException {
+    return new FormattingJavaFileObject(
+        delegate.createSourceFile(name, originatingElements), formatter, messager);
+  }
+
+  @Override
+  public JavaFileObject createClassFile(CharSequence name, Element... originatingElements)
+      throws IOException {
+    return delegate.createClassFile(name, originatingElements);
+  }
+
+  @Override
+  public FileObject createResource(
+      JavaFileManager.Location location,
+      CharSequence pkg,
+      CharSequence relativeName,
+      Element... originatingElements)
+      throws IOException {
+    return delegate.createResource(location, pkg, relativeName, originatingElements);
+  }
+
+  @Override
+  public FileObject getResource(
+      JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName)
+      throws IOException {
+    return delegate.getResource(location, pkg, relativeName);
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java
new file mode 100644
index 0000000..c8ae807
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.filer;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.io.CharSink;
+import com.google.common.io.CharSource;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.FormatterException;
+import java.io.IOException;
+import java.io.Writer;
+import javax.annotation.processing.Messager;
+import javax.tools.Diagnostic;
+import javax.tools.ForwardingJavaFileObject;
+import javax.tools.JavaFileObject;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** A {@link JavaFileObject} decorator which {@linkplain Formatter formats} source code. */
+final class FormattingJavaFileObject extends ForwardingJavaFileObject<JavaFileObject> {
+  /** A rough estimate of the average file size: 80 chars per line, 500 lines. */
+  private static final int DEFAULT_FILE_SIZE = 80 * 500;
+
+  private final Formatter formatter;
+  private final Messager messager;
+
+  /**
+   * Create a new {@link FormattingJavaFileObject}.
+   *
+   * @param delegate {@link JavaFileObject} to decorate
+   * @param messager to log messages with.
+   */
+  FormattingJavaFileObject(
+      JavaFileObject delegate, Formatter formatter, @Nullable Messager messager) {
+    super(checkNotNull(delegate));
+    this.formatter = checkNotNull(formatter);
+    this.messager = messager;
+  }
+
+  @Override
+  public Writer openWriter() throws IOException {
+    final StringBuilder stringBuilder = new StringBuilder(DEFAULT_FILE_SIZE);
+    return new Writer() {
+      @Override
+      public void write(char[] chars, int start, int end) throws IOException {
+        stringBuilder.append(chars, start, end - start);
+      }
+
+      @Override
+      public void write(String string) throws IOException {
+        stringBuilder.append(string);
+      }
+
+      @Override
+      public void flush() throws IOException {}
+
+      @Override
+      public void close() throws IOException {
+        try {
+          formatter.formatSource(
+              CharSource.wrap(stringBuilder),
+              new CharSink() {
+                @Override
+                public Writer openStream() throws IOException {
+                  return fileObject.openWriter();
+                }
+              });
+        } catch (FormatterException e) {
+          // An exception will happen when the code being formatted has an error. It's better to
+          // log the exception and emit unformatted code so the developer can view the code which
+          // caused a problem.
+          try (Writer writer = fileObject.openWriter()) {
+            writer.append(stringBuilder.toString());
+          }
+          if (messager != null) {
+            messager.printMessage(Diagnostic.Kind.NOTE, "Error formatting " + getName());
+          }
+        }
+      }
+    };
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java
new file mode 100644
index 0000000..28a1103
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.java14;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.MoreCollectors.toOptional;
+
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.Op;
+import com.google.googlejavaformat.OpsBuilder;
+import com.google.googlejavaformat.java.JavaInputAstVisitor;
+import com.sun.source.tree.BindingPatternTree;
+import com.sun.source.tree.CaseTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.InstanceOfTree;
+import com.sun.source.tree.SwitchExpressionTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.YieldTree;
+import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
+import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
+import com.sun.tools.javac.tree.TreeInfo;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Extends {@link JavaInputAstVisitor} with support for AST nodes that were added or modified for
+ * Java 14.
+ */
+public class Java14InputAstVisitor extends JavaInputAstVisitor {
+
+  public Java14InputAstVisitor(OpsBuilder builder, int indentMultiplier) {
+    super(builder, indentMultiplier);
+  }
+
+  @Override
+  public Void visitBindingPattern(BindingPatternTree node, Void unused) {
+    sync(node);
+    scan(node.getType(), null);
+    builder.breakOp(" ");
+    visit(node.getBinding());
+    return null;
+  }
+
+  @Override
+  public Void visitYield(YieldTree node, Void aVoid) {
+    sync(node);
+    return super.visitYield(node, aVoid);
+  }
+
+  @Override
+  public Void visitSwitchExpression(SwitchExpressionTree node, Void aVoid) {
+    sync(node);
+    visitSwitch(node.getExpression(), node.getCases());
+    return null;
+  }
+
+  @Override
+  public Void visitClass(ClassTree tree, Void unused) {
+    switch (tree.getKind()) {
+      case ANNOTATION_TYPE:
+        visitAnnotationType(tree);
+        break;
+      case CLASS:
+      case INTERFACE:
+        visitClassDeclaration(tree);
+        break;
+      case ENUM:
+        visitEnumDeclaration(tree);
+        break;
+      case RECORD:
+        visitRecordDeclaration(tree);
+        break;
+      default:
+        throw new AssertionError(tree.getKind());
+    }
+    return null;
+  }
+
+  public void visitRecordDeclaration(ClassTree node) {
+    sync(node);
+    List<Op> breaks =
+        visitModifiers(
+            node.getModifiers(),
+            Direction.VERTICAL,
+            /* declarationAnnotationBreak= */ Optional.empty());
+    Verify.verify(node.getExtendsClause() == null);
+    boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty();
+    builder.addAll(breaks);
+    token("record");
+    builder.space();
+    visit(node.getSimpleName());
+    if (!node.getTypeParameters().isEmpty()) {
+      token("<");
+    }
+    builder.open(plusFour);
+    {
+      if (!node.getTypeParameters().isEmpty()) {
+        typeParametersRest(node.getTypeParameters(), hasSuperInterfaceTypes ? plusFour : ZERO);
+      }
+      ImmutableList<JCVariableDecl> parameters =
+          compactRecordConstructor(node)
+              .map(m -> ImmutableList.copyOf(m.getParameters()))
+              .orElseGet(() -> recordVariables(node));
+      token("(");
+      if (!parameters.isEmpty()) {
+        // Break before args.
+        builder.breakToFill("");
+      }
+      // record headers can't declare receiver parameters
+      visitFormals(/* receiver= */ Optional.empty(), parameters);
+      token(")");
+      if (hasSuperInterfaceTypes) {
+        builder.breakToFill(" ");
+        builder.open(node.getImplementsClause().size() > 1 ? plusFour : ZERO);
+        token("implements");
+        builder.space();
+        boolean first = true;
+        for (Tree superInterfaceType : node.getImplementsClause()) {
+          if (!first) {
+            token(",");
+            builder.breakOp(" ");
+          }
+          scan(superInterfaceType, null);
+          first = false;
+        }
+        builder.close();
+      }
+    }
+    builder.close();
+    if (node.getMembers() == null) {
+      token(";");
+    } else {
+      List<Tree> members =
+          node.getMembers().stream()
+              .filter(t -> (TreeInfo.flags((JCTree) t) & Flags.GENERATED_MEMBER) == 0)
+              .collect(toImmutableList());
+      addBodyDeclarations(members, BracesOrNot.YES, FirstDeclarationsOrNot.YES);
+    }
+    dropEmptyDeclarations();
+  }
+
+  private static Optional<JCMethodDecl> compactRecordConstructor(ClassTree node) {
+    return node.getMembers().stream()
+        .filter(JCMethodDecl.class::isInstance)
+        .map(JCMethodDecl.class::cast)
+        .filter(m -> (m.mods.flags & COMPACT_RECORD_CONSTRUCTOR) == COMPACT_RECORD_CONSTRUCTOR)
+        .collect(toOptional());
+  }
+
+  private static ImmutableList<JCVariableDecl> recordVariables(ClassTree node) {
+    return node.getMembers().stream()
+        .filter(JCVariableDecl.class::isInstance)
+        .map(JCVariableDecl.class::cast)
+        .filter(m -> (m.mods.flags & RECORD) == RECORD)
+        .collect(toImmutableList());
+  }
+
+  @Override
+  public Void visitInstanceOf(InstanceOfTree node, Void unused) {
+    sync(node);
+    builder.open(plusFour);
+    scan(node.getExpression(), null);
+    builder.breakOp(" ");
+    builder.open(ZERO);
+    token("instanceof");
+    builder.breakOp(" ");
+    if (node.getPattern() != null) {
+      scan(node.getPattern(), null);
+    } else {
+      scan(node.getType(), null);
+    }
+    builder.close();
+    builder.close();
+    return null;
+  }
+
+  @Override
+  public Void visitCase(CaseTree node, Void unused) {
+    sync(node);
+    markForPartialFormat();
+    builder.forcedBreak();
+    if (node.getExpressions().isEmpty()) {
+      token("default", plusTwo);
+    } else {
+      token("case", plusTwo);
+      builder.space();
+      boolean first = true;
+      for (ExpressionTree expression : node.getExpressions()) {
+        if (!first) {
+          token(",");
+          builder.space();
+        }
+        scan(expression, null);
+        first = false;
+      }
+    }
+    switch (node.getCaseKind()) {
+      case STATEMENT:
+        token(":");
+        builder.open(plusTwo);
+        visitStatements(node.getStatements());
+        builder.close();
+        break;
+      case RULE:
+        builder.space();
+        token("-");
+        token(">");
+        builder.space();
+        scan(node.getBody(), null);
+        token(";");
+        break;
+      default:
+        throw new AssertionError(node.getCaseKind());
+    }
+    return null;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java
new file mode 100644
index 0000000..8fbd49f
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.javadoc;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * String reader designed for use from the lexer. Callers invoke the {@link #tryConsume tryConsume*}
+ * methods to specify what characters they expect and then {@link #readAndResetRecorded} to retrieve
+ * and consume the matched characters. This is a slightly odd API -- why not just return the matched
+ * characters from tryConsume? -- but it is convenient for the lexer.
+ */
+final class CharStream {
+  String remaining;
+  int toConsume;
+
+  CharStream(String input) {
+    this.remaining = checkNotNull(input);
+  }
+
+  boolean tryConsume(String expected) {
+    if (!remaining.startsWith(expected)) {
+      return false;
+    }
+    toConsume = expected.length();
+    return true;
+  }
+
+  /*
+   * @param pattern the pattern to search for, which must be anchored to match only at position 0
+   */
+  boolean tryConsumeRegex(Pattern pattern) {
+    Matcher matcher = pattern.matcher(remaining);
+    if (!matcher.find()) {
+      return false;
+    }
+    checkArgument(matcher.start() == 0);
+    toConsume = matcher.end();
+    return true;
+  }
+
+  String readAndResetRecorded() {
+    String result = remaining.substring(0, toConsume);
+    remaining = remaining.substring(toConsume);
+    toConsume = 0; // TODO(cpovirk): Set this to a bogus value here and in the constructor.
+    return result;
+  }
+
+  boolean isExhausted() {
+    return remaining.isEmpty();
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java
new file mode 100644
index 0000000..5addc67
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.javadoc;
+
+import static com.google.googlejavaformat.java.javadoc.JavadocLexer.lex;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BR_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.compile;
+
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.java.javadoc.JavadocLexer.LexException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Entry point for formatting Javadoc.
+ *
+ * <p>This stateless class reads tokens from the stateful lexer and translates them to "requests"
+ * and "writes" to the stateful writer. It also munges tokens into "standardized" forms. Finally, it
+ * performs postprocessing to convert the written Javadoc to a one-liner if possible or to leave a
+ * single blank line if it's empty.
+ */
+public final class JavadocFormatter {
+
+  static final int MAX_LINE_LENGTH = 100;
+
+  /**
+   * Formats the given Javadoc comment, which must start with ∕✱✱ and end with ✱∕. The output will
+   * start and end with the same characters.
+   */
+  public static String formatJavadoc(String input, int blockIndent) {
+    ImmutableList<Token> tokens;
+    try {
+      tokens = lex(input);
+    } catch (LexException e) {
+      return input;
+    }
+    String result = render(tokens, blockIndent);
+    return makeSingleLineIfPossible(blockIndent, result);
+  }
+
+  private static String render(List<Token> input, int blockIndent) {
+    JavadocWriter output = new JavadocWriter(blockIndent);
+    for (Token token : input) {
+      switch (token.getType()) {
+        case BEGIN_JAVADOC:
+          output.writeBeginJavadoc();
+          break;
+        case END_JAVADOC:
+          output.writeEndJavadoc();
+          return output.toString();
+        case FOOTER_JAVADOC_TAG_START:
+          output.writeFooterJavadocTagStart(token);
+          break;
+        case LIST_OPEN_TAG:
+          output.writeListOpen(token);
+          break;
+        case LIST_CLOSE_TAG:
+          output.writeListClose(token);
+          break;
+        case LIST_ITEM_OPEN_TAG:
+          output.writeListItemOpen(token);
+          break;
+        case HEADER_OPEN_TAG:
+          output.writeHeaderOpen(token);
+          break;
+        case HEADER_CLOSE_TAG:
+          output.writeHeaderClose(token);
+          break;
+        case PARAGRAPH_OPEN_TAG:
+          output.writeParagraphOpen(standardizePToken(token));
+          break;
+        case BLOCKQUOTE_OPEN_TAG:
+        case BLOCKQUOTE_CLOSE_TAG:
+          output.writeBlockquoteOpenOrClose(token);
+          break;
+        case PRE_OPEN_TAG:
+          output.writePreOpen(token);
+          break;
+        case PRE_CLOSE_TAG:
+          output.writePreClose(token);
+          break;
+        case CODE_OPEN_TAG:
+          output.writeCodeOpen(token);
+          break;
+        case CODE_CLOSE_TAG:
+          output.writeCodeClose(token);
+          break;
+        case TABLE_OPEN_TAG:
+          output.writeTableOpen(token);
+          break;
+        case TABLE_CLOSE_TAG:
+          output.writeTableClose(token);
+          break;
+        case MOE_BEGIN_STRIP_COMMENT:
+          output.requestMoeBeginStripComment(token);
+          break;
+        case MOE_END_STRIP_COMMENT:
+          output.writeMoeEndStripComment(token);
+          break;
+        case HTML_COMMENT:
+          output.writeHtmlComment(token);
+          break;
+        case BR_TAG:
+          output.writeBr(standardizeBrToken(token));
+          break;
+        case WHITESPACE:
+          output.requestWhitespace();
+          break;
+        case FORCED_NEWLINE:
+          output.writeLineBreakNoAutoIndent();
+          break;
+        case LITERAL:
+          output.writeLiteral(token);
+          break;
+        case PARAGRAPH_CLOSE_TAG:
+        case LIST_ITEM_CLOSE_TAG:
+        case OPTIONAL_LINE_BREAK:
+          break;
+        default:
+          throw new AssertionError(token.getType());
+      }
+    }
+    throw new AssertionError();
+  }
+
+  /*
+   * TODO(cpovirk): Is this really the right location for the standardize* methods? Maybe the lexer
+   * should include them as part of its own postprocessing? Or even the writer could make sense.
+   */
+
+  private static Token standardizeBrToken(Token token) {
+    return standardize(token, STANDARD_BR_TOKEN);
+  }
+
+  private static Token standardizePToken(Token token) {
+    return standardize(token, STANDARD_P_TOKEN);
+  }
+
+  private static Token standardize(Token token, Token standardToken) {
+    return SIMPLE_TAG_PATTERN.matcher(token.getValue()).matches() ? standardToken : token;
+  }
+
+  private static final Token STANDARD_BR_TOKEN = new Token(BR_TAG, "<br>");
+  private static final Token STANDARD_P_TOKEN = new Token(PARAGRAPH_OPEN_TAG, "<p>");
+  private static final Pattern SIMPLE_TAG_PATTERN = compile("^<\\w+\\s*/?\\s*>", CASE_INSENSITIVE);
+
+  private static final Pattern ONE_CONTENT_LINE_PATTERN = compile(" */[*][*]\n *[*] (.*)\n *[*]/");
+
+  /**
+   * Returns the given string or a one-line version of it (e.g., "∕✱✱ Tests for foos. ✱∕") if it
+   * fits on one line.
+   */
+  private static String makeSingleLineIfPossible(int blockIndent, String input) {
+    int oneLinerContentLength = MAX_LINE_LENGTH - "/**  */".length() - blockIndent;
+    Matcher matcher = ONE_CONTENT_LINE_PATTERN.matcher(input);
+    if (matcher.matches() && matcher.group(1).isEmpty()) {
+      return "/** */";
+    } else if (matcher.matches() && matcher.group(1).length() <= oneLinerContentLength) {
+      return "/** " + matcher.group(1) + " */";
+    }
+    return input;
+  }
+
+  private JavadocFormatter() {}
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java
new file mode 100644
index 0000000..108d4a7
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.javadoc;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Verify.verify;
+import static com.google.common.collect.Iterators.peekingIterator;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BEGIN_JAVADOC;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BLOCKQUOTE_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BLOCKQUOTE_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.BR_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.CODE_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.CODE_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.END_JAVADOC;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.FOOTER_JAVADOC_TAG_START;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.FORCED_NEWLINE;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.HTML_COMMENT;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LITERAL;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.MOE_BEGIN_STRIP_COMMENT;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.MOE_END_STRIP_COMMENT;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.OPTIONAL_LINE_BREAK;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_CLOSE_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.WHITESPACE;
+import static java.lang.String.format;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.DOTALL;
+import static java.util.regex.Pattern.compile;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.PeekingIterator;
+import com.google.googlejavaformat.java.javadoc.Token.Type;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/** Lexer for the Javadoc formatter. */
+final class JavadocLexer {
+  /** Takes a Javadoc comment, including ∕✱✱ and ✱∕, and returns tokens, including ∕✱✱ and ✱∕. */
+  static ImmutableList<Token> lex(String input) throws LexException {
+    /*
+     * TODO(cpovirk): In theory, we should interpret Unicode escapes (yet output them in their
+     * original form). This would mean mean everything from an encoded ∕✱✱ to an encoded <pre> tag,
+     * so we'll probably never bother.
+     */
+    input = stripJavadocBeginAndEnd(input);
+    input = normalizeLineEndings(input);
+    return new JavadocLexer(new CharStream(input)).generateTokens();
+  }
+
+  /** The lexer crashes on windows line endings, so for now just normalize to `\n`. */
+  // TODO(cushon): use the platform line separator for output
+  private static String normalizeLineEndings(String input) {
+    return NON_UNIX_LINE_ENDING.matcher(input).replaceAll("\n");
+  }
+
+  private static final Pattern NON_UNIX_LINE_ENDING = Pattern.compile("\r\n?");
+
+  private static String stripJavadocBeginAndEnd(String input) {
+    /*
+     * We do this ahead of time so that the main part of the lexer need not say things like
+     * "(?![*]/)" to avoid accidentally swallowing ✱∕ when consuming a newline.
+     */
+    checkArgument(input.startsWith("/**"), "Missing /**: %s", input);
+    checkArgument(input.endsWith("*/") && input.length() > 4, "Missing */: %s", input);
+    return input.substring("/**".length(), input.length() - "*/".length());
+  }
+
+  private final CharStream input;
+  private final NestingCounter braceDepth = new NestingCounter();
+  private final NestingCounter preDepth = new NestingCounter();
+  private final NestingCounter codeDepth = new NestingCounter();
+  private final NestingCounter tableDepth = new NestingCounter();
+  private boolean somethingSinceNewline;
+
+  private JavadocLexer(CharStream input) {
+    this.input = checkNotNull(input);
+  }
+
+  private ImmutableList<Token> generateTokens() throws LexException {
+    ImmutableList.Builder<Token> tokens = ImmutableList.builder();
+
+    Token token = new Token(BEGIN_JAVADOC, "/**");
+    tokens.add(token);
+
+    while (!input.isExhausted()) {
+      token = readToken();
+      tokens.add(token);
+    }
+
+    checkMatchingTags();
+
+    token = new Token(END_JAVADOC, "*/");
+    tokens.add(token);
+
+    ImmutableList<Token> result = tokens.build();
+    result = joinAdjacentLiteralsAndAdjacentWhitespace(result);
+    result = inferParagraphTags(result);
+    result = optionalizeSpacesAfterLinks(result);
+    result = deindentPreCodeBlocks(result);
+    return result;
+  }
+
+  private Token readToken() throws LexException {
+    Type type = consumeToken();
+    String value = input.readAndResetRecorded();
+    return new Token(type, value);
+  }
+
+  private Type consumeToken() throws LexException {
+    boolean preserveExistingFormatting = preserveExistingFormatting();
+
+    if (input.tryConsumeRegex(NEWLINE_PATTERN)) {
+      somethingSinceNewline = false;
+      return preserveExistingFormatting ? FORCED_NEWLINE : WHITESPACE;
+    } else if (input.tryConsume(" ") || input.tryConsume("\t")) {
+      // TODO(cpovirk): How about weird whitespace chars? Ideally we'd distinguish breaking vs. not.
+      // Returning LITERAL here prevent us from breaking a <pre> line. For more info, see LITERAL.
+      return preserveExistingFormatting ? LITERAL : WHITESPACE;
+    }
+
+    /*
+     * TODO(cpovirk): Maybe try to detect things like "{@code\n@GwtCompatible}" that aren't intended
+     * as tags. But in the most likely case, in which that happens inside <pre>{@code, we have no
+     * great options for fixing it.
+     * https://github.com/google/google-java-format/issues/7#issuecomment-197383926
+     */
+    if (!somethingSinceNewline && input.tryConsumeRegex(FOOTER_TAG_PATTERN)) {
+      checkMatchingTags();
+      somethingSinceNewline = true;
+      return FOOTER_JAVADOC_TAG_START;
+    }
+    somethingSinceNewline = true;
+
+    if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) {
+      braceDepth.increment();
+      return LITERAL;
+    } else if (input.tryConsume("{")) {
+      braceDepth.incrementIfPositive();
+      return LITERAL;
+    } else if (input.tryConsume("}")) {
+      braceDepth.decrementIfPositive();
+      return LITERAL;
+    }
+
+    // Inside an inline tag, don't do any HTML interpretation.
+    if (braceDepth.isPositive()) {
+      verify(input.tryConsumeRegex(LITERAL_PATTERN));
+      return LITERAL;
+    }
+
+    if (input.tryConsumeRegex(PRE_OPEN_PATTERN)) {
+      preDepth.increment();
+      return preserveExistingFormatting ? LITERAL : PRE_OPEN_TAG;
+    } else if (input.tryConsumeRegex(PRE_CLOSE_PATTERN)) {
+      preDepth.decrementIfPositive();
+      return preserveExistingFormatting() ? LITERAL : PRE_CLOSE_TAG;
+    }
+
+    if (input.tryConsumeRegex(CODE_OPEN_PATTERN)) {
+      codeDepth.increment();
+      return preserveExistingFormatting ? LITERAL : CODE_OPEN_TAG;
+    } else if (input.tryConsumeRegex(CODE_CLOSE_PATTERN)) {
+      codeDepth.decrementIfPositive();
+      return preserveExistingFormatting() ? LITERAL : CODE_CLOSE_TAG;
+    }
+
+    if (input.tryConsumeRegex(TABLE_OPEN_PATTERN)) {
+      tableDepth.increment();
+      return preserveExistingFormatting ? LITERAL : TABLE_OPEN_TAG;
+    } else if (input.tryConsumeRegex(TABLE_CLOSE_PATTERN)) {
+      tableDepth.decrementIfPositive();
+      return preserveExistingFormatting() ? LITERAL : TABLE_CLOSE_TAG;
+    }
+
+    if (preserveExistingFormatting) {
+      verify(input.tryConsumeRegex(LITERAL_PATTERN));
+      return LITERAL;
+    }
+
+    if (input.tryConsumeRegex(PARAGRAPH_OPEN_PATTERN)) {
+      return PARAGRAPH_OPEN_TAG;
+    } else if (input.tryConsumeRegex(PARAGRAPH_CLOSE_PATTERN)) {
+      return PARAGRAPH_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(LIST_OPEN_PATTERN)) {
+      return LIST_OPEN_TAG;
+    } else if (input.tryConsumeRegex(LIST_CLOSE_PATTERN)) {
+      return LIST_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(LIST_ITEM_OPEN_PATTERN)) {
+      return LIST_ITEM_OPEN_TAG;
+    } else if (input.tryConsumeRegex(LIST_ITEM_CLOSE_PATTERN)) {
+      return LIST_ITEM_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(BLOCKQUOTE_OPEN_PATTERN)) {
+      return BLOCKQUOTE_OPEN_TAG;
+    } else if (input.tryConsumeRegex(BLOCKQUOTE_CLOSE_PATTERN)) {
+      return BLOCKQUOTE_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(HEADER_OPEN_PATTERN)) {
+      return HEADER_OPEN_TAG;
+    } else if (input.tryConsumeRegex(HEADER_CLOSE_PATTERN)) {
+      return HEADER_CLOSE_TAG;
+    } else if (input.tryConsumeRegex(BR_PATTERN)) {
+      return BR_TAG;
+    } else if (input.tryConsumeRegex(MOE_BEGIN_STRIP_COMMENT_PATTERN)) {
+      return MOE_BEGIN_STRIP_COMMENT;
+    } else if (input.tryConsumeRegex(MOE_END_STRIP_COMMENT_PATTERN)) {
+      return MOE_END_STRIP_COMMENT;
+    } else if (input.tryConsumeRegex(HTML_COMMENT_PATTERN)) {
+      return HTML_COMMENT;
+    } else if (input.tryConsumeRegex(LITERAL_PATTERN)) {
+      return LITERAL;
+    }
+    throw new AssertionError();
+  }
+
+  private boolean preserveExistingFormatting() {
+    return preDepth.isPositive() || tableDepth.isPositive() || codeDepth.isPositive();
+  }
+
+  private void checkMatchingTags() throws LexException {
+    if (braceDepth.isPositive()
+        || preDepth.isPositive()
+        || tableDepth.isPositive()
+        || codeDepth.isPositive()) {
+      throw new LexException();
+    }
+  }
+
+  /**
+   * Join together adjacent literal tokens, and join together adjacent whitespace tokens.
+   *
+   * <p>For literal tokens, this means something like {@code ["<b>", "foo", "</b>"] =>
+   * ["<b>foo</b>"]}. See {@link #LITERAL_PATTERN} for discussion of why those tokens are separate
+   * to begin with.
+   *
+   * <p>Whitespace tokens are treated analogously. We don't really "want" to join whitespace tokens,
+   * but in the course of joining literals, we incidentally join whitespace, too. We do take
+   * advantage of the joining later on: It simplifies {@link #inferParagraphTags}.
+   *
+   * <p>Note that we do <i>not</i> merge a literal token and a whitespace token together.
+   */
+  private static ImmutableList<Token> joinAdjacentLiteralsAndAdjacentWhitespace(List<Token> input) {
+    /*
+     * Note: Our final token is always END_JAVADOC. This saves us some trouble:
+     *
+     * - Our inner while() doesn't need a hasNext() check.
+     *
+     * - We don't need to check for leftover accumulated literals after we exit the loop.
+     */
+    ImmutableList.Builder<Token> output = ImmutableList.builder();
+    StringBuilder accumulated = new StringBuilder();
+
+    for (PeekingIterator<Token> tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) {
+      if (tokens.peek().getType() == LITERAL) {
+        accumulated.append(tokens.peek().getValue());
+        tokens.next();
+        continue;
+      }
+
+      /*
+       * IF we have accumulated some literals to join together (say, "foo<b>bar</b>"), and IF we'll
+       * next see whitespace followed by a "@" literal, we need to join that together with the
+       * previous literals. That ensures that we won't insert a line break before the "@," turning
+       * it into a tag.
+       */
+
+      if (accumulated.length() == 0) {
+        output.add(tokens.peek());
+        tokens.next();
+        continue;
+      }
+
+      StringBuilder seenWhitespace = new StringBuilder();
+      while (tokens.peek().getType() == WHITESPACE) {
+        seenWhitespace.append(tokens.next().getValue());
+      }
+
+      if (tokens.peek().getType() == LITERAL && tokens.peek().getValue().startsWith("@")) {
+        // OK, we're in the case described above.
+        accumulated.append(" ");
+        accumulated.append(tokens.peek().getValue());
+        tokens.next();
+        continue;
+      }
+
+      output.add(new Token(LITERAL, accumulated.toString()));
+      accumulated.setLength(0);
+
+      if (seenWhitespace.length() > 0) {
+        output.add(new Token(WHITESPACE, seenWhitespace.toString()));
+      }
+
+      // We have another token coming, possibly of type OTHER. Leave it for the next iteration.
+    }
+
+    /*
+     * TODO(cpovirk): Another case where we could try to join tokens is if a line ends with
+     * /[^ -]-/, as in "non-\nblocking."
+     */
+    return output.build();
+  }
+
+  /**
+   * Where the input has two consecutive line breaks between literals, insert a {@code <p>} tag
+   * between the literals.
+   *
+   * <p>This method must be called after {@link #joinAdjacentLiteralsAndAdjacentWhitespace}, as it
+   * assumes that adjacent whitespace tokens have already been joined.
+   */
+  private static ImmutableList<Token> inferParagraphTags(List<Token> input) {
+    ImmutableList.Builder<Token> output = ImmutableList.builder();
+
+    for (PeekingIterator<Token> tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) {
+      if (tokens.peek().getType() == LITERAL) {
+        output.add(tokens.next());
+
+        if (tokens.peek().getType() == WHITESPACE
+            && hasMultipleNewlines(tokens.peek().getValue())) {
+          output.add(tokens.next());
+
+          if (tokens.peek().getType() == LITERAL) {
+            output.add(new Token(PARAGRAPH_OPEN_TAG, "<p>"));
+          }
+        }
+      } else {
+        // TODO(cpovirk): Or just `continue` from the <p> case and move this out of the `else`?
+        output.add(tokens.next());
+      }
+    }
+
+    return output.build();
+
+    /*
+     * Note: We do not want to insert <p> tags inside <pre>. Fortunately, the formatter gets that
+     * right without special effort on our part. The reason: Line breaks inside a <pre> section are
+     * of type FORCED_NEWLINE rather than WHITESPACE.
+     */
+  }
+
+  /**
+   * Replaces whitespace after a {@code href=...>} token with an "optional link break." This allows
+   * us to output either {@code <a href=foo>foo</a>} or {@code <a href=foo>\nfoo</a>}, depending on
+   * how much space we have left on the line.
+   *
+   * <p>This method must be called after {@link #joinAdjacentLiteralsAndAdjacentWhitespace}, as it
+   * assumes that adjacent whitespace tokens have already been joined.
+   */
+  private static ImmutableList<Token> optionalizeSpacesAfterLinks(List<Token> input) {
+    ImmutableList.Builder<Token> output = ImmutableList.builder();
+
+    for (PeekingIterator<Token> tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) {
+      if (tokens.peek().getType() == LITERAL && tokens.peek().getValue().matches("^href=[^>]*>")) {
+        output.add(tokens.next());
+
+        if (tokens.peek().getType() == WHITESPACE) {
+          output.add(new Token(OPTIONAL_LINE_BREAK, tokens.next().getValue()));
+        }
+      } else {
+        output.add(tokens.next());
+      }
+    }
+
+    return output.build();
+
+    /*
+     * Note: We do not want to insert <p> tags inside <pre>. Fortunately, the formatter gets that
+     * right without special effort on our part. The reason: Line breaks inside a <pre> section are
+     * of type FORCED_NEWLINE rather than WHITESPACE.
+     */
+  }
+
+  /**
+   * Adjust indentation inside `<pre>{@code` blocks.
+   *
+   * <p>Also trim leading and trailing blank lines, and move the trailing `}` to its own line.
+   */
+  private static ImmutableList<Token> deindentPreCodeBlocks(List<Token> input) {
+    ImmutableList.Builder<Token> output = ImmutableList.builder();
+    for (PeekingIterator<Token> tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) {
+      if (tokens.peek().getType() != PRE_OPEN_TAG) {
+        output.add(tokens.next());
+        continue;
+      }
+
+      output.add(tokens.next());
+      List<Token> initialNewlines = new ArrayList<>();
+      while (tokens.hasNext() && tokens.peek().getType() == FORCED_NEWLINE) {
+        initialNewlines.add(tokens.next());
+      }
+      if (tokens.peek().getType() != LITERAL
+          || !tokens.peek().getValue().matches("[ \t]*[{]@code")) {
+        output.addAll(initialNewlines);
+        output.add(tokens.next());
+        continue;
+      }
+
+      deindentPreCodeBlock(output, tokens);
+    }
+    return output.build();
+  }
+
+  private static void deindentPreCodeBlock(
+      ImmutableList.Builder<Token> output, PeekingIterator<Token> tokens) {
+    Deque<Token> saved = new ArrayDeque<>();
+    output.add(new Token(LITERAL, tokens.next().getValue().trim()));
+    while (tokens.hasNext() && tokens.peek().getType() != PRE_CLOSE_TAG) {
+      Token token = tokens.next();
+      saved.addLast(token);
+    }
+    while (!saved.isEmpty() && saved.peekFirst().getType() == FORCED_NEWLINE) {
+      saved.removeFirst();
+    }
+    while (!saved.isEmpty() && saved.peekLast().getType() == FORCED_NEWLINE) {
+      saved.removeLast();
+    }
+    if (saved.isEmpty()) {
+      return;
+    }
+
+    // move the trailing `}` to its own line
+    Token last = saved.peekLast();
+    boolean trailingBrace = false;
+    if (last.getType() == LITERAL && last.getValue().endsWith("}")) {
+      saved.removeLast();
+      if (last.length() > 1) {
+        saved.addLast(
+            new Token(LITERAL, last.getValue().substring(0, last.getValue().length() - 1)));
+        saved.addLast(new Token(FORCED_NEWLINE, null));
+      }
+      trailingBrace = true;
+    }
+
+    int trim = -1;
+    for (Token token : saved) {
+      if (token.getType() == LITERAL) {
+        int idx = CharMatcher.isNot(' ').indexIn(token.getValue());
+        if (idx != -1 && (trim == -1 || idx < trim)) {
+          trim = idx;
+        }
+      }
+    }
+
+    output.add(new Token(FORCED_NEWLINE, "\n"));
+    for (Token token : saved) {
+      if (token.getType() == LITERAL) {
+        output.add(
+            new Token(
+                LITERAL,
+                trim > 0 && token.length() > trim
+                    ? token.getValue().substring(trim)
+                    : token.getValue()));
+      } else {
+        output.add(token);
+      }
+    }
+
+    if (trailingBrace) {
+      output.add(new Token(LITERAL, "}"));
+    } else {
+      output.add(new Token(FORCED_NEWLINE, "\n"));
+    }
+  }
+
+  private static final CharMatcher NEWLINE = CharMatcher.is('\n');
+
+  private static boolean hasMultipleNewlines(String s) {
+    return NEWLINE.countIn(s) > 1;
+  }
+
+  /*
+   * This also eats any trailing whitespace. We would be smart enough to ignore that, anyway --
+   * except in the case of <pre>/<table>, inside which we otherwise leave whitespace intact.
+   *
+   * We'd remove the trailing whitespace later on (in JavaCommentsHelper.rewrite), but I feel safer
+   * stripping it now: It otherwise might confuse our line-length count, which we use for wrapping.
+   */
+  private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n[ \t]*[*]?[ \t]?");
+
+  // We ensure elsewhere that we match this only at the beginning of a line.
+  // Only match tags that start with a lowercase letter, to avoid false matches on unescaped
+  // annotations inside code blocks.
+  // Match "@param <T>" specially in case the <T> is a <P> or other HTML tag we treat specially.
+  private static final Pattern FOOTER_TAG_PATTERN = compile("^@(param\\s+<\\w+>|[a-z]\\w*)");
+  private static final Pattern MOE_BEGIN_STRIP_COMMENT_PATTERN =
+      compile("^<!--\\s*MOE:begin_intracomment_strip\\s*-->");
+  private static final Pattern MOE_END_STRIP_COMMENT_PATTERN =
+      compile("^<!--\\s*MOE:end_intracomment_strip\\s*-->");
+  private static final Pattern HTML_COMMENT_PATTERN = fullCommentPattern();
+  private static final Pattern PRE_OPEN_PATTERN = openTagPattern("pre");
+  private static final Pattern PRE_CLOSE_PATTERN = closeTagPattern("pre");
+  private static final Pattern CODE_OPEN_PATTERN = openTagPattern("code");
+  private static final Pattern CODE_CLOSE_PATTERN = closeTagPattern("code");
+  private static final Pattern TABLE_OPEN_PATTERN = openTagPattern("table");
+  private static final Pattern TABLE_CLOSE_PATTERN = closeTagPattern("table");
+  private static final Pattern LIST_OPEN_PATTERN = openTagPattern("ul|ol|dl");
+  private static final Pattern LIST_CLOSE_PATTERN = closeTagPattern("ul|ol|dl");
+  private static final Pattern LIST_ITEM_OPEN_PATTERN = openTagPattern("li|dt|dd");
+  private static final Pattern LIST_ITEM_CLOSE_PATTERN = closeTagPattern("li|dt|dd");
+  private static final Pattern HEADER_OPEN_PATTERN = openTagPattern("h[1-6]");
+  private static final Pattern HEADER_CLOSE_PATTERN = closeTagPattern("h[1-6]");
+  private static final Pattern PARAGRAPH_OPEN_PATTERN = openTagPattern("p");
+  private static final Pattern PARAGRAPH_CLOSE_PATTERN = closeTagPattern("p");
+  private static final Pattern BLOCKQUOTE_OPEN_PATTERN = openTagPattern("blockquote");
+  private static final Pattern BLOCKQUOTE_CLOSE_PATTERN = closeTagPattern("blockquote");
+  private static final Pattern BR_PATTERN = openTagPattern("br");
+  private static final Pattern INLINE_TAG_OPEN_PATTERN = compile("^[{]@\\w*");
+  /*
+   * We exclude < so that we don't swallow following HTML tags. This lets us fix up "foo<p>" (~400
+   * hits in Google-internal code). We will join unnecessarily split "words" (like "foo<b>bar</b>")
+   * in a later step. There's a similar story for braces. I'm not sure I actually need to exclude @
+   * or *. TODO(cpovirk): Try removing them.
+   *
+   * Thanks to the "rejoin" step in joinAdjacentLiteralsAndAdjacentWhitespace(), we could get away
+   * with matching only one character here. That would eliminate the need for the regex entirely.
+   * That might be faster or slower than what we do now.
+   */
+  private static final Pattern LITERAL_PATTERN = compile("^.[^ \t\n@<{}*]*", DOTALL);
+
+  private static Pattern fullCommentPattern() {
+    return compile("^<!--.*?-->", DOTALL);
+  }
+
+  private static Pattern openTagPattern(String namePattern) {
+    return compile(format("^<(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE);
+  }
+
+  private static Pattern closeTagPattern(String namePattern) {
+    return compile(format("^</(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE);
+  }
+
+  static class LexException extends Exception {}
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
new file mode 100644
index 0000000..c2431c4
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.javadoc;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Sets.immutableEnumSet;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.AUTO_INDENT;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.NO_AUTO_INDENT;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.BLANK_LINE;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NEWLINE;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE;
+import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
+import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import com.google.googlejavaformat.java.javadoc.Token.Type;
+
+/**
+ * Stateful object that accepts "requests" and "writes," producing formatted Javadoc.
+ *
+ * <p>Our Javadoc formatter doesn't ever generate a parse tree, only a stream of tokens, so the
+ * writer must compute and store the answer to questions like "How many levels of nested HTML list
+ * are we inside?"
+ */
+final class JavadocWriter {
+  private final int blockIndent;
+  private final StringBuilder output = new StringBuilder();
+  /**
+   * Whether we are inside an {@code <li>} element, excluding the case in which the {@code <li>}
+   * contains a {@code <ul>} or {@code <ol>} that we are also inside -- unless of course we're
+   * inside an {@code <li>} element in that inner list :)
+   */
+  private boolean continuingListItemOfInnermostList;
+
+  private boolean continuingFooterTag;
+  private final NestingCounter continuingListItemCount = new NestingCounter();
+  private final NestingCounter continuingListCount = new NestingCounter();
+  private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter();
+  private int remainingOnLine;
+  private boolean atStartOfLine;
+  private RequestedWhitespace requestedWhitespace = NONE;
+  private Token requestedMoeBeginStripComment;
+  private int indentForMoeEndStripComment;
+  private boolean wroteAnythingSignificant;
+
+  JavadocWriter(int blockIndent) {
+    this.blockIndent = blockIndent;
+  }
+
+  /**
+   * Requests whitespace between the previously written token and the next written token. The
+   * request may be honored, or it may be overridden by a request for "more significant" whitespace,
+   * like a newline.
+   */
+  void requestWhitespace() {
+    requestWhitespace(WHITESPACE);
+  }
+
+  void requestMoeBeginStripComment(Token token) {
+    // We queue this up so that we can put it after any requested whitespace.
+    requestedMoeBeginStripComment = checkNotNull(token);
+  }
+
+  void writeBeginJavadoc() {
+    /*
+     * JavaCommentsHelper will make sure this is indented right. But it seems sensible enough that,
+     * if our input starts with ∕✱✱, so too does our output.
+     */
+    output.append("/**");
+    writeNewline();
+  }
+
+  void writeEndJavadoc() {
+    output.append("\n");
+    appendSpaces(blockIndent + 1);
+    output.append("*/");
+  }
+
+  void writeFooterJavadocTagStart(Token token) {
+    // Close any unclosed lists (e.g., <li> without <ul>).
+    // TODO(cpovirk): Actually generate </ul>, etc.?
+    /*
+     * TODO(cpovirk): Also generate </pre> and </table> if appropriate. This is necessary for
+     * idempotency in broken Javadoc. (We don't necessarily need that, but full idempotency may be a
+     * nice goal, especially if it helps us use a fuzzer to test.) Unfortunately, the writer doesn't
+     * currently know which of those tags are open.
+     */
+    continuingListItemOfInnermostList = false;
+    continuingListItemCount.reset();
+    continuingListCount.reset();
+    /*
+     * There's probably no need for this, since its only effect is to disable blank lines in some
+     * cases -- and we're doing that already in the footer.
+     */
+    postWriteModifiedContinuingListCount.reset();
+
+    if (!wroteAnythingSignificant) {
+      // Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides.
+    } else if (!continuingFooterTag) {
+      // First footer tag after a body tag.
+      requestBlankLine();
+    } else {
+      // Subsequent footer tag.
+      continuingFooterTag = false;
+      requestNewline();
+    }
+    writeToken(token);
+    continuingFooterTag = true;
+  }
+
+  void writeListOpen(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+    continuingListItemOfInnermostList = false;
+    continuingListCount.increment();
+    postWriteModifiedContinuingListCount.increment();
+
+    requestNewline();
+  }
+
+  void writeListClose(Token token) {
+    requestNewline();
+
+    continuingListItemCount.decrementIfPositive();
+    continuingListCount.decrementIfPositive();
+    writeToken(token);
+    postWriteModifiedContinuingListCount.decrementIfPositive();
+
+    requestBlankLine();
+  }
+
+  void writeListItemOpen(Token token) {
+    requestNewline();
+
+    if (continuingListItemOfInnermostList) {
+      continuingListItemOfInnermostList = false;
+      continuingListItemCount.decrementIfPositive();
+    }
+    writeToken(token);
+    continuingListItemOfInnermostList = true;
+    continuingListItemCount.increment();
+  }
+
+  void writeHeaderOpen(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+  }
+
+  void writeHeaderClose(Token token) {
+    writeToken(token);
+
+    requestBlankLine();
+  }
+
+  void writeParagraphOpen(Token token) {
+    if (!wroteAnythingSignificant) {
+      /*
+       * The user included an initial <p> tag. Ignore it, and don't request a blank line before the
+       * next token.
+       */
+      return;
+    }
+
+    requestBlankLine();
+
+    writeToken(token);
+  }
+
+  void writeBlockquoteOpenOrClose(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+
+    requestBlankLine();
+  }
+
+  void writePreOpen(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+  }
+
+  void writePreClose(Token token) {
+    writeToken(token);
+
+    requestBlankLine();
+  }
+
+  void writeCodeOpen(Token token) {
+    writeToken(token);
+  }
+
+  void writeCodeClose(Token token) {
+    writeToken(token);
+  }
+
+  void writeTableOpen(Token token) {
+    requestBlankLine();
+
+    writeToken(token);
+  }
+
+  void writeTableClose(Token token) {
+    writeToken(token);
+
+    requestBlankLine();
+  }
+
+  void writeMoeEndStripComment(Token token) {
+    writeLineBreakNoAutoIndent();
+    appendSpaces(indentForMoeEndStripComment);
+
+    // Or maybe just "output.append(token.getValue())?" I'm kind of surprised this is so easy.
+    writeToken(token);
+
+    requestNewline();
+  }
+
+  void writeHtmlComment(Token token) {
+    requestNewline();
+
+    writeToken(token);
+
+    requestNewline();
+  }
+
+  void writeBr(Token token) {
+    writeToken(token);
+
+    requestNewline();
+  }
+
+  void writeLineBreakNoAutoIndent() {
+    writeNewline(NO_AUTO_INDENT);
+  }
+
+  void writeLiteral(Token token) {
+    writeToken(token);
+  }
+
+  @Override
+  public String toString() {
+    return output.toString();
+  }
+
+  private void requestBlankLine() {
+    requestWhitespace(BLANK_LINE);
+  }
+
+  private void requestNewline() {
+    requestWhitespace(NEWLINE);
+  }
+
+  private void requestWhitespace(RequestedWhitespace requestedWhitespace) {
+    this.requestedWhitespace =
+        Ordering.natural().max(requestedWhitespace, this.requestedWhitespace);
+  }
+
+  /**
+   * The kind of whitespace that has been requested between the previous and next tokens. The order
+   * of the values is significant: It goes from lowest priority to highest. For example, if the
+   * previous token requests {@link #BLANK_LINE} after it but the next token requests only {@link
+   * #NEWLINE} before it, we insert {@link #BLANK_LINE}.
+   */
+  enum RequestedWhitespace {
+    NONE,
+    WHITESPACE,
+    NEWLINE,
+    BLANK_LINE,
+    ;
+  }
+
+  private void writeToken(Token token) {
+    if (requestedMoeBeginStripComment != null) {
+      requestNewline();
+    }
+
+    if (requestedWhitespace == BLANK_LINE
+        && (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) {
+      /*
+       * We don't write blank lines inside lists or footer tags, even in cases where we otherwise
+       * would (e.g., before a <p> tag). Justification: We don't write blank lines _between_ list
+       * items or footer tags, so it would be strange to write blank lines _within_ one. Of course,
+       * an alternative approach would be to go ahead and write blank lines between items/tags,
+       * either always or only in the case that an item contains a blank line.
+       */
+      requestedWhitespace = NEWLINE;
+    }
+
+    if (requestedWhitespace == BLANK_LINE) {
+      writeBlankLine();
+      requestedWhitespace = NONE;
+    } else if (requestedWhitespace == NEWLINE) {
+      writeNewline();
+      requestedWhitespace = NONE;
+    }
+    boolean needWhitespace = (requestedWhitespace == WHITESPACE);
+
+    /*
+     * Write a newline if necessary to respect the line limit. (But if we're at the beginning of the
+     * line, a newline won't help. Or it might help but only by separating "<p>veryverylongword,"
+     * which goes against our style.)
+     */
+    if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) {
+      writeNewline();
+    }
+    if (!atStartOfLine && needWhitespace) {
+      output.append(" ");
+      remainingOnLine--;
+    }
+
+    if (requestedMoeBeginStripComment != null) {
+      output.append(requestedMoeBeginStripComment.getValue());
+      requestedMoeBeginStripComment = null;
+      indentForMoeEndStripComment = innerIndent();
+      requestNewline();
+      writeToken(token);
+      return;
+    }
+
+    output.append(token.getValue());
+
+    if (!START_OF_LINE_TOKENS.contains(token.getType())) {
+      atStartOfLine = false;
+    }
+
+    /*
+     * TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the
+     * right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all
+     * graphemes? I don't think that our style guide is specific about this.). Moreover, I am
+     * probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly
+     * get away with it by joining all non-space, non-tab characters together.
+     *
+     * Possibly the "width" question has no right answer:
+     * http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html
+     */
+    remainingOnLine -= token.length();
+    requestedWhitespace = NONE;
+    wroteAnythingSignificant = true;
+  }
+
+  private void writeBlankLine() {
+    output.append("\n");
+    appendSpaces(blockIndent + 1);
+    output.append("*");
+    writeNewline();
+  }
+
+  private void writeNewline() {
+    writeNewline(AUTO_INDENT);
+  }
+
+  private void writeNewline(AutoIndent autoIndent) {
+    output.append("\n");
+    appendSpaces(blockIndent + 1);
+    output.append("*");
+    appendSpaces(1);
+    remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - 3;
+    if (autoIndent == AUTO_INDENT) {
+      appendSpaces(innerIndent());
+      remainingOnLine -= innerIndent();
+    }
+    atStartOfLine = true;
+  }
+
+  enum AutoIndent {
+    AUTO_INDENT,
+    NO_AUTO_INDENT
+  }
+
+  private int innerIndent() {
+    int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2;
+    if (continuingFooterTag) {
+      innerIndent += 4;
+    }
+    return innerIndent;
+  }
+
+  // If this is a hotspot, keep a String of many spaces around, and call append(string, start, end).
+  private void appendSpaces(int count) {
+    output.append(Strings.repeat(" ", count));
+  }
+
+  /**
+   * Tokens that are always pinned to the following token. For example, {@code <p>} in {@code <p>Foo
+   * bar} (never {@code <p> Foo bar} or {@code <p>\nFoo bar}).
+   *
+   * <p>This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens
+   * done by the lexer. The special pinning here is necessary because these tokens are not of type
+   * LITERAL (because they require other special handling).
+   */
+  private static final ImmutableSet<Type> START_OF_LINE_TOKENS =
+      immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG);
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingCounter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingCounter.java
new file mode 100644
index 0000000..43e7125
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/NestingCounter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.javadoc;
+
+/** Mutable integer for tracking the level of nesting. */
+final class NestingCounter {
+  private int value;
+
+  int value() {
+    return value;
+  }
+
+  void increment() {
+    value++;
+  }
+
+  void incrementIfPositive() {
+    if (value > 0) {
+      value++;
+    }
+  }
+
+  void decrementIfPositive() {
+    if (value > 0) {
+      value--;
+    }
+  }
+
+  boolean isPositive() {
+    return value > 0;
+  }
+
+  void reset() {
+    value = 0;
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java
new file mode 100644
index 0000000..d617824
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/Token.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.javadoc;
+
+/**
+ * Javadoc token. Our idea of what constitutes a token is often larger or smaller than what you'd
+ * naturally expect. The decision is usually pragmatic rather than theoretical. Most of the details
+ * are in {@link JavadocLexer}.
+ */
+final class Token {
+  /**
+   * Javadoc token type.
+   *
+   * <p>The general idea is that every token that requires special handling (extra line breaks,
+   * indentation, forcing or forbidding whitespace) from {@link JavadocWriter} gets its own type.
+   * But I haven't been super careful about it, so I'd imagine that we could merge or remove some of
+   * these if we wanted. (For example, PARAGRAPH_CLOSE_TAG and LIST_ITEM_CLOSE_TAG could share a
+   * common IGNORABLE token type. But their corresponding OPEN tags exist, so I've kept the CLOSE
+   * tags.)
+   *
+   * <p>Note, though, that tokens of the same type may still have been handled differently by {@link
+   * JavadocLexer} when it created them. For example, LITERAL is used for both plain text and inline
+   * tags, even though the two affect the lexer's state differently.
+   */
+  enum Type {
+    /** ∕✱✱ */
+    BEGIN_JAVADOC,
+    /** ✱∕ */
+    END_JAVADOC,
+    /** The {@code @foo} that begins a block Javadoc tag like {@code @throws}. */
+    FOOTER_JAVADOC_TAG_START,
+    LIST_OPEN_TAG,
+    LIST_CLOSE_TAG,
+    LIST_ITEM_OPEN_TAG,
+    LIST_ITEM_CLOSE_TAG,
+    HEADER_OPEN_TAG,
+    HEADER_CLOSE_TAG,
+    PARAGRAPH_OPEN_TAG,
+    PARAGRAPH_CLOSE_TAG,
+    // TODO(cpovirk): Support <div> (probably identically to <blockquote>).
+    BLOCKQUOTE_OPEN_TAG,
+    BLOCKQUOTE_CLOSE_TAG,
+    PRE_OPEN_TAG,
+    PRE_CLOSE_TAG,
+    CODE_OPEN_TAG,
+    CODE_CLOSE_TAG,
+    TABLE_OPEN_TAG,
+    TABLE_CLOSE_TAG,
+    /** {@code <!-- MOE:begin_intracomment_strip -->} */
+    MOE_BEGIN_STRIP_COMMENT,
+    /** {@code <!-- MOE:end_intracomment_strip -->} */
+    MOE_END_STRIP_COMMENT,
+    HTML_COMMENT,
+    // TODO(cpovirk): Support <hr> (probably a blank line before and after).
+    BR_TAG,
+    /**
+     * Whitespace that is not in a {@code <pre>} or {@code <table>} section. Whitespace includes
+     * leading newlines, asterisks, and tabs and spaces. In the output, it is translated to newlines
+     * (with leading spaces and asterisks) or spaces.
+     */
+    WHITESPACE,
+    /**
+     * A newline in a {@code <pre>} or {@code <table>} section. We preserve user formatting in these
+     * sections, including newlines.
+     */
+    FORCED_NEWLINE,
+    /**
+     * Token that permits but does not force a line break. The way that we accomplish this is
+     * somewhat indirect: As far as {@link JavadocWriter} is concerned, this token is meaningless.
+     * But its mere existence prevents {@link JavadocLexer} from joining two {@link #LITERAL} tokens
+     * that would otherwise be adjacent. Since this token is not real whitespace, the writer may end
+     * up writing the literals together with no space between, just as if they'd been joined.
+     * However, if they don't fit together on the line, the writer will write the first one, start a
+     * new line, and write the second. Hence, the token acts as an optional line break.
+     */
+    OPTIONAL_LINE_BREAK,
+    /**
+     * Anything else: {@code foo}, {@code <b>}, {@code {@code foo}} etc. {@link JavadocLexer}
+     * sometimes creates adjacent literal tokens, which it then merges into a single, larger literal
+     * token before returning its output.
+     *
+     * <p>This also includes whitespace in a {@code <pre>} or {@code <table>} section. We preserve
+     * user formatting in these sections, including arbitrary numbers of spaces. By treating such
+     * whitespace as a literal, we can merge it with adjacent literals, preventing us from
+     * autowrapping inside these sections -- and doing so naively, to boot. The wrapped line would
+     * have no indentation after "* " or, possibly worse, it might begin with an arbitrary amount of
+     * whitespace that didn't fit on the previous line. Of course, by doing this, we're potentially
+     * creating lines of more than 100 characters. But it seems fair to call in the humans to
+     * resolve such problems.
+     */
+    LITERAL,
+    ;
+  }
+
+  private final Type type;
+  private final String value;
+
+  Token(Type type, String value) {
+    this.type = type;
+    this.value = value;
+  }
+
+  Type getType() {
+    return type;
+  }
+
+  String getValue() {
+    return value;
+  }
+
+  int length() {
+    return value.length();
+  }
+
+  @Override
+  public String toString() {
+    return "\n" + getType() + ": " + getValue();
+  }
+}
diff --git a/core/src/main/scripts/google-java-format.el b/core/src/main/scripts/google-java-format.el
new file mode 100644
index 0000000..f9e8d2a
--- /dev/null
+++ b/core/src/main/scripts/google-java-format.el
@@ -0,0 +1,105 @@
+;;; google-java-format.el --- Format code with google-java-format -*- lexical-binding: t; -*-
+;;
+;; Copyright 2015 Google, Inc. All Rights Reserved.
+;;
+;; Package-Requires: ((emacs "24"))
+;;
+;; Licensed under the Apache License, Version 2.0 (the "License");
+;; you may not use this file except in compliance with the License.
+;; You may obtain a copy of the License at
+;;
+;;      http://www.apache.org/licenses/LICENSE-2.0
+;;
+;; Unless required `by applicable law or agreed to in writing, software
+;; distributed under the License is distributed on an "AS-IS" BASIS,
+;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+;; See the License for the specific language governing permissions and
+;; limitations under the License.
+
+;; Keywords: tools, Java
+
+;;; Commentary:
+
+;; This package allows a user to filter code through
+;; google-java-format, fixing its formatting.
+
+;; To use it, ensure the directory of this file is in your `load-path'
+;; and add
+;;
+;;   (require 'google-java-format)
+;;
+;; to your .emacs configuration.
+
+;; You may also want to bind `google-java-format-region' to a key:
+;;
+;;   (global-set-key [C-M-tab] #'google-java-format-region)
+
+;;; Code:
+
+(defgroup google-java-format nil
+  "Format code using google-java-format."
+  :group 'tools)
+
+(defcustom google-java-format-executable
+  "/usr/bin/google-java-format"
+  "Location of the google-java-format executable.
+
+A string containing the name or the full path of the executable."
+  :group 'google-java-format
+  :type '(file :must-match t :match #'file-executable-p)
+  :risky t)
+
+;;;###autoload
+(defun google-java-format-region (start end)
+  "Use google-java-format to format the code between START and END.
+If called interactively, uses the region, if there is one.  If
+there is no region, then formats the current line."
+  (interactive
+   (if (use-region-p)
+       (list (region-beginning) (region-end))
+     (list (point) (1+ (point)))))
+  (let ((cursor (point))
+        (temp-buffer (generate-new-buffer " *google-java-format-temp*"))
+        (stderr-file (make-temp-file "google-java-format")))
+    (unwind-protect
+        (let ((status (call-process-region
+                       ;; Note that emacs character positions are 1-indexed,
+                       ;; and google-java-format is 0-indexed, so we have to
+                       ;; subtract 1 from START to line it up correctly.
+                       (point-min) (point-max)
+                       google-java-format-executable
+                       nil (list temp-buffer stderr-file) t
+                       "--offset" (number-to-string (1- start))
+                       "--length" (number-to-string (- end start))
+                       "-"))
+              (stderr
+               (with-temp-buffer
+                 (insert-file-contents stderr-file)
+                 (when (> (point-max) (point-min))
+                   (insert ": "))
+                 (buffer-substring-no-properties
+                  (point-min) (line-end-position)))))
+          (cond
+           ((stringp status)
+            (error "google-java-format killed by signal %s%s" status stderr))
+           ((not (zerop status))
+            (error "google-java-format failed with code %d%s" status stderr))
+           (t (message "google-java-format succeeded%s" stderr)
+              (delete-region (point-min) (point-max))
+              (insert-buffer-substring temp-buffer)
+              (goto-char cursor))))
+      (delete-file stderr-file)
+      (when (buffer-name temp-buffer) (kill-buffer temp-buffer)))))
+
+;;;###autoload
+(defun google-java-format-buffer ()
+  "Use google-java-format to format the current buffer."
+  (interactive)
+  (google-java-format-region (point-min) (point-max)))
+
+;;;###autoload
+(defalias 'google-java-format 'google-java-format-region)
+
+(provide 'google-java-format)
+
+;;; google-java-format.el ends here
diff --git a/core/src/test/java/com/google/googlejavaformat/NewlinesTest.java b/core/src/test/java/com/google/googlejavaformat/NewlinesTest.java
new file mode 100644
index 0000000..4b7dab7
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/NewlinesTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link Newlines}Test */
+@RunWith(JUnit4.class)
+public class NewlinesTest {
+  @Test
+  public void offsets() {
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\nbar\n")))
+        .containsExactly(0, 4, 8);
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\nbar"))).containsExactly(0, 4);
+
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\rbar\r")))
+        .containsExactly(0, 4, 8);
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\rbar"))).containsExactly(0, 4);
+
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\r\nbar\r\n")))
+        .containsExactly(0, 5, 10);
+    assertThat(ImmutableList.copyOf(Newlines.lineOffsetIterator("foo\r\nbar")))
+        .containsExactly(0, 5);
+  }
+
+  @Test
+  public void lines() {
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\nbar\n")))
+        .containsExactly("foo\n", "bar\n");
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\nbar")))
+        .containsExactly("foo\n", "bar");
+
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\rbar\r")))
+        .containsExactly("foo\r", "bar\r");
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\rbar")))
+        .containsExactly("foo\r", "bar");
+
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\r\nbar\r\n")))
+        .containsExactly("foo\r\n", "bar\r\n");
+    assertThat(ImmutableList.copyOf(Newlines.lineIterator("foo\r\nbar")))
+        .containsExactly("foo\r\n", "bar");
+  }
+
+  @Test
+  public void terminalOffset() {
+    Iterator<Integer> it = Newlines.lineOffsetIterator("foo\nbar\n");
+    it.next();
+    it.next();
+    it.next();
+    try {
+      it.next();
+      fail();
+    } catch (NoSuchElementException e) {
+      // expected
+    }
+
+    it = Newlines.lineOffsetIterator("foo\nbar");
+    it.next();
+    it.next();
+    try {
+      it.next();
+      fail();
+    } catch (NoSuchElementException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void terminalLine() {
+    Iterator<String> it = Newlines.lineIterator("foo\nbar\n");
+    it.next();
+    it.next();
+    try {
+      it.next();
+      fail();
+    } catch (NoSuchElementException e) {
+      // expected
+    }
+
+    it = Newlines.lineIterator("foo\nbar");
+    it.next();
+    it.next();
+    try {
+      it.next();
+      fail();
+    } catch (NoSuchElementException e) {
+      // expected
+    }
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/ArrayDimensionTest.java b/core/src/test/java/com/google/googlejavaformat/java/ArrayDimensionTest.java
new file mode 100644
index 0000000..3da9834
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/ArrayDimensionTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for array dimension handling, especially mixed array notation and type annotations on
+ * dimensions.
+ */
+@RunWith(Parameterized.class)
+public class ArrayDimensionTest {
+  @Parameters
+  public static Iterable<Object[]> parameters() {
+    String[] inputs = {
+      // mixed array notation multi-variable declarations
+      "int a[], b @B [], c @B [][] @C [];",
+      "int a @A [], b @B [], c @B [] @C [];",
+      "int a[] @A [], b @B [], c @B [] @C [];",
+      "int a, b @B [], c @B [] @C [];",
+      "int @A [] a, b @B [], c @B [] @C [];",
+      "int @A [] a = {}, b @B [] = {{}}, c @B [] @C [] = {{{}}};",
+      // mixed array notation methods
+      "int[][][][][] argh()[] @A @B [] @C @D [][] {}",
+      "int[][] @T [] @U [] @V @W [][][] argh() @A @B [] @C @D [] {}",
+      "int e1() @A [] {}",
+      "int f1()[] @A [] {}",
+      "int g1() @A [] @B [] {}",
+      "int h1() @A @B [] @C @B [] {}",
+      "int[] e2() @A [] {}",
+      "int @X [] f2()[] @A [] {}",
+      "int[] g2() @A [] @B [] {}",
+      "int @X [] h2() @A @B [] @C @B [] {}",
+      "@X int[] e3() @A [] {}",
+      "@X int @Y [] f3()[] @A [] {}",
+      "@X int @Y [] g3() @A [] @B [] {}",
+      "@X int[] h3() @A @B [] @C @B [] {}",
+      // mixed array notation single-variable declarations
+      "int[] e2() @A [] {}",
+      "int @I [] f2()[] @A [] {}",
+      "int[] @J [] g2() @A [] @B [] {}",
+      "int @I [] @J [] h2() @A @B [] @C @B [] {}",
+      "int a1[];",
+      "int b1 @A [];",
+      "int c1[] @A [];",
+      "int d1 @A [] @B [];",
+      "int[] a2[];",
+      "int @A [] b2 @B [];",
+      "int[] c2[] @A [];",
+      "int @A [] d2 @B [] @C [];",
+      // array dimension expressions
+      "int[][] a0 = new @A int[0];",
+      "int[][] a1 = new int @A [0] @B [];",
+      "int[][] a2 = new int[0] @A [] @B [];",
+      "int[][] a4 = new int[0] @A [][] @B [];",
+      // nested array type uses
+      "List<int @A [] @B []> xs;",
+      "List<int[] @A [][] @B []> xs;",
+    };
+    return Iterables.transform(Arrays.asList(inputs), input -> new Object[] {input});
+  }
+
+  private final String input;
+
+  public ArrayDimensionTest(String input) {
+    this.input = input;
+  }
+
+  @Test
+  public void format() throws Exception {
+    String source = "class T {" + input + "}";
+    String formatted = new Formatter().formatSource(source);
+    String statement =
+        formatted.substring("class T {".length(), formatted.length() - "}\n".length());
+    // ignore line breaks after declaration-style annotations
+    statement =
+        Joiner.on(' ').join(Splitter.on('\n').omitEmptyStrings().trimResults().split(statement));
+    assertThat(statement).isEqualTo(input);
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java b/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java
new file mode 100644
index 0000000..e5fbc9f
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for command-line flags.
+ */
+@RunWith(JUnit4.class)
+public class CommandLineFlagsTest {
+
+  // TODO(eaftan): Disallow passing both -lines and -offset/-length, like clang-format.
+
+  @Test
+  public void formatInPlaceRequiresAtLeastOneFile() throws UsageException {
+    try {
+      Main.processArgs("-i");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-i", "-");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    Main.processArgs("-i", "Foo.java");
+    Main.processArgs("-i", "Foo.java", "Bar.java");
+  }
+
+  @Test
+  public void formatASubsetRequiresExactlyOneFile() throws UsageException {
+    Main.processArgs("-lines", "10", "Foo.java");
+
+    try {
+      Main.processArgs("-lines", "10");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-lines", "10", "Foo.java", "Bar.java");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    Main.processArgs("-offset", "10", "-length", "10", "Foo.java");
+
+    try {
+      Main.processArgs("-offset", "10", "-length", "10");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-offset", "10", "-length", "10", "Foo.java", "Bar.java");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+  }
+
+  // TODO(eaftan): clang-format allows a single offset with no length, which means to format
+  // up to the end of the file.  We should match that behavior.
+  @Test
+  public void numberOfOffsetsMustMatchNumberOfLengths() throws UsageException {
+    Main.processArgs("-offset", "10", "-length", "20", "Foo.java");
+
+    try {
+      Main.processArgs("-offset", "10", "-length", "20", "-offset", "50", "Foo.java");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-offset", "10", "-length", "20", "-length", "50", "Foo.java");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void noFilesToFormatRequiresEitherHelpOrVersion() throws UsageException {
+    Main.processArgs("-version");
+
+    Main.processArgs("-help");
+
+    try {
+      Main.processArgs();
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+
+    try {
+      Main.processArgs("-aosp");
+      fail();
+    } catch (UsageException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void stdinAndFiles() {
+    try {
+      Main.processArgs("-", "A.java");
+      fail();
+    } catch (UsageException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains("cannot format from standard input and files simultaneously");
+    }
+  }
+
+  @Test
+  public void inPlaceStdin() {
+    try {
+      Main.processArgs("-i", "-");
+      fail();
+    } catch (UsageException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains("in-place formatting was requested but no files were provided");
+    }
+  }
+
+  @Test
+  public void inPlaceDryRun() {
+    try {
+      Main.processArgs("-i", "-n", "A.java");
+      fail();
+    } catch (UsageException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains("cannot use --dry-run and --in-place at the same time");
+    }
+  }
+
+  @Test
+  public void assumeFileNameOnlyWorksWithStdin() {
+    try {
+      Main.processArgs("--assume-filename=Foo.java", "Foo.java");
+      fail();
+    } catch (UsageException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains("--assume-filename is only supported when formatting standard input");
+    }
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java b/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
new file mode 100644
index 0000000..8d71f4d
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Range;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link CommandLineOptionsParser}Test */
+@RunWith(JUnit4.class)
+public class CommandLineOptionsParserTest {
+
+  @Rule public TemporaryFolder testFolder = new TemporaryFolder();
+
+  @Test
+  public void defaults() {
+    CommandLineOptions options = CommandLineOptionsParser.parse(Collections.<String>emptyList());
+    assertThat(options.files()).isEmpty();
+    assertThat(options.stdin()).isFalse();
+    assertThat(options.aosp()).isFalse();
+    assertThat(options.help()).isFalse();
+    assertThat(options.lengths()).isEmpty();
+    assertThat(options.lines().asRanges()).isEmpty();
+    assertThat(options.offsets()).isEmpty();
+    assertThat(options.inPlace()).isFalse();
+    assertThat(options.version()).isFalse();
+    assertThat(options.sortImports()).isTrue();
+    assertThat(options.removeUnusedImports()).isTrue();
+    assertThat(options.dryRun()).isFalse();
+    assertThat(options.setExitIfChanged()).isFalse();
+    assertThat(options.reflowLongStrings()).isTrue();
+    assertThat(options.formatJavadoc()).isTrue();
+  }
+
+  @Test
+  public void hello() {
+    CommandLineOptions options =
+        CommandLineOptionsParser.parse(
+            Arrays.asList("-lines=1:10,20:30", "-i", "Hello.java", "Goodbye.java"));
+    assertThat(options.lines().asRanges())
+        .containsExactly(Range.closedOpen(0, 10), Range.closedOpen(19, 30));
+    assertThat(options.inPlace()).isTrue();
+    assertThat(options.files()).containsExactly("Hello.java", "Goodbye.java");
+  }
+
+  @Test
+  public void stdin() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-")).stdin()).isTrue();
+  }
+
+  @Test
+  public void aosp() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-aosp")).aosp()).isTrue();
+  }
+
+  @Test
+  public void help() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-help")).help()).isTrue();
+  }
+
+  @Test
+  public void lengths() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("-length", "1", "--length", "2"))
+                .lengths())
+        .containsExactly(1, 2);
+  }
+
+  @Test
+  public void lines() {
+    assertThat(
+            CommandLineOptionsParser.parse(
+                    Arrays.asList("--lines", "1:2", "-lines=4:5", "--line", "7:8", "-line=10:11"))
+                .lines()
+                .asRanges())
+        .containsExactly(
+            Range.closedOpen(0, 2),
+            Range.closedOpen(3, 5),
+            Range.closedOpen(6, 8),
+            Range.closedOpen(9, 11));
+  }
+
+  @Test
+  public void offset() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("-offset", "1", "--offset", "2"))
+                .offsets())
+        .containsExactly(1, 2);
+  }
+
+  @Test
+  public void inPlace() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-i", "A.java")).inPlace()).isTrue();
+  }
+
+  @Test
+  public void version() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-v")).version()).isTrue();
+  }
+
+  @Test
+  public void skipSortingImports() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--skip-sorting-imports")).sortImports())
+        .isFalse();
+  }
+
+  @Test
+  public void skipRemovingUnusedImports() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--skip-removing-unused-imports"))
+                .removeUnusedImports())
+        .isFalse();
+  }
+
+  @Test
+  public void dryRun() {
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("--dry-run")).dryRun()).isTrue();
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("-n")).dryRun()).isTrue();
+  }
+
+  @Test
+  public void setExitIfChanged() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--set-exit-if-changed"))
+                .setExitIfChanged())
+        .isTrue();
+  }
+
+  // TODO(cushon): consider handling this in the parser and reporting a more detailed error
+  @Test
+  public void illegalLines() {
+    try {
+      CommandLineOptionsParser.parse(Arrays.asList("-lines=1:1", "-lines=1:1"));
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e.getMessage()).contains("overlap");
+    }
+  }
+
+  @Test
+  public void paramsFile() throws IOException {
+    Path outer = testFolder.newFile("outer").toPath();
+    Path exit = testFolder.newFile("exit").toPath();
+    Path nested = testFolder.newFile("nested").toPath();
+
+    String[] args = {"--dry-run", "@" + exit, "L", "@" + outer, "Q"};
+
+    Files.write(exit, "--set-exit-if-changed".getBytes(UTF_8));
+    Files.write(outer, ("M\n@" + nested.toAbsolutePath() + "\nP").getBytes(UTF_8));
+    Files.write(nested, "ℕ\n\n   \n@@O\n".getBytes(UTF_8));
+
+    CommandLineOptions options = CommandLineOptionsParser.parse(Arrays.asList(args));
+    assertThat(options.files()).containsExactly("L", "M", "ℕ", "@O", "P", "Q");
+  }
+
+  @Test
+  public void assumeFilename() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--assume-filename", "Foo.java"))
+                .assumeFilename())
+        .hasValue("Foo.java");
+    assertThat(CommandLineOptionsParser.parse(Arrays.asList("Foo.java")).assumeFilename())
+        .isEmpty();
+  }
+
+  @Test
+  public void skipReflowLongStrings() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--skip-reflowing-long-strings"))
+                .reflowLongStrings())
+        .isFalse();
+  }
+
+  @Test
+  public void skipJavadocFormatting() {
+    assertThat(
+            CommandLineOptionsParser.parse(Arrays.asList("--skip-javadoc-formatting"))
+                .formatJavadoc())
+        .isFalse();
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java
new file mode 100644
index 0000000..0b81ba6
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Joiner;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Locale;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for error reporting. */
+@RunWith(JUnit4.class)
+public class DiagnosticTest {
+  @Rule public TemporaryFolder testFolder = new TemporaryFolder();
+
+  private Locale backupLocale;
+
+  @Before
+  public void setUpLocale() throws Exception {
+    backupLocale = Locale.getDefault();
+    Locale.setDefault(Locale.ROOT);
+  }
+
+  @After
+  public void restoreLocale() throws Exception {
+    Locale.setDefault(backupLocale);
+  }
+
+  @Test
+  public void parseError() throws Exception {
+    String input =
+        Joiner.on('\n')
+            .join(
+                "public class InvalidSyntax {",
+                "  private static NumPrinter {",
+                "    public static void print(int n) {",
+                "      System.out.printf(\"%d%n\", n);",
+                "    }",
+                "  }",
+                "",
+                "  public static void main(String[] args) {",
+                "    NumPrinter.print(args.length);",
+                "  }",
+                "}");
+
+    StringWriter stdout = new StringWriter();
+    StringWriter stderr = new StringWriter();
+    Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in);
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("InvalidSyntax.java");
+    Files.write(path, input.getBytes(UTF_8));
+
+    int result = main.format(path.toString());
+    assertThat(stdout.toString()).isEmpty();
+    assertThat(stderr.toString()).contains("InvalidSyntax.java:2:29: error: <identifier> expected");
+    assertThat(result).isEqualTo(1);
+  }
+
+  @Test
+  public void lexError() throws Exception {
+    String input = "\\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuu00not-actually-a-unicode-escape-sequence";
+
+    StringWriter stdout = new StringWriter();
+    StringWriter stderr = new StringWriter();
+    Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in);
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("InvalidSyntax.java");
+    Files.write(path, input.getBytes(UTF_8));
+
+    int result = main.format(path.toString());
+    assertThat(stdout.toString()).isEmpty();
+    assertThat(stderr.toString())
+        .contains("InvalidSyntax.java:1:35: error: illegal unicode escape");
+    assertThat(result).isEqualTo(1);
+  }
+
+  @Test
+  public void oneFileParseError() throws Exception {
+    String one = "class One {\n";
+    String two = "class Two {}\n";
+
+    StringWriter stdout = new StringWriter();
+    StringWriter stderr = new StringWriter();
+    Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in);
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path pathOne = tmpdir.resolve("One.java");
+    Files.write(pathOne, one.getBytes(UTF_8));
+
+    Path pathTwo = tmpdir.resolve("Two.java");
+    Files.write(pathTwo, two.getBytes(UTF_8));
+
+    int result = main.format(pathOne.toString(), pathTwo.toString());
+    assertThat(stdout.toString()).isEqualTo(two);
+    assertThat(stderr.toString()).contains("One.java:1:13: error: reached end of file");
+    assertThat(result).isEqualTo(1);
+  }
+
+  @Test
+  public void oneFileParseErrorReplace() throws Exception {
+    String one = "class One {}}\n";
+    String two = "class Two {\n}\n";
+
+    StringWriter stdout = new StringWriter();
+    StringWriter stderr = new StringWriter();
+    Main main = new Main(new PrintWriter(stdout, true), new PrintWriter(stderr, true), System.in);
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path pathOne = tmpdir.resolve("One.java");
+    Files.write(pathOne, one.getBytes(UTF_8));
+
+    Path pathTwo = tmpdir.resolve("Two.java");
+    Files.write(pathTwo, two.getBytes(UTF_8));
+
+    int result = main.format("-i", pathOne.toString(), pathTwo.toString());
+    assertThat(stdout.toString()).isEmpty();
+    assertThat(stderr.toString()).contains("One.java:1:14: error: class, interface");
+    assertThat(result).isEqualTo(1);
+    // don't edit files with parse errors
+    assertThat(Files.readAllLines(pathOne, UTF_8)).containsExactly("class One {}}");
+    assertThat(Files.readAllLines(pathTwo, UTF_8)).containsExactly("class Two {}");
+  }
+
+  @Test
+  public void parseError2() throws FormatterException, IOException, UsageException {
+    String input = "class Foo { void f() {\n g() } }";
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("A.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {path.toString()};
+    int exitCode = main.format(args);
+
+    assertThat(exitCode).isEqualTo(1);
+    assertThat(err.toString()).contains("A.java:2:6: error: ';' expected");
+  }
+
+  @Test
+  public void parseErrorStdin() throws FormatterException, IOException, UsageException {
+    String input = "class Foo { void f() {\n g() } }";
+
+    InputStream inStream = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), inStream);
+    String[] args = {"-"};
+    int exitCode = main.format(args);
+
+    assertThat(exitCode).isEqualTo(1);
+    assertThat(err.toString()).contains("<stdin>:2:6: error: ';' expected");
+  }
+
+  @Test
+  public void lexError2() throws FormatterException, IOException, UsageException {
+    String input = "class Foo { void f() {\n g('foo'); } }";
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("A.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {path.toString()};
+    int exitCode = main.format(args);
+
+    assertThat(exitCode).isEqualTo(1);
+    assertThat(err.toString()).contains("A.java:2:5: error: unclosed character literal");
+  }
+
+  @Test
+  public void lexErrorStdin() throws FormatterException, IOException, UsageException {
+    String input = "class Foo { void f() {\n g('foo'); } }";
+    InputStream inStream = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), inStream);
+    String[] args = {"-"};
+    int exitCode = main.format(args);
+
+    assertThat(exitCode).isEqualTo(1);
+    assertThat(err.toString()).contains("<stdin>:2:5: error: unclosed character literal");
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java
new file mode 100644
index 0000000..289ea1b
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_VERSION;
+import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
+import static com.google.common.io.Files.getFileExtension;
+import static com.google.common.io.Files.getNameWithoutExtension;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.CharStreams;
+import com.google.common.reflect.ClassPath;
+import com.google.common.reflect.ClassPath.ResourceInfo;
+import com.google.googlejavaformat.Newlines;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Method;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Integration test for google-java-format. */
+@RunWith(Parameterized.class)
+public class FormatterIntegrationTest {
+
+  private static final ImmutableSet<String> JAVA14_TESTS = ImmutableSet.of("java14");
+
+  @Parameters(name = "{index}: {0}")
+  public static Iterable<Object[]> data() throws IOException {
+    Path testDataPath = Paths.get("com/google/googlejavaformat/java/testdata");
+    ClassLoader classLoader = FormatterIntegrationTest.class.getClassLoader();
+    Map<String, String> inputs = new TreeMap<>();
+    Map<String, String> outputs = new TreeMap<>();
+    for (ResourceInfo resourceInfo : ClassPath.from(classLoader).getResources()) {
+      String resourceName = resourceInfo.getResourceName();
+      Path resourceNamePath = Paths.get(resourceName);
+      if (resourceNamePath.startsWith(testDataPath)) {
+        Path subPath = testDataPath.relativize(resourceNamePath);
+        assertEquals("bad testdata file names", 1, subPath.getNameCount());
+        String baseName = getNameWithoutExtension(subPath.getFileName().toString());
+        String extension = getFileExtension(subPath.getFileName().toString());
+        String contents;
+        try (InputStream stream =
+            FormatterIntegrationTest.class.getClassLoader().getResourceAsStream(resourceName)) {
+          contents = CharStreams.toString(new InputStreamReader(stream, UTF_8));
+        }
+        switch (extension) {
+          case "input":
+            inputs.put(baseName, contents);
+            break;
+          case "output":
+            outputs.put(baseName, contents);
+            break;
+          default:
+        }
+      }
+    }
+    List<Object[]> testInputs = new ArrayList<>();
+    assertEquals("unmatched inputs and outputs", inputs.size(), outputs.size());
+    for (Map.Entry<String, String> entry : inputs.entrySet()) {
+      String fileName = entry.getKey();
+      String input = inputs.get(fileName);
+      assertTrue("unmatched input", outputs.containsKey(fileName));
+      String expectedOutput = outputs.get(fileName);
+      if (JAVA14_TESTS.contains(fileName) && getMajor() < 14) {
+        continue;
+      }
+      testInputs.add(new Object[] {fileName, input, expectedOutput});
+    }
+    return testInputs;
+  }
+
+  private static int getMajor() {
+    try {
+      Method versionMethod = Runtime.class.getMethod("version");
+      Object version = versionMethod.invoke(null);
+      return (int) version.getClass().getMethod("major").invoke(version);
+    } catch (Exception e) {
+      // continue below
+    }
+    int version = (int) Double.parseDouble(JAVA_CLASS_VERSION.value());
+    if (49 <= version && version <= 52) {
+      return version - (49 - 5);
+    }
+    throw new IllegalStateException("Unknown Java version: " + JAVA_SPECIFICATION_VERSION.value());
+  }
+
+  private final String name;
+  private final String input;
+  private final String expected;
+  private final String separator;
+
+  public FormatterIntegrationTest(String name, String input, String expected) {
+    this.name = name;
+    this.input = input;
+    this.expected = expected;
+    this.separator = Newlines.getLineEnding(expected);
+  }
+
+  @Test
+  public void format() {
+    try {
+      String output = new Formatter().formatSource(input);
+      assertEquals("bad output for " + name, expected, output);
+    } catch (FormatterException e) {
+      fail(String.format("Formatter crashed on %s: %s", name, e.getMessage()));
+    }
+  }
+
+  @Test
+  public void idempotentLF() {
+    try {
+      String mangled = expected.replace(separator, "\n");
+      String output = new Formatter().formatSource(mangled);
+      assertEquals("bad output for " + name, mangled, output);
+    } catch (FormatterException e) {
+      fail(String.format("Formatter crashed on %s: %s", name, e.getMessage()));
+    }
+  }
+
+  @Test
+  public void idempotentCR() throws IOException {
+    try {
+      String mangled = expected.replace(separator, "\r");
+      String output = new Formatter().formatSource(mangled);
+      assertEquals("bad output for " + name, mangled, output);
+    } catch (FormatterException e) {
+      fail(String.format("Formatter crashed on %s: %s", name, e.getMessage()));
+    }
+  }
+
+  @Test
+  public void idempotentCRLF() {
+    try {
+      String mangled = expected.replace(separator, "\r\n");
+      String output = new Formatter().formatSource(mangled);
+      assertEquals("bad output for " + name, mangled, output);
+    } catch (FormatterException e) {
+      fail(String.format("Formatter crashed on %s: %s", name, e.getMessage()));
+    }
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java b/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java
new file mode 100644
index 0000000..3f6e974
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+import com.google.common.io.CharStreams;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration test for google-java-format. */
+@RunWith(JUnit4.class)
+public final class FormatterTest {
+
+  @Rule public TemporaryFolder testFolder = new TemporaryFolder();
+
+  @Test
+  public void testFormatAosp() throws Exception {
+    // don't forget to misspell "long", or you will be mystified for a while
+    String input =
+        "class A{void b(){while(true){weCanBeCertainThatThisWillEndUpGettingWrapped("
+            + "because, it, is, just, so, very, very, very, very, looong);}}}";
+    String expectedOutput =
+        Joiner.on("\n")
+            .join(
+                "class A {",
+                "    void b() {",
+                "        while (true) {",
+                "            weCanBeCertainThatThisWillEndUpGettingWrapped(",
+                "                    because, it, is, just, so, very, very, very, very, looong);",
+                "        }",
+                "    }",
+                "}",
+                "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("A.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"--aosp", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void testFormatNonJavaFiles() throws Exception {
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+
+    // should succeed because non-Java files are skipped
+    assertThat(main.format("foo.go")).isEqualTo(0);
+    assertThat(err.toString()).contains("Skipping non-Java file: " + "foo.go");
+
+    // format still fails on missing files
+    assertThat(main.format("Foo.java")).isEqualTo(1);
+    assertThat(err.toString()).contains("Foo.java: could not read file: ");
+  }
+
+  @Test
+  public void testFormatStdinStdoutWithDashFlag() throws Exception {
+    String input = "class Foo{\n" + "void f\n" + "() {\n" + "}\n" + "}\n";
+    String expectedOutput = "class Foo {\n" + "  void f() {}\n" + "}\n";
+
+    InputStream in = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    InputStream oldIn = System.in;
+    System.setIn(in);
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    assertThat(main.format("-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+
+    System.setIn(oldIn);
+  }
+
+  @Test
+  public void testFormatLengthUpToEOF() throws Exception {
+    String input = "class Foo{\n" + "void f\n" + "() {\n" + "}\n" + "}\n\n\n\n\n\n";
+    String expectedOutput = "class Foo {\n" + "  void f() {}\n" + "}\n";
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"--offset", "0", "--length", String.valueOf(input.length()), path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void testFormatLengthOutOfRange() throws Exception {
+    String input = "class Foo{}\n";
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"--offset", "0", "--length", "9999", path.toString()};
+    assertThat(main.format(args)).isEqualTo(1);
+    assertThat(err.toString())
+        .contains("error: invalid length 9999, offset + length (9999) is outside the file");
+  }
+
+  @Test
+  public void blankInClassBody() throws FormatterException {
+    String input = "package test;\nclass T {\n\n}\n";
+    String output = new Formatter().formatSource(input);
+    String expect = "package test;\n\nclass T {}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void blankInClassBodyNoTrailing() throws FormatterException {
+    String input = "package test;\nclass T {\n\n}";
+    String output = new Formatter().formatSource(input);
+    String expect = "package test;\n\nclass T {}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void docCommentTrailingBlank() throws FormatterException {
+    String input = "class T {\n/** asd */\n\nint x;\n}";
+    String output = new Formatter().formatSource(input);
+    String expect = "class T {\n  /** asd */\n  int x;\n}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void blockCommentInteriorTrailingBlank() throws FormatterException {
+    String input = "class T {\n/*\n* asd \n* fgh\n*/ \n\nint x;\n}";
+    String output = new Formatter().formatSource(input);
+    String expect = "class T {\n  /*\n   * asd\n   * fgh\n   */\n\n  int x;\n}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void blockCommentTrailingBlank() throws FormatterException {
+    String input = "class T {\n/* asd */ \n\nint x;\n}";
+    String output = new Formatter().formatSource(input);
+    String expect = "class T {\n  /* asd */\n\n  int x;\n}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void lineCommentTrailingBlank() throws FormatterException {
+    String input = "class T {\n// asd \n\nint x;\n}";
+    String output = new Formatter().formatSource(input);
+    String expect = "class T {\n  // asd\n\n  int x;\n}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void lineCommentTrailingThinSpace() throws FormatterException {
+    // The Unicode thin space is matched by CharMatcher.whitespace() but not trim().
+    String input = "class T {\n  // asd\u2009\n}\n";
+    String output = new Formatter().formatSource(input);
+    String expect = "class T {\n  // asd\n}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void noBlankAfterLineCommentWithInteriorBlankLine() throws FormatterException {
+    String input = "class T {\n// asd \n\n// dsa \nint x;\n}";
+    String output = new Formatter().formatSource(input);
+    String expect = "class T {\n  // asd\n\n  // dsa\n  int x;\n}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void badConstructor() throws FormatterException {
+    String input = "class X { Y() {} }";
+    String output = new Formatter().formatSource(input);
+    String expect = "class X {\n  Y() {}\n}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void voidMethod() throws FormatterException {
+    String input = "class X { void Y() {} }";
+    String output = new Formatter().formatSource(input);
+    String expect = "class X {\n  void Y() {}\n}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  private static final String UNORDERED_IMPORTS =
+      Joiner.on('\n')
+          .join(
+              "import com.google.common.base.Preconditions;",
+              "",
+              "import static org.junit.Assert.fail;",
+              "import static com.google.truth.Truth.assertThat;",
+              "",
+              "import org.junit.runners.JUnit4;",
+              "import org.junit.runner.RunWith;",
+              "",
+              "import java.util.List;",
+              "",
+              "import javax.annotations.Nullable;");
+
+  @Test
+  public void importsNotReorderedByDefault() throws FormatterException {
+    String input =
+        "package com.google.example;\n" + UNORDERED_IMPORTS + "\npublic class ExampleTest {}\n";
+    String output = new Formatter().formatSource(input);
+    String expect =
+        "package com.google.example;\n\n" + UNORDERED_IMPORTS + "\n\npublic class ExampleTest {}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void importsFixedIfRequested() throws FormatterException {
+    String input =
+        "package com.google.example;\n"
+            + UNORDERED_IMPORTS
+            + "\npublic class ExampleTest {\n"
+            + "  @Nullable List<?> xs;\n"
+            + "}\n";
+    String output = new Formatter().formatSourceAndFixImports(input);
+    String expect =
+        "package com.google.example;\n\n"
+            + "import java.util.List;\n"
+            + "import javax.annotations.Nullable;\n\n"
+            + "public class ExampleTest {\n"
+            + "  @Nullable List<?> xs;\n"
+            + "}\n";
+    assertThat(output).isEqualTo(expect);
+  }
+
+  @Test
+  public void importOrderingWithoutFormatting() throws IOException, UsageException {
+    importOrdering(
+        "--fix-imports-only", "com/google/googlejavaformat/java/testimports/A.imports-only");
+  }
+
+  @Test
+  public void importOrderingAndFormatting() throws IOException, UsageException {
+    importOrdering(null, "com/google/googlejavaformat/java/testimports/A.imports-and-formatting");
+  }
+
+  @Test
+  public void formattingWithoutImportOrdering() throws IOException, UsageException {
+    importOrdering(
+        "--skip-sorting-imports",
+        "com/google/googlejavaformat/java/testimports/A.formatting-and-unused-import-removal");
+  }
+
+  @Test
+  public void formattingWithoutRemovingUnusedImports() throws IOException, UsageException {
+    importOrdering(
+        "--skip-removing-unused-imports",
+        "com/google/googlejavaformat/java/testimports/A.formatting-and-import-sorting");
+  }
+
+  private void importOrdering(String sortArg, String outputResourceName)
+      throws IOException, UsageException {
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+
+    String inputResourceName = "com/google/googlejavaformat/java/testimports/A.input";
+    String input = getResource(inputResourceName);
+    String expectedOutput = getResource(outputResourceName);
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args =
+        sortArg != null
+            ? new String[] {sortArg, "-i", path.toString()}
+            : new String[] {"-i", path.toString()};
+    main.format(args);
+
+    assertThat(err.toString()).isEmpty();
+    assertThat(out.toString()).isEmpty();
+    String output = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  private String getResource(String resourceName) throws IOException {
+    try (InputStream stream = getClass().getClassLoader().getResourceAsStream(resourceName)) {
+      assertWithMessage("Missing resource: " + resourceName).that(stream).isNotNull();
+      return CharStreams.toString(new InputStreamReader(stream, StandardCharsets.UTF_8));
+    }
+  }
+
+  // regression test for google-java-format#47
+  @Test
+  public void testTrailingCommentWithoutTerminalNewline() throws Exception {
+    assertThat(new Formatter().formatSource("/*\n * my comment */"))
+        .isEqualTo("/*\n * my comment */\n");
+  }
+
+  @Test
+  public void testEmptyArray() throws Exception {
+    assertThat(new Formatter().formatSource("class T { int x[] = {,}; }"))
+        .isEqualTo("class T {\n  int x[] = {,};\n}\n");
+  }
+
+  @Test
+  public void stringEscapeLength() throws Exception {
+    assertThat(new Formatter().formatSource("class T {{ f(\"\\\"\"); }}"))
+        .isEqualTo("class T {\n  {\n    f(\"\\\"\");\n  }\n}\n");
+  }
+
+  @Test
+  public void wrapLineComment() throws Exception {
+    assertThat(
+            new Formatter()
+                .formatSource(
+                    "class T {\n"
+                        + "  public static void main(String[] args) { // one long incredibly"
+                        + " unbroken sentence moving from topic to topic so that no-one had a"
+                        + " chance to interrupt;\n"
+                        + "  }\n"
+                        + "}\n"))
+        .isEqualTo(
+            "class T {\n"
+                + "  public static void main(\n"
+                + "      String[]\n"
+                + "          args) { // one long incredibly unbroken sentence moving"
+                + " from topic to topic so that no-one\n"
+                + "                  // had a chance to interrupt;\n"
+                + "  }\n"
+                + "}\n");
+  }
+
+  @Test
+  public void onlyWrapLineCommentOnWhitespace() throws Exception {
+    assertThat(
+            new Formatter()
+                .formatSource(
+                    "class T {\n"
+                        + "  public static void main(String[] args) { // one_long_incredibly"
+                        + "_unbroken_sentence_moving_from_topic_to_topic_so_that_no-one_had_a"
+                        + "_chance_to_interrupt;\n"
+                        + "  }\n"
+                        + "}\n"))
+        .isEqualTo(
+            "class T {\n"
+                + "  public static void main(\n"
+                + "      String[]\n"
+                + "          args) { // one_long_incredibly"
+                + "_unbroken_sentence_moving_from_topic_to_topic_so_that_no-one_had_a"
+                + "_chance_to_interrupt;\n"
+                + "  }\n"
+                + "}\n");
+  }
+
+  @Test
+  public void onlyWrapLineCommentOnWhitespace_noLeadingWhitespace() throws Exception {
+    assertThat(
+            new Formatter()
+                .formatSource(
+                    "class T {\n"
+                        + "  public static void main(String[] args) { //one_long_incredibly"
+                        + "_unbroken_sentence_moving_from_topic_to_topic_so_that_no-one_had_a"
+                        + "_chance_to_interrupt;\n"
+                        + "  }\n"
+                        + "}\n"))
+        .isEqualTo(
+            "class T {\n"
+                + "  public static void main(\n"
+                + "      String[]\n"
+                + "          args) { // one_long_incredibly"
+                + "_unbroken_sentence_moving_from_topic_to_topic_so_that_no-one_had_a"
+                + "_chance_to_interrupt;\n"
+                + "  }\n"
+                + "}\n");
+  }
+
+  @Test
+  public void throwsFormatterException() throws Exception {
+    try {
+      new Formatter().formatSourceAndFixImports("package foo; public class {");
+      fail();
+    } catch (FormatterException expected) {
+    }
+  }
+
+  @Test
+  public void blankLinesImportComment() throws FormatterException {
+    String withBlank =
+        "package p;\n"
+            + "\n"
+            + "/** test */\n"
+            + "\n"
+            + "import a.A;\n"
+            + "\n"
+            + "class T {\n"
+            + "  A a;\n"
+            + "}\n";
+    String withoutBlank =
+        "package p;\n"
+            + "\n"
+            + "/** test */\n"
+            + "import a.A;\n"
+            + "\n"
+            + "class T {\n"
+            + "  A a;\n"
+            + "}\n";
+
+    // Formatting deletes the blank line between the "javadoc" and the first import.
+    assertThat(new Formatter().formatSource(withBlank)).isEqualTo(withoutBlank);
+    assertThat(new Formatter().formatSourceAndFixImports(withBlank)).isEqualTo(withoutBlank);
+    assertThat(new Formatter().formatSource(withoutBlank)).isEqualTo(withoutBlank);
+    assertThat(new Formatter().formatSourceAndFixImports(withoutBlank)).isEqualTo(withoutBlank);
+
+    // Just fixing imports preserves whitespace around imports.
+    assertThat(RemoveUnusedImports.removeUnusedImports(withBlank)).isEqualTo(withBlank);
+    assertThat(ImportOrderer.reorderImports(withBlank, Style.GOOGLE)).isEqualTo(withBlank);
+    assertThat(RemoveUnusedImports.removeUnusedImports(withoutBlank)).isEqualTo(withoutBlank);
+    assertThat(ImportOrderer.reorderImports(withoutBlank, Style.GOOGLE)).isEqualTo(withoutBlank);
+  }
+
+  @Test
+  public void dontWrapMoeLineComments() throws Exception {
+    assertThat(
+            new Formatter()
+                .formatSource(
+                    "class T {\n"
+                        + "  // MOE: one long incredibly"
+                        + " unbroken sentence moving from topic to topic so that no-one had a"
+                        + " chance to interrupt;\n"
+                        + "}\n"))
+        .isEqualTo(
+            "class T {\n"
+                + "  // MOE: one long incredibly"
+                + " unbroken sentence moving from topic to topic so that no-one had a"
+                + " chance to interrupt;\n"
+                + "}\n");
+  }
+
+  @Test
+  public void removeTrailingTabsInComments() throws Exception {
+    assertThat(
+            new Formatter()
+                .formatSource(
+                    "class Foo {\n"
+                        + "  void f() {\n"
+                        + "    int x = 0; // comment\t\t\t\n"
+                        + "    return;\n"
+                        + "  }\n"
+                        + "}\n"))
+        .isEqualTo(
+            "class Foo {\n"
+                + "  void f() {\n"
+                + "    int x = 0; // comment\n"
+                + "    return;\n"
+                + "  }\n"
+                + "}\n");
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java b/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java
new file mode 100644
index 0000000..5a6b1f9
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java
@@ -0,0 +1,843 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Tests for {@link ImportOrderer}. */
+@RunWith(Enclosed.class)
+public class ImportOrdererTest {
+
+  /** Tests for import ordering in Google style. */
+  @RunWith(Parameterized.class)
+  public static class GoogleStyle {
+
+    private final String input;
+    private final String reordered;
+
+    public GoogleStyle(String input, String reordered) {
+      this.input = input;
+      this.reordered = reordered;
+    }
+
+    @Parameters(name = "{index}: {0}")
+    public static Collection<Object[]> parameters() {
+      // Inputs are provided as three-dimensional arrays. Each element of the outer array is a test
+      // case. It consists of two arrays of lines. The first array of lines is the test input, and
+      // the second one is the expected output. If the second array has a single element starting
+      // with !! then it is expected that ImportOrderer will throw a FormatterException with that
+      // message.
+      //
+      // If a line ends with \ then we remove the \ and don't append a \n. That allows us to check
+      // some parsing edge cases.
+      String[][][] inputsOutputs = {
+        {
+          // Empty input produces empty output.
+          {}, //
+          {}
+        },
+        {
+          {
+            "foo", "bar",
+          },
+          {
+            "foo", "bar",
+          },
+        },
+        {
+          {
+            "package foo;", //
+            "",
+            "import com.google.first.Bar;",
+            "",
+            "public class Blim {}",
+          },
+          {
+            "package foo;", //
+            "",
+            "import com.google.first.Bar;",
+            "",
+            "public class Blim {}",
+          },
+        },
+        {
+          {
+            "package foo;",
+            "",
+            "import com.google.first.Bar;",
+            "import com.google.second.Foo;",
+            "",
+            "public class Blim {}",
+          },
+          {
+            "package foo;",
+            "",
+            "import com.google.first.Bar;",
+            "import com.google.second.Foo;",
+            "",
+            "public class Blim {}",
+          },
+        },
+        {
+          {
+            "package foo;",
+            "",
+            "import com.google.second.Foo;",
+            "import com.google.first.Bar;",
+            "",
+            "public class Blim {}",
+          },
+          {
+            "package foo;",
+            "",
+            "import com.google.first.Bar;",
+            "import com.google.second.Foo;",
+            "",
+            "public class Blim {}",
+          },
+        },
+        {
+          {
+            "import java.util.Collection;",
+            "// BUG: diagnostic contains",
+            "import java.util.List;",
+            "",
+            "class B74235047 {}"
+          },
+          {
+            "import java.util.Collection;",
+            "// BUG: diagnostic contains",
+            "import java.util.List;",
+            "",
+            "class B74235047 {}"
+          }
+        },
+        {
+          {
+            "import java.util.Set;",
+            "import java.util.Collection;",
+            "// BUG: diagnostic contains",
+            "import java.util.List;",
+            "",
+            "class B74235047 {}"
+          },
+          {
+            "import java.util.Collection;",
+            "// BUG: diagnostic contains",
+            "import java.util.List;",
+            "import java.util.Set;",
+            "",
+            "class B74235047 {}"
+          }
+        },
+        {
+          {
+            "import java.util.List;",
+            "// BUG: diagnostic contains",
+            "import java.util.Set;",
+            "import java.util.Collection;",
+            "",
+            "class B74235047 {}"
+          },
+          {
+            "import java.util.Collection;",
+            "import java.util.List;",
+            "// BUG: diagnostic contains",
+            "import java.util.Set;",
+            "",
+            "class B74235047 {}"
+          }
+        },
+        {
+          {
+            "// BEGIN-STRIP",
+            "import com.google.testing.testsize.MediumTest;",
+            "import com.google.testing.testsize.MediumTestAttribute;",
+            "// END-STRIP",
+            "",
+            "class B74235047 {}"
+          },
+          {
+            "// BEGIN-STRIP",
+            "import com.google.testing.testsize.MediumTest;",
+            "import com.google.testing.testsize.MediumTestAttribute;",
+            "// END-STRIP",
+            "",
+            "class B74235047 {}"
+          }
+        },
+        {
+          {
+            "import com.google.testing.testsize.MediumTest;          // Keep this import",
+            "import com.google.testing.testsize.MediumTestAttribute; // Keep this import",
+            "",
+            "class B74235047 {}"
+          },
+          {
+            "import com.google.testing.testsize.MediumTest;          // Keep this import",
+            "import com.google.testing.testsize.MediumTestAttribute; // Keep this import",
+            "",
+            "class B74235047 {}"
+          }
+        },
+        {
+          {
+            "import java.util.Set;",
+            "import java.util.List;",
+            "",
+            "// This comment doesn't get moved because of the blank line.",
+            "",
+            "class B74235047 {}"
+          },
+          {
+            "import java.util.List;",
+            "import java.util.Set;",
+            "",
+            "// This comment doesn't get moved because of the blank line.",
+            "",
+            "class B74235047 {}"
+          }
+        },
+        {
+          {
+            "import b.B;",
+            "// MOE: end_strip",
+            "import c.C;",
+            "// MOE: begin_strip",
+            "import a.A;",
+            "",
+            "class B74235047 {}"
+          },
+          {
+            "import a.A;",
+            "import b.B;",
+            "// MOE: end_strip",
+            "import c.C;",
+            "// MOE: begin_strip",
+            "",
+            "class B74235047 {}"
+          }
+        },
+        {
+          {
+            // Check double semicolons
+            "package foo;",
+            "",
+            "import com.google.second.Foo;;",
+            "import com.google.first.Bar;;",
+            "",
+            "public class Blim {}",
+          },
+          {
+            "package foo;",
+            "",
+            "import com.google.first.Bar;",
+            "import com.google.second.Foo;",
+            "",
+            "public class Blim {}",
+          },
+        },
+        {
+          {
+            "package foo;",
+            "",
+            "import com.google.second.Foo;",
+            "import com.google.first.Bar;",
+            "import com.google.second.Foo;",
+            "import com.google.first.Bar;",
+            "",
+            "public class Blim {}",
+          },
+          {
+            "package foo;",
+            "",
+            "import com.google.first.Bar;",
+            "import com.google.second.Foo;",
+            "",
+            "public class Blim {}",
+          },
+        },
+        {
+          // Google style frowns on wildcard imports, but we handle them all the same.
+          {
+            "package foo;",
+            "",
+            "import com.google.second.*;",
+            "import com.google.first.Bar;",
+            "import com.google.first.*;",
+            "",
+            "public class Blim {}",
+          },
+          {
+            "package foo;",
+            "",
+            "import com.google.first.*;",
+            "import com.google.first.Bar;",
+            "import com.google.second.*;",
+            "",
+            "public class Blim {}",
+          },
+        },
+        {
+          {
+            "package com.google.example;",
+            "",
+            "import com.google.common.base.Preconditions;",
+            "",
+            "import org.junit.runner.RunWith;",
+            "import org.junit.runners.JUnit4;",
+            "",
+            "import java.util.List;",
+            "",
+            "import javax.annotations.Nullable;",
+            "",
+            "import static org.junit.Assert.fail;",
+            "import static com.google.truth.Truth.assertThat;",
+            "",
+            "@RunWith(JUnit4.class)",
+            "public class SomeTest {}",
+          },
+          {
+            "package com.google.example;",
+            "",
+            "import static com.google.truth.Truth.assertThat;",
+            "import static org.junit.Assert.fail;",
+            "",
+            "import com.google.common.base.Preconditions;",
+            "import java.util.List;",
+            "import javax.annotations.Nullable;",
+            "import org.junit.runner.RunWith;",
+            "import org.junit.runners.JUnit4;",
+            "",
+            "@RunWith(JUnit4.class)",
+            "public class SomeTest {}",
+          },
+        },
+
+        // we unindent imports, if we reorder them
+        {
+          {
+            "  import  com.foo.Second;", //
+            "  import com.foo.First;",
+            "  public class Foo {}",
+          },
+          {
+            "import com.foo.First;", //
+            "import com.foo.Second;",
+            "",
+            "public class Foo {}",
+          }
+        },
+
+        // Error cases
+        {
+          {
+            "package com.google.example;", //
+            "",
+            "import\\", // \\ means there is no newline here.
+          },
+          {
+            "!!Unexpected token after import: ",
+          }
+        },
+        {
+          {
+            "package com.google.example;", //
+            "",
+            "import",
+          },
+          {
+            "!!Unexpected token after import: \n",
+          }
+        },
+        {
+          {
+            "package com.google.example;", //
+            "",
+            "import foo\\",
+          },
+          {
+            "!!Expected ; after import",
+          }
+        },
+        {
+          {
+            "package com.google.example;", //
+            "",
+            "import foo.\\",
+          },
+          {
+            "!!Could not parse imported name, at: ",
+          }
+        },
+        {
+          {
+            "import com.foo.Second;",
+            "import com.foo.First;",
+            "/* we don't support block comments",
+            "   between imports either */",
+            "import com.foo.Third;",
+          },
+          {
+            "!!Imports not contiguous (perhaps a comment separates them?)",
+          }
+        },
+        {
+          {
+            "import com.foo.Second; /* no block comments after imports */", //
+            "import com.foo.First;",
+          },
+          {
+            "!!Imports not contiguous (perhaps a comment separates them?)",
+          }
+        },
+        {
+          {
+            "import com.foo.Second;",
+            "import com.foo.First;",
+            "/* but we're not fooled by comments that look like imports:",
+            "import com.foo.Third;",
+            "*/",
+          },
+          {
+            "import com.foo.First;",
+            "import com.foo.Second;",
+            "",
+            "/* but we're not fooled by comments that look like imports:",
+            "import com.foo.Third;",
+            "*/",
+          }
+        },
+        {
+          {
+            "import com . foo . Second ;", // syntactically valid, but we don't support it
+            "import com.foo.First;",
+          },
+          {
+            "!!Expected ; after import",
+          }
+        },
+        {
+          {
+            "import com.abc.@;", //
+            "import com.abc.@@;",
+          },
+          {
+            "!!Could not parse imported name, at: @",
+          }
+        },
+        {
+          {
+            "import com.abc.3;", // digits not syntactically valid
+            "import com.abc.2;",
+          },
+          {
+            // .3 is a single token (a floating-point constant)
+            "!!Expected ; after import",
+          }
+        },
+        {
+          {
+            "import com.foo.Second", // missing semicolon
+            "import com.foo.First;",
+          },
+          {
+            "!!Expected ; after import",
+          }
+        },
+        {
+          {
+            "import com.foo.Second; import com.foo.First;", "class Test {}",
+          },
+          {
+            "import com.foo.First;", //
+            "import com.foo.Second;",
+            "",
+            "class Test {}",
+          }
+        },
+        {
+          {
+            "import com.foo.Second; import com.foo.First; class Test {}",
+          },
+          {
+            "import com.foo.First;", //
+            "import com.foo.Second;",
+            "",
+            "class Test {}",
+          }
+        },
+        {
+          {
+            "package p;", //
+            "",
+            "/** test */",
+            "",
+            "import a.A;",
+            "",
+            "/** test */",
+            "",
+            "class Test {}",
+          },
+          {
+            "package p;", //
+            "",
+            "/** test */",
+            "",
+            "import a.A;",
+            "",
+            "/** test */",
+            "",
+            "class Test {}",
+          }
+        },
+        {
+          {
+            "package p; import a.A; class Test {}",
+          },
+          {
+            "package p;", //
+            "",
+            "import a.A;",
+            "",
+            "class Test {}",
+          }
+        },
+      };
+
+      ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
+      Arrays.stream(inputsOutputs).forEach(input -> builder.add(createRow(input)));
+      return builder.build();
+    }
+
+    @Test
+    public void reorder() throws FormatterException {
+      try {
+        String output = ImportOrderer.reorderImports(input, Style.GOOGLE);
+        assertWithMessage("Expected exception").that(reordered).doesNotMatch("^!!");
+        assertWithMessage(input).that(output).isEqualTo(reordered);
+      } catch (FormatterException e) {
+        if (!reordered.startsWith("!!")) {
+          throw e;
+        }
+        assertThat(reordered).endsWith("\n");
+        assertThat(e)
+            .hasMessageThat()
+            .isEqualTo("error: " + reordered.substring(2, reordered.length() - 1));
+      }
+    }
+  }
+
+  /** Tests for import ordering in AOSP style. */
+  @RunWith(Parameterized.class)
+  public static class AospStyle {
+
+    private final String input;
+    private final String reordered;
+
+    public AospStyle(String input, String reordered) {
+      this.input = input;
+      this.reordered = reordered;
+    }
+
+    @Parameters(name = "{index}: {0}")
+    public static Collection<Object[]> parameters() {
+      // Inputs are provided as three-dimensional arrays. Each element of the outer array is a test
+      // case. It consists of two arrays of lines. The first array of lines is the test input, and
+      // the second one is the expected output. If the second array has a single element starting
+      // with !! then it is expected that ImportOrderer will throw a FormatterException with that
+      // message.
+      //
+      // If a line ends with \ then we remove the \ and don't append a \n. That allows us to check
+      // some parsing edge cases.
+      String[][][] inputsOutputs = {
+        // Capital letter before lowercase
+        {
+          {
+            "package foo;",
+            "",
+            "import android.abC.Bar;",
+            "import android.abc.Bar;",
+            "public class Blim {}",
+          },
+          {
+            "package foo;",
+            "",
+            "import android.abC.Bar;",
+            "import android.abc.Bar;",
+            "",
+            "public class Blim {}",
+          }
+        },
+        // Blank line between "com.android" and "com.anythingelse"
+        {
+          {
+            "package foo;",
+            "",
+            "import com.android.Bar;",
+            "import com.google.Bar;",
+            "public class Blim {}",
+          },
+          {
+            "package foo;",
+            "",
+            "import com.android.Bar;",
+            "",
+            "import com.google.Bar;",
+            "",
+            "public class Blim {}",
+          }
+        },
+        // Rough ordering -- statics, android, third party, then java, with blank lines between
+        // major groupings
+        {
+          {
+            "package foo;",
+            "",
+            "import static net.Bar.baz;",
+            "import static org.junit.Bar.baz;",
+            "import static com.google.Bar.baz;",
+            "import static java.lang.Bar.baz;",
+            "import static junit.Bar.baz;",
+            "import static javax.annotation.Bar.baz;",
+            "import static android.Bar.baz;",
+            "import net.Bar;",
+            "import org.junit.Bar;",
+            "import com.google.Bar;",
+            "import java.lang.Bar;",
+            "import junit.Bar;",
+            "import javax.annotation.Bar;",
+            "import android.Bar;",
+            "public class Blim {}",
+          },
+          {
+            "package foo;",
+            "",
+            "import static android.Bar.baz;",
+            "",
+            "import static com.google.Bar.baz;",
+            "",
+            "import static junit.Bar.baz;",
+            "",
+            "import static net.Bar.baz;",
+            "",
+            "import static org.junit.Bar.baz;",
+            "",
+            "import static java.lang.Bar.baz;",
+            "",
+            "import static javax.annotation.Bar.baz;",
+            "",
+            "import android.Bar;",
+            "",
+            "import com.google.Bar;",
+            "",
+            "import junit.Bar;",
+            "",
+            "import net.Bar;",
+            "",
+            "import org.junit.Bar;",
+            "",
+            "import java.lang.Bar;",
+            "",
+            "import javax.annotation.Bar;",
+            "",
+            "public class Blim {}",
+          }
+        },
+        {
+          {
+            "package foo;",
+            "",
+            "import static java.first.Bar.baz;",
+            "import static com.second.Bar.baz;",
+            "import com.first.Bar;",
+            "import static android.second.Bar.baz;",
+            "import dalvik.first.Bar;",
+            "import static dalvik.first.Bar.baz;",
+            "import static androidx.second.Bar.baz;",
+            "import java.second.Bar;",
+            "import static com.android.second.Bar.baz;",
+            "import static net.first.Bar.baz;",
+            "import gov.second.Bar;",
+            "import junit.second.Bar;",
+            "import static libcore.second.Bar.baz;",
+            "import static java.second.Bar.baz;",
+            "import static net.second.Bar.baz;",
+            "import static org.first.Bar.baz;",
+            "import static dalvik.second.Bar.baz;",
+            "import javax.first.Bar;",
+            "import static javax.second.Bar.baz;",
+            "import android.first.Bar;",
+            "import android.second.Bar;",
+            "import static javax.first.Bar.baz;",
+            "import androidx.first.Bar;",
+            "import static androidx.first.Bar.baz;",
+            "import androidx.second.Bar;",
+            "import com.android.first.Bar;",
+            "import gov.first.Bar;",
+            "import com.android.second.Bar;",
+            "import dalvik.second.Bar;",
+            "import static org.second.Bar.baz;",
+            "import net.first.Bar;",
+            "import libcore.second.Bar;",
+            "import static android.first.Bar.baz;",
+            "import com.second.Bar;",
+            "import static gov.second.Bar.baz;",
+            "import static gov.first.Bar.baz;",
+            "import static junit.first.Bar.baz;",
+            "import libcore.first.Bar;",
+            "import junit.first.Bar;",
+            "import javax.second.Bar;",
+            "import static libcore.first.Bar.baz;",
+            "import net.second.Bar;",
+            "import static com.first.Bar.baz;",
+            "import org.second.Bar;",
+            "import static junit.second.Bar.baz;",
+            "import java.first.Bar;",
+            "import org.first.Bar;",
+            "import static com.android.first.Bar.baz;",
+            "public class Blim {}",
+          },
+          {
+            "package foo;", //
+            "",
+            "import static android.first.Bar.baz;",
+            "import static android.second.Bar.baz;",
+            "",
+            "import static androidx.first.Bar.baz;",
+            "import static androidx.second.Bar.baz;",
+            "",
+            "import static com.android.first.Bar.baz;",
+            "import static com.android.second.Bar.baz;",
+            "",
+            "import static dalvik.first.Bar.baz;",
+            "import static dalvik.second.Bar.baz;",
+            "",
+            "import static libcore.first.Bar.baz;",
+            "import static libcore.second.Bar.baz;",
+            "",
+            "import static com.first.Bar.baz;",
+            "import static com.second.Bar.baz;",
+            "",
+            "import static gov.first.Bar.baz;",
+            "import static gov.second.Bar.baz;",
+            "",
+            "import static junit.first.Bar.baz;",
+            "import static junit.second.Bar.baz;",
+            "",
+            "import static net.first.Bar.baz;",
+            "import static net.second.Bar.baz;",
+            "",
+            "import static org.first.Bar.baz;",
+            "import static org.second.Bar.baz;",
+            "",
+            "import static java.first.Bar.baz;",
+            "import static java.second.Bar.baz;",
+            "",
+            "import static javax.first.Bar.baz;",
+            "import static javax.second.Bar.baz;",
+            "",
+            "import android.first.Bar;",
+            "import android.second.Bar;",
+            "",
+            "import androidx.first.Bar;",
+            "import androidx.second.Bar;",
+            "",
+            "import com.android.first.Bar;",
+            "import com.android.second.Bar;",
+            "",
+            "import dalvik.first.Bar;",
+            "import dalvik.second.Bar;",
+            "",
+            "import libcore.first.Bar;",
+            "import libcore.second.Bar;",
+            "",
+            "import com.first.Bar;",
+            "import com.second.Bar;",
+            "",
+            "import gov.first.Bar;",
+            "import gov.second.Bar;",
+            "",
+            "import junit.first.Bar;",
+            "import junit.second.Bar;",
+            "",
+            "import net.first.Bar;",
+            "import net.second.Bar;",
+            "",
+            "import org.first.Bar;",
+            "import org.second.Bar;",
+            "",
+            "import java.first.Bar;",
+            "import java.second.Bar;",
+            "",
+            "import javax.first.Bar;",
+            "import javax.second.Bar;",
+            "",
+            "public class Blim {}",
+          },
+        },
+      };
+      ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
+      Arrays.stream(inputsOutputs).forEach(input -> builder.add(createRow(input)));
+      return builder.build();
+    }
+
+    @Test
+    public void reorder() throws FormatterException {
+      try {
+        String output = ImportOrderer.reorderImports(input, Style.AOSP);
+        assertWithMessage("Expected exception").that(reordered).doesNotMatch("^!!");
+        assertWithMessage(input).that(output).isEqualTo(reordered);
+      } catch (FormatterException e) {
+        if (!reordered.startsWith("!!")) {
+          throw e;
+        }
+        assertThat(reordered).endsWith("\n");
+        assertThat(e)
+            .hasMessageThat()
+            .isEqualTo("error: " + reordered.substring(2, reordered.length() - 1));
+      }
+    }
+  }
+
+  private static Object[] createRow(String[][] inputAndOutput) {
+    assertThat(inputAndOutput).hasLength(2);
+    String[] input = inputAndOutput[0];
+    String[] output = inputAndOutput[1];
+    if (output.length == 0) {
+      output = input;
+    }
+    Object[] row = {
+      Joiner.on('\n').join(input) + '\n', //
+      Joiner.on('\n').join(output) + '\n',
+    };
+    // If a line ends with \ then we remove the \ and don't append a \n. That allows us to check
+    // some parsing edge cases.
+    row[0] = ((String) row[0]).replace("\\\n", "");
+    return row;
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java
new file mode 100644
index 0000000..4116997
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java
@@ -0,0 +1,1418 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Joiner;
+import com.google.common.io.ByteStreams;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests formatting javadoc. */
+@RunWith(JUnit4.class)
+public final class JavadocFormattingTest {
+
+  private final Formatter formatter = new Formatter();
+
+  @Test
+  public void notJavadoc() {
+    String[] input = {
+      "/**/", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**/", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void empty() {
+    String[] input = {
+      "/***/", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/***/", "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void emptyMultipleLines() {
+    String[] input = {
+      "/**", //
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** */", "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void simple() {
+    String[] input = {
+      "/** */", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** */", "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void commentMostlyUntouched() {
+    // This test isn't necessarily what we'd want to do, but it's what we do now, and it's OK-ish.
+    String[] input = {
+      "/**",
+      " * Foo.",
+      " *",
+      " *  <!--",
+      "*abc",
+      " *   def   ",
+      " * </tr>",
+      " *-->bar",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * Foo.",
+      " * <!--",
+      " *abc",
+      " *   def",
+      " * </tr>",
+      " *-->",
+      " * bar",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void moeComments() {
+    String[] input = {
+      "/**",
+      " * Deatomizes the given user.",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " * See go/deatomizer-v5 for the design doc.",
+      " * <!-- MOE:end_intracomment_strip -->",
+      " * To reatomize, call {@link reatomize}.",
+      " *",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " * <p>This method is used in the Google teleporter.",
+      " *",
+      " * <p>Yes, we have a teleporter.",
+      " * <!-- MOE:end_intracomment_strip -->",
+      " *",
+      " * @param user the person to teleport.",
+      " *     <!-- MOE:begin_intracomment_strip -->",
+      " *     Users must sign go/deatomize-waiver ahead of time.",
+      " *     <!-- MOE:end_intracomment_strip -->",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " * @deprecated Sometimes turns the user into a goat.",
+      " * <!-- MOE:end_intracomment_strip -->",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * Deatomizes the given user.",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " * See go/deatomizer-v5 for the design doc.",
+      " * <!-- MOE:end_intracomment_strip -->",
+      " * To reatomize, call {@link reatomize}.",
+      " *",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " * <p>This method is used in the Google teleporter.",
+      " *",
+      " * <p>Yes, we have a teleporter.",
+      " * <!-- MOE:end_intracomment_strip -->",
+      " *",
+      " * @param user the person to teleport.",
+      " *     <!-- MOE:begin_intracomment_strip -->",
+      " *     Users must sign go/deatomize-waiver ahead of time.",
+      " *     <!-- MOE:end_intracomment_strip -->",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " * @deprecated Sometimes turns the user into a goat.",
+      " * <!-- MOE:end_intracomment_strip -->",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void moeCommentBeginOnlyInMiddleOfDoc() {
+    // We don't really care what happens here so long as we don't explode.
+    String[] input = {
+      "/**", //
+      " * Foo.",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " * Bar.",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * Foo.",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " * Bar.",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void moeCommentBeginOnlyAtEndOfDoc() {
+    // We don't really care what happens here so long as we don't explode.
+    // TODO(cpovirk): OK, maybe try to leave it in....
+    String[] input = {
+      "/**", //
+      " * Foo.",
+      " * <!-- MOE:begin_intracomment_strip -->",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** Foo. */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void moeCommentEndOnly() {
+    // We don't really care what happens here so long as we don't explode.
+    String[] input = {
+      "/**", //
+      " * Foo.",
+      " * <!-- MOE:end_intracomment_strip -->",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * Foo.",
+      " * <!-- MOE:end_intracomment_strip -->",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void tableMostlyUntouched() {
+    String[] input = {
+      "/**",
+      " * Foo.",
+      " *",
+      " *  <table>",
+      "*<tr><td>a<td>b</tr>",
+      " * <tr>",
+      " * <td>A",
+      " *     <td>B",
+      " * </tr>",
+      " *</table>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * Foo.",
+      " *",
+      " * <table>",
+      " * <tr><td>a<td>b</tr>",
+      " * <tr>",
+      " * <td>A",
+      " *     <td>B",
+      " * </tr>",
+      " * </table>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void preMostlyUntouched() {
+    /*
+     * Arguably we shouldn't insert the space between "*" and "4," since doing so changes the
+     * rendered HTML output (by inserting a space there). However, inserting a space between "*" and
+     * "</pre>" (which has no impact on the rendered HTML AFAIK) is a good thing, and inserting
+     * similar spaces in the case of <table> is good, too. And if "* 4" breaks a user's intended
+     * formatting, that user can fix it up, just as that user would have to fix up the "*4"
+     * style violation. The main downside to "* 4" is that the user might not notice that we made
+     * the change at all. (We've also slightly complicated NEWLINE_PATTERN and writeNewline to
+     * accommodate it.)
+     */
+    String[] input = {
+      "/**", //
+      " * Example:",
+      " *",
+      " *  <pre>",
+      "*    1 2<br>    3   ",
+      " *4 5 6",
+      "7 8",
+      " *</pre>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * Example:",
+      " *",
+      " * <pre>",
+      " *    1 2<br>    3",
+      " * 4 5 6",
+      " * 7 8",
+      " * </pre>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void preCodeExample() {
+    // We should figure out whether we want a newline or blank line before <pre> or not.
+    String[] input = {
+      "/**",
+      " * Example:",
+      " *",
+      " * <pre>   {@code",
+      " *",
+      " *   Abc.def(foo, 7, true); // blah}</pre>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * Example:",
+      " *",
+      " * <pre>{@code",
+      " * Abc.def(foo, 7, true); // blah",
+      " * }</pre>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void preNotWrapped() {
+    String[] input = {
+      "/**",
+      " * Example:",
+      " *",
+      " * <pre>",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012 45678901",
+      " * </pre>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * Example:",
+      " *",
+      " * <pre>",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012 45678901",
+      " * </pre>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void javaCodeInPre() {
+    String[] input = {
+      "/**",
+      " * Example:",
+      " *",
+      " *<pre>",
+      " * aaaaa    |   a  |   +",
+      " * \"bbbb    |   b  |  \"",
+      " *</pre>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * Example:",
+      " *",
+      " * <pre>",
+      " * aaaaa    |   a  |   +",
+      " * \"bbbb    |   b  |  \"",
+      " * </pre>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void joinLines() {
+    String[] input = {
+      "/**", //
+      " * foo",
+      " * bar",
+      " * baz",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** foo bar baz */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void oneLinerIs100() {
+    String[] input = {
+      "/**",
+      " * 567890123 567890123 567890123 567890123 567890123 567890123 567890123 567890123 "
+          + "567890123 567",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** 567890123 567890123 567890123 567890123 567890123 567890123 567890123 567890123 "
+          + "567890123 567 */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void oneLinerWouldBe101() {
+    String[] input = {
+      "/**",
+      " * 567890123 567890123 567890123 567890123 567890123 567890123 567890123 567890123 "
+          + "567890123 5678",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * 567890123 567890123 567890123 567890123 567890123 567890123 567890123 567890123 "
+          + "567890123 5678",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void multilineWrap() {
+    String[] input = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012 45678901",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012",
+      " * 45678901",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void tooLong() {
+    String[] input = {
+      "/**",
+      " * abc",
+      " *",
+      " * <p>789012345678901234567890123456789012345678901234567890123456789012345678901234567"
+          + "8901234567890123456",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * abc",
+      " *",
+      " * <p>789012345678901234567890123456789012345678901234567890123456789012345678901234567"
+          + "8901234567890123456",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void joinedTokens() {
+    /*
+     * Originally, 4, <b>, and 8901 are separate tokens. Test that we join them (and thus don't
+     * split them across lines).
+     */
+    String[] input = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012 4<b>8901",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012",
+      " * 4<b>8901",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void joinedAtSign() {
+    /*
+     * The last 456789012 would fit on the first line with the others. But putting it there would
+     * mean the next line would start with @5678901, which would then be interpreted as a tag.
+     */
+    String[] input = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012 @5678901",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012",
+      " * 456789012 @5678901",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void joinedMultipleAtSign() {
+    // This is the same as above except that it tests multiple consecutive @... tokens.
+    String[] input = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "@56789012 @5678901",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012",
+      " * 456789012 @56789012 @5678901",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void noAsterisk() {
+    String[] input = {
+      "/**", //
+      " abc<p>def",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * abc",
+      " *",
+      " * <p>def",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void significantAsterisks() {
+    String[] input = {
+      "/** *", //
+      " * *",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** * * */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void links() {
+    String[] input = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012 4567 <a",
+      " * href=foo>foo</a>.",
+      " *",
+      " * <p>789012 456789012 456789012 456789012 456789012 456789012 456789012 456789 "
+          + "<a href=foo>",
+      " * foo</a>.",
+      " *",
+      " * <p>789012 456789012 456789012 456789012 456789012 456789012 456789012 4567890 "
+          + "<a href=foo>",
+      " * foo</a>.",
+      " *",
+      " * <p><a href=foo>",
+      " * foo</a>.",
+      " *",
+      " * <p>foo <a href=bar>",
+      " * bar</a>.",
+      " *",
+      " * <p>foo-<a href=bar>",
+      " * bar</a>.",
+      " *",
+      " * <p>foo<a href=bar>",
+      " * bar</a>.",
+      " *",
+      " * <p><a href=foo>foo</a> bar.",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * 456789012 456789012 456789012 456789012 456789012 456789012 456789012 456789012 "
+          + "456789012 4567 <a",
+      " * href=foo>foo</a>.",
+      " *",
+      " * <p>789012 456789012 456789012 456789012 456789012 456789012 456789012 456789 "
+          + "<a href=foo>foo</a>.",
+      " *",
+      " * <p>789012 456789012 456789012 456789012 456789012 456789012 456789012 4567890 "
+          + "<a href=foo>",
+      " * foo</a>.",
+      " *",
+      " * <p><a href=foo>foo</a>.",
+      " *",
+      " * <p>foo <a href=bar>bar</a>.",
+      " *",
+      " * <p>foo-<a href=bar>bar</a>.",
+      " *",
+      /*
+       * In this next case, we've removed a space from the output. Fortunately, the depot doesn't
+       * appear to contain any occurrences of this pattern. And if it does, the better fix is to
+       * insert a space before <a href> rather than after.
+       */
+      " * <p>foo<a href=bar>bar</a>.",
+      " *",
+      " * <p><a href=foo>foo</a> bar.",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void heading() {
+    String[] input = {
+      "/**", //
+      " * abc<h1>def</h1>ghi",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * abc",
+      " *",
+      " * <h1>def</h1>",
+      " *",
+      " * ghi",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void blockquote() {
+    String[] input = {
+      "/**", //
+      " * abc<blockquote><p>def</blockquote>ghi",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * abc",
+      " *",
+      " * <blockquote>",
+      " *",
+      " * <p>def",
+      " *",
+      " * </blockquote>",
+      " *",
+      " * ghi",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void lists() {
+    String[] input = {
+      "/**", //
+      "* hi",
+      "*",
+      "* <ul>",
+      "* <li>",
+      "* <ul>",
+      "* <li>a</li>",
+      "* </ul>",
+      "* </li>",
+      "* </ul>",
+      "*/",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * hi",
+      " *",
+      " * <ul>",
+      " *   <li>",
+      " *       <ul>",
+      " *         <li>a",
+      " *       </ul>",
+      " * </ul>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void lists2() {
+    String[] input = {
+      "/**", //
+      " * Foo.",
+      " *",
+      " * <ul><li>1<ul><li>1a<li>1b</ul>more 1<p>still more 1<li>2</ul>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * Foo.",
+      " *",
+      " * <ul>",
+      " *   <li>1",
+      " *       <ul>",
+      " *         <li>1a",
+      " *         <li>1b",
+      " *       </ul>",
+      " *       more 1",
+      " *       <p>still more 1",
+      " *   <li>2",
+      " * </ul>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void closeInnerListStillNewline() {
+    String[] input = {
+      "/**", //
+      " * Foo.",
+      " *",
+      " * <ul><li><ul><li>a</ul>b</ul>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * Foo.",
+      " *",
+      " * <ul>",
+      " *   <li>",
+      " *       <ul>",
+      " *         <li>a",
+      " *       </ul>",
+      " *       b",
+      " * </ul>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void listItemWrap() {
+    String[] input = {
+      "/**", //
+      " * Foo.",
+      " *",
+      " * <ul><li>234567890 234567890 234567890 234567890 234567890 234567890 234567890 234567890"
+          + " 234567890 234567890</ul>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * Foo.",
+      " *",
+      " * <ul>",
+      " *   <li>234567890 234567890 234567890 234567890 234567890 234567890 234567890 234567890"
+          + " 234567890",
+      " *       234567890",
+      " * </ul>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void unclosedList() {
+    String[] input = {
+      "/**", //
+      " * Foo.",
+      " *",
+      " * <ul><li>1",
+      " * @return blah",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * Foo.",
+      " *",
+      " * <ul>",
+      " *   <li>1",
+      " *",
+      " * @return blah",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void br() {
+    String[] input = {
+      "/**", //
+      " * abc<br>def",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * abc<br>",
+      " * def",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void brSpaceBug() {
+    // TODO(b/28983091): Remove the space before <br> here.
+    String[] input = {
+      "/**", //
+      " * abc <br>def",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * abc <br>",
+      " * def",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void brAtSignBug() {
+    /*
+     * This is a bug -- more of a "spec" bug than an implementation bug, and hard to fix.
+     * Fortunately, some very quick searching didn't turn up any instances in the Google codebase.
+     */
+    String[] input = {
+      "/**", //
+      " * abc<br>@foo ",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * abc<br>",
+      " * @foo", // interpreted as a block tag now!
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void unicodeCharacterCountArguableBug() {
+    /*
+     * We might prefer for multi-char characters like 𝄞 to be treated as taking up one column (or
+     * perhaps for all characters to be treated based on their width in monospace fonts). But
+     * currently we just count chars.
+     */
+    String[] input = {
+      "/**",
+      " * 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 "
+          + "456789𝄞12 456789𝄞",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12 456789𝄞12",
+      " * 456789𝄞12 456789𝄞",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void blankLineBeforeParams() {
+    String[] input = {
+      "/**", //
+      " * hello world",
+      " * @param this is a param",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * hello world",
+      " *",
+      " * @param this is a param",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void onlyParams() {
+    String[] input = {
+      "/**", //
+      " *",
+      " *",
+      " * @param this is a param",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** @param this is a param */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void paramsContinuationIndented() {
+    String[] input = {
+      "/**", //
+      " * hello world",
+      " *",
+      " * @param foo 567890123 567890123 567890123 567890123 567890123 567890123 567890123"
+          + " 567890123 567890123",
+      " * @param bar another",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * hello world",
+      " *",
+      " * @param foo 567890123 567890123 567890123 567890123 567890123 567890123 567890123"
+          + " 567890123",
+      " *     567890123",
+      " * @param bar another",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void paramsOtherIndents() {
+    String[] input = {
+      "/**", //
+      " * hello world",
+      " *",
+      " * @param foo a<p>b<ul><li>a<ul><li>x</ul></ul>",
+      " * @param bar another",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * hello world",
+      " *",
+      " * @param foo a",
+      " *     <p>b",
+      " *     <ul>",
+      " *       <li>a",
+      " *           <ul>",
+      " *             <li>x",
+      " *           </ul>",
+      " *     </ul>",
+      " *", // TODO(cpovirk): Ideally we would probably eliminate this.
+      " * @param bar another",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void paragraphTag() {
+    String[] input = {
+      "class Test {",
+      "  /**",
+      "   * hello<p>world",
+      "   */",
+      "  void f() {}",
+      "",
+      "  /**",
+      "   * hello",
+      "   * <p>",
+      "   * world",
+      "   */",
+      "  void f() {}",
+      "}",
+    };
+    String[] expected = {
+      "class Test {",
+      "  /**",
+      "   * hello",
+      "   *",
+      "   * <p>world",
+      "   */",
+      "  void f() {}",
+      "",
+      "  /**",
+      "   * hello",
+      "   *",
+      "   * <p>world",
+      "   */",
+      "  void f() {}",
+      "}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void xhtmlParagraphTag() {
+    String[] input = {
+      "class Test {",
+      "  /**",
+      "   * hello<p/>world",
+      "   */",
+      "  void f() {}",
+      "",
+      "}",
+    };
+    String[] expected = {
+      "class Test {",
+      "  /**",
+      "   * hello",
+      "   *",
+      "   * <p>world",
+      "   */",
+      "  void f() {}",
+      "}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void removeInitialParagraphTag() {
+    String[] input = {
+      "/**", //
+      " * <p>hello<p>world",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * hello",
+      " *",
+      " * <p>world",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void inferParagraphTags() {
+    String[] input = {
+      "/**",
+      " *",
+      " *",
+      " * foo",
+      " * foo",
+      " *",
+      " *",
+      " * foo",
+      " *",
+      " * bar",
+      " *",
+      " * <pre>",
+      " *",
+      " * baz",
+      " *",
+      " * </pre>",
+      " *",
+      " * <ul>",
+      " * <li>foo",
+      " *",
+      " * bar",
+      " * </ul>",
+      " *",
+      " *",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * foo foo",
+      " *",
+      " * <p>foo",
+      " *",
+      " * <p>bar",
+      " *",
+      " * <pre>",
+      " *",
+      " * baz",
+      " *",
+      " * </pre>",
+      " *",
+      " * <ul>",
+      " *   <li>foo",
+      " *       <p>bar",
+      " * </ul>",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void paragraphTagNewlines() throws Exception {
+    String input =
+        new String(
+            ByteStreams.toByteArray(getClass().getResourceAsStream("testjavadoc/B28750242.input")),
+            UTF_8);
+    String expected =
+        new String(
+            ByteStreams.toByteArray(getClass().getResourceAsStream("testjavadoc/B28750242.output")),
+            UTF_8);
+    String output = formatter.formatSource(input);
+    assertThat(output).isEqualTo(expected);
+  }
+
+  @Test
+  public void listItemSpaces() throws Exception {
+    String input =
+        new String(
+            ByteStreams.toByteArray(getClass().getResourceAsStream("testjavadoc/B31404367.input")),
+            UTF_8);
+    String expected =
+        new String(
+            ByteStreams.toByteArray(getClass().getResourceAsStream("testjavadoc/B31404367.output")),
+            UTF_8);
+    String output = formatter.formatSource(input);
+    assertThat(output).isEqualTo(expected);
+  }
+
+  @Test
+  public void htmlTagsInCode() {
+    String[] input = {
+      "/** abc {@code {} <p> <li> <pre> <table>} def */", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** abc {@code {} <p> <li> <pre> <table>} def */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void loneBraceDoesNotStartInlineTag() {
+    String[] input = {
+      "/** {  <p> } */", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * {",
+      " *",
+      " * <p>}",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void unicodeEscapesNotReplaced() {
+    // Test that we don't replace them with their interpretations.
+    String[] input = {
+      "/** foo \\u0000 bar \\u6c34 baz */", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** foo \\u0000 bar \\u6c34 baz */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void unicodeEscapesNotInterpretedBug() {
+    /*
+     * In theory, \u003C should be treated exactly like <, and so too should the escaped versions of
+     * @, *, and other special chars. We don't recognize that, though, so we don't put what is
+     * effectively "<p>" on a new line.
+     */
+    String[] input = {
+      "/** a\\u003Cp>b */", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** a\\u003Cp>b */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void trailingLink() {
+    // Eclipse's parser seems to want to discard the line break after {@link}. Test that we see it.
+    String[] input = {
+      "/**", //
+      " * abc {@link Foo}",
+      " * def",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** abc {@link Foo} def */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void codeInCode() {
+    // Eclipse's parser seems to get confused at the second {@code}. Test that we handle it.
+    String[] input = {
+      "/** abc {@code {@code foo}} def */", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** abc {@code {@code foo}} def */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void quotedTextSplitAcrossLinks() {
+    /*
+     * This demonstrates one of multiple reasons that we can't hand the Javadoc *content* to
+     * Eclipse's lexer as if it were Java code.
+     */
+    String[] input = {
+      "/**", //
+      " * abc \"foo",
+      " * bar baz\" def",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** abc \"foo bar baz\" def */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void standardizeTags() {
+    String[] input = {
+      "/**",
+      " * foo",
+      " *",
+      " * <P>bar",
+      " *",
+      " * <p class=clazz>baz<BR>",
+      " * baz",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**",
+      " * foo",
+      " *",
+      " * <p>bar",
+      " *",
+      " * <p class=clazz>baz<br>",
+      " * baz",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void removeCloseTags() {
+    String[] input = {
+      "/**", //
+      " * foo</p>",
+      " *",
+      " * <p>bar</p>",
+      " */",
+      "class Test {}",
+    };
+    String[] expected = {
+      "/**", //
+      " * foo",
+      " *",
+      " * <p>bar",
+      " */",
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void javadocFullSentences() {
+    String[] input = {
+      "/** In our application, bats are often found hanging from the ceiling, especially on"
+          + " Wednesdays.  Sometimes sick bats have issues where their claws do not close entirely."
+          + "  This class provides a nice, grippable surface for them to cling to. */",
+      "class Grippable {}",
+    };
+    String[] expected = {
+      "/**",
+      " * In our application, bats are often found hanging from the ceiling, especially on"
+          + " Wednesdays.",
+      " * Sometimes sick bats have issues where their claws do not close entirely. This class"
+          + " provides a",
+      " * nice, grippable surface for them to cling to.",
+      " */",
+      "class Grippable {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void javadocSentenceFragment() {
+    String[] input = {
+      "/** Provides a comfy, grippable surface for sick bats with claw-closing problems, which are"
+          + " sometimes found hanging from the ceiling on Wednesdays. */",
+      "class Grippable {}",
+    };
+    String[] expected = {
+      "/**",
+      " * Provides a comfy, grippable surface for sick bats with claw-closing problems, which are"
+          + " sometimes",
+      " * found hanging from the ceiling on Wednesdays.",
+      " */",
+      "class Grippable {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  @Test
+  public void javadocCanEndAnywhere() {
+    String[] input = {
+      "/** foo <pre*/", //
+      "class Test {}",
+    };
+    String[] expected = {
+      "/** foo <pre */", //
+      "class Test {}",
+    };
+    doFormatTest(input, expected);
+  }
+
+  private void doFormatTest(String[] input, String[] expected) {
+    try {
+      String actual = formatter.formatSource(Joiner.on('\n').join(input));
+      assertThat(actual).isEqualTo(Joiner.on('\n').join(expected) + "\n");
+    } catch (FormatterException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  @Test
+  public void windowsLineSeparator() throws FormatterException {
+    String[] input = {
+      "/**", " * hello", " *", " * <p>world", " */", "class Test {}",
+    };
+    for (String separator : Arrays.asList("\r", "\r\n")) {
+      String actual = formatter.formatSource(Joiner.on(separator).join(input));
+      assertThat(actual).isEqualTo(Joiner.on(separator).join(input) + separator);
+    }
+  }
+
+  @Test
+  public void u2028LineSeparator() {
+    String[] input = {
+      "public class Foo {",
+      "  /**\u2028",
+      "   * Set and enable something.",
+      "   */",
+      "  public void setSomething() {}",
+      "}",
+    };
+    String[] expected = {
+      "public class Foo {",
+      "  /**",
+      "   * \u2028 Set and enable something.",
+      "   */",
+      "  public void setSomething() {}",
+      "}",
+    };
+    doFormatTest(input, expected);
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/LineRangesToCharRangesTest.java b/core/src/test/java/com/google/googlejavaformat/java/LineRangesToCharRangesTest.java
new file mode 100644
index 0000000..ca3ad7c
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/LineRangesToCharRangesTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Formatter#lineRangesToCharRanges} */
+@RunWith(JUnit4.class)
+public class LineRangesToCharRangesTest {
+
+  @SafeVarargs
+  final Set<Range<Integer>> getCharRanges(String input, Range<Integer>... ranges) {
+    RangeSet<Integer> rangeSet = TreeRangeSet.create();
+    for (Range<Integer> range : ranges) {
+      rangeSet.add(range);
+    }
+    return Formatter.lineRangesToCharRanges(input, rangeSet).asRanges();
+  }
+
+  @Test
+  public void emptyLineRanges() throws Exception {
+    assertThat(getCharRanges("", Range.closedOpen(0, 1))).isEmpty();
+  }
+
+  @Test
+  public void lineRanges() throws Exception {
+    assertThat(getCharRanges("_\n_\n_\n", Range.closedOpen(0, 1)))
+        .containsExactly(Range.closedOpen(0, 1));
+    assertThat(getCharRanges("_\n_\n_\n", Range.closedOpen(1, 2)))
+        .containsExactly(Range.closedOpen(2, 3));
+    assertThat(getCharRanges("_\n_\n_\n", Range.closedOpen(2, 3)))
+        .containsExactly(Range.closedOpen(4, 5));
+    assertThat(getCharRanges("_\n_\n_\n", Range.closedOpen(3, 4))).isEmpty();
+  }
+
+  @Test
+  public void blankLineRange() throws Exception {
+    assertThat(getCharRanges("hello\n\nworld", Range.closedOpen(0, 1)))
+        .containsExactly(Range.closedOpen(0, 5));
+    assertThat(getCharRanges("hello\n\nworld", Range.closedOpen(1, 2))).isEmpty();
+    assertThat(getCharRanges("hello\n\nworld", Range.closedOpen(2, 3)))
+        .containsExactly(Range.closedOpen(7, 12));
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
new file mode 100644
index 0000000..613d391
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
@@ -0,0 +1,598 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.ProcessBuilder.Redirect;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.EnumSet;
+import java.util.Locale;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Main}. */
+@RunWith(JUnit4.class)
+public class MainTest {
+
+  @Rule public TemporaryFolder testFolder = new TemporaryFolder();
+
+  // PrintWriter instances used below are hard-coded to use system-default line separator.
+  private final Joiner joiner = Joiner.on(System.lineSeparator());
+
+  @Test
+  public void testUsageOutput() {
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+
+    try {
+      main.format("--help");
+      throw new AssertionError("Expected UsageException to be thrown");
+    } catch (UsageException e) {
+
+      String usage = e.getMessage();
+
+      // Check that doc links are included.
+      assertThat(usage).contains("https://github.com/google/google-java-format");
+      assertThat(usage).contains("Usage: google-java-format");
+
+      // Sanity check that a flag and description is in included.
+      assertThat(usage).contains("--length");
+      assertThat(usage).contains("Character length to format.");
+
+      // Check that some of the additional text is included.
+      assertThat(usage).contains("the result is sent to stdout");
+    }
+  }
+
+  @Test
+  public void version() throws UsageException {
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    assertThat(main.format("-version")).isEqualTo(0);
+    assertThat(err.toString()).contains("google-java-format: Version ");
+  }
+
+  @Test
+  public void preserveOriginalFile() throws Exception {
+    Path path = testFolder.newFile("Test.java").toPath();
+    Files.write(path, "class Test {}\n".getBytes(UTF_8));
+    try {
+      Files.setPosixFilePermissions(path, EnumSet.of(PosixFilePermission.OWNER_READ));
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    Main main =
+        new Main(
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8)), true),
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+            System.in);
+    int errorCode = main.format("-replace", path.toAbsolutePath().toString());
+    assertWithMessage("Error Code").that(errorCode).isEqualTo(0);
+  }
+
+  @Test
+  public void testMain() throws Exception {
+    Process process =
+        new ProcessBuilder(
+                ImmutableList.of(
+                    Paths.get(System.getProperty("java.home")).resolve("bin/java").toString(),
+                    "-cp",
+                    System.getProperty("java.class.path"),
+                    Main.class.getName()))
+            .redirectError(Redirect.PIPE)
+            .redirectOutput(Redirect.PIPE)
+            .start();
+    process.waitFor();
+    String err = new String(ByteStreams.toByteArray(process.getErrorStream()), UTF_8);
+    assertThat(err).contains("Usage: google-java-format");
+    assertThat(process.exitValue()).isEqualTo(0);
+  }
+
+  // end to end javadoc formatting test
+  @Test
+  public void javadoc() throws Exception {
+    String[] input = {
+      "/**",
+      " * graph",
+      " *",
+      " * graph",
+      " *",
+      " * @param foo lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
+          + " eiusmod tempor incididunt ut labore et dolore magna aliqua",
+      " */",
+      "class Test {",
+      "  /**",
+      "   * creates entropy",
+      "   */",
+      "  public static void main(String... args) {}",
+      "}",
+    };
+    String[] expected = {
+      "/**",
+      " * graph",
+      " *",
+      " * <p>graph",
+      " *",
+      " * @param foo lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
+          + " eiusmod tempor",
+      " *     incididunt ut labore et dolore magna aliqua",
+      " */",
+      "class Test {",
+      "  /** creates entropy */",
+      "  public static void main(String... args) {}",
+      "}",
+      "",
+    };
+    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
+    StringWriter out = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+            in);
+    assertThat(main.format("-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(joiner.join(expected));
+  }
+
+  // end to end import fixing test
+  @Test
+  public void imports() throws Exception {
+    String[] input = {
+      "import java.util.LinkedList;",
+      "import java.util.List;",
+      "import java.util.ArrayList;",
+      "class Test {",
+      "  /**",
+      "   * May be an {@link ArrayList}.",
+      "   */",
+      "  public static List<String> names;",
+      "}",
+    };
+    String[] expected = {
+      "import java.util.ArrayList;",
+      "import java.util.List;",
+      "",
+      "class Test {",
+      "  /**",
+      "   * May be an {@link ArrayList}.",
+      "   */",
+      "  public static List<String> names;",
+      "}",
+    };
+    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
+    StringWriter out = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+            in);
+    assertThat(main.format("-", "--fix-imports-only")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(joiner.join(expected));
+  }
+
+  @Test
+  public void optimizeImportsDoesNotLeaveEmptyLines() throws Exception {
+    String[] input = {
+      "package abc;",
+      "",
+      "import java.util.LinkedList;",
+      "import java.util.List;",
+      "import java.util.ArrayList;",
+      "",
+      "import static java.nio.charset.StandardCharsets.UTF_8;",
+      "",
+      "import java.util.EnumSet;",
+      "",
+      "class Test ",
+      "extends ArrayList {",
+      "}"
+    };
+    String[] expected = {
+      "package abc;", //
+      "",
+      "import java.util.ArrayList;",
+      "",
+      "class Test extends ArrayList {}",
+      ""
+    };
+
+    // pre-check expectation with local formatter instance
+    String optimized = new Formatter().formatSourceAndFixImports(joiner.join(input));
+    assertThat(optimized).isEqualTo(joiner.join(expected));
+
+    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
+    StringWriter out = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+            in);
+    assertThat(main.format("-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(joiner.join(expected));
+  }
+
+  // test that -lines handling works with import removal
+  @Test
+  public void importRemovalLines() throws Exception {
+    String[] input = {
+      "import java.util.ArrayList;",
+      "import java.util.List;",
+      "class Test {",
+      "ArrayList<String> a = new ArrayList<>();",
+      "ArrayList<String> b = new ArrayList<>();",
+      "}",
+    };
+    String[] expected = {
+      "import java.util.ArrayList;",
+      "",
+      "class Test {",
+      "  ArrayList<String> a = new ArrayList<>();",
+      "ArrayList<String> b = new ArrayList<>();",
+      "}",
+    };
+    StringWriter out = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+            new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
+    assertThat(main.format("-", "-lines", "4")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(joiner.join(expected));
+  }
+
+  // test that errors are reported on the right line when imports are removed
+  @Test
+  public void importRemoveErrorParseError() throws Exception {
+    Locale backupLocale = Locale.getDefault();
+    try {
+      Locale.setDefault(Locale.ROOT);
+
+      String[] input = {
+        "import java.util.ArrayList;", //
+        "import java.util.List;",
+        "class Test {",
+        "}}",
+      };
+      StringWriter out = new StringWriter();
+      StringWriter err = new StringWriter();
+      Main main =
+          new Main(
+              new PrintWriter(out, true),
+              new PrintWriter(err, true),
+              new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
+      assertThat(main.format("-")).isEqualTo(1);
+      assertThat(err.toString()).contains("<stdin>:4:3: error: class, interface");
+
+    } finally {
+      Locale.setDefault(backupLocale);
+    }
+  }
+
+  @Test
+  public void packageInfo() throws Exception {
+    String[] input = {
+      "@CheckReturnValue",
+      "@ParametersAreNonnullByDefault",
+      "package com.google.common.labs.base;",
+      "",
+      "import javax.annotation.CheckReturnValue;",
+      "import javax.annotation.ParametersAreNonnullByDefault;",
+      "",
+    };
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(err, true),
+            new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
+    assertThat(main.format("-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(joiner.join(input));
+  }
+
+  @Test
+  public void newline() throws Exception {
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(err, true),
+            new ByteArrayInputStream("class T {}\n\t".getBytes(UTF_8)));
+    assertThat(main.format("-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo("class T {}\n");
+  }
+
+  @Test
+  public void dryRunStdinUnchanged() throws Exception {
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(err, true),
+            new ByteArrayInputStream("class Test {}\n".getBytes(UTF_8)));
+    assertThat(main.format("-n", "-")).isEqualTo(0);
+    assertThat(out.toString()).isEmpty();
+    assertThat(err.toString()).isEmpty();
+  }
+
+  @Test
+  public void dryRunStdinChanged() throws Exception {
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    String input = "class Test {\n}\n";
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(err, true),
+            new ByteArrayInputStream(input.getBytes(UTF_8)));
+    assertThat(main.format("-n", "-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo("<stdin>" + System.lineSeparator());
+    assertThat(err.toString()).isEmpty();
+  }
+
+  @Test
+  public void dryRunFiles() throws Exception {
+    Path a = testFolder.newFile("A.java").toPath();
+    Path b = testFolder.newFile("B.java").toPath();
+    Path c = testFolder.newFile("C.java").toPath();
+    Files.write(a, "class A {}\n".getBytes(UTF_8));
+    Files.write(b, "class B {\n}\n".getBytes(UTF_8));
+    Files.write(c, "class C {\n}\n".getBytes(UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    int exitCode =
+        main.format(
+            "-n",
+            a.toAbsolutePath().toAbsolutePath().toString(),
+            b.toAbsolutePath().toString(),
+            c.toAbsolutePath().toString());
+
+    assertThat(exitCode).isEqualTo(0);
+
+    assertThat(out.toString())
+        .isEqualTo(
+            b.toAbsolutePath().toString()
+                + System.lineSeparator()
+                + c.toAbsolutePath().toString()
+                + System.lineSeparator());
+    assertThat(err.toString()).isEmpty();
+  }
+
+  @Test
+  public void keepGoingWhenFilesDontExist() throws Exception {
+    Path a = testFolder.newFile("A.java").toPath();
+    Path b = testFolder.newFile("B.java").toPath();
+    File cFile = testFolder.newFile("C.java");
+    Path c = cFile.toPath();
+    cFile.delete();
+
+    Files.write(a, "class A{}\n".getBytes(UTF_8));
+    Files.write(b, "class B{}\n".getBytes(UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+
+    int exitCode =
+        main.format(
+            "",
+            a.toAbsolutePath().toString(),
+            c.toAbsolutePath().toString(),
+            b.toAbsolutePath().toString());
+
+    // Formatter returns failure if a file was not present.
+    assertThat(exitCode).isEqualTo(1);
+
+    // Present files were correctly formatted.
+    assertThat(out.toString()).isEqualTo("class A {}\nclass B {}\n");
+
+    // File not found still showed error.
+    assertThat(err.toString()).isNotEmpty();
+  }
+
+  @Test
+  public void exitIfChangedStdin() throws Exception {
+    Path path = testFolder.newFile("Test.java").toPath();
+    Files.write(path, "class Test {\n}\n".getBytes(UTF_8));
+    Process process =
+        new ProcessBuilder(
+                ImmutableList.of(
+                    Paths.get(System.getProperty("java.home")).resolve("bin/java").toString(),
+                    "-cp",
+                    System.getProperty("java.class.path"),
+                    Main.class.getName(),
+                    "-n",
+                    "--set-exit-if-changed",
+                    "-"))
+            .redirectInput(path.toFile())
+            .redirectError(Redirect.PIPE)
+            .redirectOutput(Redirect.PIPE)
+            .start();
+    process.waitFor();
+    String err = new String(ByteStreams.toByteArray(process.getErrorStream()), UTF_8);
+    String out = new String(ByteStreams.toByteArray(process.getInputStream()), UTF_8);
+    assertThat(err).isEmpty();
+    assertThat(out).isEqualTo("<stdin>" + System.lineSeparator());
+    assertThat(process.exitValue()).isEqualTo(1);
+  }
+
+  @Test
+  public void exitIfChangedFiles() throws Exception {
+    Path path = testFolder.newFile("Test.java").toPath();
+    Files.write(path, "class Test {\n}\n".getBytes(UTF_8));
+    Process process =
+        new ProcessBuilder(
+                ImmutableList.of(
+                    Paths.get(System.getProperty("java.home")).resolve("bin/java").toString(),
+                    "-cp",
+                    System.getProperty("java.class.path"),
+                    Main.class.getName(),
+                    "-n",
+                    "--set-exit-if-changed",
+                    path.toAbsolutePath().toString()))
+            .redirectError(Redirect.PIPE)
+            .redirectOutput(Redirect.PIPE)
+            .start();
+    process.waitFor();
+    String err = new String(ByteStreams.toByteArray(process.getErrorStream()), UTF_8);
+    String out = new String(ByteStreams.toByteArray(process.getInputStream()), UTF_8);
+    assertThat(err).isEmpty();
+    assertThat(out).isEqualTo(path.toAbsolutePath().toString() + System.lineSeparator());
+    assertThat(process.exitValue()).isEqualTo(1);
+  }
+
+  @Test
+  public void assumeFilename_error() throws Exception {
+    String[] input = {
+      "class Test {}}",
+    };
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(err, true),
+            new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
+    assertThat(main.format("--assume-filename=Foo.java", "-")).isEqualTo(1);
+    assertThat(err.toString()).contains("Foo.java:1:15: error: class, interface");
+  }
+
+  @Test
+  public void assumeFilename_dryRun() throws Exception {
+    String[] input = {
+      "class Test {", //
+      "}",
+    };
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(err, true),
+            new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
+    assertThat(main.format("--dry-run", "--assume-filename=Foo.java", "-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo("Foo.java" + System.lineSeparator());
+  }
+
+  @Test
+  public void reflowLongStrings() throws Exception {
+    String[] input = {
+      "class T {", //
+      "  String s = \"one long incredibly unbroken sentence moving from topic to topic so that no"
+          + " one had a chance to interrupt\";",
+      "}"
+    };
+    String[] expected = {
+      "class T {",
+      "  String s =",
+      "      \"one long incredibly unbroken sentence moving from topic to topic so that no one had"
+          + " a\"",
+      "          + \" chance to interrupt\";",
+      "}",
+      "",
+    };
+    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
+    StringWriter out = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+            in);
+    assertThat(main.format("-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(joiner.join(expected));
+  }
+
+  @Test
+  public void noReflowLongStrings() throws Exception {
+    String[] input = {
+      "class T {", //
+      "  String s = \"one long incredibly unbroken sentence moving from topic to topic so that no"
+          + " one had a chance to interrupt\";",
+      "}"
+    };
+    String[] expected = {
+      "class T {",
+      "  String s =",
+      "      \"one long incredibly unbroken sentence moving from topic to topic so that no one had"
+          + " a chance to interrupt\";",
+      "}",
+      "",
+    };
+    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
+    StringWriter out = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+            in);
+    assertThat(main.format("--skip-reflowing-long-strings", "-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(joiner.join(expected));
+  }
+
+  @Test
+  public void noFormatJavadoc() throws Exception {
+    String[] input = {
+      "/**",
+      " * graph",
+      " *",
+      " * graph",
+      " *",
+      " * @param foo lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
+          + " eiusmod tempor incididunt ut labore et dolore magna aliqua",
+      " */",
+      "class Test {",
+      "  /**",
+      "   * creates entropy",
+      "   */",
+      "  public static void main(String... args) {}",
+      "}",
+      "",
+    };
+    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
+    StringWriter out = new StringWriter();
+    Main main =
+        new Main(
+            new PrintWriter(out, true),
+            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
+            in);
+    assertThat(main.format("--skip-javadoc-formatting", "-")).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(joiner.join(input));
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/ModifierOrdererTest.java b/core/src/test/java/com/google/googlejavaformat/java/ModifierOrdererTest.java
new file mode 100644
index 0000000..1f8fc17
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/ModifierOrdererTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Range;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link ModifierOrderer}Test */
+@RunWith(JUnit4.class)
+public class ModifierOrdererTest {
+
+  @Test
+  public void simple() throws FormatterException {
+    assertThat(ModifierOrderer.reorderModifiers("static abstract class InnerClass {}").getText())
+        .isEqualTo("abstract static class InnerClass {}");
+  }
+
+  @Test
+  public void comment() throws FormatterException {
+    assertThat(ModifierOrderer.reorderModifiers("static/*1*/abstract/*2*/public").getText())
+        .isEqualTo("public/*1*/abstract/*2*/static");
+  }
+
+  @Test
+  public void everything() throws FormatterException {
+    assertThat(
+            ModifierOrderer.reorderModifiers(
+                    "strictfp native synchronized volatile transient final static abstract"
+                        + " private protected public")
+                .getText())
+        .isEqualTo(
+            "public protected private abstract static final transient volatile synchronized"
+                + " native strictfp");
+  }
+
+  @Test
+  public void everythingIncludingDefault() throws FormatterException {
+    assertThat(
+            ModifierOrderer.reorderModifiers(
+                    "strictfp native synchronized volatile transient final static default abstract"
+                        + " private protected public")
+                .getText())
+        .isEqualTo(
+            "public protected private abstract default static final transient volatile synchronized"
+                + " native strictfp");
+  }
+
+  @Test
+  public void subRange() throws FormatterException {
+    String[] lines = {
+      "class Test {", //
+      "  static public int a;",
+      "  static public int b;",
+      "}",
+    };
+    String input = Joiner.on('\n').join(lines);
+    String substring = "static public int a";
+    int start = input.indexOf(substring);
+    int end = start + substring.length();
+    String output =
+        ModifierOrderer.reorderModifiers(
+                new JavaInput(input), Arrays.asList(Range.closedOpen(start, end)))
+            .getText();
+    assertThat(output).contains("public static int a;");
+    assertThat(output).contains("static public int b;");
+  }
+
+  @Test
+  public void whitespace() throws FormatterException {
+    String[] lines = {
+      "class Test {", //
+      "  static",
+      "  public int a;",
+      "}",
+    };
+    String input = Joiner.on('\n').join(lines);
+    String substring = "static public int a";
+    int start = input.indexOf(substring);
+    int end = start + substring.length();
+    String output =
+        ModifierOrderer.reorderModifiers(
+                new JavaInput(input), Arrays.asList(Range.closedOpen(start, end)))
+            .getText();
+    assertThat(output).contains("public\n  static int a;");
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java b/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java
new file mode 100644
index 0000000..57d55d7
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java
@@ -0,0 +1,1802 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ObjectArrays;
+import com.google.common.collect.Range;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Tests formatting parts of files. */
+@RunWith(Parameterized.class)
+public final class PartialFormattingTest {
+
+  @Parameters
+  public static Iterable<Object[]> parameters() {
+    return ImmutableList.copyOf(new Object[][] {{"\n"}, {"\r"}, {"\r\n"}});
+  }
+
+  @Rule public TemporaryFolder testFolder = new TemporaryFolder();
+
+  private final String newline;
+
+  public PartialFormattingTest(String newline) {
+    this.newline = newline;
+  }
+
+  String lines(String... args) {
+    return Joiner.on(newline).join(args);
+  }
+
+  @Test
+  public void testGetFormatReplacements0() throws Exception {
+    String input =
+        lines(
+            /* line 0 character  0 */ "class Foo{",
+            /* line 1 character 11 */ "void f",
+            /* line 2 character 18 */ "() {",
+            /* line 3 character 23 */ "}",
+            /* line 4 character 25 */ "}",
+            "");
+    String expectedOutput =
+        lines(
+
+            /* line 0 character  0 */ "class Foo{",
+            /* line 1 character 11 */ "  void f() {}",
+            /* line 2 character 25 */ "}",
+            "");
+    // Claim to have modified the parentheses.
+    int start = input.indexOf("() {");
+    String output = doGetFormatReplacements(input, start, start + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void testGetFormatReplacements1() throws Exception {
+    String input =
+        lines(
+            /* line 0 character  0 */ "class Foo{",
+            /* line 1 character 11 */ "void f",
+            /* line 2 character 18 */ "() {",
+            /* line 3 character 23 */ "}",
+            /* line 4 character 25 */ "}",
+            "");
+    String expectedOutput =
+        lines(
+            /* line 0 character  0 */ "class Foo{",
+            /* line 1 character 11 */ "  void f() {}",
+            /* line 2 character 25 */ "}",
+            "");
+    // Claim to have modified everything after the parentheses.
+    String output = doGetFormatReplacements(input, 20, 21);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void expandToStatement() throws Exception {
+    String input =
+        lines(
+            "class Foo {{",
+            "ImmutableList<Integer> ids = ImmutableList.builder()",
+            ".add(1)",
+            ".add(2)",
+            ".add(3)",
+            ".build();",
+            "}}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo {{",
+            "    ImmutableList<Integer> ids ="
+                + " ImmutableList.builder().add(1).add(2).add(3).build();",
+            "}}",
+            "");
+    int idx = input.indexOf("add(2)");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void expandToMethodSignature() throws Exception {
+    String input =
+        lines(
+            "class Foo {",
+            "void ",
+            " m() ",
+            " {",
+            "ImmutableList<Integer> ids = ImmutableList.builder()",
+            ".add(1)",
+            ".add(2)",
+            ".build();",
+            "}}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo {",
+            "  void m() {",
+            "ImmutableList<Integer> ids = ImmutableList.builder()",
+            ".add(1)",
+            ".add(2)",
+            ".build();",
+            "}}",
+            "");
+    int idx = input.indexOf("void");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void expandToClassSignature() throws Exception {
+    String input =
+        lines(
+            "class",
+            "Foo<XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,"
+                + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,"
+                + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX> {",
+            "void ",
+            " m() ",
+            " {",
+            "}}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo<",
+            "    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,",
+            "    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,",
+            "    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX> {",
+            "void ",
+            " m() ",
+            " {",
+            "}}",
+            "");
+    int idx = input.indexOf("class");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void ignoreNeighbouringStatements() throws Exception {
+    String input =
+        lines(
+            "class Test {",
+            "int ",
+            " xxx ",
+            " = 1;",
+            "int ",
+            " yyy ",
+            " = 1;",
+            "int ",
+            " zzz ",
+            " = 1;",
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Test {",
+            "int ",
+            " xxx ",
+            " = 1;",
+            "  int yyy = 1;",
+            "int ",
+            " zzz ",
+            " = 1;",
+            "}",
+            "");
+    int idx = input.indexOf("yyy");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void insertLeadingNewlines() throws Exception {
+    String input =
+        lines(
+            "class Test { int xxx = 1; int yyy = 1; int zzz = 1; }", //
+            "");
+    String expectedOutput =
+        lines(
+            "class Test { int xxx = 1;", //
+            "  int yyy = 1;",
+            "  int zzz = 1; }",
+            "");
+    int idx = input.indexOf("yyy");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void insertLeadingNewlines2() throws Exception {
+    String input =
+        lines(
+            "class Test { int xxx = 1;", //
+            "",
+            "         int yyy = 1; int zzz = 1; }");
+    String expectedOutput =
+        lines(
+            "class Test { int xxx = 1;", //
+            "", //
+            "  int yyy = 1;",
+            "  int zzz = 1; }");
+    int idx = input.indexOf("yyy");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void insertTrailingNewlines() throws Exception {
+    String input =
+        lines(
+            "class Test { int xxx = 1;", //
+            "  int yyy = 1;      int zzz = 1; }");
+    String expectedOutput =
+        lines(
+            "class Test { int xxx = 1;", //
+            "  int yyy = 1;",
+            "  int zzz = 1; }");
+    int idx = input.indexOf("yyy");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void rejoinMethodSignatureLines() throws Exception {
+    String input =
+        lines(
+            "class Test { void zzz", //
+            "() { int x; } }");
+    String expectedOutput =
+        lines(
+            "class Test {", //
+            "  void zzz() {",
+            "    int x; } }");
+    int idx = input.indexOf("zzz");
+    String output = doGetFormatReplacements(input, idx, idx);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void formatTrailingBrace() throws Exception {
+    String input =
+        lines(
+            "class Test { void f() { return; } }", //
+            "");
+    String expectedOutput =
+        lines(
+            "class Test { void f() { return;", //
+            "  }",
+            "}",
+            "");
+    int idx = input.indexOf("}");
+    String output = doGetFormatReplacements(input, idx, idx);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void formatTrailingBraceEmptyMethodBody() throws Exception {
+    String input =
+        lines(
+            "class Test { void f() {} }", //
+            "");
+    String expectedOutput =
+        lines(
+            "class Test {", //
+            "  void f() {}",
+            "}",
+            "");
+    int idx = input.indexOf("}");
+    String output = doGetFormatReplacements(input, idx, idx);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void formatTrailingBraceEmptyClassBody() throws Exception {
+    String input =
+        lines(
+            "class Test { int x; }", //
+            "");
+    String expectedOutput =
+        lines(
+            "class Test { int x;", //
+            "}",
+            "");
+    int idx = input.indexOf("}");
+    String output = doGetFormatReplacements(input, idx, idx);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void formatTrailingBraceEmptyClassBody2() throws Exception {
+    String input =
+        lines(
+            "class Test {", //
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Test {}", //
+            "");
+    int idx = input.indexOf("}");
+    String output = doGetFormatReplacements(input, idx, idx);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void onlyPackage() throws Exception {
+    String input =
+        lines(
+            "package", //
+            "test",
+            ";",
+            "class Test {}",
+            "");
+    String expectedOutput =
+        lines(
+            "package test;", //
+            "",
+            "class Test {}",
+            "");
+    int idx = input.indexOf("test");
+    String output = doGetFormatReplacements(input, idx, idx);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  private static String doGetFormatReplacements(String input, int characterILo, int characterIHi)
+      throws Exception {
+    return new Formatter()
+        .formatSource(input, ImmutableList.of(Range.closedOpen(characterILo, characterIHi + 1)));
+  }
+
+  @Test
+  public void testLength() throws Exception {
+    String input =
+        lines(
+            "class Foo{", //
+            "int xxx;",
+            "int yyy;",
+            "int zzz;",
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo{", //
+            "int xxx;",
+            "  int yyy;",
+            "int zzz;",
+            "}",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "3", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void testLengthRange() throws Exception {
+    String input =
+        lines(
+            "class Foo{", //
+            "int xxx;",
+            "int yyy;",
+            "int zzz;",
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo{", //
+            "int xxx;",
+            "  int yyy;",
+            "  int zzz;",
+            "}",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "3:4", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void statementAndComments() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {",
+            "{",
+            "// asd",
+            "int x = 1;",
+            "// asd",
+            "int y = 2;",
+            "// asd",
+            "int z = 3;",
+            "// asd",
+            "}",
+            "}",
+            "",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {",
+            "{",
+            "// asd",
+            "int x = 1;",
+            "    // asd",
+            "    int y = 2;",
+            "// asd",
+            "int z = 3;",
+            "// asd",
+            "}",
+            "}",
+            "",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "5", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void statementAndComments2() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {",
+            "{",
+            "// asd",
+            "int x = 1;",
+            "// asd",
+            "int y = 2;",
+            "// asd",
+            "int z = 3;",
+            "// asd",
+            "}",
+            "}",
+            "",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {",
+            "{",
+            "// asd",
+            "int x = 1;",
+            "    // asd",
+            "    int y = 2;",
+            "// asd",
+            "int z = 3;",
+            "// asd",
+            "}",
+            "}",
+            "",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "6", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void statementAndComments3() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {",
+            "{",
+            "// asd",
+            "int x = 1;",
+            "// asd",
+            "int y = 2;",
+            "// asd",
+            "int z = 3;",
+            "// asd",
+            "}",
+            "}",
+            "",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {",
+            "{",
+            "// asd",
+            "int x = 1;",
+            "// asd",
+            "int y = 2;",
+            "    // asd",
+            "    int z = 3;",
+            "// asd",
+            "}",
+            "}",
+            "",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "7", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void blankAndComment() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {",
+            "  public void testListDefinitions() throws Exception {",
+            "    definitionService.insert(createDefinition(1));",
+            "    definitionService.insert(createIncrementalDefinition(2));",
+            "    definitionService.insert(createDefinition(3));",
+            "    definitionService.insert(createIncrementalDefinition(4));",
+            "",
+            "    // No maxResults",
+            "    assertThat(achievementFirstPartyHelper.listDefinitionsByApplication(",
+            "            STUB_GAIA_ID, STUB_APPLICATION_ID, Optional.<Integer>absent(),",
+            "           "
+                + " Optional.<String>absent()).getAchievements()).containsExactly(createExpectedDefinition(1),"
+                + " createIncrementalExpectedDefinition(2), createExpectedDefinition(3),"
+                + " createIncrementalExpectedDefinition(4)).inOrder();",
+            "  }",
+            "}",
+            "",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {",
+            "  public void testListDefinitions() throws Exception {",
+            "    definitionService.insert(createDefinition(1));",
+            "    definitionService.insert(createIncrementalDefinition(2));",
+            "    definitionService.insert(createDefinition(3));",
+            "    definitionService.insert(createIncrementalDefinition(4));",
+            "",
+            "    // No maxResults",
+            "    assertThat(",
+            "            achievementFirstPartyHelper",
+            "                .listDefinitionsByApplication(",
+            "                    STUB_GAIA_ID,",
+            "                    STUB_APPLICATION_ID,",
+            "                    Optional.<Integer>absent(),",
+            "                    Optional.<String>absent())",
+            "                .getAchievements())",
+            "        .containsExactly(",
+            "            createExpectedDefinition(1),",
+            "            createIncrementalExpectedDefinition(2),",
+            "            createExpectedDefinition(3),",
+            "            createIncrementalExpectedDefinition(4))",
+            "        .inOrder();",
+            "  }",
+            "}",
+            "",
+            "");
+
+    String toFormat =
+        lines(
+            "    assertThat(achievementFirstPartyHelper.listDefinitionsByApplication(", //
+            "");
+    int idx = input.indexOf(toFormat);
+    String output = doGetFormatReplacements(input, idx, idx + toFormat.length());
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void emptyFile() throws Exception {
+    new Formatter().formatSource("");
+    new Formatter()
+        .formatSource(
+            lines(
+                "", //
+                ""),
+            ImmutableList.of(Range.closedOpen(0, 1)));
+  }
+
+  @Test
+  public void testGetFormatReplacementRanges() throws Exception {
+    String input =
+        lines(
+            /* line 0 character  0 */ "class Foo{",
+            /* line 1 character 11 */ "void f",
+            /* line 2 character 18 */ "() {",
+            /* line 3 character 23 */ "}",
+            /* line 4 character 25 */ "}",
+            "");
+    // Claim to have modified the parentheses.
+    int start = input.indexOf("() {");
+    ImmutableList<Replacement> ranges =
+        new Formatter()
+            .getFormatReplacements(input, ImmutableList.of(Range.closedOpen(start, start + 1)));
+    assertThat(ranges).hasSize(1);
+    Replacement replacement = ranges.get(0);
+    assertThat(replacement.getReplacementString())
+        .isEqualTo(
+            lines(
+                "", //
+                "  void f() {}"));
+    int replaceFrom = input.indexOf("void f") - newline.length();
+    assertThat(replacement.getReplaceRange().lowerEndpoint()).isEqualTo(replaceFrom);
+  }
+
+  @Test
+  public void noTokensOnLine() throws Exception {
+    String input =
+        lines(
+            "    package com.google.googlejavaformat.java;",
+            "/*",
+            " * Copyright 2015 Google Inc.",
+            " *",
+            " * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not",
+            " * in compliance with the License. You may obtain a copy of the License at",
+            " *",
+            " *     http://www.apache.org/licenses/LICENSE-2.0",
+            " *",
+            " * Unless required by applicable law or agreed to in writing, software distribute",
+            " * is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY",
+            " * or implied. See the License for the specific language governing permissions an",
+            " * the License.",
+            " */",
+            "",
+            "import com.google.googlejavaformat.FormatterDiagnostic;",
+            "import java.util.List;",
+            "",
+            "/** Checked exception class for formatter errors. */",
+            "public final class FormatterException extends Exception {",
+            "",
+            "  FormatterException(String message) {",
+            "    super(message);",
+            "  }",
+            "",
+            "  /**",
+            "   * @param errors",
+            "   */",
+            "  public FormatterException(List<FormatterDiagnostic> errors) {",
+            "    // TODO(cushon): Auto-generated constructor stub",
+            "  }",
+            "}");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("FormatterException.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "3:4", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(input);
+  }
+
+  @Test
+  public void nestedStatement1() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {{",
+            "int x = ",
+            " 1;",
+            "int y = new Runnable() {",
+            "  void run() {",
+            "    System.err.println(42);",
+            "  }",
+            "};",
+            "int z = ",
+            " 1;",
+            "}}",
+            "",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {{",
+            "int x = ",
+            " 1;",
+            "    int y =",
+            "        new Runnable() {",
+            "          void run() {",
+            "            System.err.println(42);",
+            "          }",
+            "        };",
+            "int z = ",
+            " 1;",
+            "}}",
+            "",
+            "");
+
+    String toFormat = "Runnable";
+    int idx = input.indexOf(toFormat);
+    String output = doGetFormatReplacements(input, idx, idx + toFormat.length());
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void nestedStatement2() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {",
+            "int x = ",
+            " 1;",
+            "int y = new Runnable() {",
+            "  void run() {",
+            "    System.err.println(42);",
+            "  }",
+            "};",
+            "int z = ",
+            " 1;",
+            "}",
+            "",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {",
+            "int x = ",
+            " 1;",
+            "  int y =",
+            "      new Runnable() {",
+            "        void run() {",
+            "          System.err.println(42);",
+            "        }",
+            "      };",
+            "int z = ",
+            " 1;",
+            "}",
+            "",
+            "");
+
+    String toFormat = "Runnable";
+    int idx = input.indexOf(toFormat);
+    String output = doGetFormatReplacements(input, idx, idx + toFormat.length());
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void blankLine() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {", //
+            "int x = 1;",
+            "",
+            "int y = 1;",
+            "}",
+            "",
+            "");
+    String expectedOutput = input;
+
+    testFormatLine(input, expectedOutput, 3);
+  }
+
+  @Test
+  public void lineWithIdentifier() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {", //
+            "int",
+            "y",
+            "= 1;",
+            "}",
+            "",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {", //
+            "  int y = 1;",
+            "}",
+            "",
+            "");
+
+    testFormatLine(input, expectedOutput, 3);
+  }
+
+  // formatted region expands to include entire comment
+  @Test
+  public void lineInsideComment() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {",
+            "/* This is a",
+            "            poorly indented",
+            "                       comment*/",
+            "int x;",
+            "}",
+            "",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {",
+            "  /* This is a",
+            "  poorly indented",
+            "             comment*/",
+            "  int x;",
+            "}",
+            "",
+            "");
+
+    testFormatLine(input, expectedOutput, 3);
+  }
+
+  @Test
+  public void testReplacementsSorted() throws Exception {
+    String input =
+        lines(
+            "class Test {",
+            "int a = 1;",
+            "int b = 2;",
+            "int c = 3;",
+            "int d = 4;",
+            "int e = 5;",
+            "}");
+    List<Range<Integer>> ranges = new ArrayList<>();
+    for (int i = 1; i <= 5; i += 2) {
+      int idx = input.indexOf(String.valueOf(i));
+      ranges.add(Range.closedOpen(idx, idx + 1));
+    }
+
+    ImmutableList<Replacement> replacements = new Formatter().getFormatReplacements(input, ranges);
+
+    // expect replacements in ascending order, by start position
+    List<Integer> startPositions = new ArrayList<>();
+    for (Replacement replacement : replacements) {
+      startPositions.add(replacement.getReplaceRange().lowerEndpoint());
+    }
+    assertThat(startPositions).hasSize(3);
+    assertThat(startPositions).isInStrictOrder();
+  }
+
+  @Test
+  public void testReplacementsSorted_DescendingInput() throws Exception {
+    String input =
+        lines(
+            "class Test {",
+            "int a = 1;",
+            "int b = 2;",
+            "int c = 3;",
+            "int d = 4;",
+            "int e = 5;",
+            "}");
+    List<Range<Integer>> ranges = new ArrayList<>();
+    for (int i = 5; i >= 1; i -= 2) {
+      int idx = input.indexOf(String.valueOf(i));
+      ranges.add(Range.closedOpen(idx, idx + 1));
+    }
+
+    ImmutableList<Replacement> replacements = new Formatter().getFormatReplacements(input, ranges);
+
+    // expect replacements in ascending order, by start position
+    List<Integer> startPositions = new ArrayList<>();
+    for (Replacement replacement : replacements) {
+      startPositions.add(replacement.getReplaceRange().lowerEndpoint());
+    }
+    assertThat(startPositions).hasSize(3);
+    assertThat(startPositions).isInStrictOrder();
+  }
+
+  private void testFormatLine(String input, String expectedOutput, int i) throws Exception {
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", Integer.toString(i), path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void lineWithTrailingComment() throws Exception {
+    String input =
+        lines(
+            "class Foo{", //
+            "int xxx; // asd",
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo{", //
+            "  int xxx; // asd",
+            "}",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "2", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  // Nested statements are OK as long as they're nested inside "block-like" constructs.
+  @Test
+  public void nestedStatement_allowPartial() throws Exception {
+    String input =
+        lines(
+            "public class MyTest {{",
+            "if (true) {",
+            "if (true) {",
+            "System.err.println(\"Hello\");",
+            "} else {",
+            "System.err.println(\"Goodbye\");",
+            "}",
+            "}",
+            "}}",
+            "");
+    String expectedOutput =
+        lines(
+            "public class MyTest {{",
+            "if (true) {",
+            "if (true) {",
+            "        System.err.println(\"Hello\");",
+            "} else {",
+            "System.err.println(\"Goodbye\");",
+            "}",
+            "}",
+            "}}",
+            "");
+
+    String toFormat = "Hello";
+    int idx = input.indexOf(toFormat);
+    String output = doGetFormatReplacements(input, idx, idx + toFormat.length());
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  // regression test for b/b22196513
+  @Test
+  public void noTrailingWhitespace() throws Exception {
+    String input =
+        lines(
+            "", //
+            "class Test {",
+            "  {",
+            "    {",
+            "      {",
+            "      }",
+            "    }",
+            "}}",
+            "");
+    String expected =
+        lines(
+            "", //
+            "class Test {",
+            "  {",
+            "    {",
+            "      {",
+            "      }",
+            "    }",
+            "  }",
+            "}",
+            "");
+    int start = input.indexOf(newline + "}}");
+    ImmutableList<Range<Integer>> ranges =
+        ImmutableList.of(Range.closedOpen(start, start + newline.length() + 2));
+    String output = new Formatter().formatSource(input, ranges);
+    assertEquals("bad output", expected, output);
+  }
+
+  // regression test for b/b22196513
+  @Test
+  public void trailingNonBreakingWhitespace() throws Exception {
+    String input =
+        lines(
+            "", //
+            "class Test {",
+            "  {",
+            "    int x;int y;",
+            "  }",
+            "}",
+            "");
+    String expected =
+        lines(
+            "", //
+            "class Test {",
+            "  {",
+            "    int x;",
+            "    int y;",
+            "  }",
+            "}",
+            "");
+    String match = "int x;";
+    int start = input.indexOf(match);
+    int end = start + match.length();
+    ImmutableList<Range<Integer>> ranges = ImmutableList.of(Range.closedOpen(start, end));
+    String output = new Formatter().formatSource(input, ranges);
+    assertEquals("bad output", expected, output);
+  }
+
+  @Test
+  public void outOfRangeStartLine() throws Exception {
+    String input =
+        lines(
+            "class Foo {", //
+            "int x = 1;",
+            "}");
+    String expectedOutput =
+        lines(
+            "class Foo {", //
+            "  int x = 1;",
+            "}",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "-1:3", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void outOfRangeEndLine() throws Exception {
+    String input =
+        lines(
+            "class Foo {", //
+            "int x = 1;",
+            "}");
+    String expectedOutput =
+        lines(
+            "class Foo {", //
+            "  int x = 1;",
+            "}",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "1:5", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void testOutOfRangeLines() throws Exception {
+    String input =
+        lines(
+            "class Foo {", //
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo {}", //
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines=23:27", "-lines=31:35", "-lines=52:63", "-lines=1:1", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void testEmptyFirstLine() throws Exception {
+    String input =
+        lines(
+            "", //
+            "",
+            "class Foo {",
+            "}",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines=1:1", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+  }
+
+  @Test
+  public void testEmptyLastLine() throws Exception {
+    String input =
+        lines(
+            "class Foo {", //
+            "}",
+            "",
+            "");
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines=5:5", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+  }
+
+  // Regression test for b/22872933
+  // Don't extend partial formatting ranges across switch cases.
+  @Test
+  public void switchCase() throws Exception {
+    String input =
+        lines(
+            "class Test {",
+            "  {",
+            "    switch (foo) {",
+            "      case FOO:",
+            "      f();",
+            "      break;",
+            "      case BAR:",
+            "      g();",
+            "      break;",
+            "    }",
+            "  }",
+            "}");
+    String expectedOutput =
+        lines(
+            "class Test {",
+            "  {",
+            "    switch (foo) {",
+            "      case FOO:",
+            "        f();",
+            "      break;",
+            "      case BAR:", // we deliberately only format the first case
+            "      g();",
+            "      break;",
+            "    }",
+            "  }",
+            "}");
+
+    int idx = input.indexOf("f()");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  // regression test for b/23349153
+  @Test
+  public void emptyStatement() throws Exception {
+    String input =
+        lines(
+            "class Test {{", //
+            "Object o = f();;",
+            "}}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Test {{", //
+            "    Object o = f();",
+            "    ;",
+            "}}",
+            "");
+    int idx = input.indexOf("Object o");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void preserveTrailingWhitespaceAfterNewline() throws Exception {
+    String input =
+        lines(
+            "class Test {{", //
+            "Object o = f();       ",
+            "            int x;",
+            "}}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Test {{", //
+            "    Object o = f();",
+            "            int x;",
+            "}}",
+            "");
+    int idx = input.indexOf("Object o");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void trailingWhitespace() throws Exception {
+    String input =
+        lines(
+            "class Test {{", //
+            "Object o = f();       ",
+            "            ;",
+            "}}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Test {{", //
+            "    Object o = f();",
+            "            ;",
+            "}}",
+            "");
+    int idx = input.indexOf("Object o");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  // Regression test for b/18479811
+  @Test
+  public void onNewline() throws Exception {
+
+    String line1 = "for (Integer x : Arrays.asList(1, 2, 3)) {";
+    String line2 = "System.err.println(x);";
+    String input =
+        lines(
+            "class Test {{", //
+            line1,
+            line2,
+            "}}}",
+            "");
+
+    int startOffset = input.indexOf(line1);
+    int length = 1;
+
+    String expectedFormatLine1 =
+        lines(
+            "class Test {{",
+            "    for (Integer x : Arrays.asList(1, 2, 3)) {",
+            "System.err.println(x);",
+            "}}}",
+            "");
+
+    for (; length <= line1.length() + newline.length(); length++) {
+      Range<Integer> range = Range.closedOpen(startOffset, startOffset + length);
+      String output = new Formatter().formatSource(input, ImmutableList.of(range));
+      assertEquals("bad output", expectedFormatLine1, output);
+    }
+
+    String expectedFormatLine1And2 =
+        lines(
+            "class Test {{",
+            "    for (Integer x : Arrays.asList(1, 2, 3)) {",
+            "      System.err.println(x);",
+            "}}}",
+            "");
+
+    for (; length <= line1.length() + line2.length() + 2 * newline.length(); length++) {
+      Range<Integer> range = Range.closedOpen(startOffset, startOffset + length);
+      String output = new Formatter().formatSource(input, ImmutableList.of(range));
+      assertEquals("bad output", expectedFormatLine1And2, output);
+    }
+  }
+
+  @Test
+  public void afterNewline() throws Exception {
+
+    String line1 = "for (Integer x : Arrays.asList(1, 2, 3)) {";
+    String line2 = "                  System.err.println(x);";
+    String input =
+        lines(
+            "class Test {{", //
+            line1,
+            line2,
+            "}}}",
+            "");
+
+    String expectedFormatLine1 =
+        lines(
+            "class Test {{", //
+            "    for (Integer x : Arrays.asList(1, 2, 3)) {", //
+            line2,
+            "}}}",
+            "");
+
+    String expectedFormatLine2 =
+        lines(
+            "class Test {{", //
+            line1,
+            "      System.err.println(x);",
+            "}}}",
+            "");
+
+    int line2Start = input.indexOf(line2);
+    int nonWhitespaceLine2Start = input.indexOf("System.err");
+    int start;
+    // formatting a range that touches non-whitespace characters in line2 should format line2
+    for (start = nonWhitespaceLine2Start; start > (line2Start - newline.length()); start--) {
+      Range<Integer> range = Range.closedOpen(start, nonWhitespaceLine2Start + newline.length());
+      String output = new Formatter().formatSource(input, ImmutableList.of(range));
+      assertThat(output).isEqualTo(expectedFormatLine2);
+    }
+    // formatting a range that touches whitespace characters between line1 and line2 should
+    // not result in any formatting
+    assertThat(input.substring(start, start + newline.length())).isEqualTo(newline);
+    int line1End = input.indexOf(line1) + line1.length();
+    for (; start >= line1End; start--) {
+      Range<Integer> range = Range.closedOpen(start, line2Start);
+      String output = new Formatter().formatSource(input, ImmutableList.of(range));
+      assertThat(output).isEqualTo(input);
+    }
+    // formatting a range that touches non-whitespace characters in line1 should format line1
+    assertThat(input.substring(start + 1, start + 1 + newline.length())).isEqualTo(newline);
+    int line1Start = input.indexOf(line1);
+    for (; start >= line1Start; start--) {
+      Range<Integer> range = Range.closedOpen(start, line2Start);
+      String output = new Formatter().formatSource(input, ImmutableList.of(range));
+      assertThat(output).isEqualTo(expectedFormatLine1);
+    }
+  }
+
+  @Test
+  public void commentBeforeBadConstructor() throws Exception {
+    String[] lines = {
+      "class D {", //
+      "  /** */",
+      "  F() {}",
+      "}",
+    };
+    String output = new Formatter().formatSource(lines(lines));
+    String[] expected = {
+      "class D {", //
+      "  /** */",
+      "  F() {}",
+      "}",
+      "",
+    };
+    assertThat(output).isEqualTo(lines(expected));
+  }
+
+  @Test
+  public void partialEnum() throws Exception {
+    String[] input = {
+      "enum E {", //
+      "ONE,",
+      "TWO,",
+      "THREE;",
+      "}",
+    };
+    String[] expected = {
+      "enum E {", //
+      "ONE,",
+      "  TWO,",
+      "THREE;",
+      "}",
+    };
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, lines(input).getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "3", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(lines(expected));
+  }
+
+  @Test
+  public void partialModifierOrder() throws Exception {
+    String[] input = {
+      "class T {", //
+      "final private int a = 0;",
+      "final private int b = 0;",
+      "final private int c = 0;",
+      "}",
+    };
+    String[] expected = {
+      "class T {", //
+      "final private int a = 0;",
+      "  private final int b = 0;",
+      "final private int c = 0;",
+      "}",
+    };
+
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, lines(input).getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    String[] args = {"-lines", "3", path.toString()};
+    assertThat(main.format(args)).isEqualTo(0);
+    assertThat(out.toString()).isEqualTo(lines(expected));
+  }
+
+  @Test
+  public void endOfLine() throws Exception {
+    String[] input = {
+      "class foo {",
+      "  foo(",
+      "      int aaaaaaaaaaaaaaa,",
+      "      int ccccccccccccc) {",
+      "    int a = 0;",
+      "    int c = 0;",
+      "  }",
+      "}",
+    };
+    String[] expected = {
+      "class foo {",
+      "  foo(int aaaaaaaaaaaaaaa, int ccccccccccccc) {",
+      "    int a = 0;",
+      "    int c = 0;",
+      "  }",
+      "}",
+    };
+    String in = lines(input);
+    // request partial formatting of the end of the first parameter
+    int start = in.indexOf(lines(",", "      int ccccccccccccc"));
+    assertThat(in.substring(start, start + 1)).isEqualTo(",");
+
+    assertThat(new Formatter().formatSource(in, ImmutableList.of(Range.closedOpen(start, start))))
+        .isEqualTo(lines(expected));
+
+    assertThat(formatMain(lines(input), "-offset", String.valueOf(start), "-length", "0"))
+        .isEqualTo(lines(expected));
+  }
+
+  private String formatMain(String input, String... args) throws Exception {
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Test.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    assertThat(main.format(ObjectArrays.concat(args, path.toString()))).isEqualTo(0);
+    return out.toString();
+  }
+
+  // formatting the newlinea after a statement is a no-op
+  @Test
+  public void endOfLineStatement() throws Exception {
+    String[] input = {
+      "class foo {{", //
+      "  int a = 0; ",
+      "  int c = 0;",
+      "}}",
+    };
+    String[] expected = {
+      "class foo {{", //
+      "    int a = 0;",
+      "  int c = 0;",
+      "}}",
+    };
+    String in = lines(input);
+    int idx = in.indexOf(';');
+    assertThat(new Formatter().formatSource(in, ImmutableList.of(Range.closedOpen(idx, idx))))
+        .isEqualTo(lines(expected));
+  }
+
+  // formatting trailing whitespace at the end of the line doesn't format the line on either side
+  @Test
+  public void endOfLineStatementNewline() throws Exception {
+    String[] input = {
+      "class foo {{", //
+      "  int a = 0; ",
+      "  int c = 0;",
+      "}}",
+    };
+    String in = lines(input);
+    int idx = in.indexOf(';');
+    assertThat(
+            new Formatter().formatSource(in, ImmutableList.of(Range.closedOpen(idx + 1, idx + 1))))
+        .isEqualTo(in);
+  }
+
+  @Test
+  public void importNewlines() throws Exception {
+    String input =
+        lines(
+            "package p;",
+            "import java.util.ArrayList;",
+            "class Foo {",
+            "  ArrayList<String> xs = new ArrayList<>();",
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "package p;",
+            "",
+            "import java.util.ArrayList;",
+            "",
+            "class Foo {",
+            "  ArrayList<String> xs = new ArrayList<>();",
+            "}",
+            "");
+
+    String output = runFormatter(input, new String[] {"-lines", "2"});
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void b36458607() throws Exception {
+    String input =
+        lines(
+            "// copyright",
+            "",
+            "package p;",
+            "import static c.g.I.c;",
+            "",
+            "/** */",
+            "class Foo {{ c(); }}",
+            "");
+    String expectedOutput =
+        lines(
+            "// copyright",
+            "",
+            "package p;",
+            "",
+            "import static c.g.I.c;",
+            "",
+            "/** */",
+            "class Foo {{ c(); }}",
+            "");
+
+    String output = runFormatter(input, new String[] {"-lines", "4"});
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void b32159971() throws Exception {
+    String input =
+        lines(
+            "", //
+            "",
+            "package p;",
+            "class X {}",
+            "");
+    String expectedOutput =
+        lines(
+            "package p;", //
+            "",
+            "class X {}",
+            "");
+
+    String output = runFormatter(input, new String[] {"-lines", "3"});
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void b21668189() throws Exception {
+    String input =
+        lines(
+            "class Foo {", //
+            "  {",
+            "    int x = 1;",
+            "    ",
+            "    int y = 2;",
+            "  }",
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo {", //
+            "  {",
+            "    int x = 1;",
+            "",
+            "    int y = 2;",
+            "  }",
+            "}",
+            "");
+
+    String output = runFormatter(input, new String[] {"-lines", "4:5"});
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  private String runFormatter(String input, String[] args) throws IOException, UsageException {
+    Path tmpdir = testFolder.newFolder().toPath();
+    Path path = tmpdir.resolve("Foo.java");
+    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
+    assertThat(main.format(ObjectArrays.concat(args, path.toString()))).isEqualTo(0);
+    return out.toString();
+  }
+
+  @Test
+  public void trailing() throws Exception {
+    String input =
+        lines(
+            "package foo.bar.baz;",
+            "",
+            "public class B {",
+            "  public void f() {",
+            "    int a = 7 +4;",
+            "    int b = 7 +4;",
+            "    int c = 7 +4;",
+            "    int d = 7 +4;",
+            "",
+            "    int e = 7 +4;",
+            "  }",
+            "}");
+    String expected =
+        lines(
+            "package foo.bar.baz;",
+            "",
+            "public class B {",
+            "  public void f() {",
+            "    int a = 7 +4;",
+            "    int b = 7 +4;",
+            "    int c = 7 + 4;",
+            "    int d = 7 +4;",
+            "",
+            "    int e = 7 +4;",
+            "  }",
+            "}");
+    String actual =
+        new Formatter().formatSource(input, ImmutableList.of(rangeOf(input, "int c = 7 +4")));
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  private Range<Integer> rangeOf(String input, String needle) {
+    int idx = input.indexOf(needle);
+    return Range.closedOpen(idx, idx + needle.length());
+  }
+
+  @Test
+  public void importJavadocNewlines() throws Exception {
+    String input =
+        lines(
+            "package p;",
+            "import java.util.ArrayList;",
+            "/** */",
+            "class Foo {",
+            "  ArrayList<String> xs = new ArrayList<>();",
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "package p;",
+            "",
+            "import java.util.ArrayList;",
+            "",
+            "/** */",
+            "class Foo {",
+            "  ArrayList<String> xs = new ArrayList<>();",
+            "}",
+            "");
+
+    String output = runFormatter(input, new String[] {"-lines", "2"});
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void nestedSwitchCase() throws Exception {
+    String input =
+        lines(
+            "class Test {",
+            "  {",
+            "    switch (foo) {",
+            "      case FOO:",
+            "      f();",
+            "      break;",
+            "      case BAR:",
+            "      switch (bar) {",
+            "        case BAZ:",
+            "        h();",
+            "        break;",
+            "        case BOZ:",
+            "        i();",
+            "        break;",
+            "      }",
+            "    }",
+            "  }",
+            "}");
+    String expectedOutput =
+        lines(
+            "class Test {",
+            "  {",
+            "    switch (foo) {",
+            "      case FOO:",
+            "      f();",
+            "      break;",
+            "      case BAR:",
+            "      switch (bar) {",
+            "        case BAZ:",
+            "            h();",
+            "        break;",
+            "        case BOZ:",
+            "        i();",
+            "        break;",
+            "      }",
+            "    }",
+            "  }",
+            "}");
+
+    int idx = input.indexOf("h()");
+    String output = doGetFormatReplacements(input, idx, idx + 1);
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+
+  @Test
+  public void b117602702() throws Exception {
+    String input =
+        lines(
+            "class Foo {", //
+            "private Foo () {};",
+            "}",
+            "");
+    String expectedOutput =
+        lines(
+            "class Foo {", //
+            "  private Foo() {}",
+            "  ;",
+            "}",
+            "");
+
+    String output = runFormatter(input, new String[] {"-offset", "13", "-length", "1"});
+    assertThat(output).isEqualTo(expectedOutput);
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java b/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java
new file mode 100644
index 0000000..1965feb
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.googlejavaformat.java.RemoveUnusedImports.removeUnusedImports;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** {@link RemoveUnusedImports}Test */
+@RunWith(Parameterized.class)
+public class RemoveUnusedImportsTest {
+  @Parameters(name = "{index}: {0}")
+  public static Collection<Object[]> parameters() {
+    String[][][] inputsOutputs = {
+      {
+        {
+          "import java.util.List;",
+          "import java.util.ArrayList;",
+          "",
+          "class Test {",
+          "  /** could be an {@link ArrayList} */",
+          "  List<String> xs;",
+          "}",
+        },
+        {
+          "import java.util.List;",
+          "import java.util.ArrayList;",
+          "",
+          "class Test {",
+          "  /** could be an {@link ArrayList} */",
+          "  List<String> xs;",
+          "}",
+        },
+      },
+      {
+        {
+          "import java.util.ArrayList;", //
+          "import java.util.Collection;",
+          "/** {@link ArrayList#add} {@link Collection#remove(Object)} */",
+          "class Test {}",
+        },
+        {
+          "import java.util.ArrayList;", //
+          "import java.util.Collection;",
+          "/** {@link ArrayList#add} {@link Collection#remove(Object)} */",
+          "class Test {}",
+        },
+      },
+      {
+        {
+          "import a.A;",
+          "import a.B;",
+          "import a.C;",
+          "class Test {",
+          "  /** a",
+          "   * {@link A} */",
+          "  void f() {}",
+          "}",
+        },
+        {
+          "import a.A;", //
+          "class Test {",
+          "  /** a",
+          "   * {@link A} */",
+          "  void f() {}",
+          "}",
+        },
+      },
+      {
+        {
+          "import a.A;import a.B;", //
+          "import a.C; // hello",
+          "class Test {",
+          "  B b;",
+          "}",
+        },
+        {
+          "import a.B;", //
+          "// hello",
+          "class Test {",
+          "  B b;",
+          "}",
+        },
+      },
+      {
+        {
+          "import a.A;",
+          "import b.B;",
+          "import c.C;",
+          "import d.D;",
+          "import e.E;",
+          "import f.F;",
+          "import g.G;",
+          "import h.H;",
+          "/**",
+          " * {@link A} {@linkplain B} {@value D#FOO}",
+          " *",
+          " * @exception E",
+          " * @throws F",
+          " * @see C",
+          " * @see H#foo",
+          " * @see <a href=\"whatever\">",
+          " */",
+          "class Test {",
+          "}",
+        },
+        {
+          "import a.A;",
+          "import b.B;",
+          "import c.C;",
+          "import d.D;",
+          "import e.E;",
+          "import f.F;",
+          "import h.H;",
+          "/**",
+          " * {@link A} {@linkplain B} {@value D#FOO}",
+          " *",
+          " * @exception E",
+          " * @throws F",
+          " * @see C",
+          " * @see H#foo",
+          " * @see <a href=\"whatever\">",
+          " */",
+          "class Test {",
+          "}",
+        },
+      },
+      {
+        {
+          "import java.util.Map;",
+          "/** {@link Map.Entry#containsKey(Object)} } */",
+          "class Test {}",
+        },
+        {
+          "import java.util.Map;",
+          "/** {@link Map.Entry#containsKey(Object)} } */",
+          "class Test {}",
+        },
+      },
+      {
+        {
+          "/** {@link #containsKey(Object)} } */", //
+          "class Test {}",
+        },
+        {
+          "/** {@link #containsKey(Object)} } */", //
+          "class Test {}",
+        },
+      },
+      {
+        {
+          "import java.util.*;", //
+          "class Test {",
+          "  List<String> xs;",
+          "}",
+        },
+        {
+          "import java.util.*;", //
+          "class Test {",
+          "  List<String> xs;",
+          "}",
+        },
+      },
+      {
+        {
+          "package com.foo;",
+          "import static com.foo.Outer.A;",
+          "import com.foo.*;",
+          "import com.foo.B;",
+          "import com.bar.C;",
+          "class Test {",
+          "  A a;",
+          "  B b;",
+          "  C c;",
+          "}",
+        },
+        {
+          "package com.foo;",
+          "import static com.foo.Outer.A;",
+          "import com.bar.C;",
+          "class Test {",
+          "  A a;",
+          "  B b;",
+          "  C c;",
+          "}",
+        }
+      },
+      {
+        {
+          "import java.util.Map;", //
+          "import java.util.Map.Entry;",
+          "/** {@link #foo(Map.Entry[])} */",
+          "public class Test {}",
+        },
+        {
+          "import java.util.Map;", //
+          "/** {@link #foo(Map.Entry[])} */",
+          "public class Test {}",
+        },
+      },
+      {
+        {
+          "import java.util.List;",
+          "import java.util.Collection;",
+          "/** {@link java.util.List#containsAll(Collection)} */",
+          "public class Test {}",
+        },
+        {
+          "import java.util.Collection;",
+          "/** {@link java.util.List#containsAll(Collection)} */",
+          "public class Test {}",
+        },
+      },
+      {
+        {
+          "package p;",
+          "import java.lang.Foo;",
+          "import java.lang.Foo.Bar;",
+          "import p.Baz;",
+          "import p.Baz.Bork;",
+          "public class Test implements Foo, Bar, Baz, Bork {}",
+        },
+        {
+          "package p;",
+          "import java.lang.Foo.Bar;",
+          "import p.Baz.Bork;",
+          "public class Test implements Foo, Bar, Baz, Bork {}",
+        },
+      },
+      {
+        {
+          "import java.lang.Foo;", //
+          "interface Test { private static void foo() {} }",
+        },
+        {
+          "interface Test { private static void foo() {} }",
+        },
+      },
+    };
+    ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
+    for (String[][] inputAndOutput : inputsOutputs) {
+      assertThat(inputAndOutput.length).isEqualTo(2);
+      String[] input = inputAndOutput[0];
+      String[] output = inputAndOutput[1];
+      String[] parameters = {
+        Joiner.on('\n').join(input) + '\n', Joiner.on('\n').join(output) + '\n',
+      };
+      builder.add(parameters);
+    }
+    return builder.build();
+  }
+
+  private final String input;
+  private final String expected;
+
+  public RemoveUnusedImportsTest(String input, String expected) {
+    this.input = input;
+    this.expected = expected;
+  }
+
+  @Test
+  public void removeUnused() throws FormatterException {
+    assertThat(removeUnusedImports(input)).isEqualTo(expected);
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/SnippetFormatterTest.java b/core/src/test/java/com/google/googlejavaformat/java/SnippetFormatterTest.java
new file mode 100644
index 0000000..f763dd5
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/SnippetFormatterTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Range;
+import com.google.googlejavaformat.java.SnippetFormatter.SnippetKind;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link SnippetFormatter}Test */
+@RunWith(JUnit4.class)
+public class SnippetFormatterTest {
+  @Test
+  public void expression() throws FormatterException {
+    String input = "x\n=42";
+    List<Replacement> replacements =
+        new SnippetFormatter()
+            .format(
+                SnippetKind.EXPRESSION,
+                input,
+                ImmutableList.of(Range.closedOpen(0, input.length())),
+                4,
+                false);
+    assertThat(replacements)
+        .containsExactly(Replacement.create(1, 2, " "), Replacement.create(3, 3, " "));
+  }
+
+  @Test
+  public void statement() throws FormatterException {
+    String input = "int x\n=42;";
+    List<Replacement> replacements =
+        new SnippetFormatter()
+            .format(
+                SnippetKind.STATEMENTS,
+                input,
+                ImmutableList.of(Range.closedOpen(0, input.length())),
+                4,
+                false);
+    assertThat(replacements)
+        .containsExactly(Replacement.create(5, 6, " "), Replacement.create(7, 7, " "));
+  }
+
+  @Test
+  public void classMember() throws FormatterException {
+    String input = "void f() {\n}";
+    List<Replacement> replacements =
+        new SnippetFormatter()
+            .format(
+                SnippetKind.CLASS_BODY_DECLARATIONS,
+                input,
+                ImmutableList.of(Range.closedOpen(0, input.length())),
+                4,
+                false);
+    assertThat(replacements).containsExactly(Replacement.create(10, 11, ""));
+  }
+
+  @Test
+  public void compilation() throws FormatterException {
+    String input = "/** a\nb*/\nclass Test {\n}";
+    List<Replacement> replacements =
+        new SnippetFormatter()
+            .format(
+                SnippetKind.COMPILATION_UNIT,
+                input,
+                ImmutableList.of(Range.closedOpen(input.indexOf("class"), input.length())),
+                4,
+                false);
+    assertThat(replacements).containsExactly(Replacement.create(22, 23, ""));
+  }
+
+  @Test
+  public void compilationWithComments() throws FormatterException {
+    String input = "/** a\nb*/\nclass Test {\n}";
+    List<Replacement> replacements =
+        new SnippetFormatter()
+            .format(
+                SnippetKind.COMPILATION_UNIT,
+                input,
+                ImmutableList.of(Range.closedOpen(0, input.length())),
+                4,
+                true);
+    assertThat(replacements)
+        .containsExactly(Replacement.create(0, 24, "/** a b */\nclass Test {}\n"));
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java
new file mode 100644
index 0000000..89c94ea
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** {@link StringWrapper}IntegrationTest */
+@RunWith(Parameterized.class)
+public class StringWrapperIntegrationTest {
+
+  @Parameters
+  public static Collection<Object[]> parameters() {
+    String[][][] inputsAndOutputs = {
+      {
+        {
+          "class T {", //
+          "  String s =",
+          "      \"one long incredibly unbroken sentence\"",
+          "          + \" moving from topic to topic\"",
+          "          + \" so that no-one had a chance to\"",
+          "          + \" interrupt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"one long incredibly unbroken\"",
+          "          + \" sentence moving from\"",
+          "          + \" topic to topic so that\"",
+          "          + \" no-one had a chance to\"",
+          "          + \" interrupt\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s =",
+          "      \"one long incredibly unbroken\"",
+          "          + \" sentence moving from topic to topic so that\"",
+          "          + \" no-one had a chance to\"",
+          "          + \" interrupt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"one long incredibly unbroken\"",
+          "          + \" sentence moving from\"",
+          "          + \" topic to topic so that\"",
+          "          + \" no-one had a chance to\"",
+          "          + \" interrupt\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s =",
+          "      \"one long incredibly unbroken\"",
+          "          + \" sentence moving from topic to topic\"",
+          "          + \" so that no-one had a chance to interr\"",
+          "          + \"upt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"one long incredibly unbroken\"",
+          "          + \" sentence moving from\"",
+          "          + \" topic to topic so that\"",
+          "          + \" no-one had a chance to\"",
+          "          + \" interrupt\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s = \"one long incredibly unbroken sentence moving from topic to topic so that"
+              + " no-one had a chance to interrupt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"one long incredibly unbroken\"",
+          "          + \" sentence moving from\"",
+          "          + \" topic to topic so that\"",
+          "          + \" no-one had a chance to\"",
+          "          + \" interrupt\";",
+          "}",
+        },
+      },
+      {
+        {
+          "class T {", //
+          "  String s =",
+          "      \"one long incredibly unbroken sentence\"",
+          "          + \" moving from topic to topic\"",
+          "          + 42",
+          "          + \" so that no-one had a chance to interr\"",
+          "          + \"upt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"one long incredibly unbroken\"",
+          "          + \" sentence moving from\"",
+          "          + \" topic to topic\"",
+          "          + 42",
+          "          + \" so that no-one had a\"",
+          "          + \" chance to interrupt\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s ="
+              + " \"onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehadachanceto"
+              + " interrupt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "     "
+              + " \"onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehadachanceto\"",
+          "          + \" interrupt\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s = \"\\n\\none\\nlong\\nincredibly\\nunbroken\\nsentence\\nmoving\\nfrom\\n"
+              + " topic\\nto\\n topic\\nso\\nthat\\nno-one\\nhad\\na\\nchance\\nto\\ninterrupt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"\\n\\n\"",
+          "          + \"one\\n\"",
+          "          + \"long\\n\"",
+          "          + \"incredibly\\n\"",
+          "          + \"unbroken\\n\"",
+          "          + \"sentence\\n\"",
+          "          + \"moving\\n\"",
+          "          + \"from\\n\"",
+          "          + \" topic\\n\"",
+          "          + \"to\\n\"",
+          "          + \" topic\\n\"",
+          "          + \"so\\n\"",
+          "          + \"that\\n\"",
+          "          + \"no-one\\n\"",
+          "          + \"had\\n\"",
+          "          + \"a\\n\"",
+          "          + \"chance\\n\"",
+          "          + \"to\\n\"",
+          "          + \"interrupt\";",
+          "}",
+        },
+      },
+      {
+        {
+          "class T {", //
+          "  String s = \"\\n\\n\\none\\n\\nlong\\n\\nincredibly\\n\\nunbroken\\n\\nsentence\\n\\n"
+              + "moving\\n\\nfrom\\n\\n topic\\n\\nto\\n\\n topic\\n\\nso\\n\\nthat\\n\\nno-one"
+              + "\\n\\nhad\\n\\na\\n\\nchance\\n\\nto\\n\\ninterrupt\\n\\n\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"\\n\\n\\n\"",
+          "          + \"one\\n\\n\"",
+          "          + \"long\\n\\n\"",
+          "          + \"incredibly\\n\\n\"",
+          "          + \"unbroken\\n\\n\"",
+          "          + \"sentence\\n\\n\"",
+          "          + \"moving\\n\\n\"",
+          "          + \"from\\n\\n\"",
+          "          + \" topic\\n\\n\"",
+          "          + \"to\\n\\n\"",
+          "          + \" topic\\n\\n\"",
+          "          + \"so\\n\\n\"",
+          "          + \"that\\n\\n\"",
+          "          + \"no-one\\n\\n\"",
+          "          + \"had\\n\\n\"",
+          "          + \"a\\n\\n\"",
+          "          + \"chance\\n\\n\"",
+          "          + \"to\\n\\n\"",
+          "          + \"interrupt\\n\\n\";",
+          "}",
+        },
+      },
+      {
+        {
+          "class T {", //
+          "  String s = \"onelongincrediblyunbrokensenten\\tcemovingfromtopictotopicsothatnoonehada"
+              + "chance tointerrupt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"onelongincrediblyunbrokensenten\"",
+          "          + \"\\tcemovingfromtopictotopicsothatnoonehadachance\"",
+          "          + \" tointerrupt\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s = \"onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehada"
+              + "chancetointerrupt_____________________)_\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehada"
+              + "chancetointerrupt_____________________)_\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s = \"onelongincrediblyunbrokensentencemovingfromtopictotopicsot atnoonehada"
+              + "chancetointerrupt______________________\";;",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"onelongincrediblyunbrokensentencemovingfromtopictotopicsot\"",
+          "          + \" atnoonehadachancetointerrupt______________________\";",
+          "  ;",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s = \"__ onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehada"
+              + "chanceto interrupt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"__"
+              + " onelongincrediblyunbrokensentencemovingfromtopictotopicsothatnoonehadachanceto\"",
+          "          + \" interrupt\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s =",
+          "      \"one long incredibly unbroken sentence\"",
+          "          // comment",
+          "          + \" moving from topic to topic\"",
+          "          // comment",
+          "          + \" so that no-one had a chance to\"",
+          "          // comment",
+          "          + \" interrupt\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"one long incredibly unbroken\"",
+          "          + \" sentence\"",
+          "          // comment",
+          "          + \" moving from topic to\"",
+          "          + \" topic\"",
+          "          // comment",
+          "          + \" so that no-one had a\"",
+          "          + \" chance to\"",
+          "          // comment",
+          "          + \" interrupt\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s =",
+          "      \"aaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccccc"
+              + " dddddddddddddddddd\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"aaaaaaaaaaaaaaaaaaaaaaaa\"",
+          "          + \" bbbbbbbbbbbbbbbbbb\"",
+          "          + \" ccccccccccccccccccccccccccc\"",
+          "          + \" dddddddddddddddddd\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  String s =",
+          "      \"aaaaaaaaaaaaaaaaaaaaaaaa \"",
+          "          + \"bbbbbbbbbbbbbbbbbb \"",
+          "          + \"ccccccccccccccccccccccccccc \"",
+          "          + \"dddddddddddddddddd\";",
+          "}"
+        },
+        {
+          "class T {",
+          "  String s =",
+          "      \"aaaaaaaaaaaaaaaaaaaaaaaa\"",
+          "          + \" bbbbbbbbbbbbbbbbbb\"",
+          "          + \" ccccccccccccccccccccccccccc\"",
+          "          + \" dddddddddddddddddd\";",
+          "}",
+        }
+      },
+      {
+        {
+          "class T {", //
+          "  byte[] bytes =",
+          "      \"one long incredibly unbroken sentence moving from topic to topic so that no-one"
+              + " had a chance to interrupt\".getBytes();",
+          "}"
+        },
+        {
+          "class T {", //
+          "  byte[] bytes =",
+          "      \"one long incredibly unbroken sentence moving from topic to topic so that no-one"
+              + " had a chance to interrupt\"",
+          "          .getBytes();",
+          "}"
+        },
+      },
+    };
+    return Arrays.stream(inputsAndOutputs)
+        .map(
+            inputAndOutput -> {
+              assertThat(inputAndOutput).hasLength(2);
+              return new String[] {
+                Joiner.on('\n').join(inputAndOutput[0]) + '\n', //
+                Joiner.on('\n').join(inputAndOutput[1]) + '\n',
+              };
+            })
+        .collect(toImmutableList());
+  }
+
+  private final Formatter formatter = new Formatter();
+
+  private final String input;
+  private final String output;
+
+  public StringWrapperIntegrationTest(String input, String output) {
+    this.input = input;
+    this.output = output;
+  }
+
+  @Test
+  public void test() throws Exception {
+    assertThat(StringWrapper.wrap(40, formatter.formatSource(input), formatter)).isEqualTo(output);
+  }
+
+  @Test
+  public void testCR() throws Exception {
+    assertThat(StringWrapper.wrap(40, formatter.formatSource(input.replace("\n", "\r")), formatter))
+        .isEqualTo(output.replace("\n", "\r"));
+  }
+
+  @Test
+  public void testCRLF() throws Exception {
+    assertThat(
+            StringWrapper.wrap(40, formatter.formatSource(input.replace("\n", "\r\n")), formatter))
+        .isEqualTo(output.replace("\n", "\r\n"));
+  }
+
+  @Test
+  public void idempotent() throws Exception {
+    String wrap = StringWrapper.wrap(40, formatter.formatSource(input), formatter);
+    assertThat(wrap).isEqualTo(formatter.formatSource(wrap));
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java
new file mode 100644
index 0000000..99e1b2f
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link StringWrapper}Test */
+@RunWith(JUnit4.class)
+public class StringWrapperTest {
+  @Test
+  public void testAwkwardLineEndWrapping() throws Exception {
+    String input =
+        lines(
+            "class T {",
+            // This is a wide line, but has to be split in code because of 100-char limit.
+            "  String s = someMethodWithQuiteALongNameThatWillGetUsUpCloseToTheColumnLimit() "
+                + "+ \"foo bar foo bar foo bar\";",
+            "",
+            "  String someMethodWithQuiteALongNameThatWillGetUsUpCloseToTheColumnLimit() {",
+            "    return null;",
+            "  }",
+            "}");
+    String output =
+        lines(
+            "class T {",
+            "  String s =",
+            "      someMethodWithQuiteALongNameThatWillGetUsUpCloseToTheColumnLimit()",
+            "          + \"foo bar foo bar foo bar\";",
+            "",
+            "  String someMethodWithQuiteALongNameThatWillGetUsUpCloseToTheColumnLimit() {",
+            "    return null;",
+            "  }",
+            "}");
+
+    assertThat(StringWrapper.wrap(100, input, new Formatter())).isEqualTo(output);
+  }
+
+  private static String lines(String... line) {
+    return Joiner.on('\n').join(line) + '\n';
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/TypeNameClassifierTest.java b/core/src/test/java/com/google/googlejavaformat/java/TypeNameClassifierTest.java
new file mode 100644
index 0000000..9d1e00a
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/TypeNameClassifierTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.common.base.Splitter;
+import com.google.googlejavaformat.java.TypeNameClassifier.JavaCaseFormat;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link TypeNameClassifier}Test */
+@RunWith(JUnit4.class)
+public final class TypeNameClassifierTest {
+  @Test
+  public void caseFormat() throws Exception {
+    assertThat(JavaCaseFormat.from("CONST")).isEqualTo(JavaCaseFormat.UPPERCASE);
+    assertThat(JavaCaseFormat.from("TypeName")).isEqualTo(JavaCaseFormat.UPPER_CAMEL);
+    assertThat(JavaCaseFormat.from("fieldName")).isEqualTo(JavaCaseFormat.LOWER_CAMEL);
+    assertThat(JavaCaseFormat.from("com")).isEqualTo(JavaCaseFormat.LOWERCASE);
+
+    assertThat(JavaCaseFormat.from("CONST_$")).isEqualTo(JavaCaseFormat.UPPERCASE);
+    assertThat(JavaCaseFormat.from("TypeName_$")).isEqualTo(JavaCaseFormat.UPPER_CAMEL);
+    assertThat(JavaCaseFormat.from("fieldName_$")).isEqualTo(JavaCaseFormat.LOWER_CAMEL);
+    assertThat(JavaCaseFormat.from("com_$")).isEqualTo(JavaCaseFormat.LOWERCASE);
+
+    assertThat(JavaCaseFormat.from("A_$")).isEqualTo(JavaCaseFormat.UPPERCASE);
+    assertThat(JavaCaseFormat.from("a_$")).isEqualTo(JavaCaseFormat.LOWERCASE);
+    assertThat(JavaCaseFormat.from("_")).isEqualTo(JavaCaseFormat.LOWERCASE);
+    assertThat(JavaCaseFormat.from("_A")).isEqualTo(JavaCaseFormat.UPPERCASE);
+  }
+
+  private static Optional<Integer> getPrefix(String qualifiedName) {
+    return TypeNameClassifier.typePrefixLength(Splitter.on('.').splitToList(qualifiedName));
+  }
+
+  @Test
+  public void typePrefixLength() {
+    assertThat(getPrefix("fieldName")).isEmpty();
+    assertThat(getPrefix("CONST")).isEmpty();
+    assertThat(getPrefix("ClassName")).hasValue(0);
+    assertThat(getPrefix("com.ClassName")).hasValue(1);
+    assertThat(getPrefix("ClassName.foo")).hasValue(1);
+    assertThat(getPrefix("com.ClassName.foo")).hasValue(2);
+    assertThat(getPrefix("ClassName.foo.bar")).hasValue(1);
+    assertThat(getPrefix("com.ClassName.foo.bar")).hasValue(2);
+    assertThat(getPrefix("ClassName.CONST")).hasValue(1);
+    assertThat(getPrefix("ClassName.varName")).hasValue(1);
+    assertThat(getPrefix("ClassName.Inner.varName")).hasValue(2);
+  }
+
+  @Test
+  public void ambiguousClass() {
+    assertThat(getPrefix("com.google.security.acl.proto2api.ACL.Entry.newBuilder")).hasValue(7);
+    // A human would probably identify this as "class-shaped", but just looking
+    // at the case we have to assume it could be something like `field1.field2.CONST`.
+    assertThat(getPrefix("com.google.security.acl.proto2api.ACL.newBuilder")).isEmpty();
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/filer/FormattingFilerTest.java b/core/src/test/java/com/google/googlejavaformat/java/filer/FormattingFilerTest.java
new file mode 100644
index 0000000..4fef207
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/filer/FormattingFilerTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java.filer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.testing.compile.CompilationRule;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.Messager;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.tools.FileObject;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.SimpleJavaFileObject;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link com.google.googlejavaformat.java.filer.FormattingFiler}. */
+@RunWith(JUnit4.class)
+public class FormattingFilerTest {
+
+  @Rule public CompilationRule compilationRule = new CompilationRule();
+
+  @Test
+  public void invalidSyntaxDoesNotThrowError() throws IOException {
+    List<String> logMessages = new ArrayList<>();
+    Messager messager =
+        new Messager() {
+          @Override
+          public void printMessage(javax.tools.Diagnostic.Kind kind, CharSequence msg) {
+            logMessages.add(kind.toString() + ";" + msg);
+          }
+
+          @Override
+          public void printMessage(javax.tools.Diagnostic.Kind kind, CharSequence msg, Element e) {}
+
+          @Override
+          public void printMessage(
+              javax.tools.Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a) {}
+
+          @Override
+          public void printMessage(
+              javax.tools.Diagnostic.Kind kind,
+              CharSequence msg,
+              Element e,
+              AnnotationMirror a,
+              AnnotationValue v) {}
+        };
+
+    String file = Joiner.on('\n').join("package foo;", "public class Bar {");
+    FormattingFiler formattingFiler = new FormattingFiler(new FakeFiler(), messager);
+    Writer writer = formattingFiler.createSourceFile("foo.Bar").openWriter();
+    writer.write(file);
+    writer.close();
+
+    assertThat(logMessages).containsExactly("NOTE;Error formatting foo.Bar");
+  }
+
+  @Test
+  public void formatsFile() throws IOException {
+    FormattingFiler formattingFiler = new FormattingFiler(new FakeFiler());
+    JavaFileObject sourceFile = formattingFiler.createSourceFile("foo.Bar");
+    try (Writer writer = sourceFile.openWriter()) {
+      writer.write("package foo;class Bar{private String      baz;\n\n\n\n}");
+    }
+
+    assertThat(sourceFile.getCharContent(false).toString())
+        .isEqualTo(
+            Joiner.on('\n')
+                .join(
+                    "package foo;",
+                    "",
+                    "class Bar {",
+                    "  private String baz;",
+                    "}",
+                    "")); // trailing newline
+  }
+
+  private static class FakeFiler implements Filer {
+    @Override
+    public JavaFileObject createSourceFile(CharSequence name, Element... originatingElements)
+        throws IOException {
+      return new ObservingJavaFileObject(name.toString(), Kind.SOURCE);
+    }
+
+    @Override
+    public JavaFileObject createClassFile(CharSequence name, Element... originatingElements)
+        throws IOException {
+      return new ObservingJavaFileObject(name.toString(), Kind.CLASS);
+    }
+
+    @Override
+    public FileObject createResource(
+        Location location,
+        CharSequence pkg,
+        CharSequence relativeName,
+        Element... originatingElements)
+        throws IOException {
+      return new ObservingJavaFileObject(pkg.toString() + relativeName, Kind.OTHER);
+    }
+
+    @Override
+    public FileObject getResource(Location location, CharSequence pkg, CharSequence relativeName)
+        throws IOException {
+      return new ObservingJavaFileObject(pkg.toString() + relativeName, Kind.OTHER);
+    }
+  }
+
+  private static class ObservingJavaFileObject extends SimpleJavaFileObject {
+    private final StringWriter output = new StringWriter();
+
+    ObservingJavaFileObject(String name, Kind kind) {
+      super(URI.create(name), kind);
+    }
+
+    @Override
+    public Writer openWriter() throws IOException {
+      return output;
+    }
+
+    @Override
+    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+      return output.toString();
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.input
new file mode 100644
index 0000000..81d13aa
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.input
@@ -0,0 +1,84 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for AbstractTypeDeclarations, AnnotationTypeDeclarations, AnnotationTypeMemberDeclarations,
+ * Annotations, AnonymousClassDeclarations, ArrayAccesses, ArrayCreations, ArrayInitializers,
+ * ArrayTypes, AssertStatements, and Assignments.
+ */
+class A {
+  public @interface X {
+    int x();
+
+    int y() default
+        1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+            + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
+  }
+
+  @X(x = 1)
+  private @interface Y {}
+
+  // TODO(jdd): Add annotation declaration with empty body.
+
+  @X(x = 1)
+  @Y
+  protected @interface Z {}
+
+  // TODO(jdd): Include type annotations once we can include a higher language level.
+
+  int[] array1 = new int[5];
+  int[] array2 =
+      new int[] {
+            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
+            24, 25, 26, 27, 28, 29, 30, 31
+          };
+  int[] array3 = {
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
+    26, 27, 28, 29, 30, 31
+  };
+  int[][] array4 = {
+    {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}, {16, 17}, {18, 19},
+    {20, 21}, {22, 23}
+  };
+  int[][][] arrayWithLongName =
+      new int
+          [0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+              + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+          [0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+              + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+          [];
+
+  A a1 =
+      new A() {
+        int x =
+            array1[
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+                + array2[
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+                + array3[
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+                + array4[
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0][
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0];
+      };
+
+  void f(int something) {
+    assert 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+        == 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
+    assert 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+            == 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+        : "that was certainly unexpected!";
+    arrayWithLongName[
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0] =
+        arrayWithLongName[
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0];
+    something =
+        2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2
+            + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2
+            + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.output
new file mode 100644
index 0000000..3eff456
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.output
@@ -0,0 +1,84 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for AbstractTypeDeclarations, AnnotationTypeDeclarations, AnnotationTypeMemberDeclarations,
+ * Annotations, AnonymousClassDeclarations, ArrayAccesses, ArrayCreations, ArrayInitializers,
+ * ArrayTypes, AssertStatements, and Assignments.
+ */
+class A {
+  public @interface X {
+    int x();
+
+    int y() default
+        1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+            + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
+  }
+
+  @X(x = 1)
+  private @interface Y {}
+
+  // TODO(jdd): Add annotation declaration with empty body.
+
+  @X(x = 1)
+  @Y
+  protected @interface Z {}
+
+  // TODO(jdd): Include type annotations once we can include a higher language level.
+
+  int[] array1 = new int[5];
+  int[] array2 =
+      new int[] {
+        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
+        25, 26, 27, 28, 29, 30, 31
+      };
+  int[] array3 = {
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
+    26, 27, 28, 29, 30, 31
+  };
+  int[][] array4 = {
+    {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}, {16, 17}, {18, 19},
+    {20, 21}, {22, 23}
+  };
+  int[][][] arrayWithLongName =
+      new int
+          [0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+              + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+          [0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+              + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+          [];
+
+  A a1 =
+      new A() {
+        int x =
+            array1[
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+                + array2[
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+                + array3[
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]
+                + array4[
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0][
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0];
+      };
+
+  void f(int something) {
+    assert 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+        == 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
+    assert 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+            == 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+        : "that was certainly unexpected!";
+    arrayWithLongName[
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0] =
+        arrayWithLongName[
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0];
+    something =
+        2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2
+            + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2
+            + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B.input
new file mode 100644
index 0000000..321f721
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B.input
@@ -0,0 +1,18 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for Blocks, BodyDeclarations, BooleanLiterals, and BreakStatements. */
+class B {
+  int x;
+  private int y;
+  public int z;
+
+  void f() {
+    LABEL:
+    while (true != false) {
+      if (false == true)
+        break;
+      if (false == false)
+        break LABEL;
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B.output
new file mode 100644
index 0000000..6eb0119
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B.output
@@ -0,0 +1,16 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for Blocks, BodyDeclarations, BooleanLiterals, and BreakStatements. */
+class B {
+  int x;
+  private int y;
+  public int z;
+
+  void f() {
+    LABEL:
+    while (true != false) {
+      if (false == true) break;
+      if (false == false) break LABEL;
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B112853497.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B112853497.input
new file mode 100644
index 0000000..e38d0b3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B112853497.input
@@ -0,0 +1,12 @@
+class B112853497 {
+  {
+    XxxxxxxxxxXxxx xxxXxxxx =
+        xxxXxxxx
+            .xxxxXxXxxxx(xxxxXxxx, XXXXXX_XXXX_XXXXXXXX_XX_XXXXXXXX)
+            .toBuilder()
+            .xxxXxxxxXxxxXx(xxxxxXx.xxxXxxxxXxxxXx())
+            .xxxXxxxxXxxxXx(xxxxxXx.xxxXxxxxXxxxXx())
+            .xxxXxxxxx(xxxxxx.xxxXxxxxxXxxx())
+            .xxxxx();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B112853497.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B112853497.output
new file mode 100644
index 0000000..f5079e1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B112853497.output
@@ -0,0 +1,10 @@
+class B112853497 {
+  {
+    XxxxxxxxxxXxxx xxxXxxxx =
+        xxxXxxxx.xxxxXxXxxxx(xxxxXxxx, XXXXXX_XXXX_XXXXXXXX_XX_XXXXXXXX).toBuilder()
+            .xxxXxxxxXxxxXx(xxxxxXx.xxxXxxxxXxxxXx())
+            .xxxXxxxxXxxxXx(xxxxxXx.xxxXxxxxXxxxXx())
+            .xxxXxxxxx(xxxxxx.xxxXxxxxxXxxx())
+            .xxxxx();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B124394008.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B124394008.input
new file mode 100644
index 0000000..0b51407
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B124394008.input
@@ -0,0 +1,8 @@
+class B124394008 {
+  {
+    LocalReviewProto.Builder newReview =
+        readProfileResponse.flatMap(authorExtractor::extract)
+            .map(luReviewsExtractor::toLocalReviewProto)
+            .orElse(LocalReviewProto.getDefaultInstance()).toBuilder();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B124394008.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B124394008.output
new file mode 100644
index 0000000..176b46a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B124394008.output
@@ -0,0 +1,10 @@
+class B124394008 {
+  {
+    LocalReviewProto.Builder newReview =
+        readProfileResponse
+            .flatMap(authorExtractor::extract)
+            .map(luReviewsExtractor::toLocalReviewProto)
+            .orElse(LocalReviewProto.getDefaultInstance())
+            .toBuilder();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B126411718.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B126411718.input
new file mode 100644
index 0000000..c5cedba
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B126411718.input
@@ -0,0 +1,10 @@
+class B126411718 {
+  {
+    logger
+        .atDebug()
+        .log(
+            "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+            deletedPersistentNames.size(),
+            deletedPersistentNames);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B126411718.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B126411718.output
new file mode 100644
index 0000000..3f51285
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B126411718.output
@@ -0,0 +1,7 @@
+class B126411718 {
+  {
+    logger.atDebug().log(
+        "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+        deletedPersistentNames.size(), deletedPersistentNames);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B142553964.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B142553964.input
new file mode 100644
index 0000000..fb49cc6
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B142553964.input
@@ -0,0 +1,8 @@
+/*************************************************************************************************************************************************************
+ * Copyright
+ *
+ * Some
+ *
+ * Company
+ *************************************************************************************************************************************************************/
+class T {}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B142553964.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B142553964.output
new file mode 100644
index 0000000..c00d84e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B142553964.output
@@ -0,0 +1,8 @@
+/*************************************************************************************************************************************************************
+ * Copyright
+ *
+ * Some
+ *
+ * Company
+ *************************************************************************************************************************************************************/
+class T {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B154342628.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B154342628.input
new file mode 100644
index 0000000..dbde8f7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B154342628.input
@@ -0,0 +1,8 @@
+class B154342628 {
+  void f() {
+    var var = 42;
+    return writtenVariables.stream()
+        .filter(var -> deletedVariableIds.contains(var.getId()))
+        .collect(toImmutableList());
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B154342628.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B154342628.output
new file mode 100644
index 0000000..dbde8f7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B154342628.output
@@ -0,0 +1,8 @@
+class B154342628 {
+  void f() {
+    var var = 42;
+    return writtenVariables.stream()
+        .filter(var -> deletedVariableIds.contains(var.getId()))
+        .collect(toImmutableList());
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B18479811.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B18479811.input
new file mode 100644
index 0000000..f805f67
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B18479811.input
@@ -0,0 +1,15 @@
+class Test {
+  public void testPrimitiveFields() {
+    ConverterVerifier.verifyFieldsAreAnnotatedWithToPojo(
+        Contact.class, DomainContact.class,
+        new ImmutableList.Builder<String>().add(
+            "address1", "address2", "assistant", "assistantPhone", "birthDate", "committed",
+            "createdBy", "createdDate", "deleted", "department", "description",
+            "domainObjId", "email1", "email2", "emailOptOut", "firstName", "lastName",
+            "leadSource", "level", "modifiedBy", "modifiedDate",
+            "phoneFax", "phoneHome", "phoneMobile", "phoneOther", "phoneWork",
+            "salutation", "title"
+
+        ).build());
+  }
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B18479811.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B18479811.output
new file mode 100644
index 0000000..04f392c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B18479811.output
@@ -0,0 +1,38 @@
+class Test {
+  public void testPrimitiveFields() {
+    ConverterVerifier.verifyFieldsAreAnnotatedWithToPojo(
+        Contact.class,
+        DomainContact.class,
+        new ImmutableList.Builder<String>()
+            .add(
+                "address1",
+                "address2",
+                "assistant",
+                "assistantPhone",
+                "birthDate",
+                "committed",
+                "createdBy",
+                "createdDate",
+                "deleted",
+                "department",
+                "description",
+                "domainObjId",
+                "email1",
+                "email2",
+                "emailOptOut",
+                "firstName",
+                "lastName",
+                "leadSource",
+                "level",
+                "modifiedBy",
+                "modifiedDate",
+                "phoneFax",
+                "phoneHome",
+                "phoneMobile",
+                "phoneOther",
+                "phoneWork",
+                "salutation",
+                "title")
+            .build());
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950219.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950219.input
new file mode 100644
index 0000000..b4dd884
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950219.input
@@ -0,0 +1,8 @@
+class B19950219 {
+  @Override
+  String normalizeWord(String word) {
+    return Ascii.toLowerCase(word);
+  }
+  @Override
+  String convert(CaseFormat format, String s) {
+  }}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950219.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950219.output
new file mode 100644
index 0000000..c00a09c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950219.output
@@ -0,0 +1,9 @@
+class B19950219 {
+  @Override
+  String normalizeWord(String word) {
+    return Ascii.toLowerCase(word);
+  }
+
+  @Override
+  String convert(CaseFormat format, String s) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950468.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950468.input
new file mode 100644
index 0000000..9b4a8e2
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950468.input
@@ -0,0 +1,5 @@
+class B19950468 {
+  int code() {}
+
+  // comment
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950468.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950468.output
new file mode 100644
index 0000000..9b4a8e2
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950468.output
@@ -0,0 +1,5 @@
+class B19950468 {
+  int code() {}
+
+  // comment
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950815.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950815.input
new file mode 100644
index 0000000..369b8d1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950815.input
@@ -0,0 +1,12 @@
+class B19950815 {
+  void m() {
+    checkArgument(
+        truncationLength >= 0, "maxLength (%s) must be >= length of the truncation indicator (%s)",
+        maxLength, truncationIndicator.length());
+  }
+
+  private String finishCollapseFrom(
+      CharSequence sequence, int start, int end, char replacement, StringBuilder builder,
+      boolean inMatchingGroup) {
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950815.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950815.output
new file mode 100644
index 0000000..108a51e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19950815.output
@@ -0,0 +1,17 @@
+class B19950815 {
+  void m() {
+    checkArgument(
+        truncationLength >= 0,
+        "maxLength (%s) must be >= length of the truncation indicator (%s)",
+        maxLength,
+        truncationIndicator.length());
+  }
+
+  private String finishCollapseFrom(
+      CharSequence sequence,
+      int start,
+      int end,
+      char replacement,
+      StringBuilder builder,
+      boolean inMatchingGroup) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19996259.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19996259.input
new file mode 100644
index 0000000..a029186
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19996259.input
@@ -0,0 +1,16 @@
+class B19996259 {
+  void g( // line comment
+  int x) {}
+
+  void //
+  g //
+  ( //
+  int //
+  x //
+  , //
+  int //
+  y //
+  ) //
+  { //
+  } //
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19996259.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19996259.output
new file mode 100644
index 0000000..f0584c2
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B19996259.output
@@ -0,0 +1,16 @@
+class B19996259 {
+  void g( // line comment
+      int x) {}
+
+  void //
+      g //
+      ( //
+      int //
+          x //
+          , //
+      int //
+          y //
+      ) //
+      { //
+  } //
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128174.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128174.input
new file mode 100644
index 0000000..a4ae333
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128174.input
@@ -0,0 +1,8 @@
+class B20128174 {
+
+  enum Suit { DIAMONDS, HEARTS, CLUBS, SPADES };
+
+  enum Suit { DIAMONDS, HEARTS, CLUBS, SPADES; };
+
+  enum Suit { DIAMONDS, HEARTS, CLUBS, SPADES,; };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128174.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128174.output
new file mode 100644
index 0000000..7550737
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128174.output
@@ -0,0 +1,24 @@
+class B20128174 {
+
+  enum Suit {
+    DIAMONDS,
+    HEARTS,
+    CLUBS,
+    SPADES
+  };
+
+  enum Suit {
+    DIAMONDS,
+    HEARTS,
+    CLUBS,
+    SPADES;
+  };
+
+  enum Suit {
+    DIAMONDS,
+    HEARTS,
+    CLUBS,
+    SPADES,
+    ;
+  };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128588.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128588.input
new file mode 100644
index 0000000..74ed3db
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128588.input
@@ -0,0 +1,84 @@
+@Foo @Bar @Baz package edu.oswego.cs.dl.util.concurrent;
+
+@Foo @Bar @Baz class Test {
+
+  @Foo @Bar @Baz Object f() {
+  }
+
+  @Foo @Bar @Baz public Object f() {
+  }
+
+  @Foo @Bar @Baz void f() {
+  }
+
+  @Foo @Bar @Baz static Object field;
+
+  static @Foo @Bar @Baz Object field;
+
+  @Foo @Bar @Baz Object field;
+
+  @Foo(xs=42) @Bar @Baz Object field;
+
+  {
+    @Foo @Bar @Baz final Object var;
+
+    final @Foo @Bar @Baz Object var;
+
+    @Foo @Bar @Baz Object var;
+
+    @Foo(xs=42) @Bar @Baz Object var;
+  }
+
+  void f(
+      @Foo @Bar @Baz final Object var,
+      final @Foo @Bar @Baz Object var,
+      @Foo @Bar @Baz Object var,
+      @Foo(xs=42) @Bar @Baz Object var) {}
+
+  <@TA T extends @TA Object> @TA T f(List<? extends @TA T> a, List<? super @TA T> b) throws @TA Exception {}
+
+  @FooXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @BarXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @BazXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX int x; 
+
+@FooXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @BarXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX @BazXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX int x; 
+
+  @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated Object var; 
+
+  @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated Object var; 
+
+  @Deprecated(x=42) @Deprecated @Deprecated @Deprecated @Deprecated Object var;
+
+  @Deprecated(
+      x = 42,
+      y = {
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+      })
+  Object var;
+
+  @Deprecated(
+      y = {
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+      })
+  Object var;
+}
+
+@Frozzle({
+  @Mirror(in = edu.oswego.cs.dl.util.concurrent.F.class, methods = "foo"),
+  @Mirror(in = edu.oswego.cs.dl.util.concurrent.F.class, methods = "foo"),
+  @Mirror(in = edu.oswego.cs.dl.util.concurrent.F.class, enable = false, methods = {
+    "foo", "foo", "foo", "foo", "foo",
+    "foo", "foo", "foo", "foo", "foo",
+    "foo", "foo", "foo", "foo", "foo",
+  }),
+  @Mirror(
+    in = edu.oswego.cs.dl.util.concurrent.F.class,
+    enable = false,
+    methods = {"foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo"}),
+  @Mirror(in = edu.oswego.cs.dl.util.concurrent.F.class, methods = "foo"),
+})
+class C {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128588.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128588.output
new file mode 100644
index 0000000..273fa79
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128588.output
@@ -0,0 +1,137 @@
+@Foo
+@Bar
+@Baz
+package edu.oswego.cs.dl.util.concurrent;
+
+@Foo
+@Bar
+@Baz
+class Test {
+
+  @Foo
+  @Bar
+  @Baz
+  Object f() {}
+
+  @Foo
+  @Bar
+  @Baz
+  public Object f() {}
+
+  @Foo
+  @Bar
+  @Baz
+  void f() {}
+
+  @Foo @Bar @Baz static Object field;
+
+  static @Foo @Bar @Baz Object field;
+
+  @Foo @Bar @Baz Object field;
+
+  @Foo(xs = 42)
+  @Bar
+  @Baz
+  Object field;
+
+  {
+    @Foo
+    @Bar
+    @Baz
+    final Object var;
+
+    final @Foo @Bar @Baz Object var;
+
+    @Foo
+    @Bar
+    @Baz
+    Object var;
+
+    @Foo(xs = 42)
+    @Bar
+    @Baz
+    Object var;
+  }
+
+  void f(
+      @Foo @Bar @Baz final Object var,
+      final @Foo @Bar @Baz Object var,
+      @Foo @Bar @Baz Object var,
+      @Foo(xs = 42) @Bar @Baz Object var) {}
+
+  <@TA T extends @TA Object> @TA T f(List<? extends @TA T> a, List<? super @TA T> b)
+      throws @TA Exception {}
+
+  @FooXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+  @BarXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+  @BazXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+  int x;
+
+  @FooXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+  @BarXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+  @BazXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+  int x;
+
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  Object var;
+
+  @Deprecated @Deprecated @Deprecated @Deprecated @Deprecated Object var;
+
+  @Deprecated(x = 42)
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  @Deprecated
+  Object var;
+
+  @Deprecated(
+      x = 42,
+      y = {
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+      })
+  Object var;
+
+  @Deprecated(
+      y = {
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+        XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+      })
+  Object var;
+}
+
+@Frozzle({
+  @Mirror(in = edu.oswego.cs.dl.util.concurrent.F.class, methods = "foo"),
+  @Mirror(in = edu.oswego.cs.dl.util.concurrent.F.class, methods = "foo"),
+  @Mirror(
+      in = edu.oswego.cs.dl.util.concurrent.F.class,
+      enable = false,
+      methods = {
+        "foo", "foo", "foo", "foo", "foo",
+        "foo", "foo", "foo", "foo", "foo",
+        "foo", "foo", "foo", "foo", "foo",
+      }),
+  @Mirror(
+      in = edu.oswego.cs.dl.util.concurrent.F.class,
+      enable = false,
+      methods = {"foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo", "foo"}),
+  @Mirror(in = edu.oswego.cs.dl.util.concurrent.F.class, methods = "foo"),
+})
+class C {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128760.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128760.input
new file mode 100644
index 0000000..30d17f4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128760.input
@@ -0,0 +1,107 @@
+class B20128760 {
+  void f() {
+    x =
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    XxxxxxxxxXxx.<Xxxxxx, Xxxxxx>xxxxxxx()
+        .xxx(XxxxxxxxxXxxXxxx.Xxxxx.XXXXX, "Xxxxx xxxxxxxxx xxxxx")
+        .xxx(
+            XxxxxxxxxXxxXxxx.Xxxxx.XXXXXxXXXXX,
+            XxxxXxxxxxxxxx.xxxXxxxxxxxxXxxxxXxxxxXxXxxxxx(Xxxxxxxxxx.xxxXxxxXxxxx(x)))
+        .xxxxx();
+
+    xxxxxxXxxx(
+            xxx
+                .xxxXxXxxxxxxx(
+                    new XxxxxxxxxXxxxxxxx.Xxxxxxx()
+                        .xxxxXxxxXxx(
+                            Xxxxx.xxxXxxxxXxxx(
+                                new XxxxXx(
+                                    xxxxxxxxx1.xxxXxxxXx().xxxXxxxx()
+                                        + xxxxxxxxx2.xxxXxxxXx().xxxXxxxx())))
+                        .xxxxx())
+                .xxxXxxxxxx())
+        .xxxxxxxxXxxxxxx();
+
+    XXxxxx<Xxxx, XxxxxxxXxxxxxxxxxxXxxxxxxxx> xxxxxxxxxXxxXxxXxXxxxxxxxXx =
+        XX
+            .xxxxXxxXxxx2(
+                "XxxxXxxxxxxxXxxxXxxxXxxxxxxxXxxxXxxXxxxxxxXxxxxxxxx",
+                xxxxxxxxXxxxXxXxxxxxxxXx,
+                xxxxxxxxXxxxXxxxxXxxxXxXxxxxxxxXx)
+            .xxxxxxxxXx(
+                "XxxxxxxxXxxxxxxxxXxx",
+                xxxxxxxxXxxxxxxxxXxxXx,
+                XX.xxxxxXx(XX.xxxxx(), XX.xxxxxx(XxxxxxxXxxxxxxxxxxXxxxxxxxx.xxxxx)));
+
+    xxXxxxxxxxXxxxXxxx =
+        (XxxxXxxx)
+            xxxxxxxxXxxxxxxxxx.xxx()
+                .xxxxxXxxxxxx
+                .xxxXxxxxxxx(xxxxxxxxXxxxxxxxxx.xxx().xxxxXxxxx.xxxXxxxxxxXxxx() - 1)
+                .xxxXxxx()
+                .xxxxXxxxXxXx(X.xx.xxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    xxxxxxXxxx(
+            xxxxxxxxxx
+                .xxxXxxxXxxxxXxxxxxxxxxxXxxxXxxxXxxxxXxxx(XXXxXXXXxXX, XXXXXXxXXXxXXXXxXX, XXXXXxXX)
+                .xxxxxxxxxxxXxxxXxxxXxxxxx())
+        .xxxxxxxxXxxxxxx(
+            xxxxXxxxxxxxxxxXxxxXxxxXxxxx(xxxx1Xxxx, Xxxxx.XXxXXX),
+            xxxxXxxxxxxxxxxXxxxXxxxXxxxx(xxxx2Xxxx, Xxxxx.XXxXXX));
+
+    xxxx(
+        xxxx(
+            xxxx(
+                xxxx(
+                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx))));
+
+    assertThat(
+            main
+                .format("--offset", "0", "--length", String.valueOf(input.length()), path.toString()))
+        .isEqualTo(0);
+  }
+
+  private static class GenerateDashboardRowFn
+      extends
+          XX.XxXx<
+              Xxxx<
+                  Xxxx,
+                  Xxxxx2.XxxXxxxXxxxxxxxxxx<XxxxxxxxXxxx, Xxxx<XxxxxxxxXxxx, XxxxxxxXxxxxxxxx>>>,
+              Xxxx<Xxxx, XxxxxxxXxxxxxxxxxxXxxxxxxxx>> {}
+
+  public class XxxxxxxXxxxxxxxXxxxXxxxxXxxxxxx<X>
+      extends
+          XxxxxxxXxxxxxxxXxxxXxxxxXxxxxxx<
+              XxxxxxxXxxxxxxxXxxxXxxxxXxxxxxx<X>, XxxxXxxxxxxXxxxxxxxx<? extends Xxxxxxxx<X>>> {}
+
+  public class Foo
+      extends
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  public class Foo
+      extends
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      implements
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  public class Foo
+      extends
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      implements
+          Xxxxxxxxxxxxx<Xxxxxxxxxxxxxxxxxxxxxxxxxxxxx, Xxxxxxxxxxxxxxxxxxxxxxxxxxxxx, Xxxxxxxxxxxxxxxxxxxxxxxxxxxxx>,
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  {
+    Stream<ItemKey> itemIdsStream = stream(members)
+        .flatMap(m -> m.getFieldValues()
+            .entrySet()
+            .stream()
+            .filter(fv -> itemLinkFieldIds.contains(fv.getKey()))
+            .flatMap(fv -> FieldDTO.deserializeStringToListOfStrings(fv.getValue())
+                .stream()
+                .map(id -> new ItemKey(fieldsById.get(fv.getKey()).getItemTypeId(), id))));
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128760.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128760.output
new file mode 100644
index 0000000..b5b27fa
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128760.output
@@ -0,0 +1,105 @@
+class B20128760 {
+  void f() {
+    x =
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    XxxxxxxxxXxx.<Xxxxxx, Xxxxxx>xxxxxxx()
+        .xxx(XxxxxxxxxXxxXxxx.Xxxxx.XXXXX, "Xxxxx xxxxxxxxx xxxxx")
+        .xxx(
+            XxxxxxxxxXxxXxxx.Xxxxx.XXXXXxXXXXX,
+            XxxxXxxxxxxxxx.xxxXxxxxxxxxXxxxxXxxxxXxXxxxxx(Xxxxxxxxxx.xxxXxxxXxxxx(x)))
+        .xxxxx();
+
+    xxxxxxXxxx(
+            xxx.xxxXxXxxxxxxx(
+                    new XxxxxxxxxXxxxxxxx.Xxxxxxx()
+                        .xxxxXxxxXxx(
+                            Xxxxx.xxxXxxxxXxxx(
+                                new XxxxXx(
+                                    xxxxxxxxx1.xxxXxxxXx().xxxXxxxx()
+                                        + xxxxxxxxx2.xxxXxxxXx().xxxXxxxx())))
+                        .xxxxx())
+                .xxxXxxxxxx())
+        .xxxxxxxxXxxxxxx();
+
+    XXxxxx<Xxxx, XxxxxxxXxxxxxxxxxxXxxxxxxxx> xxxxxxxxxXxxXxxXxXxxxxxxxXx =
+        XX.xxxxXxxXxxx2(
+                "XxxxXxxxxxxxXxxxXxxxXxxxxxxxXxxxXxxXxxxxxxXxxxxxxxx",
+                xxxxxxxxXxxxXxXxxxxxxxXx,
+                xxxxxxxxXxxxXxxxxXxxxXxXxxxxxxxXx)
+            .xxxxxxxxXx(
+                "XxxxxxxxXxxxxxxxxXxx",
+                xxxxxxxxXxxxxxxxxXxxXx,
+                XX.xxxxxXx(XX.xxxxx(), XX.xxxxxx(XxxxxxxXxxxxxxxxxxXxxxxxxxx.xxxxx)));
+
+    xxXxxxxxxxXxxxXxxx =
+        (XxxxXxxx)
+            xxxxxxxxXxxxxxxxxx
+                .xxx()
+                .xxxxxXxxxxxx
+                .xxxXxxxxxxx(xxxxxxxxXxxxxxxxxx.xxx().xxxxXxxxx.xxxXxxxxxxXxxx() - 1)
+                .xxxXxxx()
+                .xxxxXxxxXxXx(X.xx.xxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    xxxxxxXxxx(
+            xxxxxxxxxx
+                .xxxXxxxXxxxxXxxxxxxxxxxXxxxXxxxXxxxxXxxx(XXXxXXXXxXX, XXXXXXxXXXxXXXXxXX, XXXXXxXX)
+                .xxxxxxxxxxxXxxxXxxxXxxxxx())
+        .xxxxxxxxXxxxxxx(
+            xxxxXxxxxxxxxxxXxxxXxxxXxxxx(xxxx1Xxxx, Xxxxx.XXxXXX),
+            xxxxXxxxxxxxxxxXxxxXxxxXxxxx(xxxx2Xxxx, Xxxxx.XXxXXX));
+
+    xxxx(
+        xxxx(
+            xxxx(
+                xxxx(
+                    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx))));
+
+    assertThat(
+            main.format(
+                "--offset", "0", "--length", String.valueOf(input.length()), path.toString()))
+        .isEqualTo(0);
+  }
+
+  private static class GenerateDashboardRowFn
+      extends XX.XxXx<
+          Xxxx<Xxxx, Xxxxx2.XxxXxxxXxxxxxxxxxx<XxxxxxxxXxxx, Xxxx<XxxxxxxxXxxx, XxxxxxxXxxxxxxxx>>>,
+          Xxxx<Xxxx, XxxxxxxXxxxxxxxxxxXxxxxxxxx>> {}
+
+  public class XxxxxxxXxxxxxxxXxxxXxxxxXxxxxxx<X>
+      extends XxxxxxxXxxxxxxxXxxxXxxxxXxxxxxx<
+          XxxxxxxXxxxxxxxXxxxXxxxxXxxxxxx<X>, XxxxXxxxxxxXxxxxxxxx<? extends Xxxxxxxx<X>>> {}
+
+  public class Foo
+      extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  public class Foo
+      extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      implements Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  public class Foo
+      extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      implements Xxxxxxxxxxxxx<
+              Xxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+              Xxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+              Xxxxxxxxxxxxxxxxxxxxxxxxxxxxx>,
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  {
+    Stream<ItemKey> itemIdsStream =
+        stream(members)
+            .flatMap(
+                m ->
+                    m.getFieldValues().entrySet().stream()
+                        .filter(fv -> itemLinkFieldIds.contains(fv.getKey()))
+                        .flatMap(
+                            fv ->
+                                FieldDTO.deserializeStringToListOfStrings(fv.getValue()).stream()
+                                    .map(
+                                        id ->
+                                            new ItemKey(
+                                                fieldsById.get(fv.getKey()).getItemTypeId(), id))));
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128921.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128921.input
new file mode 100644
index 0000000..5d3a9ee
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128921.input
@@ -0,0 +1,10 @@
+public class B20128921 {
+
+  <T> void m(Iterable<T> ax) {
+
+    for (T a : ax) {
+
+      System.err.println(x);
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128921.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128921.output
new file mode 100644
index 0000000..5d3a9ee
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20128921.output
@@ -0,0 +1,10 @@
+public class B20128921 {
+
+  <T> void m(Iterable<T> ax) {
+
+    for (T a : ax) {
+
+      System.err.println(x);
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20341001.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20341001.input
new file mode 100644
index 0000000..504ba77
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20341001.input
@@ -0,0 +1,54 @@
+public class B20341001 {
+  int[] xs = {
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+  };
+  int[] xs = {
+    Foo.CONSTxx,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+  };
+  int[] xs = {
+    Foo_CONST,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+  };
+  int[] xs = {
+    "foo",
+    "0123456",
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+  };
+  int[] xs = {
+    "foo",
+    "01234567",
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+  };
+  {
+    return ImmutableList.of(
+        item1,
+        item2,
+        item3,
+        item4,
+        item5,
+        item6,
+        item7,
+        item8,
+        item9,
+        item10,
+        item11,
+        item12,
+        item13);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20341001.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20341001.output
new file mode 100644
index 0000000..09a8f1b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20341001.output
@@ -0,0 +1,303 @@
+public class B20341001 {
+  int[] xs = {
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+        1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+        1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+        1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+        1
+  };
+  int[] xs = {
+    Foo.CONSTxx,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1
+  };
+  int[] xs = {
+    Foo_CONST, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1
+  };
+  int[] xs = {
+    "foo", "0123456", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+  };
+  int[] xs = {
+    "foo",
+    "01234567",
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1
+  };
+
+  {
+    return ImmutableList.of(
+        item1, item2, item3, item4, item5, item6, item7, item8, item9, item10, item11, item12,
+        item13);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20524742.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20524742.input
new file mode 100644
index 0000000..7ea5a29
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20524742.input
@@ -0,0 +1,3 @@
+class Test {{
+  int x;
+}}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20524742.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20524742.output
new file mode 100644
index 0000000..bbd5dc3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20524742.output
@@ -0,0 +1,5 @@
+class Test {
+  {
+    int x;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20529113.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20529113.input
new file mode 100644
index 0000000..8f65948
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20529113.input
@@ -0,0 +1,12 @@
+@BugPattern(category = GUAVA, severity = NOT_A_PROBLEM, maturity = EXPERIMENTAL)
+@BugPattern(
+    name = "AsyncFunctionReturnsImmediate", summary = SIMPLIFY,
+    category = GUAVA, severity = NOT_A_PROBLEM, maturity = EXPERIMENTAL)
+@BugPattern(
+    name = "AsyncFunctionReturnsImmediate", summary = SIMPLIFY,
+    explanation =
+        "If an AsyncFunction always returns immediateFuture() and never throws, it can "
+        + "be replaced with a Function.",
+    category = GUAVA, severity = NOT_A_PROBLEM, maturity = EXPERIMENTAL)
+class Test {
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20529113.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20529113.output
new file mode 100644
index 0000000..de0c052
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20529113.output
@@ -0,0 +1,17 @@
+@BugPattern(category = GUAVA, severity = NOT_A_PROBLEM, maturity = EXPERIMENTAL)
+@BugPattern(
+    name = "AsyncFunctionReturnsImmediate",
+    summary = SIMPLIFY,
+    category = GUAVA,
+    severity = NOT_A_PROBLEM,
+    maturity = EXPERIMENTAL)
+@BugPattern(
+    name = "AsyncFunctionReturnsImmediate",
+    summary = SIMPLIFY,
+    explanation =
+        "If an AsyncFunction always returns immediateFuture() and never throws, it can "
+            + "be replaced with a Function.",
+    category = GUAVA,
+    severity = NOT_A_PROBLEM,
+    maturity = EXPERIMENTAL)
+class Test {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20531711.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20531711.input
new file mode 100644
index 0000000..01f410f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20531711.input
@@ -0,0 +1,16 @@
+class B20531711 {
+  void m() {
+    when(mLoginHelper.getAllAccounts()).thenReturn(
+    new Account[] {
+          new Account("test1", "test1"),
+          new Account("test2", "test2"),
+        });
+
+    when(mLoginHelper.getAllAccounts()).thenReturn(
+    new Account[] {
+          new Account("test1", "test1"),
+          new Account("test2", "test2"),
+// asd
+        });
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20531711.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20531711.output
new file mode 100644
index 0000000..806f4bd
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20531711.output
@@ -0,0 +1,16 @@
+class B20531711 {
+  void m() {
+    when(mLoginHelper.getAllAccounts())
+        .thenReturn(
+            new Account[] {
+              new Account("test1", "test1"), new Account("test2", "test2"),
+            });
+
+    when(mLoginHelper.getAllAccounts())
+        .thenReturn(
+            new Account[] {
+              new Account("test1", "test1"), new Account("test2", "test2"),
+              // asd
+            });
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.input
new file mode 100644
index 0000000..30c232b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.input
@@ -0,0 +1,146 @@
+package com.google.googlejavaformat.java.test;
+
+class Test {
+  void m() {
+    if (!metadata.ignoreOutputTransformations()
+        && Producers.isListenableFutureMapKey(outputKey)) {
+      ImmutableList<ProducerNode<?>> nodes = createMapNodes((ProducerNode) node);
+      checkCollectionNodesAgainstWhitelist(nodes, whitelist);
+      return nodes;
+
+    } else if (!metadata.ignoreOutputTransformations()
+        && Producers.isListenableFutureListKey(outputKey)) {
+      ImmutableList<ProducerNode<?>> nodes = createListNodes((ProducerNode) node);
+      return nodes;
+    }
+
+    Set<Short> shorts = new HashSet<>();
+    for (short i = 0; i < 99; ++i) {
+
+      shorts.add(i);
+      shorts.remove(i - 1);
+
+    }
+
+    short i = 0;
+    do {
+
+      shorts.add(i);
+      shorts.remove(i - 1);
+      i++;
+
+    } while (i < 99);
+
+    if (true) {
+
+      System.err.println("Hi");
+
+    }
+
+    if (true) {
+
+      System.err.println("Hi");
+
+    } else {
+
+      System.err.println("Hi");
+
+    }
+
+    if (true) {
+
+      System.err.println("Hi");
+
+    } else if (true) {
+
+      System.err.println("Hi");
+
+    } else {
+
+      System.err.println("Hi");
+
+    }
+
+    try {
+
+      throw new Exception();
+
+    } catch (Exception e) {
+
+      System.err.println("Hi");
+
+    }
+
+    try {
+
+      throw new Exception();
+
+    } finally {
+
+      System.err.println("Hi");
+
+    }
+
+    try {
+
+      throw new Exception();
+
+    } catch (Exception e) {
+
+      System.err.println("Hi");
+
+    } finally {
+
+      System.err.println("Hi");
+
+    }
+
+    try {
+
+      throw new Exception();
+
+    } catch (Exception e) {
+
+      System.err.println("Hi");
+
+    } catch (Exception e) {
+
+      System.err.println("Hi");
+
+    } finally {
+
+      System.err.println("Hi");
+
+    }
+
+    try (Lock l = lock.lock()) {}
+
+    try (Lock l = lock.lock()) {} finally {}
+
+    for (;;) {}
+
+    while (true) {}
+
+    do {} while (true);
+
+    if (true) {}
+
+    if (true) {} else if (true) {}
+
+    if (true) {} else if (true) {} else if (true) {}
+
+    if (true) {} else if (true) {} else {}
+
+    if (true) {} else if (true) {} else if (true) {} else {}
+
+    try {} catch (Exception e) {} catch (Exception e) {} finally {}
+
+    try {} catch (Exception e) {} finally {}
+
+    try {} catch (Exception e) {} catch (Exception e) {}
+
+    try {} catch (Exception e) {}
+
+  }
+
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.output
new file mode 100644
index 0000000..950f4eb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.output
@@ -0,0 +1,161 @@
+package com.google.googlejavaformat.java.test;
+
+class Test {
+  void m() {
+    if (!metadata.ignoreOutputTransformations() && Producers.isListenableFutureMapKey(outputKey)) {
+      ImmutableList<ProducerNode<?>> nodes = createMapNodes((ProducerNode) node);
+      checkCollectionNodesAgainstWhitelist(nodes, whitelist);
+      return nodes;
+
+    } else if (!metadata.ignoreOutputTransformations()
+        && Producers.isListenableFutureListKey(outputKey)) {
+      ImmutableList<ProducerNode<?>> nodes = createListNodes((ProducerNode) node);
+      return nodes;
+    }
+
+    Set<Short> shorts = new HashSet<>();
+    for (short i = 0; i < 99; ++i) {
+
+      shorts.add(i);
+      shorts.remove(i - 1);
+    }
+
+    short i = 0;
+    do {
+
+      shorts.add(i);
+      shorts.remove(i - 1);
+      i++;
+
+    } while (i < 99);
+
+    if (true) {
+
+      System.err.println("Hi");
+    }
+
+    if (true) {
+
+      System.err.println("Hi");
+
+    } else {
+
+      System.err.println("Hi");
+    }
+
+    if (true) {
+
+      System.err.println("Hi");
+
+    } else if (true) {
+
+      System.err.println("Hi");
+
+    } else {
+
+      System.err.println("Hi");
+    }
+
+    try {
+
+      throw new Exception();
+
+    } catch (Exception e) {
+
+      System.err.println("Hi");
+    }
+
+    try {
+
+      throw new Exception();
+
+    } finally {
+
+      System.err.println("Hi");
+    }
+
+    try {
+
+      throw new Exception();
+
+    } catch (Exception e) {
+
+      System.err.println("Hi");
+
+    } finally {
+
+      System.err.println("Hi");
+    }
+
+    try {
+
+      throw new Exception();
+
+    } catch (Exception e) {
+
+      System.err.println("Hi");
+
+    } catch (Exception e) {
+
+      System.err.println("Hi");
+
+    } finally {
+
+      System.err.println("Hi");
+    }
+
+    try (Lock l = lock.lock()) {}
+
+    try (Lock l = lock.lock()) {
+    } finally {
+    }
+
+    for (; ; ) {}
+
+    while (true) {}
+
+    do {} while (true);
+
+    if (true) {}
+
+    if (true) {
+    } else if (true) {
+    }
+
+    if (true) {
+    } else if (true) {
+    } else if (true) {
+    }
+
+    if (true) {
+    } else if (true) {
+    } else {
+    }
+
+    if (true) {
+    } else if (true) {
+    } else if (true) {
+    } else {
+    }
+
+    try {
+    } catch (Exception e) {
+    } catch (Exception e) {
+    } finally {
+    }
+
+    try {
+    } catch (Exception e) {
+    } finally {
+    }
+
+    try {
+    } catch (Exception e) {
+    } catch (Exception e) {
+    }
+
+    try {
+    } catch (Exception e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20567842.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20567842.input
new file mode 100644
index 0000000..5a7b3c5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20567842.input
@@ -0,0 +1,10 @@
+public class B20567842 {
+  // don't try to wrap the rhs as '{1, 2, 3}', go to block-like initializer style:
+  int[] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = {1, 2, 3};
+
+  // force block style, but not one-per-line for trailing ',':
+  int[] x = {a.b, true ? 1 : 2, CONST,};
+
+  // don't format one-per-line here:
+  int[] x = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20567842.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20567842.output
new file mode 100644
index 0000000..ec2a4df
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20567842.output
@@ -0,0 +1,17 @@
+public class B20567842 {
+  // don't try to wrap the rhs as '{1, 2, 3}', go to block-like initializer style:
+  int[] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = {
+    1, 2, 3
+  };
+
+  // force block style, but not one-per-line for trailing ',':
+  int[] x = {
+    a.b, true ? 1 : 2, CONST,
+  };
+
+  // don't format one-per-line here:
+  int[] x = {
+    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+    1, 1, 1, 1
+  };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20569245.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20569245.input
new file mode 100644
index 0000000..fcdf5ed
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20569245.input
@@ -0,0 +1,35 @@
+class B20569245 {
+  void m() {
+    if (true) System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else if (true) System.err.println("asd");
+    else System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else if (true) System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else System.err.println("asd");
+
+    if (true) 
+      System.err.println("asd");
+
+    if (true) 
+      System.err.println("asd");
+    else if (true) 
+      System.err.println("asd");
+    else 
+      System.err.println("asd");
+
+    if (true) 
+      System.err.println("asd");
+    else if (true) 
+      System.err.println("asd");
+
+    if (true) 
+      System.err.println("asd");
+    else 
+      System.err.println("asd");
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20569245.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20569245.output
new file mode 100644
index 0000000..d21176c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20569245.output
@@ -0,0 +1,27 @@
+class B20569245 {
+  void m() {
+    if (true) System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else if (true) System.err.println("asd");
+    else System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else if (true) System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else System.err.println("asd");
+
+    if (true) System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else if (true) System.err.println("asd");
+    else System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else if (true) System.err.println("asd");
+
+    if (true) System.err.println("asd");
+    else System.err.println("asd");
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20577626.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20577626.input
new file mode 100644
index 0000000..b14efeb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20577626.input
@@ -0,0 +1,18 @@
+class B20577626 {
+  private @Mock
+  GsaConfigFlags mGsaConfig;
+
+  @Foo
+  @Bar
+  private @Mock
+  GsaConfigFlags mGsaConfig;
+
+  @Foo
+  abstract @Bar
+  void m() {}
+
+  @Foo
+  @Baz
+  abstract @Bar
+  void m() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20577626.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20577626.output
new file mode 100644
index 0000000..bc3df14
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20577626.output
@@ -0,0 +1,12 @@
+class B20577626 {
+  private @Mock GsaConfigFlags mGsaConfig;
+
+  @Foo @Bar private @Mock GsaConfigFlags mGsaConfig;
+
+  @Foo
+  abstract @Bar void m() {}
+
+  @Foo
+  @Baz
+  abstract @Bar void m() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20578077.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20578077.input
new file mode 100644
index 0000000..11b12ed
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20578077.input
@@ -0,0 +1,13 @@
+class B20578077 {
+  {
+    new IteratorTester<Integer>(
+        4, MODIFIABLE, newArrayList(1, 2), IteratorTester.KnownOrder.KNOWN_ORDER) {
+      @Override
+      protected Iterator<Integer> newTargetIterator() {
+        Iterator<Integer> iterator = Lists.newArrayList(1, 2).iterator();
+        return new IteratorWithSunJavaBug6529795<Integer>(iterator);
+      }
+    }
+    .test();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20578077.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20578077.output
new file mode 100644
index 0000000..d5b545e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20578077.output
@@ -0,0 +1,12 @@
+class B20578077 {
+  {
+    new IteratorTester<Integer>(
+        4, MODIFIABLE, newArrayList(1, 2), IteratorTester.KnownOrder.KNOWN_ORDER) {
+      @Override
+      protected Iterator<Integer> newTargetIterator() {
+        Iterator<Integer> iterator = Lists.newArrayList(1, 2).iterator();
+        return new IteratorWithSunJavaBug6529795<Integer>(iterator);
+      }
+    }.test();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20580212.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20580212.input
new file mode 100644
index 0000000..c203e3b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20580212.input
@@ -0,0 +1,16 @@
+class B20580212 {
+  void m() {
+    GroupExpansionReply mockIsgReply =
+        buildRecipientListSubGroupReply(
+            RECIPIENT1,
+            alternatives /* isgExpansions */,
+            alternativesDeltas /* isgExpansionsScoreDeltas */,
+            false /* withRecipient */,
+            0 /* recipientScoreDelta */);
+
+    try {
+    } catch (IllegalStateException e) { /* expected */ }
+  }
+
+  static class ThrowsAtEndException extends RuntimeException { /* nothing */ }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20580212.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20580212.output
new file mode 100644
index 0000000..df3f684
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20580212.output
@@ -0,0 +1,20 @@
+class B20580212 {
+  void m() {
+    GroupExpansionReply mockIsgReply =
+        buildRecipientListSubGroupReply(
+            RECIPIENT1,
+            alternatives /* isgExpansions */,
+            alternativesDeltas /* isgExpansionsScoreDeltas */,
+            false /* withRecipient */,
+            0 /* recipientScoreDelta */);
+
+    try {
+    } catch (IllegalStateException e) {
+      /* expected */
+    }
+  }
+
+  static class ThrowsAtEndException extends RuntimeException {
+    /* nothing */
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.input
new file mode 100644
index 0000000..957c2df
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.input
@@ -0,0 +1,185 @@
+class B20701054 {
+  void m() {
+    ImmutableList<String> x = ImmutableList.builder().add(1).build();
+    OptionalBinder.<ASD>newOptionalBinder(binder(), InputWhitelist.class).setBinding().to(
+        AllInputWhitelist.class);
+
+    Foo z = Foo.INSTANCE.field;
+    Foo z = Foo.INSTANCE.field.field;
+    Foo z = Foo.INSTANCE.field.field.field;
+    Foo z = Foo.INSTANCE.field.field.field.field;
+    Foo z = Foo.INSTANCE.field.field.field.field.field;
+
+    ImmutableList<String> x = ImmutableList.INSTANCE.add(1).build();
+    ImmutableList<String> x = ImmutableList.INSTANCE.add(1).add(2).build();
+    ImmutableList<String> x = ImmutableList.INSTANCE.add(1).add(2).add(3).build();
+    ImmutableList<String> x = ImmutableList.INSTANCE.add(1).add(2).add(3).add(4).build();
+
+    ImmutableList<String> x = ImmutableList.builder().add(1).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).add(3).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).add(3).add(4).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).add(3).add(4).add(5).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).add(3).add(4).add(5).add(6).build();
+
+    ImmutableList<String> x =
+        new ImmutableList.Builder<>()
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .build();
+
+    ImmutableList<String> x =
+        ImmutableList.new Builder<>()
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .build();
+
+    System.err.println(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    Class.my.contrived.example.function(
+        veryLongArgumentxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    PTable<Long, List<PageSpeedUrlResult>> latestResults =
+        FJ.joinOneShot2(accountSummaryProvider.get(), results).parallelDo(
+            "extractPageSpeedUrls", new ExtractPageSpeedUrlsFn(false));
+
+    PTable<Long, Long> adImpressionsByAccount =
+        impressionExtractor.getImpressionBreakdownByAccountId().parallelDo(
+            "rekeyAdImpressionsByAccountId", new ExtractTotalImpressionsFn());
+
+    PTable<String, CrawlError> crawlerErrorsByCode =
+        crawlReportSource.read("readCrawlReportTable").parallelDo(
+            "reKeyErrorsByPropertyCode", new RekeyErrorsByPropertyCodeFn());
+
+    if (ImmutableList.builder().add(1).add(2).add(3)) {
+    }
+
+    if (ImmutableList.builder()
+        .add(1)
+        .add(2)
+        .add(3)
+        .add(4)) {}
+
+    if (value.fst.name.toString().equals("value")) {
+    }
+
+    analysis().analyze(compilationUnit, context, configuration, new DescriptionListener() {
+      @Override
+      public void onDescribed(Description description) {
+        listener.onDescribed(description.filterFixes(new Predicate<Fix>() {
+          @Override
+          public boolean apply(Fix fix) {
+            return compiles(fix, (JCCompilationUnit) compilationUnit, context);
+          }
+        }));
+      }
+    });
+    
+    (test ? xxx : yyy).zzz(xxx);
+
+    (test ? xxx : yyy)
+        .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    (test
+            ? xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+            : yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy)
+        .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    zzzzzzzzzzzzzzzzzzzz.zzzzzzzzzzzzzzzzzzzzzzzzzzz.zzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    field.field.field.field.field.field.field.field.field.field.field.field.field.field.field.field
+        .field.field.field.field.field.method(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    (test
+            ? xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+            : yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy)
+        .field.field.field.field.field.field.field.field.field.field.field.field.field.method(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    (test
+            ? xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+            : yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy)
+        .field.field.field.field.field.field.field.field.field.field.field.field.field.field.field
+            .field.field.field.field.field.field.method(
+                xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    field.field.field.field.field.field.method(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    field.field.field.field.field.field.method(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
+        .yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+        .yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+        .yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+        .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
+        .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
+        .yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy();
+
+    edu.oswego.cs.dl.util.concurrent.misc.ImmutableList.builder(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz);
+
+    edu.oswego.cs.dl.util.concurrent.misc.zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+                .ImmutableList.builder(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz);
+
+    int u =
+        edu.oswego.cs.dl.util.concurrent.misc.zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+                    .ImmutableList.builder(
+                zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+            .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+            .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+            .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+            .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz;
+
+    for (String configured : errorProneOptions().getSeverityMap().keySet()) {}
+
+    return fffffffffff(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    return fffffffffff(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
+        .fffffffffffff(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.output
new file mode 100644
index 0000000..7ce6fda
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.output
@@ -0,0 +1,196 @@
+class B20701054 {
+  void m() {
+    ImmutableList<String> x = ImmutableList.builder().add(1).build();
+    OptionalBinder.<ASD>newOptionalBinder(binder(), InputWhitelist.class)
+        .setBinding()
+        .to(AllInputWhitelist.class);
+
+    Foo z = Foo.INSTANCE.field;
+    Foo z = Foo.INSTANCE.field.field;
+    Foo z = Foo.INSTANCE.field.field.field;
+    Foo z = Foo.INSTANCE.field.field.field.field;
+    Foo z = Foo.INSTANCE.field.field.field.field.field;
+
+    ImmutableList<String> x = ImmutableList.INSTANCE.add(1).build();
+    ImmutableList<String> x = ImmutableList.INSTANCE.add(1).add(2).build();
+    ImmutableList<String> x = ImmutableList.INSTANCE.add(1).add(2).add(3).build();
+    ImmutableList<String> x = ImmutableList.INSTANCE.add(1).add(2).add(3).add(4).build();
+
+    ImmutableList<String> x = ImmutableList.builder().add(1).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).add(3).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).add(3).add(4).build();
+    ImmutableList<String> x = ImmutableList.builder().add(1).add(2).add(3).add(4).add(5).build();
+    ImmutableList<String> x =
+        ImmutableList.builder().add(1).add(2).add(3).add(4).add(5).add(6).build();
+
+    ImmutableList<String> x =
+        new ImmutableList.Builder<>()
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .build();
+
+    ImmutableList<String> x =
+        ImmutableList.new Builder<>()
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .add(xxxxx)
+            .build();
+
+    System.err.println(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    Class.my.contrived.example.function(
+        veryLongArgumentxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    PTable<Long, List<PageSpeedUrlResult>> latestResults =
+        FJ.joinOneShot2(accountSummaryProvider.get(), results)
+            .parallelDo("extractPageSpeedUrls", new ExtractPageSpeedUrlsFn(false));
+
+    PTable<Long, Long> adImpressionsByAccount =
+        impressionExtractor
+            .getImpressionBreakdownByAccountId()
+            .parallelDo("rekeyAdImpressionsByAccountId", new ExtractTotalImpressionsFn());
+
+    PTable<String, CrawlError> crawlerErrorsByCode =
+        crawlReportSource
+            .read("readCrawlReportTable")
+            .parallelDo("reKeyErrorsByPropertyCode", new RekeyErrorsByPropertyCodeFn());
+
+    if (ImmutableList.builder().add(1).add(2).add(3)) {}
+
+    if (ImmutableList.builder().add(1).add(2).add(3).add(4)) {}
+
+    if (value.fst.name.toString().equals("value")) {}
+
+    analysis()
+        .analyze(
+            compilationUnit,
+            context,
+            configuration,
+            new DescriptionListener() {
+              @Override
+              public void onDescribed(Description description) {
+                listener.onDescribed(
+                    description.filterFixes(
+                        new Predicate<Fix>() {
+                          @Override
+                          public boolean apply(Fix fix) {
+                            return compiles(fix, (JCCompilationUnit) compilationUnit, context);
+                          }
+                        }));
+              }
+            });
+
+    (test ? xxx : yyy).zzz(xxx);
+
+    (test ? xxx : yyy)
+        .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    (test
+            ? xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+            : yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy)
+        .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    zzzzzzzzzzzzzzzzzzzz.zzzzzzzzzzzzzzzzzzzzzzzzzzz.zzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    field.field.field.field.field.field.field.field.field.field.field.field.field.field.field.field
+        .field.field.field.field.field.method(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    (test
+            ? xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+            : yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy)
+        .field.field.field.field.field.field.field.field.field.field.field.field.field.method(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    (test
+            ? xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+            : yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy)
+        .field.field.field.field.field.field.field.field.field.field.field.field.field.field.field
+            .field.field.field.field.field.field.method(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    field.field.field.field.field.field.method(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    field
+        .field
+        .field
+        .field
+        .field
+        .field
+        .method(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
+        .yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+        .yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+        .yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
+        .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
+        .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
+        .yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy();
+
+    edu.oswego.cs.dl.util.concurrent.misc.ImmutableList.builder(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz);
+
+    edu.oswego.cs.dl.util.concurrent.misc.zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+        .ImmutableList.builder(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+        .zzzzzzz(
+            zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz);
+
+    int u =
+        edu.oswego.cs.dl.util.concurrent.misc.zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+            .ImmutableList.builder(
+                zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
+            .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+            .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+            .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+            .zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz;
+
+    for (String configured : errorProneOptions().getSeverityMap().keySet()) {}
+
+    return fffffffffff(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    return fffffffffff(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
+        .fffffffffffff(
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input
new file mode 100644
index 0000000..86e46d5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input
@@ -0,0 +1,31 @@
+public class B20844369 {
+  private static final String ID_PATTERN =
+  // TODO(daw): add min/max lengths for the numbers here, e.g. android ID
+  "(?:(?<androidId>\\d+)\\+)?" // optional Android ID
+      + "(?<type>\\d+)" // type
+      + ":"
+      + "(?<timestamp>\\d+)" // timestamp
+      + "(?<subtype>%" // begin optional subtype
+      + "(?:(?<userId>\\d+)#)?" // subtype's optional user ID, followed by a hash
+      + "(?<categoryHash>[0-9a-fA-F]{8})" // subtype's category hash
+      + "(?<tokenHash>[0-9a-fA-F]{8})" // subtype's token hash
+      + ")?"; // end optional subtype
+
+  int x = //foo
+      42 + //bar
+          1;
+
+  int x =
+      //foo
+      42 + //bar
+          1;
+
+  int x = /*foo*/
+      42 + //bar
+          1;
+
+  int x =
+      /*foo*/
+      42 + //bar
+          1;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output
new file mode 100644
index 0000000..982dc2b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output
@@ -0,0 +1,33 @@
+public class B20844369 {
+  private static final String ID_PATTERN =
+      // TODO(daw): add min/max lengths for the numbers here, e.g. android ID
+      "(?:(?<androidId>\\d+)\\+)?" // optional Android ID
+          + "(?<type>\\d+)" // type
+          + ":"
+          + "(?<timestamp>\\d+)" // timestamp
+          + "(?<subtype>%" // begin optional subtype
+          + "(?:(?<userId>\\d+)#)?" // subtype's optional user ID, followed by a hash
+          + "(?<categoryHash>[0-9a-fA-F]{8})" // subtype's category hash
+          + "(?<tokenHash>[0-9a-fA-F]{8})" // subtype's token hash
+          + ")?"; // end optional subtype
+
+  int x = // foo
+      42 + // bar
+          1;
+
+  int x =
+      // foo
+      42
+          + // bar
+          1;
+
+  int x = /*foo*/
+      42 + // bar
+          1;
+
+  int x =
+      /*foo*/
+      42
+          + // bar
+          1;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20915776.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20915776.input
new file mode 100644
index 0000000..29d59e5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20915776.input
@@ -0,0 +1,10 @@
+class B20915776 {
+  @Inject
+    void ChunkAnalysis(
+    // TODO(user) ...
+    @Parent DatasetSource supporters,
+    @Child DatasetSource children,
+    @Parent Provider<SupportsDecider> parentSupportsDecider,
+    @Child Provider<SupportsDecider> childSupportsDecider) {
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20915776.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20915776.output
new file mode 100644
index 0000000..54f2a6d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20915776.output
@@ -0,0 +1,9 @@
+class B20915776 {
+  @Inject
+  void ChunkAnalysis(
+      // TODO(user) ...
+      @Parent DatasetSource supporters,
+      @Child DatasetSource children,
+      @Parent Provider<SupportsDecider> parentSupportsDecider,
+      @Child Provider<SupportsDecider> childSupportsDecider) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21031147.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21031147.input
new file mode 100644
index 0000000..ac2bf98
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21031147.input
@@ -0,0 +1,6 @@
+public class B21031147 {
+  {
+    return new StringBuilder(maxLength)
+        .append(seq, 0, truncationLength).append(truncationIndicator).toString();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21031147.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21031147.output
new file mode 100644
index 0000000..8fbeeae
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21031147.output
@@ -0,0 +1,8 @@
+public class B21031147 {
+  {
+    return new StringBuilder(maxLength)
+        .append(seq, 0, truncationLength)
+        .append(truncationIndicator)
+        .toString();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21105569.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21105569.input
new file mode 100644
index 0000000..49b6d13
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21105569.input
@@ -0,0 +1,9 @@
+class B21105569 {
+  void f() {
+    // asd
+  }
+  void g() {
+    /* asd */
+  }
+  void h() { /* asd */ }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21105569.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21105569.output
new file mode 100644
index 0000000..8b5dfee
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21105569.output
@@ -0,0 +1,13 @@
+class B21105569 {
+  void f() {
+    // asd
+  }
+
+  void g() {
+    /* asd */
+  }
+
+  void h() {
+    /* asd */
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21185365.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21185365.input
new file mode 100644
index 0000000..7ffc27c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21185365.input
@@ -0,0 +1,5 @@
+class B21185365 {
+  {
+    for (int i = 0; i < s.length();) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21185365.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21185365.output
new file mode 100644
index 0000000..06beb1d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21185365.output
@@ -0,0 +1,5 @@
+class B21185365 {
+  {
+    for (int i = 0; i < s.length(); ) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21192435.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21192435.input
new file mode 100644
index 0000000..ffebae4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21192435.input
@@ -0,0 +1,12 @@
+package testdata.output;
+
+public class B21192435 {
+
+  static class Test extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx implements Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx{}
+
+  static class Testxxxxxxxxx extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx implements Xxxxx{}
+
+  static class Testxxxxxxxxx extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx implements Xxxxxxxxxxxxxxxx{}
+
+  static class Test<Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx> extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx implements Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21192435.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21192435.output
new file mode 100644
index 0000000..9ddb4b8
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21192435.output
@@ -0,0 +1,18 @@
+package testdata.output;
+
+public class B21192435 {
+
+  static class Test extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      implements Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  static class Testxxxxxxxxx
+      extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx implements Xxxxx {}
+
+  static class Testxxxxxxxxx
+      extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      implements Xxxxxxxxxxxxxxxx {}
+
+  static class Test<
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx>
+      extends Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx implements Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21278211.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21278211.input
new file mode 100644
index 0000000..13e1559
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21278211.input
@@ -0,0 +1,23 @@
+class B21278211 {
+  void m() {
+    return names.isEmpty() || names.size() == 1 && names.get(0).toString().isEmpty();
+
+    // use unified breaks for all chained dereferences
+
+    x.f().g();
+    x.fzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz()
+        .gzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz();
+
+    x.f().g().h();
+    x
+        .f()
+        .g()
+        .hzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz();
+
+    x
+        .f()
+        .g()
+        .h()
+        .i();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21278211.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21278211.output
new file mode 100644
index 0000000..fa8ba52
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21278211.output
@@ -0,0 +1,18 @@
+class B21278211 {
+  void m() {
+    return names.isEmpty() || names.size() == 1 && names.get(0).toString().isEmpty();
+
+    // use unified breaks for all chained dereferences
+
+    x.f().g();
+    x.fzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz()
+        .gzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz();
+
+    x.f().g().h();
+    x.f()
+        .g()
+        .hzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz();
+
+    x.f().g().h().i();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21283374.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21283374.input
new file mode 100644
index 0000000..0402caa
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21283374.input
@@ -0,0 +1,57 @@
+class B21283374 {
+  void f(boolean flagA, boolean flagB, boolean flagC, boolean flagD, boolean flagE, boolean flagF, boolean flagG, boolean flagH, boolean flagI, boolean flagJ, boolean flagK) {}
+
+  {
+    f(
+        /*flagA=*/Boolean.TRUE,
+        /*flagB=*/Boolean.FALSE,
+        /*flagC=*/Boolean.FALSE,
+        /*flagD=*/Boolean.FALSE,
+        /*flagE=*/Boolean.FALSE,
+        /*flagF=*/Boolean.FALSE,
+        /*flagG=*/Boolean.FALSE,
+        /*flagH=*/Boolean.FALSE,
+        /*flagI=*/Boolean.FALSE,
+        /*flagJ=*/Boolean.FALSE,
+        /*flagK=*/Boolean.FALSE);
+
+    f(
+        /*flagA=*/ Boolean.TRUE,
+        /*flagB=*/ Boolean.FALSE,
+        /*flagC=*/ Boolean.FALSE,
+        /*flagD=*/ Boolean.FALSE,
+        /*flagE=*/ Boolean.FALSE,
+        /*flagF=*/ Boolean.FALSE,
+        /*flagG=*/ Boolean.FALSE,
+        /*flagH=*/ Boolean.FALSE,
+        /*flagI=*/ Boolean.FALSE,
+        /*flagJ=*/ Boolean.FALSE,
+        /*flagK=*/ Boolean.FALSE);
+
+    assertThat(foo.barAndBaz(/*paramName=*/false, thingy)).isEqualTo(new Something(""));
+    assertThat(foo.barAndBaz( /*paramName=*/false, thingy)).isEqualTo(new Something(""));
+    assertThat(foo.barAndBaz(/*paramName=*/ false, thingy)).isEqualTo(new Something(""));
+
+    f(/*paramName=*/ false);
+
+    assertThat__________________________________________________________(/*paramName=*/ false, thingy);
+    assertThat__________________________________________________________(
+        /*paramName=*/ false, thingy);
+
+    f(
+        arg1, /* which arg is this attached to? */
+        arg2,
+        arg3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+
+    f(
+        /* paramName1 */ arg1,
+        /* paramName2 */ arg2,
+        arg3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    f(
+        arg1, /* paramName1 */
+        arg2, /* paramName2 */
+        arg3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21283374.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21283374.output
new file mode 100644
index 0000000..f98a37b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21283374.output
@@ -0,0 +1,68 @@
+class B21283374 {
+  void f(
+      boolean flagA,
+      boolean flagB,
+      boolean flagC,
+      boolean flagD,
+      boolean flagE,
+      boolean flagF,
+      boolean flagG,
+      boolean flagH,
+      boolean flagI,
+      boolean flagJ,
+      boolean flagK) {}
+
+  {
+    f(
+        /*flagA=*/ Boolean.TRUE,
+        /*flagB=*/ Boolean.FALSE,
+        /*flagC=*/ Boolean.FALSE,
+        /*flagD=*/ Boolean.FALSE,
+        /*flagE=*/ Boolean.FALSE,
+        /*flagF=*/ Boolean.FALSE,
+        /*flagG=*/ Boolean.FALSE,
+        /*flagH=*/ Boolean.FALSE,
+        /*flagI=*/ Boolean.FALSE,
+        /*flagJ=*/ Boolean.FALSE,
+        /*flagK=*/ Boolean.FALSE);
+
+    f(
+        /*flagA=*/ Boolean.TRUE,
+        /*flagB=*/ Boolean.FALSE,
+        /*flagC=*/ Boolean.FALSE,
+        /*flagD=*/ Boolean.FALSE,
+        /*flagE=*/ Boolean.FALSE,
+        /*flagF=*/ Boolean.FALSE,
+        /*flagG=*/ Boolean.FALSE,
+        /*flagH=*/ Boolean.FALSE,
+        /*flagI=*/ Boolean.FALSE,
+        /*flagJ=*/ Boolean.FALSE,
+        /*flagK=*/ Boolean.FALSE);
+
+    assertThat(foo.barAndBaz(/*paramName=*/ false, thingy)).isEqualTo(new Something(""));
+    assertThat(foo.barAndBaz(/*paramName=*/ false, thingy)).isEqualTo(new Something(""));
+    assertThat(foo.barAndBaz(/*paramName=*/ false, thingy)).isEqualTo(new Something(""));
+
+    f(/*paramName=*/ false);
+
+    assertThat__________________________________________________________(
+        /*paramName=*/ false, thingy);
+    assertThat__________________________________________________________(
+        /*paramName=*/ false, thingy);
+
+    f(
+        arg1, /* which arg is this attached to? */
+        arg2,
+        arg3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    f(
+        /* paramName1 */ arg1,
+        /* paramName2 */ arg2,
+        arg3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+
+    f(
+        arg1, /* paramName1 */
+        arg2, /* paramName2 */
+        arg3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21305044.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21305044.input
new file mode 100644
index 0000000..049233c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21305044.input
@@ -0,0 +1,32 @@
+class B21305044 {
+
+  class T<@Nullable X extends @Nullable Object,
+      @Nullable Y extends @Nullable Object,
+      @Nullable Z extends @Nullable Object>
+      extends Function<@Nullable X, @Nullable Y>
+      implements Predicate<@Nullable Z> {
+    public T(@Nullable T this, List<@Nullable X> xs) {
+    }
+
+    public T(@Nullable T B21305044.this, List<@Nullable X> xs) {
+    }
+   
+    List<@Nullable ?> a;
+    List<@Nullable ? extends @Nullable Object> b;
+    List<@Nullable ? super @Nullable Object> c;
+
+    List<? super @Crazy(key=value, other=@Crazier({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})) Object> c;
+  }
+
+  Function f = com.foo.bar.Baz::func;
+  Function g = new Baz()::func;
+  Function h = Baz::new;
+  Function i = com.foo.bar.VeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongBaz::new;
+
+  {
+    Function f = () -> moderatelyLongResult;
+    Function f = () -> breakableResult + breakableResult + breakableResult + breakableResult + breakableResult + breakableResult + breakableResult + breakableResult + breakableResult;
+    Function f = () -> System.err.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+    Function f = (someParam) -> System.err.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21305044.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21305044.output
new file mode 100644
index 0000000..3273809
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21305044.output
@@ -0,0 +1,57 @@
+class B21305044 {
+
+  class T<
+          @Nullable X extends @Nullable Object,
+          @Nullable Y extends @Nullable Object,
+          @Nullable Z extends @Nullable Object>
+      extends Function<@Nullable X, @Nullable Y> implements Predicate<@Nullable Z> {
+    public T(@Nullable T this, List<@Nullable X> xs) {}
+
+    public T(@Nullable T B21305044.this, List<@Nullable X> xs) {}
+
+    List<@Nullable ?> a;
+    List<@Nullable ? extends @Nullable Object> b;
+    List<@Nullable ? super @Nullable Object> c;
+
+    List<
+            ? super
+                @Crazy(
+                    key = value,
+                    other =
+                        @Crazier({
+                          1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+                        }))
+                Object>
+        c;
+  }
+
+  Function f = com.foo.bar.Baz::func;
+  Function g = new Baz()::func;
+  Function h = Baz::new;
+  Function i =
+      com.foo.bar
+              .VeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongBaz
+          ::new;
+
+  {
+    Function f = () -> moderatelyLongResult;
+    Function f =
+        () ->
+            breakableResult
+                + breakableResult
+                + breakableResult
+                + breakableResult
+                + breakableResult
+                + breakableResult
+                + breakableResult
+                + breakableResult
+                + breakableResult;
+    Function f =
+        () ->
+            System.err.println(
+                "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+    Function f =
+        (someParam) ->
+            System.err.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21327412.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21327412.input
new file mode 100644
index 0000000..2b2d4e0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21327412.input
@@ -0,0 +1,3 @@
+class B21327412 {
+  int i = Data._ID;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21327412.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21327412.output
new file mode 100644
index 0000000..2b2d4e0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21327412.output
@@ -0,0 +1,3 @@
+class B21327412 {
+  int i = Data._ID;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21329312.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21329312.input
new file mode 100644
index 0000000..8fa68c0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21329312.input
@@ -0,0 +1,6 @@
+public class B21329312 {
+  boolean test() {
+    return wasEdited || didContactsChange || medicalInfoManager.getTemporaryProfilePicture() != null
+        || inMemoryAccessMedicalProfileAboveKeyguard != originalAccessMedicalProfileAboveKeyguard;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21329312.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21329312.output
new file mode 100644
index 0000000..5731bd9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21329312.output
@@ -0,0 +1,8 @@
+public class B21329312 {
+  boolean test() {
+    return wasEdited
+        || didContactsChange
+        || medicalInfoManager.getTemporaryProfilePicture() != null
+        || inMemoryAccessMedicalProfileAboveKeyguard != originalAccessMedicalProfileAboveKeyguard;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21331232.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21331232.input
new file mode 100644
index 0000000..3d2f94e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21331232.input
@@ -0,0 +1,13 @@
+class B21331232 {
+  private <SF, S>
+    S instantiate(
+      Class<?> wrapperType, Type t, FailureStrategy fs, SF factory, Iterable<T> data) {
+  }
+
+  public <T, C extends Iterable<T>>
+      IterableSubject<? extends IterableSubject<?, T, C>, T, C> that(@Nullable Iterable<T> target) {
+    return IterableSubject.create(getFailureStrategy(), target);
+  }
+
+  <T> boolean mightContain(T object, Funnel<? super T> funnels, int numHashFunctions, BitArray bits);
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21331232.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21331232.output
new file mode 100644
index 0000000..fb58ccf
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21331232.output
@@ -0,0 +1,12 @@
+class B21331232 {
+  private <SF, S> S instantiate(
+      Class<?> wrapperType, Type t, FailureStrategy fs, SF factory, Iterable<T> data) {}
+
+  public <T, C extends Iterable<T>> IterableSubject<? extends IterableSubject<?, T, C>, T, C> that(
+      @Nullable Iterable<T> target) {
+    return IterableSubject.create(getFailureStrategy(), target);
+  }
+
+  <T> boolean mightContain(
+      T object, Funnel<? super T> funnels, int numHashFunctions, BitArray bits);
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465217.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465217.input
new file mode 100644
index 0000000..3973420
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465217.input
@@ -0,0 +1,12 @@
+class B21465217 {
+  void m() {
+    try (JimfsOutputStream out2 = newOutputStream(false);
+    BufferedOutputStream bout = new BufferedOutputStream(out2);
+    OutputStreamWriter writer = new OutputStreamWriter(bout, UTF_8___________________________)) {
+    }
+
+    try (Writer sourceWriter = env.getFiler().createSourceFile(qualifiedNamezzzzzzzz).openWriter()) {
+      sourceWriter.append(fileContents);
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465217.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465217.output
new file mode 100644
index 0000000..add19d3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465217.output
@@ -0,0 +1,13 @@
+class B21465217 {
+  void m() {
+    try (JimfsOutputStream out2 = newOutputStream(false);
+        BufferedOutputStream bout = new BufferedOutputStream(out2);
+        OutputStreamWriter writer =
+            new OutputStreamWriter(bout, UTF_8___________________________)) {}
+
+    try (Writer sourceWriter =
+        env.getFiler().createSourceFile(qualifiedNamezzzzzzzz).openWriter()) {
+      sourceWriter.append(fileContents);
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465477.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465477.input
new file mode 100644
index 0000000..3f0ca4b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465477.input
@@ -0,0 +1,10 @@
+class B21465477 {
+
+  @Nullable private final String simpleFieldName;
+  @Nullable private final String shortFlagName;
+  private final String containerClassName;
+  private final String type;
+  private final String doc;
+  private final DocLevel docLevel;
+  @Nullable private final String altName;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465477.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465477.output
new file mode 100644
index 0000000..3f0ca4b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21465477.output
@@ -0,0 +1,10 @@
+class B21465477 {
+
+  @Nullable private final String simpleFieldName;
+  @Nullable private final String shortFlagName;
+  private final String containerClassName;
+  private final String type;
+  private final String doc;
+  private final DocLevel docLevel;
+  @Nullable private final String altName;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21585653.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21585653.input
new file mode 100644
index 0000000..7c6a25b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21585653.input
@@ -0,0 +1,6 @@
+class B21585653 {
+  {
+    try (X x = x;) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21585653.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21585653.output
new file mode 100644
index 0000000..09c2c87
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21585653.output
@@ -0,0 +1,5 @@
+class B21585653 {
+  {
+    try (X x = x; ) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21608216.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21608216.input
new file mode 100644
index 0000000..e986a8a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21608216.input
@@ -0,0 +1,56 @@
+@PluginInfo(shortName = "chromesuggestions",
+            owner = "mathp",
+            demoLink = "unspecified",
+            modules = { BackendsModule.class,
+                        CommonBackendsModule.class,
+                        ChromeSuggestionsPlugin.ChromeSuggestionsModule.class })
+@Module(library = true, complete = false)
+@Module(library = true, complete = false, injects = {ClassInfoTracker.class, JslayoutMetaProto.class})
+@Module(
+  library = true,
+  complete = false,
+  injects = {
+    ClassInfoTracker.class, JslayoutMetaProto.class, JslayoutMetaProto.class, JslayoutMeta.class
+  }
+)
+@Module(
+  library = true,
+  complete = false,
+  injects = {
+    ClassInfoTracker.class, JslayoutMetaProto.class, JslayoutMetaProto.class, JslayoutMeta.class, JslayoutMeta.class
+  }
+)
+@Module(injects = {One.class, Two.class}, includes = {Three.class, Four.class})
+@Module(
+  library = true,
+  complete = false,
+  numbers = {1, 2, 3}
+)
+@Module(
+  library = true,
+  complete = false,
+  numbers = {
+    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26
+  }
+)
+@Module(
+  library = true,
+  complete = false,
+  numbers = {
+    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
+    25, 26, 27, 28, 29, 30
+  }
+)
+@Module({ClassInfoTracker.class, JslayoutMetaProto.class, JslayoutMetaProto.class})
+@Module({
+  ClassInfoTracker.class, JslayoutMetaProto.class, JslayoutMetaProto.class, JslayoutMeta.class
+})
+@Module({
+  ClassInfoTracker.class,
+  JslayoutMetaProto.class,
+   JslayoutMetaProto.class,
+   JslayoutMeta.class,
+   JslayoutMeta.class
+})
+class B21608216 {
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21608216.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21608216.output
new file mode 100644
index 0000000..93b3f86
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21608216.output
@@ -0,0 +1,68 @@
+@PluginInfo(
+    shortName = "chromesuggestions",
+    owner = "mathp",
+    demoLink = "unspecified",
+    modules = {
+      BackendsModule.class,
+      CommonBackendsModule.class,
+      ChromeSuggestionsPlugin.ChromeSuggestionsModule.class
+    })
+@Module(library = true, complete = false)
+@Module(
+    library = true,
+    complete = false,
+    injects = {ClassInfoTracker.class, JslayoutMetaProto.class})
+@Module(
+    library = true,
+    complete = false,
+    injects = {
+      ClassInfoTracker.class,
+      JslayoutMetaProto.class,
+      JslayoutMetaProto.class,
+      JslayoutMeta.class
+    })
+@Module(
+    library = true,
+    complete = false,
+    injects = {
+      ClassInfoTracker.class,
+      JslayoutMetaProto.class,
+      JslayoutMetaProto.class,
+      JslayoutMeta.class,
+      JslayoutMeta.class
+    })
+@Module(
+    injects = {One.class, Two.class},
+    includes = {Three.class, Four.class})
+@Module(
+    library = true,
+    complete = false,
+    numbers = {1, 2, 3})
+@Module(
+    library = true,
+    complete = false,
+    numbers = {
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26
+    })
+@Module(
+    library = true,
+    complete = false,
+    numbers = {
+      1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
+      27, 28, 29, 30
+    })
+@Module({ClassInfoTracker.class, JslayoutMetaProto.class, JslayoutMetaProto.class})
+@Module({
+  ClassInfoTracker.class,
+  JslayoutMetaProto.class,
+  JslayoutMetaProto.class,
+  JslayoutMeta.class
+})
+@Module({
+  ClassInfoTracker.class,
+  JslayoutMetaProto.class,
+  JslayoutMetaProto.class,
+  JslayoutMeta.class,
+  JslayoutMeta.class
+})
+class B21608216 {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21647014.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21647014.input
new file mode 100644
index 0000000..949c7a3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21647014.input
@@ -0,0 +1,15 @@
+package test;
+
+import java.util.List;;
+
+;
+class Test {
+  ;
+  public int x = 42;;
+  ;;;
+  {
+    int x = 1;;;
+  }
+  ;
+}
+;
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21647014.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21647014.output
new file mode 100644
index 0000000..095ff07
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21647014.output
@@ -0,0 +1,22 @@
+package test;
+
+import java.util.List;
+;
+;
+
+class Test {
+  ;
+  public int x = 42;
+  ;
+  ;
+  ;
+  ;
+
+  {
+    int x = 1;
+    ;
+    ;
+  }
+  ;
+}
+;
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21954779.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21954779.input
new file mode 100644
index 0000000..804eb98
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21954779.input
@@ -0,0 +1,46 @@
+class B21954779 {
+  {
+    when(computeApi.findOrCreateVm(
+            VM_METADATA,
+            Optional.<InstanceProto>absent(),
+            AssignReservedIp.YES,
+            AttachDataDisk.YES))
+        .thenReturn(OPERATION);
+
+    fail(String.format(
+        "       Experiment name %s is used multiple times in %s", 
+        expName, envName, zzzzzzzzzzzz, zzzzzzzzzzzz));
+
+    new Handler(Looper.getMainLooper())
+        .post(
+            new Runnable() {
+              @Override
+              public void run() {
+                utils.showShortToast(
+                    application.getString(
+                        R.string.working_on_project_format,
+                        Utils.getProjectDisplayName(selectedProject)));
+              }
+            });
+
+    (new Handler())
+        .post(
+            new Runnable() {
+              @Override
+              public void run() {
+                showErrorAndSetDone(error);
+              }
+            });
+
+    getContext()
+        .getErrorReporter()
+        .log(new RuntimeException("Called layout with a non-zero segment index: " + segmentIndex));
+
+    bind(
+        new Key<ServiceMethodRunner<SyncReferenceNumberRequest, SyncReferenceNumberResponse>>() {});
+
+    // this isn't a feature; we'd prefer to keep `new Key...` as one unit
+    bind(
+        new Key<ServiceMethodRunner<SyncReferenceNumberRequest, SyncReferenceNumberResponses>>() {}).then();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21954779.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21954779.output
new file mode 100644
index 0000000..c1628b1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B21954779.output
@@ -0,0 +1,48 @@
+class B21954779 {
+  {
+    when(computeApi.findOrCreateVm(
+            VM_METADATA,
+            Optional.<InstanceProto>absent(),
+            AssignReservedIp.YES,
+            AttachDataDisk.YES))
+        .thenReturn(OPERATION);
+
+    fail(
+        String.format(
+            "       Experiment name %s is used multiple times in %s",
+            expName, envName, zzzzzzzzzzzz, zzzzzzzzzzzz));
+
+    new Handler(Looper.getMainLooper())
+        .post(
+            new Runnable() {
+              @Override
+              public void run() {
+                utils.showShortToast(
+                    application.getString(
+                        R.string.working_on_project_format,
+                        Utils.getProjectDisplayName(selectedProject)));
+              }
+            });
+
+    (new Handler())
+        .post(
+            new Runnable() {
+              @Override
+              public void run() {
+                showErrorAndSetDone(error);
+              }
+            });
+
+    getContext()
+        .getErrorReporter()
+        .log(new RuntimeException("Called layout with a non-zero segment index: " + segmentIndex));
+
+    bind(
+        new Key<ServiceMethodRunner<SyncReferenceNumberRequest, SyncReferenceNumberResponse>>() {});
+
+    // this isn't a feature; we'd prefer to keep `new Key...` as one unit
+    bind(new Key<
+            ServiceMethodRunner<SyncReferenceNumberRequest, SyncReferenceNumberResponses>>() {})
+        .then();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22166687.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22166687.input
new file mode 100644
index 0000000..33b4c13
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22166687.input
@@ -0,0 +1,18 @@
+class B22166687 {
+  public static class Wrapper
+      extends CrossComponentSetBindingsWrapper<
+      ExtensionWrapper<FootprintsGenericSearchResponseExtension>>
+      implements CrossComponentSetBindingsWrapper<
+      ExtensionWrapper<FootprintsGenericSearchResponseExtension>> {
+    void m() {}
+  }
+  public static class Wrapper
+      extends CrossComponentSetBindingsWrapper<
+      ExtensionWrapper<FootprintsGenericSearchResponseExtension>>
+      implements CrossComponentSetBindingsWrapper<
+      ExtensionWrapper<FootprintsGenericSearchResponseExtension>>,
+      CrossComponentSetBindingsWrapper<
+      ExtensionWrapper<FootprintsGenericSearchResponseExtension>> {
+    void m() {}
+  }
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22166687.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22166687.output
new file mode 100644
index 0000000..a3deda7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22166687.output
@@ -0,0 +1,19 @@
+class B22166687 {
+  public static class Wrapper
+      extends CrossComponentSetBindingsWrapper<
+          ExtensionWrapper<FootprintsGenericSearchResponseExtension>>
+      implements CrossComponentSetBindingsWrapper<
+          ExtensionWrapper<FootprintsGenericSearchResponseExtension>> {
+    void m() {}
+  }
+
+  public static class Wrapper
+      extends CrossComponentSetBindingsWrapper<
+          ExtensionWrapper<FootprintsGenericSearchResponseExtension>>
+      implements CrossComponentSetBindingsWrapper<
+              ExtensionWrapper<FootprintsGenericSearchResponseExtension>>,
+          CrossComponentSetBindingsWrapper<
+              ExtensionWrapper<FootprintsGenericSearchResponseExtension>> {
+    void m() {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22169269.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22169269.input
new file mode 100644
index 0000000..3caf937
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22169269.input
@@ -0,0 +1,14 @@
+class B22169269 {
+  {
+    try {
+    } catch (
+        ClassNotFoundException
+        | NoSuchMethodException
+        | InvocationTargetException
+        | IllegalAccessException e) {
+    }
+    try {
+    } catch (ClassNotFoundException | NoSuchMethodException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22169269.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22169269.output
new file mode 100644
index 0000000..6e46a36
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22169269.output
@@ -0,0 +1,13 @@
+class B22169269 {
+  {
+    try {
+    } catch (ClassNotFoundException
+        | NoSuchMethodException
+        | InvocationTargetException
+        | IllegalAccessException e) {
+    }
+    try {
+    } catch (ClassNotFoundException | NoSuchMethodException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22424362.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22424362.input
new file mode 100644
index 0000000..800b1f0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22424362.input
@@ -0,0 +1,8 @@
+class Test {
+  {
+    int x =
+      1
+
+      + 2;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22424362.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22424362.output
new file mode 100644
index 0000000..98fa938
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22424362.output
@@ -0,0 +1,5 @@
+class Test {
+  {
+    int x = 1 + 2;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22469536.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22469536.input
new file mode 100644
index 0000000..9f06d6f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22469536.input
@@ -0,0 +1,6 @@
+public class Test {
+  public static void foo() {
+    long x = -9223372036854775808L;
+    int y = -2147483648;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22469536.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22469536.output
new file mode 100644
index 0000000..9f06d6f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22469536.output
@@ -0,0 +1,6 @@
+public class Test {
+  public static void foo() {
+    long x = -9223372036854775808L;
+    int y = -2147483648;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22488373.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22488373.input
new file mode 100644
index 0000000..11747be
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22488373.input
@@ -0,0 +1,11 @@
+class B22488373 {
+  {
+    if (enumBindingKeys.contains(bindingKey)
+        && (bindingKey
+                .key()
+                .type()
+                .getKind()
+                .equals(DECLARED)
+            && !((DeclaredType) bindingKey.key().type()).getTypeArguments().isEmpty())) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22488373.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22488373.output
new file mode 100644
index 0000000..950c1dd
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22488373.output
@@ -0,0 +1,7 @@
+class B22488373 {
+  {
+    if (enumBindingKeys.contains(bindingKey)
+        && (bindingKey.key().type().getKind().equals(DECLARED)
+            && !((DeclaredType) bindingKey.key().type()).getTypeArguments().isEmpty())) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22610221.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22610221.input
new file mode 100644
index 0000000..eb05cfd
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22610221.input
@@ -0,0 +1,12 @@
+class B22610221 {
+  {
+    for (Map.Entry<Key<Object>, Object> entry :
+        FOO
+            .bar()
+            .bazs()
+            .<Object>thing(someThing)
+            .entrySet()) {
+      output.put(entry.getKey(), entry.getValue());
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22610221.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22610221.output
new file mode 100644
index 0000000..d0f375d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22610221.output
@@ -0,0 +1,8 @@
+class B22610221 {
+  {
+    for (Map.Entry<Key<Object>, Object> entry :
+        FOO.bar().bazs().<Object>thing(someThing).entrySet()) {
+      output.put(entry.getKey(), entry.getValue());
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22815364.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22815364.input
new file mode 100644
index 0000000..85fbf64
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22815364.input
@@ -0,0 +1,46 @@
+class B22815364 {
+  @Xxx({
+      "=0|=0", "Xxx xxxx xx xxxxxxxx xx xxxxxxxxxxxxx.",
+      "=0|xxx", "Xxx xxxx x xxxxxxxxxxxx.",
+      "xxx|=0", "Xxx xxxx x xxxxxxx.",
+      "xxx|xxx", "Xxx xxxx xxx xxxxxxx xxx xxx xxxxxxxxxxxx.",
+      "xxxxx|xxx", "Xxx xxxx {0} xxxxxxxx xxx xxx xxxxxxxxxxxx.",
+      "xxx|xxxxx", "Xxx xxxx xxx xxxxxxx xxx {1} xxxxxxxxxxxxx."
+  })
+  int x;
+
+  @Xxx({
+    "a", "b",
+    "c", "d",
+    //
+  })
+  int y;
+
+  int[] xs = {
+    1, 2, 3,
+    1, 2, 3,
+    1, 2, 3,
+  };
+
+  int[] xs = {
+    1, 2, 3,
+    1, 2, 3,
+    1, 2,
+  };
+
+  int[] xs = {
+    1, 2,
+    1,
+  };
+
+  int[][] xs = {
+    {0b111111111111111111, 0b111111111111111111, 0b111111111111111111, 0b111111111111111111,
+        0b111111111111111111, 0b111111111111111111, 0b111111111111111111}, {0b111111111111111111,
+        0b111111111111111111, 0b111111111111111111, 0b111111111111111111, 0b111111111111111111,
+        0b111111111111111111, 0b111111111111111111},
+    {0b111111111111111111, 0b111111111111111111, 0b111111111111111111, 0b111111111111111111,
+        0b111111111111111111, 0b111111111111111111, 0b111111111111111111}, {0b111111111111111111,
+        0b111111111111111111, 0b111111111111111111, 0b111111111111111111, 0b111111111111111111,
+        0b111111111111111111, 0b111111111111111111},
+  };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22815364.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22815364.output
new file mode 100644
index 0000000..91f3936
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22815364.output
@@ -0,0 +1,73 @@
+class B22815364 {
+  @Xxx({
+    "=0|=0", "Xxx xxxx xx xxxxxxxx xx xxxxxxxxxxxxx.",
+    "=0|xxx", "Xxx xxxx x xxxxxxxxxxxx.",
+    "xxx|=0", "Xxx xxxx x xxxxxxx.",
+    "xxx|xxx", "Xxx xxxx xxx xxxxxxx xxx xxx xxxxxxxxxxxx.",
+    "xxxxx|xxx", "Xxx xxxx {0} xxxxxxxx xxx xxx xxxxxxxxxxxx.",
+    "xxx|xxxxx", "Xxx xxxx xxx xxxxxxx xxx {1} xxxxxxxxxxxxx."
+  })
+  int x;
+
+  @Xxx({
+    "a", "b",
+    "c", "d",
+    //
+  })
+  int y;
+
+  int[] xs = {
+    1, 2, 3,
+    1, 2, 3,
+    1, 2, 3,
+  };
+
+  int[] xs = {
+    1, 2, 3,
+    1, 2, 3,
+    1, 2,
+  };
+
+  int[] xs = {
+    1, 2, 1,
+  };
+
+  int[][] xs = {
+    {
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111
+    },
+    {
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111
+    },
+    {
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111
+    },
+    {
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111,
+      0b111111111111111111
+    },
+  };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22848286.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22848286.input
new file mode 100644
index 0000000..18796c1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22848286.input
@@ -0,0 +1,8 @@
+class B22848286 {
+
+  @FxxxSxxx(xxxx = "xxxxxx_xxxxxxx_xxxxxxxxx_xxxx", xxxx = "Xxxxx xxxx xx xxxxxx xxxxxxx xxxxxxxxx.")
+  int x;
+
+  @FxxxGxxxxxxxx(xxxx = "Xxx xxxxxx xxxxxxxxy xxx xxx xxxxxxxxx, x.x. xxx xxxxxx xxxxxxxx xxxxxxxxy")
+  int y;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22848286.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22848286.output
new file mode 100644
index 0000000..e073fb3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22848286.output
@@ -0,0 +1,11 @@
+class B22848286 {
+
+  @FxxxSxxx(
+      xxxx = "xxxxxx_xxxxxxx_xxxxxxxxx_xxxx",
+      xxxx = "Xxxxx xxxx xx xxxxxx xxxxxxx xxxxxxxxx.")
+  int x;
+
+  @FxxxGxxxxxxxx(
+      xxxx = "Xxx xxxxxx xxxxxxxxy xxx xxx xxxxxxxxx, x.x. xxx xxxxxx xxxxxxxx xxxxxxxxy")
+  int y;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22873322.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22873322.input
new file mode 100644
index 0000000..48984c4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22873322.input
@@ -0,0 +1,14 @@
+class B22873322 {
+  {
+    f(param -> veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongExpr(param));
+    f((param1, param2) -> veryLooooooooooooooooooooooooooooooooooooooooooooooooongExpr(param1, param2));
+    f((int param) -> veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongExpr(param));
+    f((param1, param2) -> { 
+      return expr(param1, param2);
+    });
+    f((param1, param2) -> { 
+      Object foo = expr(param1, param2);
+      return foo;
+    });
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22873322.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22873322.output
new file mode 100644
index 0000000..7768383
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22873322.output
@@ -0,0 +1,23 @@
+class B22873322 {
+  {
+    f(
+        param ->
+            veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongExpr(
+                param));
+    f(
+        (param1, param2) ->
+            veryLooooooooooooooooooooooooooooooooooooooooooooooooongExpr(param1, param2));
+    f(
+        (int param) ->
+            veryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongExpr(param));
+    f(
+        (param1, param2) -> {
+          return expr(param1, param2);
+        });
+    f(
+        (param1, param2) -> {
+          Object foo = expr(param1, param2);
+          return foo;
+        });
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22913048.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22913048.input
new file mode 100644
index 0000000..512c2b3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22913048.input
@@ -0,0 +1,9 @@
+private class Yellow<B> extends Red<B>.Orange {
+  Yellow(Red<B> red) {
+    red.super();
+  }
+
+  Class<?> getClassB() {
+    return new TypeToken<B>(getClass()) {}.getRawType();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22913048.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22913048.output
new file mode 100644
index 0000000..512c2b3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B22913048.output
@@ -0,0 +1,9 @@
+private class Yellow<B> extends Red<B>.Orange {
+  Yellow(Red<B> red) {
+    red.super();
+  }
+
+  Class<?> getClassB() {
+    return new TypeToken<B>(getClass()) {}.getRawType();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23349153.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23349153.input
new file mode 100644
index 0000000..574e345
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23349153.input
@@ -0,0 +1,28 @@
+class B23349153 {
+  enum Foo {
+    A, B,;
+  }
+  {
+    System.err.println("Hello");;
+  }
+}
+
+private enum UnsafeAtomicHelperFactory {
+  REALLY_TRY_TO_CREATE {
+    @Override
+      AtomicHelper tryCreateUnsafeAtomicHelper() {
+        return new UnsafeAtomicHelper();
+      }
+  },
+
+  DONT_EVEN_TRY_TO_CREATE {
+    @Override
+      AtomicHelper tryCreateUnsafeAtomicHelper() {
+        return null;
+      }
+  },
+  
+  ;
+
+  abstract AtomicHelper tryCreateUnsafeAtomicHelper();
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23349153.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23349153.output
new file mode 100644
index 0000000..7ed9a6a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23349153.output
@@ -0,0 +1,31 @@
+class B23349153 {
+  enum Foo {
+    A,
+    B,
+    ;
+  }
+
+  {
+    System.err.println("Hello");
+    ;
+  }
+}
+
+private enum UnsafeAtomicHelperFactory {
+  REALLY_TRY_TO_CREATE {
+    @Override
+    AtomicHelper tryCreateUnsafeAtomicHelper() {
+      return new UnsafeAtomicHelper();
+    }
+  },
+
+  DONT_EVEN_TRY_TO_CREATE {
+    @Override
+    AtomicHelper tryCreateUnsafeAtomicHelper() {
+      return null;
+    }
+  },
+  ;
+
+  abstract AtomicHelper tryCreateUnsafeAtomicHelper();
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23514513.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23514513.input
new file mode 100644
index 0000000..b8351cb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23514513.input
@@ -0,0 +1,7 @@
+class Test {
+  {
+    f(
+        rrr.kkkkk.uuuuuuuu, rrr.iiiiii.ggggggggggggggggg, 
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23514513.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23514513.output
new file mode 100644
index 0000000..5ec70f6
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23514513.output
@@ -0,0 +1,8 @@
+class Test {
+  {
+    f(
+        rrr.kkkkk.uuuuuuuu,
+        rrr.iiiiii.ggggggggggggggggg,
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23626035.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23626035.input
new file mode 100644
index 0000000..bfcedc5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23626035.input
@@ -0,0 +1 @@
+class Test2 { void f(Test2... xs) {} }
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23626035.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23626035.output
new file mode 100644
index 0000000..6d429ba
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23626035.output
@@ -0,0 +1,3 @@
+class Test2 {
+  void f(Test2... xs) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23708487.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23708487.input
new file mode 100644
index 0000000..e08556a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23708487.input
@@ -0,0 +1,10 @@
+class B23708487 {
+  static {
+    TASK_AS_OWNER = 
+        com.google.security.acl.proto2api.ACL.Entry.newBuilder()
+          .setRole(com.google.security.acl.proto2api.ACL.Entry.ROLE.OWNER.getNumber())
+          .setScope(com.google.security.acl.proto2api.ACL.Scope.newBuilder()
+              .setMdbUser(BorgTaskInfo.getMdbUser().getMdbUserName()))
+          .build();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23708487.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23708487.output
new file mode 100644
index 0000000..6dc379d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23708487.output
@@ -0,0 +1,11 @@
+class B23708487 {
+  static {
+    TASK_AS_OWNER =
+        com.google.security.acl.proto2api.ACL.Entry.newBuilder()
+            .setRole(com.google.security.acl.proto2api.ACL.Entry.ROLE.OWNER.getNumber())
+            .setScope(
+                com.google.security.acl.proto2api.ACL.Scope.newBuilder()
+                    .setMdbUser(BorgTaskInfo.getMdbUser().getMdbUserName()))
+            .build();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23749160.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23749160.input
new file mode 100644
index 0000000..b624e8d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23749160.input
@@ -0,0 +1,43 @@
+class B23749160 {
+  {
+    mockDailySummaryModuleListFiller
+        .buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDayxxxxxxxxxxx(
+            anyInt(), anyInt(), anyInt());
+
+    mockDailySummaryModuleListFiller
+        .<XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+            YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY>
+            buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDayxxxxxxxxxxx(
+                anyInt(), anyInt(), anyInt());
+
+    foo
+        .<XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+            YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY>
+            buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDayxxxxxxxxxxx(
+                anyInt(), anyInt(), anyInt())
+        .bar();
+
+    // `.build`... and `anotherThing`... are technically at different syntactic
+    // levels, but the indentation required to express that starts to get out
+    // of hand
+    MockDailySummaryModuleListFiller
+        .buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDayxxxxxxxxxxx(
+            anyInt(), anyInt(), anyInt())
+        .anotherThing();
+
+    when(
+            mockDailySummaryModuleListFiller
+                .buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDay(
+                    anyInt(), anyInt(), anyInt()))
+        .then();
+
+    final List<Integer> XXXXXXXX =
+        ImmutableList.of(
+            XXXXXXX, //
+            XXXXXXX, //
+            XXXXXXX, //
+            XXXXXXX, //
+            XXXXXXX //
+        );
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23749160.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23749160.output
new file mode 100644
index 0000000..e1b2e55
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23749160.output
@@ -0,0 +1,40 @@
+class B23749160 {
+  {
+    mockDailySummaryModuleListFiller
+        .buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDayxxxxxxxxxxx(
+            anyInt(), anyInt(), anyInt());
+
+    mockDailySummaryModuleListFiller
+        .<XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+            YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY>
+            buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDayxxxxxxxxxxx(
+                anyInt(), anyInt(), anyInt());
+
+    foo.<XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,
+            YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY>
+            buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDayxxxxxxxxxxx(
+                anyInt(), anyInt(), anyInt())
+        .bar();
+
+    // `.build`... and `anotherThing`... are technically at different syntactic
+    // levels, but the indentation required to express that starts to get out
+    // of hand
+    MockDailySummaryModuleListFiller
+        .buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDayxxxxxxxxxxx(
+            anyInt(), anyInt(), anyInt())
+        .anotherThing();
+
+    when(mockDailySummaryModuleListFiller.buildDailySummaryModulesWithHeadersOnlyAndOneExpandedDay(
+            anyInt(), anyInt(), anyInt()))
+        .then();
+
+    final List<Integer> XXXXXXXX =
+        ImmutableList.of(
+            XXXXXXX, //
+            XXXXXXX, //
+            XXXXXXX, //
+            XXXXXXX, //
+            XXXXXXX //
+            );
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23804934.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23804934.input
new file mode 100644
index 0000000..87b75ab
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23804934.input
@@ -0,0 +1,34 @@
+class B23804934{ 
+
+  @Zzzzzz
+  B23804934(
+      ZzzzzzzzZzzzzzZzzzzZzzzzzzz zzzzzzzzZzzzzzZzzzzZzzzzzzz,
+      ZzzzzzzZzzzzzzz<ZzzzzzZzzzZzzz> zzzzzzZzzzZzzzZzzzzzzz,
+      @ZzzzZzzzzZzzzzzz Zzzzzz zzzzzZzzzzzz,
+      @ZzzzzzzzzzzZzzzzzz(Zz.ZZZZZZZ_ZZZZZ_ZZZZ_ZZZZZZ_ZZZZ_ZZZZZZZZZZ)
+      Zzzzzzzz<Zzzzzzz> zzzzzZzzzZzzzzzZzzzZzzzzzzzzz) {
+    zzzz.zzzzzzzzZzzzzzZzzzzZzzzzzzz = zzzzzzzzZzzzzzZzzzzZzzzzzzz;
+    zzzz.zzzzzzZzzzZzzzZzzzzzzz = zzzzzzZzzzZzzzZzzzzzzz;
+    zzzz.zzzzzZzzzzzz = zzzzzZzzzzzz;
+    zzzz.zzzzzZzzzZzzzzzZzzzZzzzzzzzzz = zzzzzZzzzZzzzzzZzzzZzzzzzzzzz;
+  }
+
+  zzzz z(
+      zzz z,
+      @Zzzzzzzz @Zzzzzzzzzz
+      ZzzzzzzzzZzzz<Zzzzzz>
+          zzzzZzzzZzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) {}
+
+  @ZzzZzzzZzzzzzzzzzz
+  @Zzzzzzzz(zzzzzzzzz = Zzzzzzzzz.ZZ)
+  ZzzzzzzzzzZzzzzz<ZzzZzzzzzzzzzzZzzzzzzzz> zzzzzzzZzzzzzzzzzz(
+      @Zzz @Zzz zzzz zzzzZzzzZz,
+      @Zzz @ZzzzzzzzzzZzzzz(zzzz = ZzzzzZzzzzzZzzzz.ZZZZZZ_ZZZ_ZZZ_ZZZZZZZ)
+      zzzzzzz zzzzzzZzzZzzZzzzzzz,
+      @Zzzzz @ZzzzzzzzzzZzzzz(zzzz = ZzzzzZzzzzzZzzzz.ZZZZZ_ZZZ_ZZZZZZZZZZZ)
+      zzzzzzz zzzzzZzzZzzzzzzzzzz,
+      @Zzzzz ZzzzzZzzz zzzzzZzzz) {}
+
+  void fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(
+      final zzzzzzzzzzzzzz zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23804934.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23804934.output
new file mode 100644
index 0000000..310d4c7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B23804934.output
@@ -0,0 +1,35 @@
+class B23804934 {
+
+  @Zzzzzz
+  B23804934(
+      ZzzzzzzzZzzzzzZzzzzZzzzzzzz zzzzzzzzZzzzzzZzzzzZzzzzzzz,
+      ZzzzzzzZzzzzzzz<ZzzzzzZzzzZzzz> zzzzzzZzzzZzzzZzzzzzzz,
+      @ZzzzZzzzzZzzzzzz Zzzzzz zzzzzZzzzzzz,
+      @ZzzzzzzzzzzZzzzzzz(Zz.ZZZZZZZ_ZZZZZ_ZZZZ_ZZZZZZ_ZZZZ_ZZZZZZZZZZ)
+          Zzzzzzzz<Zzzzzzz> zzzzzZzzzZzzzzzZzzzZzzzzzzzzz) {
+    zzzz.zzzzzzzzZzzzzzZzzzzZzzzzzzz = zzzzzzzzZzzzzzZzzzzZzzzzzzz;
+    zzzz.zzzzzzZzzzZzzzZzzzzzzz = zzzzzzZzzzZzzzZzzzzzzz;
+    zzzz.zzzzzZzzzzzz = zzzzzZzzzzzz;
+    zzzz.zzzzzZzzzZzzzzzZzzzZzzzzzzzzz = zzzzzZzzzZzzzzzZzzzZzzzzzzzzz;
+  }
+
+  zzzz z(
+      zzz z,
+      @Zzzzzzzz @Zzzzzzzzzz
+          ZzzzzzzzzZzzz<Zzzzzz>
+              zzzzZzzzZzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) {}
+
+  @ZzzZzzzZzzzzzzzzzz
+  @Zzzzzzzz(zzzzzzzzz = Zzzzzzzzz.ZZ)
+  ZzzzzzzzzzZzzzzz<ZzzZzzzzzzzzzzZzzzzzzzz> zzzzzzzZzzzzzzzzzz(
+      @Zzz @Zzz zzzz zzzzZzzzZz,
+      @Zzz @ZzzzzzzzzzZzzzz(zzzz = ZzzzzZzzzzzZzzzz.ZZZZZZ_ZZZ_ZZZ_ZZZZZZZ)
+          zzzzzzz zzzzzzZzzZzzZzzzzzz,
+      @Zzzzz @ZzzzzzzzzzZzzzz(zzzz = ZzzzzZzzzzzZzzzz.ZZZZZ_ZZZ_ZZZZZZZZZZZ)
+          zzzzzzz zzzzzZzzZzzzzzzzzzz,
+      @Zzzzz ZzzzzZzzz zzzzzZzzz) {}
+
+  void fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(
+      final zzzzzzzzzzzzzz
+          zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24202287.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24202287.input
new file mode 100644
index 0000000..ea42154
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24202287.input
@@ -0,0 +1,35 @@
+class B24202287 {
+  {
+    this.overflowContactCompositeSupportedRenderers =
+        this.getSharePanelResponse.contents.unifiedSharePanelRenderer.contents[0]
+                        .connectionSection
+                        .connectionsOverflowMenu
+                        .connectionsOverflowMenuRenderer
+                        .contents[
+                    0]
+                .overflowConnectionSectionRenderer
+                .contacts[
+            0];
+
+    int y = ((int[]) null)[0];
+  }
+
+  IntentSource getSource() {
+    return mIntent.isPresent() && mIntent.get().hasExtra(INTENT_SOURCE_KEY)
+        ? IntentSource.values()[
+            mIntent.get().getIntExtra(INTENT_SOURCE_KEY, IntentSource.OTHER.ordinal())]
+        : IntentSource.OTHER;
+  }
+
+  {
+    mTabUiParts[mRenderedTabIndex]
+        .setData(
+            tabs[mRenderedTabIndex],
+            true /* isSelected */,
+            mIsLoadingState,
+            mErrorMessageType,
+            mOnDailySummaryHeaderClickListener,
+            mShowSpacerForSnackbar,
+            mResetScrollAndAnimationAndReplaceDoodle);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24202287.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24202287.output
new file mode 100644
index 0000000..05d418c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24202287.output
@@ -0,0 +1,35 @@
+class B24202287 {
+  {
+    this.overflowContactCompositeSupportedRenderers =
+        this.getSharePanelResponse
+            .contents
+            .unifiedSharePanelRenderer
+            .contents[0]
+            .connectionSection
+            .connectionsOverflowMenu
+            .connectionsOverflowMenuRenderer
+            .contents[0]
+            .overflowConnectionSectionRenderer
+            .contacts[0];
+
+    int y = ((int[]) null)[0];
+  }
+
+  IntentSource getSource() {
+    return mIntent.isPresent() && mIntent.get().hasExtra(INTENT_SOURCE_KEY)
+        ? IntentSource.values()[
+            mIntent.get().getIntExtra(INTENT_SOURCE_KEY, IntentSource.OTHER.ordinal())]
+        : IntentSource.OTHER;
+  }
+
+  {
+    mTabUiParts[mRenderedTabIndex].setData(
+        tabs[mRenderedTabIndex],
+        true /* isSelected */,
+        mIsLoadingState,
+        mErrorMessageType,
+        mOnDailySummaryHeaderClickListener,
+        mShowSpacerForSnackbar,
+        mResetScrollAndAnimationAndReplaceDoodle);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24494875.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24494875.input
new file mode 100644
index 0000000..c96fcbb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24494875.input
@@ -0,0 +1,4 @@
+public enum B24494875 {
+  ;
+  public static final String ONE = "ONE";
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24494875.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24494875.output
new file mode 100644
index 0000000..c96fcbb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24494875.output
@@ -0,0 +1,4 @@
+public enum B24494875 {
+  ;
+  public static final String ONE = "ONE";
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24543625.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24543625.input
new file mode 100644
index 0000000..4c8dfb5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24543625.input
@@ -0,0 +1,76 @@
+/**              
+* This<br>              
+* is<br>              
+* a<br>              
+* train.              
+*/              
+class TheTrain {
+/*              
+                               ______              
+                            .-"""".._'.       _,##              
+                     _..__ |.-"""-.|  |   _,##'`-._              
+                    (_____)||_____||  |_,##'`-._,##'`              
+                    _|   |.;-""-.  |  |#'`-._,##'`              
+                 _.;_ `--' `\    \ |.'`\._,##'`              
+                /.-.\ `\     |.-";.`_, |##'`              
+                |\__/   | _..;__  |'-' /              
+                '.____.'_.-`)\--' /'-'`              
+                 //||\\(_.-'_,'-'`              
+               (`-...-')_,##'`              
+        jgs _,##`-..,-;##`              
+         _,##'`-._,##'`              
+      _,##'`-._,##'`              
+        `-._,##'`              
+*/
+  public static final Object THE_TRAIN = null;
+
+/*
+BLOCK COMMENT
+
+*/
+  int x;
+
+            /*               .
+            BLOCK COMMENT    .
+
+            */
+  int y;
+
+            /*    .
+  BLOCK COMMENT   .
+
+  */
+  int z;
+
+           /*      .
+ BLOCK COMMENT     .
+
+ */
+  int z;
+
+  /*
+  * This is almost javadoc.
+  *
+  */
+  int z;
+
+  /**
+  * This is (was) malformed javadoc.
+  * <pre>
+  System.err.println
+  * </pre>
+  */
+  int z;
+
+  private Object f; /*-{
+    System.err.println("asd");
+  }-*/
+
+  private native Object f() /*-{
+    System.err.println("asd");
+  }-*/;
+
+  private native Object f() /*-{
+System.err.println("asd");
+}-*/;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24543625.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24543625.output
new file mode 100644
index 0000000..5bd8d5a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24543625.output
@@ -0,0 +1,77 @@
+/**
+ * This<br>
+ * is<br>
+ * a<br>
+ * train.
+ */
+class TheTrain {
+  /*
+                                 ______
+                              .-"""".._'.       _,##
+                       _..__ |.-"""-.|  |   _,##'`-._
+                      (_____)||_____||  |_,##'`-._,##'`
+                      _|   |.;-""-.  |  |#'`-._,##'`
+                   _.;_ `--' `\    \ |.'`\._,##'`
+                  /.-.\ `\     |.-";.`_, |##'`
+                  |\__/   | _..;__  |'-' /
+                  '.____.'_.-`)\--' /'-'`
+                   //||\\(_.-'_,'-'`
+                 (`-...-')_,##'`
+          jgs _,##`-..,-;##`
+           _,##'`-._,##'`
+        _,##'`-._,##'`
+          `-._,##'`
+  */
+  public static final Object THE_TRAIN = null;
+
+  /*
+  BLOCK COMMENT
+
+  */
+  int x;
+
+  /*               .
+  BLOCK COMMENT    .
+
+  */
+  int y;
+
+  /*    .
+  BLOCK COMMENT   .
+
+  */
+  int z;
+
+  /*      .
+  BLOCK COMMENT     .
+
+  */
+  int z;
+
+  /*
+   * This is almost javadoc.
+   *
+   */
+  int z;
+
+  /**
+   * This is (was) malformed javadoc.
+   *
+   * <pre>
+   * System.err.println
+   * </pre>
+   */
+  int z;
+
+  private Object f; /*-{
+    System.err.println("asd");
+  }-*/
+
+  private native Object f() /*-{
+    System.err.println("asd");
+  }-*/;
+
+  private native Object f() /*-{
+System.err.println("asd");
+}-*/;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24702438.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24702438.input
new file mode 100644
index 0000000..631f076
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24702438.input
@@ -0,0 +1,54 @@
+class B24702438 {
+
+  @Inject int x;
+
+  @Inject int y;
+
+  @Inject int z;
+
+  @Inject int x;
+  @Inject int y;
+  @Inject int z;
+
+  // this is a comment
+
+  // another comment
+  int x;
+
+  // another comment
+
+  int y;
+
+  {
+    switch (x) {
+
+      case 1:
+
+        break;
+
+      case 1:
+
+        break;
+
+      default:
+
+        break;
+
+    }
+
+    // this is a comment
+
+    // another comment
+    System.err.println("asd");
+  }
+
+  void f(
+      int a,
+      @Nullable @Deprecated ImmutableList<String> veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      @Nullable @Deprecated ImmutableList<String> veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      @Nullable @Deprecated ImmutableList<String> veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      int c) {}
+  void g(
+      @Nullable @Deprecated ImmutableList<String> veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      @Nullable @Deprecated ImmutableList<String> veryVeryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooong) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24702438.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24702438.output
new file mode 100644
index 0000000..b9d5a6d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24702438.output
@@ -0,0 +1,60 @@
+class B24702438 {
+
+  @Inject int x;
+
+  @Inject int y;
+
+  @Inject int z;
+
+  @Inject int x;
+  @Inject int y;
+  @Inject int z;
+
+  // this is a comment
+
+  // another comment
+  int x;
+
+  // another comment
+
+  int y;
+
+  {
+    switch (x) {
+      case 1:
+        break;
+
+      case 1:
+        break;
+
+      default:
+        break;
+    }
+
+    // this is a comment
+
+    // another comment
+    System.err.println("asd");
+  }
+
+  void f(
+      int a,
+      @Nullable @Deprecated
+          ImmutableList<String>
+              veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      @Nullable @Deprecated
+          ImmutableList<String>
+              veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      @Nullable @Deprecated
+          ImmutableList<String>
+              veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      int c) {}
+
+  void g(
+      @Nullable @Deprecated
+          ImmutableList<String>
+              veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      @Nullable @Deprecated
+          ImmutableList<String>
+              veryVeryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooong) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24862959.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24862959.input
new file mode 100644
index 0000000..6f729a5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24862959.input
@@ -0,0 +1,8 @@
+public enum XxxxxxxXxxx implements XxxxxxXxxxxxxXxxx {
+  XXXXXXXXX_XXXX_XXX_XXXXXXXX(
+      "                                       ", Xxxxxx.XXXXXXX, XxxxXxxxxxxXxxxxxxx.XXXXX),
+      //
+      //
+      //
+}
+
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24862959.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24862959.output
new file mode 100644
index 0000000..acb80a0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24862959.output
@@ -0,0 +1,7 @@
+public enum XxxxxxxXxxx implements XxxxxxXxxxxxxXxxx {
+  XXXXXXXXX_XXXX_XXX_XXXXXXXX(
+      "                                       ", Xxxxxx.XXXXXXX, XxxxXxxxxxxXxxxxxxx.XXXXX),
+  //
+  //
+  //
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24909927.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24909927.input
new file mode 100644
index 0000000..c9d4fbb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24909927.input
@@ -0,0 +1,353 @@
+class Xxx {
+
+  public xxxx xxx() {
+    XxxxxxXxxx xxxxxxxxXxxx = XxxxxxXxxx.xxxXxxxxxx()
+        .xxxXxxxxxXxxx(XxxxxxXxxx.XXXXXXX)
+        .xxxXxxxxxXxxx("                   ")
+        .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+            .xxxXxxxxxxx(Xxxxxxxx.XX)
+            .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                    .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                    .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                        .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                            .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                                .xxxXxxxxxXxxx(
+                                    XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                        .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxxxxxxXxxxxxxxx(
+                                                    XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                        .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                .xxxxx())
+                                            .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxxx(-42)
+                                                .xxxXxxx(XxxxXxxx.XXX)
+                                                .xxxxx())
+                                            .xxxxx())
+                                        .xxxxx())
+                                    .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                        .xxxXxxxx(42)
+                                        .xxxXxxx(XxxxXxxx.XXX)
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                    .xxxXxxxxxx()
+                                    .xxxXxxxxxxxxxXxxxXx(42)
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxxx())
+                .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                    .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                    .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                        .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                            .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                                .xxxXxxxxxXxxx(
+                                    XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                        .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxxxxxxXxxxxxxxx(
+                                                    XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                        .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                .xxxxx())
+                                            .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxxx(-42)
+                                                .xxxXxxx(XxxxXxxx.XXX)
+                                                .xxxxx())
+                                            .xxxxx())
+                                        .xxxxx())
+                                    .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                        .xxxXxxxx(42)
+                                        .xxxXxxx(XxxxXxxx.XXX)
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                    .xxxXxxxxxx()
+                                    .xxxXxxxxxxxxxXxxxXx(42)
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxxx())
+                .xxxxx())
+            .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                    .xxxXxxxxxXxxx(XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                    .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxxXxxxxxxxx(XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                        .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                    .xxxxx())
+                                .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                    .xxxXxxxx(-42)
+                                    .xxxXxxx(XxxxXxxx.XXX)
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                            .xxxXxxxx(42)
+                            .xxxXxxx(XxxxXxxx.XXX)
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxxxxXxxxXx(42)
+                        .xxxxx())
+                    .xxxxx())
+                .xxxxx())
+            .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                .xxxXxxxxxxx(Xxxxxxxx.XX)
+                .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                    .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                    .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                        .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                            .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxXxxx(
+                                        XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                    .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                                        .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxxXxxxxxxxx(
+                                                        XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                            .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                    .xxxxx())
+                                                .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxxx(-42)
+                                                    .xxxXxxx(XxxxXxxx.XXX)
+                                                    .xxxxx())
+                                                .xxxxx())
+                                            .xxxxx())
+                                        .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxx(42)
+                                            .xxxXxxx(XxxxXxxx.XXX)
+                                            .xxxxx())
+                                        .xxxxx())
+                                    .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                        .xxxXxxxxxx()
+                                        .xxxXxxxxxxxxxXxxxXx(42)
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                        .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                            .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxXxxx(
+                                        XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                    .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                                        .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxxXxxxxxxxx(
+                                                        XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                            .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                    .xxxxx())
+                                                .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxxx(-42)
+                                                    .xxxXxxx(XxxxXxxx.XXX)
+                                                    .xxxxx())
+                                                .xxxxx())
+                                            .xxxxx())
+                                        .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxx(42)
+                                            .xxxXxxx(XxxxXxxx.XXX)
+                                            .xxxxx())
+                                        .xxxxx())
+                                    .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                        .xxxXxxxxxx()
+                                        .xxxXxxxxxxxxxXxxxXx(42)
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxxx())
+                .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                    .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                    .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                        .xxxXxxxxxXxxx(XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                        .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                    .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                        .xxxXxxxxxxxXxxxxxxxx(XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                            .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                        .xxxxx())
+                                    .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                        .xxxXxxxx(-42)
+                                        .xxxXxxx(XxxxXxxx.XXX)
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                .xxxXxxxx(42)
+                                .xxxXxxx(XxxxXxxx.XXX)
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxxxxXxxxXx(42)
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxxx())
+                .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                    .xxxXxxxxxxx(Xxxxxxxx.XX)
+                    .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                        .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                            .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                                .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                    .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                                        .xxxXxxxxxXxxx(
+                                            XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                        .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                        .xxxXxxxxxxxXxxxxxxxx(
+                                                            XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                        .xxxxx())
+                                                    .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                                        .xxxXxxxx(-42)
+                                                        .xxxXxxx(XxxxXxxx.XXX)
+                                                        .xxxxx())
+                                                    .xxxxx())
+                                                .xxxxx())
+                                            .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxxx(42)
+                                                .xxxXxxx(XxxxXxxx.XXX)
+                                                .xxxxx())
+                                            .xxxxx())
+                                        .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                            .xxxXxxxxxx()
+                                            .xxxXxxxxxxxxxXxxxXx(42)
+                                            .xxxxx())
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                            .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                                .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                    .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                                        .xxxXxxxxxXxxx(
+                                            XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                        .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                        .xxxXxxxxxxxXxxxxxxxx(
+                                                            XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                        .xxxxx())
+                                                    .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                                        .xxxXxxxx(-42)
+                                                        .xxxXxxx(XxxxXxxx.XXX)
+                                                        .xxxxx())
+                                                    .xxxxx())
+                                                .xxxxx())
+                                            .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                                .xxxXxxxx(42)
+                                                .xxxXxxx(XxxxXxxx.XXX)
+                                                .xxxxx())
+                                            .xxxxx())
+                                        .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                            .xxxXxxxxxx()
+                                            .xxxXxxxxxxxxxXxxxXx(42)
+                                            .xxxxx())
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                        .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxXxxx(XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                            .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                        .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxxXxxxxxxxx(
+                                                XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                    .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                            .xxxxx())
+                                        .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxx(-42)
+                                            .xxxXxxx(XxxxXxxx.XXX)
+                                            .xxxxx())
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                    .xxxXxxxx(42)
+                                    .xxxXxxx(XxxxXxxx.XXX)
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxxxxXxxxXx(42)
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxXxxxxxx(XxxxxxxXxxx.xxxXxxxxxx()
+                        .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                        .xxxXxxxxxXxxx(XxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxXxxx(XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                            .xxxXxxxXxxxxx(XxxxXxxxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxxxXxxxx(XxxxXxxxxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxxXxxxxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                        .xxxXxxx(XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxxXxxxxxxxx(
+                                                XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                    .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                            .xxxxx())
+                                        .xxxXxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxx(-42)
+                                            .xxxXxxx(XxxxXxxx.XXX)
+                                            .xxxxx())
+                                        .xxxxx())
+                                    .xxxxx())
+                                .xxxXxxxxxxx(XxxxXxxxxxxx.xxxXxxxxxx()
+                                    .xxxXxxxx(42)
+                                    .xxxXxxx(XxxxXxxx.XXX)
+                                    .xxxxx())
+                                .xxxxx())
+                            .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(XxxxxxxxxxXxxXxxxXxxxxxxxx.xxxXxxxxxx()
+                                .xxxXxxxxxxxxxXxxxXx(42)
+                                .xxxxx())
+                            .xxxxx())
+                        .xxxxx())
+                    .xxxxx())
+                .xxxxx())
+            .xxxxx())
+        .xxxxx();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24909927.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24909927.output
new file mode 100644
index 0000000..dce6c50
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24909927.output
@@ -0,0 +1,546 @@
+class Xxx {
+
+  public xxxx xxx() {
+    XxxxxxXxxx xxxxxxxxXxxx =
+        XxxxxxXxxx.xxxXxxxxxx()
+            .xxxXxxxxxXxxx(XxxxxxXxxx.XXXXXXX)
+            .xxxXxxxxxXxxx("                   ")
+            .xxxXxxxxxx(
+                XxxxxxxXxxx.xxxXxxxxxx()
+                    .xxxXxxxxxxx(Xxxxxxxx.XX)
+                    .xxxXxxxxxx(
+                        XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                            .xxxXxxxxxx(
+                                XxxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                                    .xxxXxxxxxx(
+                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                                            .xxxXxxxxxx(
+                                                XxxxxxxXxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                                    .xxxXxxxxxXxxx(
+                                                        XxxxxxXxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxXxxx(
+                                                                XxxxxxXxxx
+                                                                    .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                                            .xxxXxxxXxxxxx(
+                                                                XxxxXxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxxxxXxxxx(
+                                                                        XxxxXxxxxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxxxxxXxxxxxx(
+                                                                                XxxxxxxxXxxxXxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxx(
+                                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxxxxxxXxxxxxxxx(
+                                                                                                XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                                    .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                                            .xxxxx())
+                                                                                    .xxxXxxxxx(
+                                                                                        XxxxXxxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxxx(
+                                                                                                -42)
+                                                                                            .xxxXxxx(
+                                                                                                XxxxXxxx
+                                                                                                    .XXX)
+                                                                                            .xxxxx())
+                                                                                    .xxxxx())
+                                                                            .xxxxx())
+                                                                    .xxxXxxxxxxx(
+                                                                        XxxxXxxxxxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxx(42)
+                                                                            .xxxXxxx(XxxxXxxx.XXX)
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                                XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                                                    .xxxXxxxxxx()
+                                                                    .xxxXxxxxxxxxxXxxxXx(42)
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxxx())
+                            .xxxXxxxxxx(
+                                XxxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                                    .xxxXxxxxxx(
+                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                                            .xxxXxxxxxx(
+                                                XxxxxxxXxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                                    .xxxXxxxxxXxxx(
+                                                        XxxxxxXxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxXxxx(
+                                                                XxxxxxXxxx
+                                                                    .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                                            .xxxXxxxXxxxxx(
+                                                                XxxxXxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxxxxXxxxx(
+                                                                        XxxxXxxxxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxxxxxXxxxxxx(
+                                                                                XxxxxxxxXxxxXxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxx(
+                                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxxxxxxXxxxxxxxx(
+                                                                                                XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                                    .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                                            .xxxxx())
+                                                                                    .xxxXxxxxx(
+                                                                                        XxxxXxxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxxx(
+                                                                                                -42)
+                                                                                            .xxxXxxx(
+                                                                                                XxxxXxxx
+                                                                                                    .XXX)
+                                                                                            .xxxxx())
+                                                                                    .xxxxx())
+                                                                            .xxxxx())
+                                                                    .xxxXxxxxxxx(
+                                                                        XxxxXxxxxxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxx(42)
+                                                                            .xxxXxxx(XxxxXxxx.XXX)
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                                XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                                                    .xxxXxxxxxx()
+                                                                    .xxxXxxxxxxxxxXxxxXx(42)
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxxx())
+                            .xxxxx())
+                    .xxxXxxxxxx(
+                        XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                            .xxxXxxxxxXxxx(
+                                XxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxXxxx(
+                                        XxxxxxXxxx.XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                    .xxxXxxxXxxxxx(
+                                        XxxxXxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxxxXxxxx(
+                                                XxxxXxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxxXxxxxxx(
+                                                        XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                            .xxxXxxx(
+                                                                XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxxxXxxxxxxxx(
+                                                                        XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                            .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                    .xxxxx())
+                                                            .xxxXxxxxx(
+                                                                XxxxXxxxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxx(-42)
+                                                                    .xxxXxxx(XxxxXxxx.XXX)
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxXxxxxxxx(
+                                                XxxxXxxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxxx(42)
+                                                    .xxxXxxx(XxxxXxxx.XXX)
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                        XxxxxxxxxxXxxXxxxXxxxxxxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxxxxXxxxXx(42)
+                                            .xxxxx())
+                                    .xxxxx())
+                            .xxxxx())
+                    .xxxXxxxxxx(
+                        XxxxxxxXxxx.xxxXxxxxxx()
+                            .xxxXxxxxxxx(Xxxxxxxx.XX)
+                            .xxxXxxxxxx(
+                                XxxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                                    .xxxXxxxxxx(
+                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                                            .xxxXxxxxxx(
+                                                XxxxxxxXxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                                                    .xxxXxxxxxx(
+                                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                                            .xxxXxxxxxXxxx(
+                                                                XxxxxxXxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxXxxx(
+                                                                        XxxxxxXxxx
+                                                                            .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                                                    .xxxXxxxXxxxxx(
+                                                                        XxxxXxxxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxxxxxxXxxxx(
+                                                                                XxxxXxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxxxxxXxxxxxx(
+                                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxx(
+                                                                                                XxxxxxxxXxxxXxxxxxx
+                                                                                                    .xxxXxxxxxx()
+                                                                                                    .xxxXxxxxxxxXxxxxxxxx(
+                                                                                                        XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                                            .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                                                    .xxxxx())
+                                                                                            .xxxXxxxxx(
+                                                                                                XxxxXxxxxxxx
+                                                                                                    .xxxXxxxxxx()
+                                                                                                    .xxxXxxxx(
+                                                                                                        -42)
+                                                                                                    .xxxXxxx(
+                                                                                                        XxxxXxxx
+                                                                                                            .XXX)
+                                                                                                    .xxxxx())
+                                                                                            .xxxxx())
+                                                                                    .xxxxx())
+                                                                            .xxxXxxxxxxx(
+                                                                                XxxxXxxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxx(42)
+                                                                                    .xxxXxxx(
+                                                                                        XxxxXxxx
+                                                                                            .XXX)
+                                                                                    .xxxxx())
+                                                                            .xxxxx())
+                                                                    .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                                        XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                                                            .xxxXxxxxxx()
+                                                                            .xxxXxxxxxxxxxXxxxXx(42)
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxXxxxxxx(
+                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                                            .xxxXxxxxxx(
+                                                XxxxxxxXxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                                                    .xxxXxxxxxx(
+                                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                                            .xxxXxxxxxXxxx(
+                                                                XxxxxxXxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxXxxx(
+                                                                        XxxxxxXxxx
+                                                                            .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                                                    .xxxXxxxXxxxxx(
+                                                                        XxxxXxxxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxxxxxxXxxxx(
+                                                                                XxxxXxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxxxxxXxxxxxx(
+                                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxx(
+                                                                                                XxxxxxxxXxxxXxxxxxx
+                                                                                                    .xxxXxxxxxx()
+                                                                                                    .xxxXxxxxxxxXxxxxxxxx(
+                                                                                                        XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                                            .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                                                    .xxxxx())
+                                                                                            .xxxXxxxxx(
+                                                                                                XxxxXxxxxxxx
+                                                                                                    .xxxXxxxxxx()
+                                                                                                    .xxxXxxxx(
+                                                                                                        -42)
+                                                                                                    .xxxXxxx(
+                                                                                                        XxxxXxxx
+                                                                                                            .XXX)
+                                                                                                    .xxxxx())
+                                                                                            .xxxxx())
+                                                                                    .xxxxx())
+                                                                            .xxxXxxxxxxx(
+                                                                                XxxxXxxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxx(42)
+                                                                                    .xxxXxxx(
+                                                                                        XxxxXxxx
+                                                                                            .XXX)
+                                                                                    .xxxxx())
+                                                                            .xxxxx())
+                                                                    .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                                        XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                                                            .xxxXxxxxxx()
+                                                                            .xxxXxxxxxxxxxXxxxXx(42)
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxxx())
+                            .xxxXxxxxxx(
+                                XxxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                    .xxxXxxxxxXxxx(
+                                        XxxxxxXxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxXxxx(
+                                                XxxxxxXxxx
+                                                    .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                            .xxxXxxxXxxxxx(
+                                                XxxxXxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxxxXxxxx(
+                                                        XxxxXxxxxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxxXxxxxxx(
+                                                                XxxxxxxxXxxxXxxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxx(
+                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                            .xxxXxxxxxx()
+                                                                            .xxxXxxxxxxxXxxxxxxxx(
+                                                                                XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                    .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                            .xxxxx())
+                                                                    .xxxXxxxxx(
+                                                                        XxxxXxxxxxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxx(-42)
+                                                                            .xxxXxxx(XxxxXxxx.XXX)
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxXxxxxxxx(
+                                                        XxxxXxxxxxxx.xxxXxxxxxx()
+                                                            .xxxXxxxx(42)
+                                                            .xxxXxxx(XxxxXxxx.XXX)
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                XxxxxxxxxxXxxXxxxXxxxxxxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxxxxXxxxXx(42)
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxxx())
+                            .xxxXxxxxxx(
+                                XxxxxxxXxxx.xxxXxxxxxx()
+                                    .xxxXxxxxxxx(Xxxxxxxx.XX)
+                                    .xxxXxxxxxx(
+                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                                            .xxxXxxxxxx(
+                                                XxxxxxxXxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                                                    .xxxXxxxxxx(
+                                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                                                            .xxxXxxxxxx(
+                                                                XxxxxxxXxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                                                    .xxxXxxxxxXxxx(
+                                                                        XxxxxxXxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxxxXxxx(
+                                                                                XxxxxxXxxx
+                                                                                    .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                                                            .xxxXxxxXxxxxx(
+                                                                                XxxxXxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxxxxxxXxxxx(
+                                                                                        XxxxXxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxxxxxxXxxxxxx(
+                                                                                                XxxxxxxxXxxxXxxxxxx
+                                                                                                    .xxxXxxxxxx()
+                                                                                                    .xxxXxxx(
+                                                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                                                            .xxxXxxxxxx()
+                                                                                                            .xxxXxxxxxxxXxxxxxxxx(
+                                                                                                                XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                                                    .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                                                            .xxxxx())
+                                                                                                    .xxxXxxxxx(
+                                                                                                        XxxxXxxxxxxx
+                                                                                                            .xxxXxxxxxx()
+                                                                                                            .xxxXxxxx(
+                                                                                                                -42)
+                                                                                                            .xxxXxxx(
+                                                                                                                XxxxXxxx
+                                                                                                                    .XXX)
+                                                                                                            .xxxxx())
+                                                                                                    .xxxxx())
+                                                                                            .xxxxx())
+                                                                                    .xxxXxxxxxxx(
+                                                                                        XxxxXxxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxxx(
+                                                                                                42)
+                                                                                            .xxxXxxx(
+                                                                                                XxxxXxxx
+                                                                                                    .XXX)
+                                                                                            .xxxxx())
+                                                                                    .xxxxx())
+                                                                            .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                                                XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxxxxxxxXxxxXx(
+                                                                                        42)
+                                                                                    .xxxxx())
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxXxxxxxx(
+                                                XxxxxxxXxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxxx(Xxxxxxxx.XXX)
+                                                    .xxxXxxxxxx(
+                                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxx(Xxxxxxxx.XX_XXXXX)
+                                                            .xxxXxxxxxx(
+                                                                XxxxxxxXxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                                                    .xxxXxxxxxXxxx(
+                                                                        XxxxxxXxxx.xxxXxxxxxx()
+                                                                            .xxxXxxxxxXxxx(
+                                                                                XxxxxxXxxx
+                                                                                    .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                                                            .xxxXxxxXxxxxx(
+                                                                                XxxxXxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxxxxxxXxxxx(
+                                                                                        XxxxXxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxxxxxxXxxxxxx(
+                                                                                                XxxxxxxxXxxxXxxxxxx
+                                                                                                    .xxxXxxxxxx()
+                                                                                                    .xxxXxxx(
+                                                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                                                            .xxxXxxxxxx()
+                                                                                                            .xxxXxxxxxxxXxxxxxxxx(
+                                                                                                                XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                                                    .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                                                            .xxxxx())
+                                                                                                    .xxxXxxxxx(
+                                                                                                        XxxxXxxxxxxx
+                                                                                                            .xxxXxxxxxx()
+                                                                                                            .xxxXxxxx(
+                                                                                                                -42)
+                                                                                                            .xxxXxxx(
+                                                                                                                XxxxXxxx
+                                                                                                                    .XXX)
+                                                                                                            .xxxxx())
+                                                                                                    .xxxxx())
+                                                                                            .xxxxx())
+                                                                                    .xxxXxxxxxxx(
+                                                                                        XxxxXxxxxxxx
+                                                                                            .xxxXxxxxxx()
+                                                                                            .xxxXxxxx(
+                                                                                                42)
+                                                                                            .xxxXxxx(
+                                                                                                XxxxXxxx
+                                                                                                    .XXX)
+                                                                                            .xxxxx())
+                                                                                    .xxxxx())
+                                                                            .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                                                XxxxxxxxxxXxxXxxxXxxxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxxxxxxxXxxxXx(
+                                                                                        42)
+                                                                                    .xxxxx())
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxXxxxxxx(
+                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                            .xxxXxxxxxXxxx(
+                                                XxxxxxXxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxXxxx(
+                                                        XxxxxxXxxx
+                                                            .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                                    .xxxXxxxXxxxxx(
+                                                        XxxxXxxxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxxxXxxxx(
+                                                                XxxxXxxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxxxXxxxxxx(
+                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                            .xxxXxxxxxx()
+                                                                            .xxxXxxx(
+                                                                                XxxxxxxxXxxxXxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxxxxxXxxxxxxxx(
+                                                                                        XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                            .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                                    .xxxxx())
+                                                                            .xxxXxxxxx(
+                                                                                XxxxXxxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxx(-42)
+                                                                                    .xxxXxxx(
+                                                                                        XxxxXxxx
+                                                                                            .XXX)
+                                                                                    .xxxxx())
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxXxxxxxxx(
+                                                                XxxxXxxxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxx(42)
+                                                                    .xxxXxxx(XxxxXxxx.XXX)
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                        XxxxxxxxxxXxxXxxxXxxxxxxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxxxxXxxxXx(42)
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxXxxxxxx(
+                                        XxxxxxxXxxx.xxxXxxxxxx()
+                                            .xxxXxxxxxxx(Xxxxxxxx.XXXX)
+                                            .xxxXxxxxxXxxx(
+                                                XxxxxxXxxx.xxxXxxxxxx()
+                                                    .xxxXxxxxxXxxx(
+                                                        XxxxxxXxxx
+                                                            .XXX_XXXXXXXXXXXXXXX_XXXX_XXXXXXX_XXXXX_XXXXX)
+                                                    .xxxXxxxXxxxxx(
+                                                        XxxxXxxxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxxxXxxxx(
+                                                                XxxxXxxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxxxxxXxxxxxx(
+                                                                        XxxxxxxxXxxxXxxxxxx
+                                                                            .xxxXxxxxxx()
+                                                                            .xxxXxxx(
+                                                                                XxxxxxxxXxxxXxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxxxxxXxxxxxxxx(
+                                                                                        XxxxxxxxXxxxXxxxxxxXxxxxxxxXxxxxxxxx
+                                                                                            .XXXX_XXXXXX_XXXX_XXXXX_XX_XXX)
+                                                                                    .xxxxx())
+                                                                            .xxxXxxxxx(
+                                                                                XxxxXxxxxxxx
+                                                                                    .xxxXxxxxxx()
+                                                                                    .xxxXxxxx(-42)
+                                                                                    .xxxXxxx(
+                                                                                        XxxxXxxx
+                                                                                            .XXX)
+                                                                                    .xxxxx())
+                                                                            .xxxxx())
+                                                                    .xxxxx())
+                                                            .xxxXxxxxxxx(
+                                                                XxxxXxxxxxxx.xxxXxxxxxx()
+                                                                    .xxxXxxxx(42)
+                                                                    .xxxXxxx(XxxxXxxx.XXX)
+                                                                    .xxxxx())
+                                                            .xxxxx())
+                                                    .xxxXxxxxxxxxxXxxXxxxXxxxxxxxx(
+                                                        XxxxxxxxxxXxxXxxxXxxxxxxxx.xxxXxxxxxx()
+                                                            .xxxXxxxxxxxxxXxxxXx(42)
+                                                            .xxxxx())
+                                                    .xxxxx())
+                                            .xxxxx())
+                                    .xxxxx())
+                            .xxxxx())
+                    .xxxxx())
+            .xxxxx();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24988078.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24988078.input
new file mode 100644
index 0000000..1a69ffc
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24988078.input
@@ -0,0 +1,7 @@
+class B24988078 {
+  public static <T> Optional</*@NonNull*/ T> fromNullable(@Nullable T nullableReference) {}
+
+  public static <T extends /*@NonNull*/ Object> Optional<T> fromNullable(@Nullable T nullableReference) {}
+
+  Table< /*@Nullable*/ Object, /*@Nullable*/ Object, /*@Nullable*/ Object > foo;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24988078.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24988078.output
new file mode 100644
index 0000000..1870892
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B24988078.output
@@ -0,0 +1,8 @@
+class B24988078 {
+  public static <T> Optional</*@NonNull*/ T> fromNullable(@Nullable T nullableReference) {}
+
+  public static <T extends /*@NonNull*/ Object> Optional<T> fromNullable(
+      @Nullable T nullableReference) {}
+
+  Table</*@Nullable*/ Object, /*@Nullable*/ Object, /*@Nullable*/ Object> foo;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25372815.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25372815.input
new file mode 100644
index 0000000..c03a6ea
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25372815.input
@@ -0,0 +1,32 @@
+class Test {
+  {
+    // For "All goals" view, show only advertiser goals tab.
+    return newPageTypeSet(
+        false /* advertisers */,
+        false /* userManagement */,
+        false /* campaigns */,
+        false /* adGroups */,
+        false /* ads */,
+        false /* keywords */,
+        false /* negativeKeywords */,
+        false /* advertiserBidStrategies */,
+        false /* bidStrategies */,
+        false /* bidStrategyRecommendations */,
+        false /* bidKeywords */,
+        false /* engineBidStrategies */,
+        false /* conversionTrackers */,
+        false /* labels */,
+        false /* labelKeywords */,
+        false /* evergreen labels */,
+        false /* searchQueries */,
+        false /* engineSearchQueries */,
+        false /* remarketingTarget */,
+        false /* sitelinks */,
+        false /* feedSitelinks */,
+        false /* locationExtensions */,
+        false /* callExtensions */,
+        false /* calloutExtensions */,
+        false /* appExtensions */,
+        false /* events */);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25372815.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25372815.output
new file mode 100644
index 0000000..c03a6ea
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25372815.output
@@ -0,0 +1,32 @@
+class Test {
+  {
+    // For "All goals" view, show only advertiser goals tab.
+    return newPageTypeSet(
+        false /* advertisers */,
+        false /* userManagement */,
+        false /* campaigns */,
+        false /* adGroups */,
+        false /* ads */,
+        false /* keywords */,
+        false /* negativeKeywords */,
+        false /* advertiserBidStrategies */,
+        false /* bidStrategies */,
+        false /* bidStrategyRecommendations */,
+        false /* bidKeywords */,
+        false /* engineBidStrategies */,
+        false /* conversionTrackers */,
+        false /* labels */,
+        false /* labelKeywords */,
+        false /* evergreen labels */,
+        false /* searchQueries */,
+        false /* engineSearchQueries */,
+        false /* remarketingTarget */,
+        false /* sitelinks */,
+        false /* feedSitelinks */,
+        false /* locationExtensions */,
+        false /* callExtensions */,
+        false /* calloutExtensions */,
+        false /* appExtensions */,
+        false /* events */);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25749989.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25749989.input
new file mode 100644
index 0000000..ca0e0ae
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25749989.input
@@ -0,0 +1,4 @@
+class Dummy {
+  /**
+ */private static void method(){MISSING();}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25749989.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25749989.output
new file mode 100644
index 0000000..bcee8b3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25749989.output
@@ -0,0 +1,6 @@
+class Dummy {
+  /** */
+  private static void method() {
+    MISSING();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25787055.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25787055.input
new file mode 100644
index 0000000..4aae1ce
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25787055.input
@@ -0,0 +1,30 @@
+class B25787055 {
+  {
+    logger
+        .atInfo()
+        .log(
+            "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+            deletedPersistentNames.size(),
+            deletedPersistentNames);
+    logger.atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo()
+        .log(
+            "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+            deletedPersistentNames.size(),
+            deletedPersistentNames);
+    logger // interior
+        .atInfo() // comments
+        .log( // are best comments
+            "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+            deletedPersistentNames.size(),
+            deletedPersistentNames);
+    logger
+        .log(
+            "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+            deletedPersistentNames.size(),
+            deletedPersistentNames)
+        .log(
+            "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+            deletedPersistentNames.size(),
+            deletedPersistentNames);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25787055.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25787055.output
new file mode 100644
index 0000000..1572889
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25787055.output
@@ -0,0 +1,23 @@
+class B25787055 {
+  {
+    logger.atInfo().log(
+        "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+        deletedPersistentNames.size(), deletedPersistentNames);
+    logger.atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo().atInfo()
+        .atInfo().log(
+        "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+        deletedPersistentNames.size(), deletedPersistentNames);
+    logger // interior
+        .atInfo() // comments
+        .log( // are best comments
+        "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+        deletedPersistentNames.size(), deletedPersistentNames);
+    logger
+        .log(
+            "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+            deletedPersistentNames.size(), deletedPersistentNames)
+        .log(
+            "Scratch Session Cleaner exiting. Number of deleted sessions: %d, names: %s",
+            deletedPersistentNames.size(), deletedPersistentNames);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25811323.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25811323.input
new file mode 100644
index 0000000..291e34c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25811323.input
@@ -0,0 +1,5 @@
+class B25811323 {
+  void f() {
+    class Inner {};
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25811323.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25811323.output
new file mode 100644
index 0000000..0ed1ade
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B25811323.output
@@ -0,0 +1,6 @@
+class B25811323 {
+  void f() {
+    class Inner {}
+    ;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26159561.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26159561.input
new file mode 100644
index 0000000..853202d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26159561.input
@@ -0,0 +1,6 @@
+class B26159561 {
+  {
+    try (A a = a(); B b = b()) {}
+    try (A a = a();) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26159561.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26159561.output
new file mode 100644
index 0000000..d9ae269
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26159561.output
@@ -0,0 +1,7 @@
+class B26159561 {
+  {
+    try (A a = a();
+        B b = b()) {}
+    try (A a = a(); ) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26207047.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26207047.input
new file mode 100644
index 0000000..5f53f8c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26207047.input
@@ -0,0 +1,49 @@
+class B26207047 {
+  {
+    String.format(
+        "%s                                        ", xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "%s                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "{0}                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "%s                                                                           "
+            + "                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "%s                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "%s                                                                           ",
+        xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx,
+        xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx,
+        xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx);
+    String.format(
+        "%s    ", "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ",
+        "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ",
+        "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ");
+    Set<TimeZone> ZONES =
+        ImmutableSet.of(
+            TimeZone.getTimeZone("EST"),
+            // TODO:
+            TimeZone.getTimeZone("GMT"),
+            TimeZone.getTimeZone("HST"),
+            TimeZone.getTimeZone("MST"));
+    String.format(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + "%s",
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26207047.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26207047.output
new file mode 100644
index 0000000..4eea2ea
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26207047.output
@@ -0,0 +1,51 @@
+class B26207047 {
+  {
+    String.format(
+        "%s                                        ", xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "%s                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "{0}                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "%s                                                                           "
+            + "                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "%s                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "%s                                                                           ",
+        xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx,
+        xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx,
+        xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx, xxxxxx);
+    String.format(
+        "%s    ",
+        "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ",
+        "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ",
+        "      ", "      ", "      ", "      ", "      ", "      ", "      ", "      ");
+    Set<TimeZone> ZONES =
+        ImmutableSet.of(
+            TimeZone.getTimeZone("EST"),
+            // TODO:
+            TimeZone.getTimeZone("GMT"),
+            TimeZone.getTimeZone("HST"),
+            TimeZone.getTimeZone("MST"));
+    String.format(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + "%s",
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx);
+    String.format(
+        "                                                                           ",
+        xxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26275739.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26275739.input
new file mode 100644
index 0000000..70fdd90
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26275739.input
@@ -0,0 +1,18 @@
+public class B26275739 {
+  {
+    f(
+        /* x */ 1,
+        /* xx */ 2,
+        /* x */ 3,
+        /* xx */ 4);
+    f(
+        1,
+            2,
+        3,
+            4);
+    f(
+            /* asd */
+        1, 2,
+        3, 4);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26275739.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26275739.output
new file mode 100644
index 0000000..ffb0b22
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26275739.output
@@ -0,0 +1,12 @@
+public class B26275739 {
+  {
+    f(/* x */ 1, /* xx */ 2, /* x */ 3, /* xx */ 4);
+    f(
+        1, 2,
+        3, 4);
+    f(
+        /* asd */
+        1, 2,
+        3, 4);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26293162.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26293162.input
new file mode 100644
index 0000000..691bebe
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26293162.input
@@ -0,0 +1,43 @@
+class Xxxx {
+
+  public static final class XxxxXxxxxxxxXxxxxxxxxXxxxxxXxxxxxxx<
+      X, X, X extends XxxxxxxxxXxxxxxxxxXxxxxxxx<X, X>> {
+
+    int x;
+  }
+
+  public static final class XxxxXxxxxxxxXxxxxxxxxXxxxxxXxxxxxxx<
+          X, X, X extends XxxxxxxxxXxxxxxxxxXxxxxxxx<X, X>> extends Xxxxx {
+
+    int x;
+  }
+
+  class XxxxxxxxXxxxxxxxxXxxxXxxx<
+          X extends XxxxxxxxxXxxxxxx, X extends XxxxxxxxxXxxxxx, X extends Xxxxxxxxxxxxx, X>
+      extends XxxxXxxx {
+
+    int x;
+  }
+
+  @Xxx
+  class XxxxxxxxxxxXxxxxxXx<
+          X extends XxxxxxXxxxxxxxxxxXxxxxxXxxxxx, X extends Xxxxxxx, XX extends Xxxxxxxxx<X>>
+      extends XxxxxxxxXx<X, XX> {
+
+    int x;
+  }
+
+  static class XxxxxXxxxxXxxx<
+      XXXXX extends XxxxxxxxXxxxxxx<XXXXX>, XXX extends XxxxxxxxXxxxxxx<XXX>> {
+
+    int x;
+  }
+
+  @Xxx
+  class XxxxxxxxxxxXxxxxxXx<
+          X extends XxxxxxXxxxxxxxxxxXxxxxxXxxxxx, X extends Xxxxxxx, X extends Xxxxxxx,
+          XX extends Xxxxxxxxx<X>> extends XxxxxxxxXx<X, XX> {
+
+    int x;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26293162.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26293162.output
new file mode 100644
index 0000000..96361ef
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26293162.output
@@ -0,0 +1,47 @@
+class Xxxx {
+
+  public static final class XxxxXxxxxxxxXxxxxxxxxXxxxxxXxxxxxxx<
+      X, X, X extends XxxxxxxxxXxxxxxxxxXxxxxxxx<X, X>> {
+
+    int x;
+  }
+
+  public static final class XxxxXxxxxxxxXxxxxxxxxXxxxxxXxxxxxxx<
+          X, X, X extends XxxxxxxxxXxxxxxxxxXxxxxxxx<X, X>>
+      extends Xxxxx {
+
+    int x;
+  }
+
+  class XxxxxxxxXxxxxxxxxXxxxXxxx<
+          X extends XxxxxxxxxXxxxxxx, X extends XxxxxxxxxXxxxxx, X extends Xxxxxxxxxxxxx, X>
+      extends XxxxXxxx {
+
+    int x;
+  }
+
+  @Xxx
+  class XxxxxxxxxxxXxxxxxXx<
+          X extends XxxxxxXxxxxxxxxxxXxxxxxXxxxxx, X extends Xxxxxxx, XX extends Xxxxxxxxx<X>>
+      extends XxxxxxxxXx<X, XX> {
+
+    int x;
+  }
+
+  static class XxxxxXxxxxXxxx<
+      XXXXX extends XxxxxxxxXxxxxxx<XXXXX>, XXX extends XxxxxxxxXxxxxxx<XXX>> {
+
+    int x;
+  }
+
+  @Xxx
+  class XxxxxxxxxxxXxxxxxXx<
+          X extends XxxxxxXxxxxxxxxxxXxxxxxXxxxxx,
+          X extends Xxxxxxx,
+          X extends Xxxxxxx,
+          XX extends Xxxxxxxxx<X>>
+      extends XxxxxxxxXx<X, XX> {
+
+    int x;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26694550.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26694550.input
new file mode 100644
index 0000000..7af690a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26694550.input
@@ -0,0 +1,21 @@
+class B26694550 {
+  {
+    /* === not a param comment */
+    fffffffffffffffffffffffffffffff(
+        ImmutableList.copyOf(keys), /*&=*//*keepGoing=*/
+        false,
+        ggggggggggggggggggggggggggggggggggggggggggg);
+    fffffffffffffffffffffffffffffff(
+        ImmutableList.copyOf(keys),
+        /*keepGoing=*/
+        false,
+        ggggggggggggggggggggggggggggggggggggggggggg);
+    fffffffffffffffffffffffffffffff(
+        ImmutableList.copyOf(keys),
+        /*foo_bar=*/
+        false,
+        /*foo-bar=*/
+        false,
+        ggggggggggggggggggggggggggggggggggggggggggg);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26694550.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26694550.output
new file mode 100644
index 0000000..945a40f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26694550.output
@@ -0,0 +1,18 @@
+class B26694550 {
+  {
+    /* === not a param comment */
+    fffffffffffffffffffffffffffffff(
+        ImmutableList.copyOf(keys), /*&=*/
+        /*keepGoing=*/ false,
+        ggggggggggggggggggggggggggggggggggggggggggg);
+    fffffffffffffffffffffffffffffff(
+        ImmutableList.copyOf(keys),
+        /*keepGoing=*/ false,
+        ggggggggggggggggggggggggggggggggggggggggggg);
+    fffffffffffffffffffffffffffffff(
+        ImmutableList.copyOf(keys),
+        /*foo_bar=*/ false,
+        /*foo-bar=*/ false,
+        ggggggggggggggggggggggggggggggggggggggggggg);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26884608.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26884608.input
new file mode 100644
index 0000000..52764b0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26884608.input
@@ -0,0 +1,34 @@
+class X26884608 {
+  {
+    this
+        .<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+            XXXXXXXXX_XXXXXXXXX);
+    this
+        .xxxXxxxxXxxxxxx(
+            XxxxxXxxx.xxxxxXxxxx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+    Xxxxx.this
+        .<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+            XXXXXXXXX_XXXXXXXXX);
+    Xxxxx.this
+        .xxxXxxxxXxxx(XxxxxXxxx.xxxxxXx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+    xxx.xxxxxx.Xxxxx.this
+        .<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+            XXXXXXXXX_XXXXXXXXX);
+    xxx.xxxxxx.Xxxxx.this
+        .xxxXxxxxXxxx(XxxxxXxxx.xxxxxXx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+
+    super.<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+        XXXXXXXXX_XXXXXXXXX);
+    super.xxxXxxxxXxxx(
+        XxxxxXxxxx.xxxxxXxxxx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+    Xxxxx.super.<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+        XXXXXXXXX_XXXXXXXXX);
+    Xxxxx.super.xxxXxxxxXxxx(
+        XxxxxXxxx.xxxxxXx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+    xxx.xxxxxx.Xxxxx.super.<XxxxxXxxxxxxxXxxxxx,
+        XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(XXXXXXXXX_XXXXXXXXX);
+    xxx.xxxxxx.Xxxxx.super.xxxXxxxxXxxx(
+        XxxxxXxxx.xxxxxXx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+  }
+}
+
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26884608.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26884608.output
new file mode 100644
index 0000000..9a1cccf
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26884608.output
@@ -0,0 +1,31 @@
+class X26884608 {
+  {
+    this.<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+        XXXXXXXXX_XXXXXXXXX);
+    this.xxxXxxxxXxxxxxx(
+        XxxxxXxxx.xxxxxXxxxx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+    Xxxxx.this.<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+        XXXXXXXXX_XXXXXXXXX);
+    Xxxxx.this.xxxXxxxxXxxx(
+        XxxxxXxxx.xxxxxXx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+    xxx.xxxxxx.Xxxxx.this
+        .<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+            XXXXXXXXX_XXXXXXXXX);
+    xxx.xxxxxx.Xxxxx.this.xxxXxxxxXxxx(
+        XxxxxXxxx.xxxxxXx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+
+    super.<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+        XXXXXXXXX_XXXXXXXXX);
+    super.xxxXxxxxXxxx(
+        XxxxxXxxxx.xxxxxXxxxx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+    Xxxxx.super.<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+        XXXXXXXXX_XXXXXXXXX);
+    Xxxxx.super.xxxXxxxxXxxx(
+        XxxxxXxxx.xxxxxXx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+    xxx.xxxxxx.Xxxxx.super
+        .<XxxxxXxxxxxxxXxxxxx, XxxxxxxxxXxxxxxxXxxxxxxxx>xxxxxxxxxXxxxxxXxxxxxxXxxxxx(
+            XXXXXXXXX_XXXXXXXXX);
+    xxx.xxxxxx.Xxxxx.super.xxxXxxxxXxxx(
+        XxxxxXxxx.xxxxxXx(xxxxxx.xxxxxxxXxxxxxXxxxx(xxxxxx), xxxxxx.xxXxxxxxx()));
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26928842.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26928842.input
new file mode 100644
index 0000000..1ce9dc9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26928842.input
@@ -0,0 +1,9 @@
+class B26928842 {
+  {
+    curr.setData(curr.getData().toBuilder()
+        .setPushCertificate(                    // New
+            curr.getData().getPushCertficate()) // Old, misspelled
+        .clearPushCertficate()                  // Old, misspelled
+        .build());
+  }
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26928842.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26928842.output
new file mode 100644
index 0000000..6623676
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26928842.output
@@ -0,0 +1,10 @@
+class B26928842 {
+  {
+    curr.setData(
+        curr.getData().toBuilder()
+            .setPushCertificate( // New
+                curr.getData().getPushCertficate()) // Old, misspelled
+            .clearPushCertficate() // Old, misspelled
+            .build());
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26952926.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26952926.input
new file mode 100644
index 0000000..dd3451d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26952926.input
@@ -0,0 +1,51 @@
+class T {
+  {
+    this(
+        x, // line comment
+        x // line comment
+    );
+    super(
+        x, // line comment
+        x // line comment
+    );
+    new t(
+        x, // line comment
+        x // line comment
+    );
+    f(
+        x, // line comment
+        x // line comment
+        );
+    fffff.fffff(
+        x, // line comment
+        x // line comment
+        );
+    fffff
+        .fffff(
+            x, // line comment
+            x // line comment
+        )
+        .fffff(
+            x, // line comment
+            x // line comment
+        );
+  }
+
+  {
+    this(x // line comment
+    );
+    super(x // line comment
+    );
+    new t(x // line comment
+    );
+    f(x // line comment
+        );
+    fffff.fffff(x // line comment
+        );
+    fffff
+        .fffff(x // line comment
+        )
+        .fffff(x // line comment
+        );
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26952926.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26952926.output
new file mode 100644
index 0000000..fa93da7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B26952926.output
@@ -0,0 +1,58 @@
+class T {
+  {
+    this(
+        x, // line comment
+        x // line comment
+        );
+    super(
+        x, // line comment
+        x // line comment
+        );
+    new t(
+        x, // line comment
+        x // line comment
+        );
+    f(
+        x, // line comment
+        x // line comment
+        );
+    fffff.fffff(
+        x, // line comment
+        x // line comment
+        );
+    fffff
+        .fffff(
+            x, // line comment
+            x // line comment
+            )
+        .fffff(
+            x, // line comment
+            x // line comment
+            );
+  }
+
+  {
+    this(
+        x // line comment
+        );
+    super(
+        x // line comment
+        );
+    new t(
+        x // line comment
+        );
+    f(
+        x // line comment
+        );
+    fffff.fffff(
+        x // line comment
+        );
+    fffff
+        .fffff(
+            x // line comment
+            )
+        .fffff(
+            x // line comment
+            );
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27078833.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27078833.input
new file mode 100644
index 0000000..597c635
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27078833.input
@@ -0,0 +1,19 @@
+enum OpKind {
+
+  POST_INCR(Precedence.POSTFIX),
+  POST_DECR(Precedence.POSTFIX),
+
+  PRE_INCR(Precedence.UNARY),
+  PRE_DECR(Precedence.UNARY),
+  PLUS(Precedence.UNARY),
+  NEG(Precedence.UNARY),
+  COMP(Precedence.UNARY),
+  NOT(Precedence.UNARY),
+
+  MULT(Precedence.MULTIPLICATIVE),
+  DIVIDE(Precedence.MULTIPLICATIVE),
+  MODULO(Precedence.MULTIPLICATIVE),
+  ;
+
+  // ...
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27078833.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27078833.output
new file mode 100644
index 0000000..8ff9fde
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27078833.output
@@ -0,0 +1,18 @@
+enum OpKind {
+  POST_INCR(Precedence.POSTFIX),
+  POST_DECR(Precedence.POSTFIX),
+
+  PRE_INCR(Precedence.UNARY),
+  PRE_DECR(Precedence.UNARY),
+  PLUS(Precedence.UNARY),
+  NEG(Precedence.UNARY),
+  COMP(Precedence.UNARY),
+  NOT(Precedence.UNARY),
+
+  MULT(Precedence.MULTIPLICATIVE),
+  DIVIDE(Precedence.MULTIPLICATIVE),
+  MODULO(Precedence.MULTIPLICATIVE),
+  ;
+
+  // ...
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.input
new file mode 100644
index 0000000..bf4076d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.input
@@ -0,0 +1,7 @@
+enum TrailingComment {
+  /** foo */ FOO, /** bar */
+  BAR;
+
+  /** a */ Object a; /** b */
+  Object b;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.output
new file mode 100644
index 0000000..b1e33e9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.output
@@ -0,0 +1,11 @@
+enum TrailingComment {
+  /** foo */
+  FOO,
+  /** bar */
+  BAR;
+
+  /** a */
+  Object a;
+  /** b */
+  Object b;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27602933.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27602933.input
new file mode 100644
index 0000000..1046a63
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27602933.input
@@ -0,0 +1,11 @@
+class B27602933 {
+  {
+    try {
+    } catch (final A | B x) {
+    }
+
+    try {
+    } catch (@SuppressWarnings("unused") IllegalArgumentException | RuntimeException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27602933.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27602933.output
new file mode 100644
index 0000000..1046a63
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27602933.output
@@ -0,0 +1,11 @@
+class B27602933 {
+  {
+    try {
+    } catch (final A | B x) {
+    }
+
+    try {
+    } catch (@SuppressWarnings("unused") IllegalArgumentException | RuntimeException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28066276.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28066276.input
new file mode 100644
index 0000000..f8d336b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28066276.input
@@ -0,0 +1,13 @@
+class B28066276 {
+  class A extends S
+      implements I________________, I________________, I________________, I________________,
+          I________________, I________________, I________________, I________________,
+          I________________, I________________, I________________, I________________,
+          I________________, I________________, I________________, I________________ {}
+
+  interface B
+      extends I________________, I________________, I________________, I________________,
+          I________________, I________________, I________________, I________________,
+          I________________, I________________, I________________, I________________,
+          I________________, I________________, I________________, I________________ {}
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28066276.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28066276.output
new file mode 100644
index 0000000..36f2217
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28066276.output
@@ -0,0 +1,37 @@
+class B28066276 {
+  class A extends S
+      implements I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________ {}
+
+  interface B
+      extends I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________,
+          I________________ {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28774859.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28774859.input
new file mode 100644
index 0000000..796e7d4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28774859.input
@@ -0,0 +1,3 @@
+class B28774859 {
+  java.util. /*comment*/ Map. /*comment*/ Entry e;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28774859.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28774859.output
new file mode 100644
index 0000000..ae59d75
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28774859.output
@@ -0,0 +1,3 @@
+class B28774859 {
+  java.util./*comment*/ Map./*comment*/ Entry e;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28786284.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28786284.input
new file mode 100644
index 0000000..18ac2d7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28786284.input
@@ -0,0 +1,5 @@
+enum E {
+  ONE,
+  TWO;
+  ;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28786284.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28786284.output
new file mode 100644
index 0000000..18ac2d7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28786284.output
@@ -0,0 +1,5 @@
+enum E {
+  ONE,
+  TWO;
+  ;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28788559.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28788559.input
new file mode 100644
index 0000000..a38d10b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28788559.input
@@ -0,0 +1,7 @@
+import a.A;;
+import b.B;
+
+class Test {
+  A a;
+  B b;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28788559.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28788559.output
new file mode 100644
index 0000000..8a1db80
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B28788559.output
@@ -0,0 +1,8 @@
+import a.A;
+;
+import b.B;
+
+class Test {
+  A a;
+  B b;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.input
new file mode 100644
index 0000000..32da76c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.input
@@ -0,0 +1,60 @@
+public class B29368546 {
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   *     class T {
+   *     }
+   * }</pre>
+   */
+  int x;
+
+  /**
+   * Example:
+   *
+   * <pre>    {@code
+   *   class T {
+   *   }
+   * }</pre>
+   */
+  int x;
+
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   *   class T {
+   *   }}</pre>
+   */
+  int x;
+
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   * foo bar
+   * }
+   *
+   * more stuff
+   * </pre>
+   *
+   * <pre>{@code
+   * foo bar
+   * }
+   *
+   * more stuff that ends with {}
+   * </pre>
+   */
+  int x;
+
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   * class T {}
+   * </pre> // oops, we forgot the close brace
+   *
+   * more stuff that ends with {}
+   */
+  int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.output
new file mode 100644
index 0000000..9f0fdd2
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29368546.output
@@ -0,0 +1,61 @@
+public class B29368546 {
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   * class T {
+   * }
+   * }</pre>
+   */
+  int x;
+
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   * class T {
+   * }
+   * }</pre>
+   */
+  int x;
+
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   * class T {
+   * }
+   * }</pre>
+   */
+  int x;
+
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   * foo bar
+   * }
+   *
+   * more stuff
+   * </pre>
+   *
+   * <pre>{@code
+   * foo bar
+   * }
+   *
+   * more stuff that ends with {
+   * }</pre>
+   */
+  int x;
+
+  /**
+   * Example:
+   *
+   * <pre>{@code
+   * class T {}
+   * </pre> // oops, we forgot the close brace
+   *
+   * more stuff that ends with {}
+   */
+  int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29618429.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29618429.input
new file mode 100644
index 0000000..2beb2a7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29618429.input
@@ -0,0 +1,13 @@
+class B29618429 {
+  /**
+   * Hello
+   *
+   * <p>World
+   * <pre>
+   * @LooksLikeATag(
+   *   foo = "bar"
+   * )
+   * </pre>
+   */
+  int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29618429.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29618429.output
new file mode 100644
index 0000000..f118c53
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29618429.output
@@ -0,0 +1,14 @@
+class B29618429 {
+  /**
+   * Hello
+   *
+   * <p>World
+   *
+   * <pre>
+   * @LooksLikeATag(
+   *   foo = "bar"
+   * )
+   * </pre>
+   */
+  int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29705613.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29705613.input
new file mode 100644
index 0000000..0adb884
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29705613.input
@@ -0,0 +1,8 @@
+class B29705613 {
+	/**
+	 * This tag isn't closed: {@link Foo
+	 *
+	 * <p>A paragraph
+	 */
+  int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29705613.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29705613.output
new file mode 100644
index 0000000..df46d19
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B29705613.output
@@ -0,0 +1,8 @@
+class B29705613 {
+  /**
+   * This tag isn't closed: {@link Foo
+   *
+   * <p>A paragraph
+   */
+  int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30153869.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30153869.input
new file mode 100644
index 0000000..d5fae20
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30153869.input
@@ -0,0 +1,26 @@
+/**
+ * one
+ *
+ * <ol>
+ * <li>two
+ * </ol>
+ *
+ * <p>
+ * <li>four
+ */
+class T {
+
+  /**
+   * One
+   *
+   * <ul>
+   * <li>a
+   *     <ul>
+   *     <li>b
+   *     <li>c
+   *     </ul>
+   * <li>e
+   * </ul>
+   */
+  int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30153869.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30153869.output
new file mode 100644
index 0000000..5cef3b8
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30153869.output
@@ -0,0 +1,26 @@
+/**
+ * one
+ *
+ * <ol>
+ *   <li>two
+ * </ol>
+ *
+ * <p>
+ * <li>four
+ */
+class T {
+
+  /**
+   * One
+   *
+   * <ul>
+   *   <li>a
+   *       <ul>
+   *         <li>b
+   *         <li>c
+   *       </ul>
+   *   <li>e
+   * </ul>
+   */
+  int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30223986.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30223986.input
new file mode 100644
index 0000000..5b6fbb2
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30223986.input
@@ -0,0 +1,45 @@
+/**
+ * Base class for {@link Foo}.
+ *
+ * <p>The subclass should implement {@link #bar()}, with the implementation returning a
+ * new instance of the foo relevant to that baz.
+ *
+ * Example:
+ * <code>
+ *   @Override
+ *   protected Foo bar() {
+ *     return new Foo();
+ *   }
+ * </code>
+ *
+ * The subclass should call {@link #get()} to get the foo object,
+ * and should not cache it in the subclass.
+ * Example:
+ * <code>
+ *   @Annotation("something")
+ *   public void thing() {
+ *   }
+ * </code>
+ *
+ * @param <T> concrete subclass
+ */
+class Test {
+  /**
+   * Field
+   *
+   * Example:
+   * <pre><code>
+   *   @Annotation("something")
+   *   public void thing() {
+   *   }
+   * </code></pre>
+   *
+   * @param asd
+   */
+  int x() {}
+
+  /**
+   * Inline <code>foo</code>.
+   */
+  int y() {}
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30223986.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30223986.output
new file mode 100644
index 0000000..7e7c427
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30223986.output
@@ -0,0 +1,39 @@
+/**
+ * Base class for {@link Foo}.
+ *
+ * <p>The subclass should implement {@link #bar()}, with the implementation returning a new instance
+ * of the foo relevant to that baz.
+ *
+ * <p>Example: <code>
+ *   @Override
+ *   protected Foo bar() {
+ *     return new Foo();
+ *   }
+ * </code> The subclass should call {@link #get()} to get the foo object, and should not cache it in
+ * the subclass. Example: <code>
+ *   @Annotation("something")
+ *   public void thing() {
+ *   }
+ * </code>
+ *
+ * @param <T> concrete subclass
+ */
+class Test {
+  /**
+   * Field
+   *
+   * <p>Example:
+   *
+   * <pre><code>
+   *   @Annotation("something")
+   *   public void thing() {
+   *   }
+   * </code></pre>
+   *
+   * @param asd
+   */
+  int x() {}
+
+  /** Inline <code>foo</code>. */
+  int y() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30764810.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30764810.input
new file mode 100644
index 0000000..0643db0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30764810.input
@@ -0,0 +1,16 @@
+class B30764810 {
+  @Foo(
+    x = {0},
+    y = 0
+    // comment
+  )
+  int a;
+
+  @Foo(
+    x = {0},
+    y = 0
+    /* comment
+     */
+  )
+  int b;
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30764810.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30764810.output
new file mode 100644
index 0000000..94f0f9b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30764810.output
@@ -0,0 +1,16 @@
+class B30764810 {
+  @Foo(
+      x = {0},
+      y = 0
+      // comment
+      )
+  int a;
+
+  @Foo(
+      x = {0},
+      y = 0
+      /* comment
+       */
+      )
+  int b;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30789352.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30789352.input
new file mode 100644
index 0000000..2559b1f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30789352.input
@@ -0,0 +1,6 @@
+/**
+ * Body.
+ *
+ * @param <P> the protobuf type
+ */
+class B30789352<P> {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30789352.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30789352.output
new file mode 100644
index 0000000..2559b1f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B30789352.output
@@ -0,0 +1,6 @@
+/**
+ * Body.
+ *
+ * @param <P> the protobuf type
+ */
+class B30789352<P> {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32114928.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32114928.input
new file mode 100644
index 0000000..95da03c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32114928.input
@@ -0,0 +1,9 @@
+class B32114928 {
+  {
+    Class<T> tClass =
+        (Class<T>)
+            verifyNotNull((ParameterizedType) getClass().getGenericSuperclass())
+                    .getActualTypeArguments()[
+                0];
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32114928.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32114928.output
new file mode 100644
index 0000000..6535ae1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32114928.output
@@ -0,0 +1,8 @@
+class B32114928 {
+  {
+    Class<T> tClass =
+        (Class<T>)
+            verifyNotNull((ParameterizedType) getClass().getGenericSuperclass())
+                .getActualTypeArguments()[0];
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32284705.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32284705.input
new file mode 100644
index 0000000..916c588
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32284705.input
@@ -0,0 +1,8 @@
+/**
+ * Foo <code>
+ * <pre>
+ * bar
+ * </pre>
+ * </code>
+ */
+class B32284705 {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32284705.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32284705.output
new file mode 100644
index 0000000..916c588
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32284705.output
@@ -0,0 +1,8 @@
+/**
+ * Foo <code>
+ * <pre>
+ * bar
+ * </pre>
+ * </code>
+ */
+class B32284705 {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32397217.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32397217.input
new file mode 100644
index 0000000..b402a4a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32397217.input
@@ -0,0 +1,6 @@
+class T {
+  {
+    f(
+        1 /* 1                                       */, 2 /* 2                                         */);
+  }
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32397217.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32397217.output
new file mode 100644
index 0000000..a962f63
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32397217.output
@@ -0,0 +1,7 @@
+class T {
+  {
+    f(
+        1 /* 1                                       */,
+        2 /* 2                                         */);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32729600.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32729600.input
new file mode 100644
index 0000000..4fd5fc5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32729600.input
@@ -0,0 +1,5 @@
+class B32729600 {
+  {
+    f(() -> {});
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32729600.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32729600.output
new file mode 100644
index 0000000..4fd5fc5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B32729600.output
@@ -0,0 +1,5 @@
+class B32729600 {
+  {
+    f(() -> {});
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B33358723.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B33358723.input
new file mode 100644
index 0000000..5d461e0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B33358723.input
@@ -0,0 +1,24 @@
+class B33358723 {
+  {
+    f(
+        //
+        x ->
+        System.err.println(
+        //
+        "hello"));
+    f(
+        //
+        ( //
+        x) ->
+        System.err.println(
+        //
+        "hello"));
+    f(
+        //
+        (int //
+        x) ->
+        System.err.println(
+        //
+        "hello"));
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B33358723.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B33358723.output
new file mode 100644
index 0000000..da0000c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B33358723.output
@@ -0,0 +1,24 @@
+class B33358723 {
+  {
+    f(
+        //
+        x ->
+            System.err.println(
+                //
+                "hello"));
+    f(
+        //
+        ( //
+            x) ->
+            System.err.println(
+                //
+                "hello"));
+    f(
+        //
+        (int //
+                x) ->
+            System.err.println(
+                //
+                "hello"));
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35644813.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35644813.input
new file mode 100644
index 0000000..37ae322
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35644813.input
@@ -0,0 +1,19 @@
+class B35644813 {
+  {
+    return foo____________
+        .bar__________()
+        .baz____________()
+        .stream()
+        .map(Baz::getId)
+        .collect(toList());
+  }
+
+  private static final ImmutableSet<String> SCANDINAVIA =
+      ImmutableSet.of(
+              DENMARK____________________________________________________,
+              NORWAY_____________________________________________________,
+              SWEDEN_____________________________________________________)
+          .stream()
+          .map(x -> String.format("country: %s", x.toLowerCase()))
+          .collect(toImmutableSet());
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35644813.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35644813.output
new file mode 100644
index 0000000..cee0e3b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35644813.output
@@ -0,0 +1,16 @@
+class B35644813 {
+  {
+    return foo____________.bar__________().baz____________().stream()
+        .map(Baz::getId)
+        .collect(toList());
+  }
+
+  private static final ImmutableSet<String> SCANDINAVIA =
+      ImmutableSet.of(
+              DENMARK____________________________________________________,
+              NORWAY_____________________________________________________,
+              SWEDEN_____________________________________________________)
+          .stream()
+          .map(x -> String.format("country: %s", x.toLowerCase()))
+          .collect(toImmutableSet());
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35797074.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35797074.input
new file mode 100644
index 0000000..aa4dc09
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35797074.input
@@ -0,0 +1,4 @@
+class B35797074 {
+  /** javadoc */
+  /* package */ final int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35797074.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35797074.output
new file mode 100644
index 0000000..aa4dc09
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B35797074.output
@@ -0,0 +1,4 @@
+class B35797074 {
+  /** javadoc */
+  /* package */ final int x;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B36194204.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B36194204.input
new file mode 100644
index 0000000..2a3c702
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B36194204.input
@@ -0,0 +1,5 @@
+class B36194204 {
+  {
+    Function<String[], String> f = (String... s) -> s[0];
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B36194204.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B36194204.output
new file mode 100644
index 0000000..2a3c702
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B36194204.output
@@ -0,0 +1,5 @@
+class B36194204 {
+  {
+    Function<String[], String> f = (String... s) -> s[0];
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37629610.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37629610.input
new file mode 100644
index 0000000..65d9d47
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37629610.input
@@ -0,0 +1,16 @@
+class B37629610 {
+  private static ImmutableList<InputOutputPair> toInputOutputPairs(Options options) {
+    for (Iterator<Path> inputIt = options.inputJars.iterator(),
+                outputIt = options.outputJars.iterator();
+        inputIt.hasNext();
+        ) {}
+    for (Iterator<Path> inputIt = options.inputJars.iterator(),
+                outputIt = options.outputJars.iterator();
+        ;
+        inputIt.next()) {}
+    for (inputIt = options.inputJars.iterator(), //
+            outputIt = options.outputJars.iterator();
+        ;
+        inputIt.next()) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37629610.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37629610.output
new file mode 100644
index 0000000..9942776
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37629610.output
@@ -0,0 +1,15 @@
+class B37629610 {
+  private static ImmutableList<InputOutputPair> toInputOutputPairs(Options options) {
+    for (Iterator<Path> inputIt = options.inputJars.iterator(),
+            outputIt = options.outputJars.iterator();
+        inputIt.hasNext(); ) {}
+    for (Iterator<Path> inputIt = options.inputJars.iterator(),
+            outputIt = options.outputJars.iterator();
+        ;
+        inputIt.next()) {}
+    for (inputIt = options.inputJars.iterator(), //
+            outputIt = options.outputJars.iterator();
+        ;
+        inputIt.next()) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37895033.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37895033.input
new file mode 100644
index 0000000..9ec5e46
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37895033.input
@@ -0,0 +1,20 @@
+class B37895033 {
+  String[][] xs =
+      new String[][] {
+        {"B3", "C3", "D3"},
+        {"B4", "C4", "D4"},
+        {"B5", "C5", "D5"}
+      };
+  String[] ys =
+      new String[] {
+        "B3",
+        "B4",
+        "B5",
+      };
+  String[][] zs =
+      new String[][] {
+        {"B3"},
+        {"B4"},
+        {"B5"},
+      };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37895033.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37895033.output
new file mode 100644
index 0000000..1ea50bb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B37895033.output
@@ -0,0 +1,16 @@
+class B37895033 {
+  String[][] xs =
+      new String[][] {
+        {"B3", "C3", "D3"},
+        {"B4", "C4", "D4"},
+        {"B5", "C5", "D5"}
+      };
+  String[] ys =
+      new String[] {
+        "B3", "B4", "B5",
+      };
+  String[][] zs =
+      new String[][] {
+        {"B3"}, {"B4"}, {"B5"},
+      };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38203081.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38203081.input
new file mode 100644
index 0000000..1241987
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38203081.input
@@ -0,0 +1,5 @@
+class B38203081 {
+  void f(int @A []... xs) {}
+
+  void g(int @A [] @B... xs) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38203081.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38203081.output
new file mode 100644
index 0000000..39bb11a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38203081.output
@@ -0,0 +1,5 @@
+class B38203081 {
+  void f(int @A []... xs) {}
+
+  void g(int @A [] @B ... xs) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38241237.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38241237.input
new file mode 100644
index 0000000..1909e49
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38241237.input
@@ -0,0 +1,7 @@
+class B38241237 {
+  //foo
+  //bar
+  //one long incredibly unbroken sentence moving from topic to topic so that no-one had a chance to interrupt
+  ///baz
+  ////
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38241237.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38241237.output
new file mode 100644
index 0000000..3dcebb1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38241237.output
@@ -0,0 +1,8 @@
+class B38241237 {
+  // foo
+  // bar
+  // one long incredibly unbroken sentence moving from topic to topic so that no-one had a chance to
+  // interrupt
+  /// baz
+  ////
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38352414.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38352414.input
new file mode 100644
index 0000000..60726fb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38352414.input
@@ -0,0 +1,2 @@
+@Redacted(Redacted.REDACTED + "/redacted", redacted = Redacted.class)
+class B38352414 {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38352414.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38352414.output
new file mode 100644
index 0000000..60726fb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B38352414.output
@@ -0,0 +1,2 @@
+@Redacted(Redacted.REDACTED + "/redacted", redacted = Redacted.class)
+class B38352414 {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B64493529.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B64493529.input
new file mode 100644
index 0000000..e0d65cb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B64493529.input
@@ -0,0 +1,10 @@
+/** Copies from {@link Foo} to {@link Bar}. */
+// XXXX.XxXxxxxx()
+class B64493529 {
+  // XXXX.XxxxXxxxxx(//xxxxx/xxxxxxx/xxxx/xxx/xxxxxx/xxx/xxxxxxx/xxxx/xxx/xxxxxxx/xxxxxxx/\
+  //                 XxxxxxXxxxxxXXxxxxxxXxxxxx.java)
+  // XXXX.XxxxXxxxxx(//xxxxx/xxxxxxx/xxxx/xxx/xxxxxx/xxx/xxxxxxx/xxxx/xxx/xxxxxxx/xxxxxxx/\
+  //                 XxxxxxXxxxxxXXxxxxxxXxxxxx.java)
+}
+// XXXX.XxxxXxxxxx(//xxxxx/xxxxxxx/xxxx/xxx/xxxxxx/xxx/xxxxxxx/xxxx/xxx/xxxxxxx/xxxxxxx/\
+//                 XxxxxxXxxxxxXXxxxxxxXxxxxx.java)
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B64493529.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B64493529.output
new file mode 100644
index 0000000..e0d65cb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B64493529.output
@@ -0,0 +1,10 @@
+/** Copies from {@link Foo} to {@link Bar}. */
+// XXXX.XxXxxxxx()
+class B64493529 {
+  // XXXX.XxxxXxxxxx(//xxxxx/xxxxxxx/xxxx/xxx/xxxxxx/xxx/xxxxxxx/xxxx/xxx/xxxxxxx/xxxxxxx/\
+  //                 XxxxxxXxxxxxXXxxxxxxXxxxxx.java)
+  // XXXX.XxxxXxxxxx(//xxxxx/xxxxxxx/xxxx/xxx/xxxxxx/xxx/xxxxxxx/xxxx/xxx/xxxxxxx/xxxxxxx/\
+  //                 XxxxxxXxxxxxXXxxxxxxXxxxxx.java)
+}
+// XXXX.XxxxXxxxxx(//xxxxx/xxxxxxx/xxxx/xxx/xxxxxx/xxx/xxxxxxx/xxxx/xxx/xxxxxxx/xxxxxxx/\
+//                 XxxxxxXxxxxxXXxxxxxxXxxxxx.java)
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B65214682.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B65214682.input
new file mode 100644
index 0000000..d6e22f6
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B65214682.input
@@ -0,0 +1,17 @@
+@BugPattern(
+  name = "AsyncFunctionReturnsImmediate",
+  summary = SIMPLIFY,
+  category = GUAVA,
+  severity = NOT_A_PROBLEM,
+  maturity = EXPERIMENTAL
+)
+@BugPattern(
+    name = "AsyncFunctionReturnsImmediate",
+    summary = SIMPLIFY,
+    explanation =
+        "If an AsyncFunction always returns immediateFuture() and never throws, it can "
+            + "be replaced with a Function.",
+    category = GUAVA,
+    severity = NOT_A_PROBLEM,
+    maturity = EXPERIMENTAL)
+class Test {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B65214682.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B65214682.output
new file mode 100644
index 0000000..ba49493
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B65214682.output
@@ -0,0 +1,16 @@
+@BugPattern(
+    name = "AsyncFunctionReturnsImmediate",
+    summary = SIMPLIFY,
+    category = GUAVA,
+    severity = NOT_A_PROBLEM,
+    maturity = EXPERIMENTAL)
+@BugPattern(
+    name = "AsyncFunctionReturnsImmediate",
+    summary = SIMPLIFY,
+    explanation =
+        "If an AsyncFunction always returns immediateFuture() and never throws, it can "
+            + "be replaced with a Function.",
+    category = GUAVA,
+    severity = NOT_A_PROBLEM,
+    maturity = EXPERIMENTAL)
+class Test {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B72507902.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B72507902.input
new file mode 100644
index 0000000..18d1900
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B72507902.input
@@ -0,0 +1,6 @@
+class B72507902 {
+  private static class Xxxxxxxxxxxxxxxxxxxx
+      extends Xxxxxxxxxxx<
+          Pair<xxxxxxxxxxxxxxxxxxx, Xxxxxxxxxx<Xxxxxxxxx>>, Xxxxxxxxxxxxxxxxxxxx,
+          List<Xxxxxxxxx>> {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B72507902.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B72507902.output
new file mode 100644
index 0000000..cbcfe0f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B72507902.output
@@ -0,0 +1,7 @@
+class B72507902 {
+  private static class Xxxxxxxxxxxxxxxxxxxx
+      extends Xxxxxxxxxxx<
+          Pair<xxxxxxxxxxxxxxxxxxx, Xxxxxxxxxx<Xxxxxxxxx>>,
+          Xxxxxxxxxxxxxxxxxxxx,
+          List<Xxxxxxxxx>> {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.input
new file mode 100644
index 0000000..7baed6c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.input
@@ -0,0 +1,81 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for CastExpressions, CatchClauses, CharacterLiterals, ClassInstanceCreations,
+ * CommentHelper, ConditionalExpressions, ConstructorInvocations, ContinueStatements, and
+ * CreationReferences.
+ */
+class C<T> {
+  // TODO(jdd): Test higher-language-level constructs.
+
+  C() {
+    this(
+        0, 1,
+        2, 3,
+        4, 5,
+        6, 7,
+        8, 9,
+        10, 11,
+        12, 13,
+        14, 15,
+        16, 17,
+        18, 19,
+        20, 21,
+        22, 23,
+        24, 25,
+        26, 27,
+        28, 29,
+        30, 31);
+  }
+
+  C(int... x) {}
+
+  void f() {
+    try {
+    } catch (RuntimeException e) {
+      Object x =
+          (Pair<
+                  Pair<
+                      Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                      Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>,
+                  Pair<
+                      Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                      Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>>)
+              null;
+      C<Integer> c =
+          new C<Integer>(
+              0, 1,
+              2, 3,
+              4, 5,
+              6, 7,
+              8, 9,
+              10, 11,
+              12, 13,
+              14, 15,
+              16, 17,
+              18, 19,
+              20, 21,
+              22, 23,
+              24, 25,
+              26, 27,
+              28, 29,
+              30, 31);
+      int i = 0;
+      int j =
+          i == 0
+              ? 0
+              : i == 1
+                  ? 1
+                  : i == 2 ? 2 : i == 3 ? 3 : i == 4 ? 4 : i == 5 ? 5 : i == 6 ? 6 : i == 7 ? 7 : i;
+      LABEL:
+      while (true != false) {
+        if (false == true)
+          continue;
+        if (false == false)
+          continue LABEL;
+        // Comment indented +2
+      // Comment indented +2
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.output
new file mode 100644
index 0000000..fcf773e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.output
@@ -0,0 +1,79 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for CastExpressions, CatchClauses, CharacterLiterals, ClassInstanceCreations,
+ * CommentHelper, ConditionalExpressions, ConstructorInvocations, ContinueStatements, and
+ * CreationReferences.
+ */
+class C<T> {
+  // TODO(jdd): Test higher-language-level constructs.
+
+  C() {
+    this(
+        0, 1,
+        2, 3,
+        4, 5,
+        6, 7,
+        8, 9,
+        10, 11,
+        12, 13,
+        14, 15,
+        16, 17,
+        18, 19,
+        20, 21,
+        22, 23,
+        24, 25,
+        26, 27,
+        28, 29,
+        30, 31);
+  }
+
+  C(int... x) {}
+
+  void f() {
+    try {
+    } catch (RuntimeException e) {
+      Object x =
+          (Pair<
+                  Pair<
+                      Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                      Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>,
+                  Pair<
+                      Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                      Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>>)
+              null;
+      C<Integer> c =
+          new C<Integer>(
+              0, 1,
+              2, 3,
+              4, 5,
+              6, 7,
+              8, 9,
+              10, 11,
+              12, 13,
+              14, 15,
+              16, 17,
+              18, 19,
+              20, 21,
+              22, 23,
+              24, 25,
+              26, 27,
+              28, 29,
+              30, 31);
+      int i = 0;
+      int j =
+          i == 0
+              ? 0
+              : i == 1
+                  ? 1
+                  : i == 2 ? 2 : i == 3 ? 3 : i == 4 ? 4 : i == 5 ? 5 : i == 6 ? 6 : i == 7 ? 7 : i;
+      LABEL:
+      while (true != false) {
+        if (false == true) continue;
+        if (false == false) continue LABEL;
+        // Comment indented +2
+        // Comment indented +2
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/CL240367479.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/CL240367479.input
new file mode 100644
index 0000000..be76390
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/CL240367479.input
@@ -0,0 +1,6 @@
+package foo;;
+
+import com.google.second.Foo;
+import com.google.first.Bar;
+
+public class Blim {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/CL240367479.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/CL240367479.output
new file mode 100644
index 0000000..025d237
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/CL240367479.output
@@ -0,0 +1,6 @@
+package foo;
+;
+import com.google.second.Foo;
+import com.google.first.Bar;
+
+public class Blim {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.input
new file mode 100644
index 0000000..daca973
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.input
@@ -0,0 +1,20 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for Dimensions and DoStatements. */
+class D {
+  // TODO(jdd): Test higher-language-level features.
+
+  int[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
+          [][][][]
+      array[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
+          [][][][][][][];
+
+  void f() {
+    do {
+      assert false;
+    } while (1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+            + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+        == 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+            + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.output
new file mode 100644
index 0000000..daca973
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.output
@@ -0,0 +1,20 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for Dimensions and DoStatements. */
+class D {
+  // TODO(jdd): Test higher-language-level features.
+
+  int[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
+          [][][][]
+      array[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
+          [][][][][][][];
+
+  void f() {
+    do {
+      assert false;
+    } while (1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+            + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+        == 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
+            + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.input
new file mode 100644
index 0000000..0e98139
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.input
@@ -0,0 +1,87 @@
+package com.google.googlejavaformat.java.test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+/**
+ * Tests for EmptyStatements, EnhancedForStatements, EnumConstantDeclarations, EnumDeclarations,
+ * ExpressionMethodReferences, ExpressionStatements, Expressions, and ExtendedModifiers.
+ */
+@MarkerAnnotation
+class E<T> {
+  // TODO(jdd): Test higher language-level features.
+
+  enum Enum1 {
+    A, B, C, D;
+
+    Enum1() {}
+  }
+
+  @MarkerAnnotation
+  public enum Enum2 {
+    A,
+    B,
+    C,
+    D,
+    ;
+
+    Enum2() {}
+  }
+
+  enum Enum3 {
+    A(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0),
+    B(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1),
+    C(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2),
+    D(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 3);
+
+    Enum3(int x) {}
+  }
+
+  enum Enum4 {
+    A(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0),
+    B(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1),
+    C(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2),
+    D(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 3),
+    ;
+
+    Enum4(int x) {}
+  }
+
+  int f(int value) {
+    ;
+    ;
+    ;
+    ;
+    ;
+    for (Integer x :
+        ImmutableList.<Integer>of(
+            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) {}
+    for (Pair<
+            Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+            Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+        x :
+            Lists
+                .<Pair<
+                        Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+                        Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>>
+                    newArrayList()) {}
+    f(10);
+    return f(20);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.output
new file mode 100644
index 0000000..4dd603a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.output
@@ -0,0 +1,90 @@
+package com.google.googlejavaformat.java.test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+/**
+ * Tests for EmptyStatements, EnhancedForStatements, EnumConstantDeclarations, EnumDeclarations,
+ * ExpressionMethodReferences, ExpressionStatements, Expressions, and ExtendedModifiers.
+ */
+@MarkerAnnotation
+class E<T> {
+  // TODO(jdd): Test higher language-level features.
+
+  enum Enum1 {
+    A,
+    B,
+    C,
+    D;
+
+    Enum1() {}
+  }
+
+  @MarkerAnnotation
+  public enum Enum2 {
+    A,
+    B,
+    C,
+    D,
+    ;
+
+    Enum2() {}
+  }
+
+  enum Enum3 {
+    A(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0),
+    B(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1),
+    C(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2),
+    D(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 3);
+
+    Enum3(int x) {}
+  }
+
+  enum Enum4 {
+    A(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0),
+    B(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1),
+    C(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2),
+    D(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 3),
+    ;
+
+    Enum4(int x) {}
+  }
+
+  int f(int value) {
+    ;
+    ;
+    ;
+    ;
+    ;
+    for (Integer x :
+        ImmutableList.<Integer>of(
+            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) {}
+    for (Pair<
+            Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+            Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+        x :
+            Lists
+                .<Pair<
+                        Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+                        Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>>
+                    newArrayList()) {}
+    f(10);
+    return f(20);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/EnumAnnotation.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/EnumAnnotation.input
new file mode 100644
index 0000000..43384fd
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/EnumAnnotation.input
@@ -0,0 +1,5 @@
+enum E {
+  @A ONE,
+  TWO,
+  @B @C THREE;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/EnumAnnotation.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/EnumAnnotation.output
new file mode 100644
index 0000000..0ff2a7a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/EnumAnnotation.output
@@ -0,0 +1,8 @@
+enum E {
+  @A
+  ONE,
+  TWO,
+  @B
+  @C
+  THREE;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/F.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/F.input
new file mode 100644
index 0000000..dbefdaf
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/F.input
@@ -0,0 +1,61 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for FieldAccesses, FieldDeclarations, and ForStatements. */
+class F {
+  int x;
+  int x1 = 0;
+  int y, z;
+  int y1 = 0, z1 = 0;
+  F f = null;
+  Object field0 =
+      true
+          ? true ? true ? null : null : true ? null : null
+          : true ? true ? null : null : true ? null : null;
+
+  Pair<
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>
+      field1 = null;
+  Pair<
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>
+      field2 =
+          true
+              ? true ? true ? null : null : true ? null : null
+              : true ? true ? null : null : true ? null : null;
+
+  @MarkerAnnotation
+  Pair<
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>
+      field3 =
+          true
+              ? true ? true ? null : null : true ? null : null
+              : true ? true ? null : null : true ? null : null;
+
+  void f() {
+    for (int i =
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0;
+        i
+            < 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 10;
+        i++) {
+      f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.Ff = null;
+    }
+    for (int i =
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0;
+        i
+            < 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 10;
+        i++) {
+      f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.Ff =
+          null;
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/F.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/F.output
new file mode 100644
index 0000000..dbefdaf
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/F.output
@@ -0,0 +1,61 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for FieldAccesses, FieldDeclarations, and ForStatements. */
+class F {
+  int x;
+  int x1 = 0;
+  int y, z;
+  int y1 = 0, z1 = 0;
+  F f = null;
+  Object field0 =
+      true
+          ? true ? true ? null : null : true ? null : null
+          : true ? true ? null : null : true ? null : null;
+
+  Pair<
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>
+      field1 = null;
+  Pair<
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>
+      field2 =
+          true
+              ? true ? true ? null : null : true ? null : null
+              : true ? true ? null : null : true ? null : null;
+
+  @MarkerAnnotation
+  Pair<
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+          Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>
+      field3 =
+          true
+              ? true ? true ? null : null : true ? null : null
+              : true ? true ? null : null : true ? null : null;
+
+  void f() {
+    for (int i =
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0;
+        i
+            < 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 10;
+        i++) {
+      f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.Ff = null;
+    }
+    for (int i =
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0;
+        i
+            < 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 10;
+        i++) {
+      f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.f.Ff =
+          null;
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.input
new file mode 100644
index 0000000..9dc3426
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.input
@@ -0,0 +1,7 @@
+class Fields {
+
+  int x = 1;
+
+  int y = 1;
+
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.output
new file mode 100644
index 0000000..376e8b6
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.output
@@ -0,0 +1,6 @@
+class Fields {
+
+  int x = 1;
+
+  int y = 1;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/ForMany.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/ForMany.input
new file mode 100644
index 0000000..6927ee9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/ForMany.input
@@ -0,0 +1,6 @@
+class ForMany {
+  {
+    A a;
+    for (b = B; c != C; d = D) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/ForMany.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/ForMany.output
new file mode 100644
index 0000000..6927ee9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/ForMany.output
@@ -0,0 +1,6 @@
+class ForMany {
+  {
+    A a;
+    for (b = B; c != C; d = D) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I.input
new file mode 100644
index 0000000..9f53760
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I.input
@@ -0,0 +1,77 @@
+package com.google.googlejavaformat.java.test;
+
+import java.util.List;
+
+/**
+ * Tests for IfStatements, ImportDeclarations, InfixExpressions, Initializers,
+ * InstanceofExpressions, and IntersectionTypes.
+ */
+class I {
+  interface I0 {}
+
+  interface I1 {}
+
+  interface I2 {}
+
+  interface I3 {}
+
+  interface I4 {}
+
+  interface I5 {}
+
+  interface I6 {}
+
+  interface I7 {}
+
+  interface I8 {}
+
+  interface I9 {}
+
+  interface I10 {}
+
+  interface I11 {}
+
+  interface I12 {}
+
+  interface I13 {}
+
+  interface I14 {}
+
+  interface I15 {}
+
+  interface I16 {}
+
+  interface I17 {}
+
+  interface I18 {}
+
+  interface I19 {}
+
+  class II<
+      T extends
+          I0 & I1 & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9 & I10 & I11 & I12 & I13 & I14 & I15 & I16
+              & I17 & I18 & I19> {}
+
+  static class CC {
+    static {
+      int i = 0;
+    }
+  }
+
+  int x =
+      0 >>> 0 + 0 / 0 * 0 - 0 & 0 << 0 * 0 / 0 >> 0 - 0 ^ 0 * 0 / 0 >>> 0 << 0 * 0 - 0 / 0
+          | 0 * 0 >> 0 + 0 / 0 * 0 - 0 << 0
+              & 0 * 0 / 0 >>> 0 - 0 * 0 >> 0 / 0 << 0 * 0 + 0 - 0 / 0 * 0
+          | 0 - 0 * 0 >>> 0 << 0 / 0 * 0 >> 0 - 0 ^ 0 * 0 / 0 & 0 << 0 + 0;
+
+  void f() {
+    if (0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+        == 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0) {
+    } else if (null instanceof List) {
+    } else {
+    }
+    if (true) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I.output
new file mode 100644
index 0000000..9f53760
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I.output
@@ -0,0 +1,77 @@
+package com.google.googlejavaformat.java.test;
+
+import java.util.List;
+
+/**
+ * Tests for IfStatements, ImportDeclarations, InfixExpressions, Initializers,
+ * InstanceofExpressions, and IntersectionTypes.
+ */
+class I {
+  interface I0 {}
+
+  interface I1 {}
+
+  interface I2 {}
+
+  interface I3 {}
+
+  interface I4 {}
+
+  interface I5 {}
+
+  interface I6 {}
+
+  interface I7 {}
+
+  interface I8 {}
+
+  interface I9 {}
+
+  interface I10 {}
+
+  interface I11 {}
+
+  interface I12 {}
+
+  interface I13 {}
+
+  interface I14 {}
+
+  interface I15 {}
+
+  interface I16 {}
+
+  interface I17 {}
+
+  interface I18 {}
+
+  interface I19 {}
+
+  class II<
+      T extends
+          I0 & I1 & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9 & I10 & I11 & I12 & I13 & I14 & I15 & I16
+              & I17 & I18 & I19> {}
+
+  static class CC {
+    static {
+      int i = 0;
+    }
+  }
+
+  int x =
+      0 >>> 0 + 0 / 0 * 0 - 0 & 0 << 0 * 0 / 0 >> 0 - 0 ^ 0 * 0 / 0 >>> 0 << 0 * 0 - 0 / 0
+          | 0 * 0 >> 0 + 0 / 0 * 0 - 0 << 0
+              & 0 * 0 / 0 >>> 0 - 0 * 0 >> 0 / 0 << 0 * 0 + 0 - 0 / 0 * 0
+          | 0 - 0 * 0 >>> 0 << 0 / 0 * 0 >> 0 - 0 ^ 0 * 0 / 0 & 0 << 0 + 0;
+
+  void f() {
+    if (0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+        == 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0) {
+    } else if (null instanceof List) {
+    } else {
+    }
+    if (true) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1.input
new file mode 100644
index 0000000..7a1cd14
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1.input
@@ -0,0 +1,49 @@
+class Test {
+  final CreationMechanism creationMechanism;
+  final @Nullable String creationUserAgent;
+  final ClientId clientId;
+  final @Nullable String creationUserAgentXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;
+  final Token externalId;
+
+  {
+    final CreationMechanism creationMechanism;
+    final @Nullable String creationUserAgent;
+    final ClientId clientId;
+    final @Nullable String creationUserAgentXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;
+    final Token externalId;
+  }
+}
+
+class Test {
+  final CreationMechanism creationMechanism;
+  @Nullable final String creationUserAgent;
+  final ClientId clientId;
+  @Nullable final String creationUserAgentXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;
+  final Token externalId;
+
+  {
+    final CreationMechanism creationMechanism;
+    @Nullable final String creationUserAgent;
+    final ClientId clientId;
+    @Nullable final String creationUserAgentXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;
+    final Token externalId;
+  }
+}
+
+class Test {
+
+  @Nullable
+  final String creationUserAgent;
+
+  {
+    @Nullable
+    final String creationUserAgent;
+  }
+
+  {
+
+    @Nullable
+    final String creationUserAgent;
+
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1.output
new file mode 100644
index 0000000..2637dde
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1.output
@@ -0,0 +1,50 @@
+class Test {
+  final CreationMechanism creationMechanism;
+  final @Nullable String creationUserAgent;
+  final ClientId clientId;
+  final @Nullable String
+      creationUserAgentXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;
+  final Token externalId;
+
+  {
+    final CreationMechanism creationMechanism;
+    final @Nullable String creationUserAgent;
+    final ClientId clientId;
+    final @Nullable String
+        creationUserAgentXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;
+    final Token externalId;
+  }
+}
+
+class Test {
+  final CreationMechanism creationMechanism;
+  @Nullable final String creationUserAgent;
+  final ClientId clientId;
+
+  @Nullable
+  final String creationUserAgentXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;
+
+  final Token externalId;
+
+  {
+    final CreationMechanism creationMechanism;
+    @Nullable final String creationUserAgent;
+    final ClientId clientId;
+    @Nullable
+    final String creationUserAgentXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;
+    final Token externalId;
+  }
+}
+
+class Test {
+
+  @Nullable final String creationUserAgent;
+
+  {
+    @Nullable final String creationUserAgent;
+  }
+
+  {
+    @Nullable final String creationUserAgent;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I12.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I12.input
new file mode 100644
index 0000000..5d8a058
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I12.input
@@ -0,0 +1,19 @@
+public class Test {
+
+
+
+}
+
+public class Test {
+
+
+}
+
+public class Test {
+
+}
+
+public class Test {
+}
+
+public class Test {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I12.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I12.output
new file mode 100644
index 0000000..0b72fab
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I12.output
@@ -0,0 +1,9 @@
+public class Test {}
+
+public class Test {}
+
+public class Test {}
+
+public class Test {}
+
+public class Test {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I13.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I13.input
new file mode 100644
index 0000000..079e78b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I13.input
@@ -0,0 +1,10 @@
+class I13 {
+
+  @Nullable
+
+  public int f;
+
+  @Override
+
+  public void m() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I13.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I13.output
new file mode 100644
index 0000000..b902fe6
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I13.output
@@ -0,0 +1,7 @@
+class I13 {
+
+  @Nullable public int f;
+
+  @Override
+  public void m() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I202.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I202.input
new file mode 100644
index 0000000..c3766ec
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I202.input
@@ -0,0 +1,6 @@
+class I202 {
+  {
+    //noinspection CheckResult
+    methodWhoseResultShouldBeChecked();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I202.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I202.output
new file mode 100644
index 0000000..c3766ec
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I202.output
@@ -0,0 +1,6 @@
+class I202 {
+  {
+    //noinspection CheckResult
+    methodWhoseResultShouldBeChecked();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I365.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I365.input
new file mode 100644
index 0000000..e3866d8
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I365.input
@@ -0,0 +1,10 @@
+class I365 {
+  {
+    return foo____________
+        .bar__________()
+        .baz____________()
+        .parallelStream()
+        .map(Baz::getId)
+        .collect(toList());
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I365.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I365.output
new file mode 100644
index 0000000..0985d2a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I365.output
@@ -0,0 +1,7 @@
+class I365 {
+  {
+    return foo____________.bar__________().baz____________().parallelStream()
+        .map(Baz::getId)
+        .collect(toList());
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I374.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I374.input
new file mode 100644
index 0000000..2364f0c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I374.input
@@ -0,0 +1,9 @@
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+@interface MyTypeAnno {}
+
+public class GjfFailure {
+  void m(int a @MyTypeAnno []) {}
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I374.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I374.output
new file mode 100644
index 0000000..a36919e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I374.output
@@ -0,0 +1,9 @@
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
+@interface MyTypeAnno {}
+
+public class GjfFailure {
+  void m(int a @MyTypeAnno []) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.input
new file mode 100644
index 0000000..f0b3e66
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.input
@@ -0,0 +1,11 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for LabeledStatements and LambdaExpressions. */
+class L {
+  // TODO(jdd): Include high language-level tests.
+
+  void f() {
+    LABEL:
+    for (int i = 0; i < 10; i++) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.output
new file mode 100644
index 0000000..f0b3e66
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.output
@@ -0,0 +1,11 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for LabeledStatements and LambdaExpressions. */
+class L {
+  // TODO(jdd): Include high language-level tests.
+
+  void f() {
+    LABEL:
+    for (int i = 0; i < 10; i++) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/LocalAnnotations.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/LocalAnnotations.input
new file mode 100644
index 0000000..87b9aa2
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/LocalAnnotations.input
@@ -0,0 +1,7 @@
+class LocalAnnotations {
+  {
+    @Foo final Object x;
+    @Foo(1) final Object y;
+    @Foo(x=1) final Object y;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/LocalAnnotations.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/LocalAnnotations.output
new file mode 100644
index 0000000..181d729
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/LocalAnnotations.output
@@ -0,0 +1,9 @@
+class LocalAnnotations {
+  {
+    @Foo final Object x;
+    @Foo(1)
+    final Object y;
+    @Foo(x = 1)
+    final Object y;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/M.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M.input
new file mode 100644
index 0000000..56f4ccf
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M.input
@@ -0,0 +1,399 @@
+package com.google.googlejavaformat.java.test;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * Tests for MarkerAnnotations, MemberValuePairs, MethodDeclarations, MethodInvocations, and
+ * Modifiers.
+ */
+@MarkerAnnotation
+class M {
+  class Exception0 extends Exception {}
+
+  class Exception1 extends Exception {}
+
+  class Exception2 extends Exception {}
+
+  class Exception3 extends Exception {}
+
+  class Exception4 extends Exception {}
+
+  class Exception5 extends Exception {}
+
+  class Exception6 extends Exception {}
+
+  class Exception7 extends Exception {}
+
+  class Exception8 extends Exception {}
+
+  class Exception9 extends Exception {}
+
+  void methodName1(int x) {}
+
+  void methodName2(int x) throws Exception {}
+
+  /**
+   * JavaDoc.
+   */
+  void methodName3(int x)
+      throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+          Exception7, Exception8, Exception9 {}
+
+  /**
+   * JavaDoc.
+   */
+  void methodName4(
+      int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+      int n, int o, int p, int q, int r, int s, int t, int u, int v) {}
+
+  /**
+   * JavaDoc.
+   */
+  void methodName5(
+      int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+      int n, int o, int p, int q, int r, int s, int t, int u, int v)
+      throws Exception {}
+
+  /**
+   * JavaDoc.
+   */
+  void methodName6(
+      int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+      int n, int o, int p, int q, int r, int s, int t, int u, int v)
+      throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+          Exception7, Exception8, Exception9 {}
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName10(int x) {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName12(int x) throws Exception {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName11(int x)
+          throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+              Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName15(
+          int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+          int n, int o, int p, int q, int r, int s, int t, int u, int v) {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName14(
+          int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+          int n, int o, int p, int q, int r, int s, int t, int u, int v)
+          throws Exception {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName13(
+          int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+          int n, int o, int p, int q, int r, int s, int t, int u, int v)
+          throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+              Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      T methodName20(int x) {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      T methodName21(int x) throws Exception {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      T methodName22(int x)
+          throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+              Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      T methodName22(
+          int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+          int n, int o, int p, int q, int r, int s, int t, int u, int v) {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      T methodName23(
+          int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+          int n, int o, int p, int q, int r, int s, int t, int u, int v)
+          throws Exception {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      T methodName30(
+          int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m,
+          int n, int o, int p, int q, int r, int s, int t, int u, int v)
+          throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+              Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName31(int x) {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName32(int x) throws Exception {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName(int x)
+              throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5,
+                  Exception6, Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName41(
+              int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l,
+              int m, int n, int o, int p, int q, int r, int s, int t, int u, int v) {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName40(
+              int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l,
+              int m, int n, int o, int p, int q, int r, int s, int t, int u, int v)
+              throws Exception {
+    return null;
+  }
+
+  <T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20,
+          T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName(
+              int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l,
+              int m, int n, int o, int p, int q, int r, int s, int t, int u, int v)
+              throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5,
+                  Exception6, Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  static <T0, T1, T2> void m() {}
+
+  M identity() {
+    return this;
+  }
+
+  void f(double x) {
+    List<Pair<Integer, Integer>> list1 = ImmutableList.<Pair<Integer, Integer>>of(null);
+    List<Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>> list2 =
+        ImmutableList.<Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>of(null);
+    List<
+            Pair<
+                Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>>
+        list3 =
+            ImmutableList
+                .<Pair<
+                        Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                        Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>>
+                    of(null);
+    Pair<Integer, Integer> pair1 =
+        ImmutableList.<Pair<Integer, Integer>>of(null).get(
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+    Pair<Pair<Integer, Integer>, Pair<Integer, Integer>> pair2 =
+        ImmutableList.<Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>of(null).get(
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+    Pair<
+            Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+            Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>
+        pair3 =
+            ImmutableList
+                .<Pair<
+                        Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                        Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>>
+                    of(null)
+                .get(
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+  }
+
+  void f(int... x) {
+    M m = null;
+    ((m.identity().identity().identity().identity()).identity().identity().identity().identity())
+        .identity()
+        .identity()
+        .identity()
+        .identity();
+    f(
+        x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8] + x[9] + x[11] + x[12] + x[13]
+            + x[14] + x[15]);
+  }
+
+  static void method999(Object... args) {
+    method999(1, 2, 3, 4, 5);
+    method999(1, 2, 3, 4, 5);
+    method999(
+        1, 2,
+        3, 4,
+        5, 6);
+    method999(1, 2, 3, 4, 5, 6);
+    method999("one", 1, "two", 2, "three", 3);
+    method999("one", 1);
+    method999(
+        "one", 1,
+        "two", 2);
+    method999(
+        "one", 1,
+        "two", 1 + 1,
+        "three", 1 + 1 + 1);
+    method999(
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx");
+  }
+
+  static class Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {
+    static Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx fff(
+        int x) {
+      return null;
+    }
+  }
+
+  static
+  class Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {
+    static Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx fff(
+        int x) {
+      return null;
+    }
+
+    static Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+        ffffffffff(int x) {
+      return null;
+    }
+  }
+
+  M xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = null;
+  M xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx =
+      null;
+
+  M fff(int x) {
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+        .ffffffffff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(0).fff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(0 + 0).fff(
+        0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+        .fff(
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0)
+        .fff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+        .ffffffffff(0)
+        .fff(0);
+    return this;
+  }
+
+  M ffffffffff(int x) {
+    return this;
+  }
+
+  M ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(int x) {
+    new M().ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(
+        0 + 0 + 0 + 0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(0).f(0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(0)
+        .fff(0)
+        .fff(0)
+        .f(0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(0).ffffffffff(
+        0);
+    return this;
+  }
+
+  <XXXXXXXXXX, XXXXXXXXXX, XXXXXXXXXX, XXXXXXXXXX, XXXXXXXXXX, XXXXXXXXXX, XXXXXXXXXX, XXXXXXXXXX,
+          XXXXXXXXXX, XXXXXXXXXX, XXXXXXXXXX, XXXXXXXXXX>
+      M(
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z)
+          throws EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE,
+              EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/M.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M.output
new file mode 100644
index 0000000..dbad1bb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M.output
@@ -0,0 +1,1016 @@
+package com.google.googlejavaformat.java.test;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * Tests for MarkerAnnotations, MemberValuePairs, MethodDeclarations, MethodInvocations, and
+ * Modifiers.
+ */
+@MarkerAnnotation
+class M {
+  class Exception0 extends Exception {}
+
+  class Exception1 extends Exception {}
+
+  class Exception2 extends Exception {}
+
+  class Exception3 extends Exception {}
+
+  class Exception4 extends Exception {}
+
+  class Exception5 extends Exception {}
+
+  class Exception6 extends Exception {}
+
+  class Exception7 extends Exception {}
+
+  class Exception8 extends Exception {}
+
+  class Exception9 extends Exception {}
+
+  void methodName1(int x) {}
+
+  void methodName2(int x) throws Exception {}
+
+  /** JavaDoc. */
+  void methodName3(int x)
+      throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+          Exception7, Exception8, Exception9 {}
+
+  /** JavaDoc. */
+  void methodName4(
+      int a,
+      int b,
+      int c,
+      int d,
+      int e,
+      int f,
+      int g,
+      int h,
+      int i,
+      int j,
+      int k,
+      int l,
+      int m,
+      int n,
+      int o,
+      int p,
+      int q,
+      int r,
+      int s,
+      int t,
+      int u,
+      int v) {}
+
+  /** JavaDoc. */
+  void methodName5(
+      int a,
+      int b,
+      int c,
+      int d,
+      int e,
+      int f,
+      int g,
+      int h,
+      int i,
+      int j,
+      int k,
+      int l,
+      int m,
+      int n,
+      int o,
+      int p,
+      int q,
+      int r,
+      int s,
+      int t,
+      int u,
+      int v)
+      throws Exception {}
+
+  /** JavaDoc. */
+  void methodName6(
+      int a,
+      int b,
+      int c,
+      int d,
+      int e,
+      int f,
+      int g,
+      int h,
+      int i,
+      int j,
+      int k,
+      int l,
+      int m,
+      int n,
+      int o,
+      int p,
+      int q,
+      int r,
+      int s,
+      int t,
+      int u,
+      int v)
+      throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+          Exception7, Exception8, Exception9 {}
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName10(int x) {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName12(int x) throws Exception {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName11(int x)
+          throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+              Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName15(
+          int a,
+          int b,
+          int c,
+          int d,
+          int e,
+          int f,
+          int g,
+          int h,
+          int i,
+          int j,
+          int k,
+          int l,
+          int m,
+          int n,
+          int o,
+          int p,
+          int q,
+          int r,
+          int s,
+          int t,
+          int u,
+          int v) {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName14(
+          int a,
+          int b,
+          int c,
+          int d,
+          int e,
+          int f,
+          int g,
+          int h,
+          int i,
+          int j,
+          int k,
+          int l,
+          int m,
+          int n,
+          int o,
+          int p,
+          int q,
+          int r,
+          int s,
+          int t,
+          int u,
+          int v)
+          throws Exception {
+    return null;
+  }
+
+  Pair<
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+          Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+      methodName13(
+          int a,
+          int b,
+          int c,
+          int d,
+          int e,
+          int f,
+          int g,
+          int h,
+          int i,
+          int j,
+          int k,
+          int l,
+          int m,
+          int n,
+          int o,
+          int p,
+          int q,
+          int r,
+          int s,
+          int t,
+          int u,
+          int v)
+          throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+              Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      T methodName20(int x) {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      T methodName21(int x) throws Exception {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      T methodName22(int x)
+          throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+              Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      T methodName22(
+          int a,
+          int b,
+          int c,
+          int d,
+          int e,
+          int f,
+          int g,
+          int h,
+          int i,
+          int j,
+          int k,
+          int l,
+          int m,
+          int n,
+          int o,
+          int p,
+          int q,
+          int r,
+          int s,
+          int t,
+          int u,
+          int v) {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      T methodName23(
+          int a,
+          int b,
+          int c,
+          int d,
+          int e,
+          int f,
+          int g,
+          int h,
+          int i,
+          int j,
+          int k,
+          int l,
+          int m,
+          int n,
+          int o,
+          int p,
+          int q,
+          int r,
+          int s,
+          int t,
+          int u,
+          int v)
+          throws Exception {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      T methodName30(
+          int a,
+          int b,
+          int c,
+          int d,
+          int e,
+          int f,
+          int g,
+          int h,
+          int i,
+          int j,
+          int k,
+          int l,
+          int m,
+          int n,
+          int o,
+          int p,
+          int q,
+          int r,
+          int s,
+          int t,
+          int u,
+          int v)
+          throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5, Exception6,
+              Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName31(int x) {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName32(int x) throws Exception {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName(int x)
+              throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5,
+                  Exception6, Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName41(
+              int a,
+              int b,
+              int c,
+              int d,
+              int e,
+              int f,
+              int g,
+              int h,
+              int i,
+              int j,
+              int k,
+              int l,
+              int m,
+              int n,
+              int o,
+              int p,
+              int q,
+              int r,
+              int s,
+              int t,
+              int u,
+              int v) {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName40(
+              int a,
+              int b,
+              int c,
+              int d,
+              int e,
+              int f,
+              int g,
+              int h,
+              int i,
+              int j,
+              int k,
+              int l,
+              int m,
+              int n,
+              int o,
+              int p,
+              int q,
+              int r,
+              int s,
+              int t,
+              int u,
+              int v)
+              throws Exception {
+    return null;
+  }
+
+  <
+          T0,
+          T1,
+          T2,
+          T3,
+          T4,
+          T5,
+          T6,
+          T7,
+          T8,
+          T9,
+          T10,
+          T11,
+          T12,
+          T13,
+          T14,
+          T15,
+          T16,
+          T17,
+          T18,
+          T19,
+          T20,
+          T21,
+          T22,
+          T23,
+          T24,
+          T25,
+          T26,
+          T27,
+          T28,
+          T29,
+          T30,
+          T31>
+      Pair<
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>,
+              Pair<Pair<Pair<T, T>, Pair<T, T>>, Pair<Pair<T, T>, Pair<T, T>>>>
+          methodName(
+              int a,
+              int b,
+              int c,
+              int d,
+              int e,
+              int f,
+              int g,
+              int h,
+              int i,
+              int j,
+              int k,
+              int l,
+              int m,
+              int n,
+              int o,
+              int p,
+              int q,
+              int r,
+              int s,
+              int t,
+              int u,
+              int v)
+              throws Exception0, Exception1, Exception2, Exception3, Exception4, Exception5,
+                  Exception6, Exception7, Exception8, Exception9 {
+    return null;
+  }
+
+  static <T0, T1, T2> void m() {}
+
+  M identity() {
+    return this;
+  }
+
+  void f(double x) {
+    List<Pair<Integer, Integer>> list1 = ImmutableList.<Pair<Integer, Integer>>of(null);
+    List<Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>> list2 =
+        ImmutableList.<Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>of(null);
+    List<
+            Pair<
+                Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>>
+        list3 =
+            ImmutableList
+                .<Pair<
+                        Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                        Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>>
+                    of(null);
+    Pair<Integer, Integer> pair1 =
+        ImmutableList.<Pair<Integer, Integer>>of(null)
+            .get(
+                0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                    + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+    Pair<Pair<Integer, Integer>, Pair<Integer, Integer>> pair2 =
+        ImmutableList.<Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>of(null)
+            .get(
+                0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                    + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+    Pair<
+            Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+            Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>
+        pair3 =
+            ImmutableList
+                .<Pair<
+                        Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>,
+                        Pair<Pair<Integer, Integer>, Pair<Integer, Integer>>>>
+                    of(null)
+                .get(
+                    0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                        + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+  }
+
+  void f(int... x) {
+    M m = null;
+    ((m.identity().identity().identity().identity()).identity().identity().identity().identity())
+        .identity()
+        .identity()
+        .identity()
+        .identity();
+    f(
+        x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8] + x[9] + x[11] + x[12] + x[13]
+            + x[14] + x[15]);
+  }
+
+  static void method999(Object... args) {
+    method999(1, 2, 3, 4, 5);
+    method999(1, 2, 3, 4, 5);
+    method999(
+        1, 2,
+        3, 4,
+        5, 6);
+    method999(1, 2, 3, 4, 5, 6);
+    method999("one", 1, "two", 2, "three", 3);
+    method999("one", 1);
+    method999(
+        "one", 1,
+        "two", 2);
+    method999(
+        "one", 1,
+        "two", 1 + 1,
+        "three", 1 + 1 + 1);
+    method999(
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx",
+        "xxxxxxxxxxxxxx");
+  }
+
+  static class Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {
+    static Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx fff(
+        int x) {
+      return null;
+    }
+  }
+
+  static
+  class Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {
+    static Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx fff(
+        int x) {
+      return null;
+    }
+
+    static Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+        ffffffffff(int x) {
+      return null;
+    }
+  }
+
+  M xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = null;
+  M xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx =
+      null;
+
+  M fff(int x) {
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+        .ffffffffff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(0).fff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(0 + 0)
+        .fff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.fff(
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0)
+        .fff(0);
+    Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+        .ffffffffff(0)
+        .fff(0);
+    return this;
+  }
+
+  M ffffffffff(int x) {
+    return this;
+  }
+
+  M ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(int x) {
+    new M()
+        .ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(
+            0 + 0 + 0 + 0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(
+        0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+            + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(0).f(0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(0)
+        .fff(0)
+        .fff(0)
+        .f(0);
+    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff(0)
+        .ffffffffff(0);
+    return this;
+  }
+
+  <
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX,
+          XXXXXXXXXX>
+      M(
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z,
+          ZZZZZZZZZZ z)
+          throws EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE,
+              EEEEEEEEEE, EEEEEEEEEE, EEEEEEEEEE {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/M1.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M1.input
new file mode 100644
index 0000000..c5ff9b5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M1.input
@@ -0,0 +1,22 @@
+import a.A;
+import a.B;
+import com.google.Foo;
+
+@A @B
+module com.google.m {
+requires com.google.r1;
+requires transitive com.google.r2;
+requires static com.google.r3;
+exports com.google.e1;
+exports com.google.e1 to com.google.e2;
+exports com.google.e1 to com.google.e2, com.google.e3;
+exports com.google.e1 to com.google.e2, com.google.e3, com.google.e4;
+opens com.google.o1;
+opens com.google.o1 to com.google.o2;
+opens com.google.o1 to com.google.o2, com.google.o3;
+opens com.google.o1 to com.google.o2, com.google.o3, com.googleoe4;
+uses Foo;
+uses com.google.Bar;
+provides com.google.Baz with Foo;
+provides com.google.Baz with Foo, com.google.Bar;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/M1.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M1.output
new file mode 100644
index 0000000..514662b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M1.output
@@ -0,0 +1,42 @@
+import a.A;
+import a.B;
+import com.google.Foo;
+
+@A
+@B
+module com.google.m {
+  requires com.google.r1;
+  requires transitive com.google.r2;
+  requires static com.google.r3;
+
+  exports com.google.e1;
+  exports com.google.e1 to
+      com.google.e2;
+  exports com.google.e1 to
+      com.google.e2,
+      com.google.e3;
+  exports com.google.e1 to
+      com.google.e2,
+      com.google.e3,
+      com.google.e4;
+
+  opens com.google.o1;
+  opens com.google.o1 to
+      com.google.o2;
+  opens com.google.o1 to
+      com.google.o2,
+      com.google.o3;
+  opens com.google.o1 to
+      com.google.o2,
+      com.google.o3,
+      com.googleoe4;
+
+  uses Foo;
+  uses com.google.Bar;
+
+  provides com.google.Baz with
+      Foo;
+  provides com.google.Baz with
+      Foo,
+      com.google.Bar;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/M2.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M2.input
new file mode 100644
index 0000000..c7d8b16
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M2.input
@@ -0,0 +1,2 @@
+open module com.google.m {
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/M2.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M2.output
new file mode 100644
index 0000000..dd22a0c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M2.output
@@ -0,0 +1 @@
+open module com.google.m {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/M3.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M3.input
new file mode 100644
index 0000000..c5d2966
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M3.input
@@ -0,0 +1,6 @@
+@Deprecated
+module moduletags {
+    requires transitive static moduleA;
+
+    exports testpkgmdltags;
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/M3.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M3.output
new file mode 100644
index 0000000..563ef46
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/M3.output
@@ -0,0 +1,6 @@
+@Deprecated
+module moduletags {
+  requires transitive static moduleA;
+
+  exports testpkgmdltags;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/MarkerAnnotation.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/MarkerAnnotation.input
new file mode 100644
index 0000000..b8dd3e3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/MarkerAnnotation.input
@@ -0,0 +1,4 @@
+package com.google.googlejavaformat.java.test;
+
+/** JavaDoc comment. */
+public @interface MarkerAnnotation {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/MarkerAnnotation.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/MarkerAnnotation.output
new file mode 100644
index 0000000..b8dd3e3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/MarkerAnnotation.output
@@ -0,0 +1,4 @@
+package com.google.googlejavaformat.java.test;
+
+/** JavaDoc comment. */
+public @interface MarkerAnnotation {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Multivariables.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Multivariables.input
new file mode 100644
index 0000000..a5e0b4d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Multivariables.input
@@ -0,0 +1,17 @@
+class Multivariables {
+  int xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+      yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,
+      zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz;
+
+  // Horizontal and veritcal paddings used for most cells in the standings table.
+  private final int hpad, vpad;
+
+  {
+    {
+      for (c = cx.iterator(), m = mx.iterator();
+          c.hasNext() && m.hasNext(); ) {
+        f(c, m);
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Multivariables.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Multivariables.output
new file mode 100644
index 0000000..fc9a05e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Multivariables.output
@@ -0,0 +1,16 @@
+class Multivariables {
+  int xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+      yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,
+      zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz;
+
+  // Horizontal and veritcal paddings used for most cells in the standings table.
+  private final int hpad, vpad;
+
+  {
+    {
+      for (c = cx.iterator(), m = mx.iterator(); c.hasNext() && m.hasNext(); ) {
+        f(c, m);
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/N.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/N.input
new file mode 100644
index 0000000..0359141
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/N.input
@@ -0,0 +1,13 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for NameQualifiedTypes, Names, NormalAnnotations, NullLiterals, and NumberLiterals. */
+class N {
+  @NormalAnnotation(
+      a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 8, i = 9, j = 10, k = 11, l = 12, m = 13,
+      n = 14, o = 15, p = 16, q = 17, r = 18, s = 19, t = 20, u = 21, v = 22, w = 23, x = 24,
+      y = 25, z = 26)
+  void f() {
+    assert 00 == 0 && 0x0 == 0 && 1.0e0f == 1.00e0f && 0.2D == .2D;
+    java.lang. /* @MarkerAnnotation */String s = null;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/N.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/N.output
new file mode 100644
index 0000000..764390a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/N.output
@@ -0,0 +1,36 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for NameQualifiedTypes, Names, NormalAnnotations, NullLiterals, and NumberLiterals. */
+class N {
+  @NormalAnnotation(
+      a = 1,
+      b = 2,
+      c = 3,
+      d = 4,
+      e = 5,
+      f = 6,
+      g = 7,
+      h = 8,
+      i = 9,
+      j = 10,
+      k = 11,
+      l = 12,
+      m = 13,
+      n = 14,
+      o = 15,
+      p = 16,
+      q = 17,
+      r = 18,
+      s = 19,
+      t = 20,
+      u = 21,
+      v = 22,
+      w = 23,
+      x = 24,
+      y = 25,
+      z = 26)
+  void f() {
+    assert 00 == 0 && 0x0 == 0 && 1.0e0f == 1.00e0f && 0.2D == .2D;
+    java.lang./* @MarkerAnnotation */ String s = null;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/NormalAnnotation.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/NormalAnnotation.input
new file mode 100644
index 0000000..4a3a07e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/NormalAnnotation.input
@@ -0,0 +1,56 @@
+package com.google.googlejavaformat.java.test;
+
+/** JavaDoc comment. */
+public @interface NormalAnnotation {
+  int a();
+
+  int b();
+
+  int c();
+
+  int d();
+
+  int e();
+
+  int f();
+
+  int g();
+
+  int h();
+
+  int i();
+
+  int j();
+
+  int k();
+
+  int l();
+
+  int m();
+
+  int n();
+
+  int o();
+
+  int p();
+
+  int q();
+
+  int r();
+
+  int s();
+
+  int t();
+
+  int u();
+
+  int v();
+
+  int w();
+
+  int x();
+
+  int y();
+
+  int z();
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/NormalAnnotation.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/NormalAnnotation.output
new file mode 100644
index 0000000..4a3a07e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/NormalAnnotation.output
@@ -0,0 +1,56 @@
+package com.google.googlejavaformat.java.test;
+
+/** JavaDoc comment. */
+public @interface NormalAnnotation {
+  int a();
+
+  int b();
+
+  int c();
+
+  int d();
+
+  int e();
+
+  int f();
+
+  int g();
+
+  int h();
+
+  int i();
+
+  int j();
+
+  int k();
+
+  int l();
+
+  int m();
+
+  int n();
+
+  int o();
+
+  int p();
+
+  int q();
+
+  int r();
+
+  int s();
+
+  int t();
+
+  int u();
+
+  int v();
+
+  int w();
+
+  int x();
+
+  int y();
+
+  int z();
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/P.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/P.input
new file mode 100644
index 0000000..8cc377c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/P.input
@@ -0,0 +1,21 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for PackageDeclarations, ParameterizedTypes, ParenthesizedExpressions, PostfixExpressions,
+ * PrefixExpressions, and PrimitiveTypes.
+ */
+class P<
+    T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21,
+    T22, T23, T24> {
+  void f() {
+    int x = (1 + 2) * 3;
+    ++x;
+    x++;
+    int j = + + +x;
+    int k = + + ++x;
+    int jj = - - -x;
+    int kk = - - --x;
+    boolean b = false;
+    boolean bb = !!b;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/P.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/P.output
new file mode 100644
index 0000000..9c63831
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/P.output
@@ -0,0 +1,43 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for PackageDeclarations, ParameterizedTypes, ParenthesizedExpressions, PostfixExpressions,
+ * PrefixExpressions, and PrimitiveTypes.
+ */
+class P<
+    T1,
+    T2,
+    T3,
+    T4,
+    T5,
+    T6,
+    T7,
+    T8,
+    T9,
+    T10,
+    T11,
+    T12,
+    T13,
+    T14,
+    T15,
+    T16,
+    T17,
+    T18,
+    T19,
+    T20,
+    T21,
+    T22,
+    T23,
+    T24> {
+  void f() {
+    int x = (1 + 2) * 3;
+    ++x;
+    x++;
+    int j = + + +x;
+    int k = + + ++x;
+    int jj = - - -x;
+    int kk = - - --x;
+    boolean b = false;
+    boolean bb = !!b;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Pair.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Pair.input
new file mode 100644
index 0000000..a596748
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Pair.input
@@ -0,0 +1,12 @@
+package com.google.googlejavaformat.java.test;
+
+/** JavaDoc comment. */
+public class Pair<T1, T2> {
+  public final T1 x;
+  public final T2 y;
+
+  Pair(T1 x, T2 y) {
+    this.x = x;
+    this.y = y;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Pair.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Pair.output
new file mode 100644
index 0000000..a596748
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Pair.output
@@ -0,0 +1,12 @@
+package com.google.googlejavaformat.java.test;
+
+/** JavaDoc comment. */
+public class Pair<T1, T2> {
+  public final T1 x;
+  public final T2 y;
+
+  Pair(T1 x, T2 y) {
+    this.x = x;
+    this.y = y;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/PairedArguments.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/PairedArguments.input
new file mode 100644
index 0000000..1a654d0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/PairedArguments.input
@@ -0,0 +1,40 @@
+class T {
+  {
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+            xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+             xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+              xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+               xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxx(), xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxx(),
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxx(),
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxx(),
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/PairedArguments.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/PairedArguments.output
new file mode 100644
index 0000000..a88a1b1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/PairedArguments.output
@@ -0,0 +1,55 @@
+class T {
+  {
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxx(),
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxx(),
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxx(),
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxx(),
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx);
+    f(
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Q.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Q.input
new file mode 100644
index 0000000..f467201
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Q.input
@@ -0,0 +1,15 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for QualifiedNames and QualifiedTypes. */
+class Q {
+  static final int zero = 0;
+
+  class T {}
+
+  static void f() {
+    Q. /* @MarkerAnnotation */T t;
+    int zero = Q.zero;
+    java.lang. /* @MarkerAnnotation */String s = null;
+    Q.f();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Q.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Q.output
new file mode 100644
index 0000000..369f38e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Q.output
@@ -0,0 +1,15 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for QualifiedNames and QualifiedTypes. */
+class Q {
+  static final int zero = 0;
+
+  class T {}
+
+  static void f() {
+    Q./* @MarkerAnnotation */ T t;
+    int zero = Q.zero;
+    java.lang./* @MarkerAnnotation */ String s = null;
+    Q.f();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/R.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/R.input
new file mode 100644
index 0000000..053d46e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/R.input
@@ -0,0 +1,13 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for ReturnStatements. */
+class R {
+  void f() {
+    return;
+  }
+
+  int f(int x) {
+    return x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x
+        + x + x + x + x + x + x + x + x + x;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/R.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/R.output
new file mode 100644
index 0000000..053d46e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/R.output
@@ -0,0 +1,13 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for ReturnStatements. */
+class R {
+  void f() {
+    return;
+  }
+
+  int f(int x) {
+    return x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x + x
+        + x + x + x + x + x + x + x + x + x;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.input
new file mode 100644
index 0000000..a1e07d1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.input
@@ -0,0 +1,46 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for SimpleNames, SimpleTypes, SingleMemberAnnotations, SingleVariableDeclarations,
+ * Statements, StringLiterals, SuperConstructorInvocations, SuperFieldAccesses,
+ * SuperMethodInvocations, SuperMethodReferences, SwitchCases, SwitchStatements, and
+ * SynchronizedStatements.
+ */
+class S {
+  // TODO(jdd): Add tests for higher language levels.
+
+  int x = 0;
+
+  @SingleMemberAnnotation(
+      0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+          + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0)
+  S() {
+    super();
+  }
+
+  class SS extends S {
+    SS() {
+      super();
+      super.x = 0;
+      super.foo();
+    }
+  }
+
+  void foo() {
+    Object[] object = null;
+    synchronized (
+        object[
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]) {
+      switch ("abc") {
+        case "one":
+          break;
+        case "two":
+          break;
+        case "three":
+        default:
+          break;
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.output
new file mode 100644
index 0000000..a1e07d1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.output
@@ -0,0 +1,46 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for SimpleNames, SimpleTypes, SingleMemberAnnotations, SingleVariableDeclarations,
+ * Statements, StringLiterals, SuperConstructorInvocations, SuperFieldAccesses,
+ * SuperMethodInvocations, SuperMethodReferences, SwitchCases, SwitchStatements, and
+ * SynchronizedStatements.
+ */
+class S {
+  // TODO(jdd): Add tests for higher language levels.
+
+  int x = 0;
+
+  @SingleMemberAnnotation(
+      0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+          + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0)
+  S() {
+    super();
+  }
+
+  class SS extends S {
+    SS() {
+      super();
+      super.x = 0;
+      super.foo();
+    }
+  }
+
+  void foo() {
+    Object[] object = null;
+    synchronized (
+        object[
+            0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
+                + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0]) {
+      switch ("abc") {
+        case "one":
+          break;
+        case "two":
+          break;
+        case "three":
+        default:
+          break;
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SingleMemberAnnotation.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SingleMemberAnnotation.input
new file mode 100644
index 0000000..66045d5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SingleMemberAnnotation.input
@@ -0,0 +1,5 @@
+package com.google.googlejavaformat.java.test;
+
+public @interface SingleMemberAnnotation {
+  int value();
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SingleMemberAnnotation.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SingleMemberAnnotation.output
new file mode 100644
index 0000000..66045d5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SingleMemberAnnotation.output
@@ -0,0 +1,5 @@
+package com.google.googlejavaformat.java.test;
+
+public @interface SingleMemberAnnotation {
+  int value();
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SuperQualifier.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SuperQualifier.input
new file mode 100644
index 0000000..993a96d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SuperQualifier.input
@@ -0,0 +1,18 @@
+class SuperQualifier {
+  String a() {
+    return this.toStringHelper()
+      .add("foo", foo_________________________________________)
+      .add("bar", bar_________________________________________);
+  }
+
+  String b() {
+    return super.toStringHelper()
+      .add("foo", foo_________________________________________)
+      .add("bar", bar_________________________________________);
+  }
+
+  {
+    this.aaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbb(
+        ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SuperQualifier.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SuperQualifier.output
new file mode 100644
index 0000000..193ef99
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SuperQualifier.output
@@ -0,0 +1,18 @@
+class SuperQualifier {
+  String a() {
+    return this.toStringHelper()
+        .add("foo", foo_________________________________________)
+        .add("bar", bar_________________________________________);
+  }
+
+  String b() {
+    return super.toStringHelper()
+        .add("foo", foo_________________________________________)
+        .add("bar", bar_________________________________________);
+  }
+
+  {
+    this.aaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbb(
+        ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.input
new file mode 100644
index 0000000..c8cd293
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.input
@@ -0,0 +1,19 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for ThisExpressions, ThrowStatements, TryStatements, TypeDeclarationStatements,
+ * TypeDeclarations, TypeLiterals, TypeMethodReferences, TypeParameters, and Types.
+ */
+class T<T1, T2, T3> {
+  // TODO(jdd): Add tests for higher language levels.
+
+  T f(int x) throws Exception {
+    class TT {}
+    if (x == 0 || T.class == null) {
+      return this;
+    }
+    try (AutoCloseable y = null) {
+      throw new RuntimeException();
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.output
new file mode 100644
index 0000000..c8cd293
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.output
@@ -0,0 +1,19 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for ThisExpressions, ThrowStatements, TryStatements, TypeDeclarationStatements,
+ * TypeDeclarations, TypeLiterals, TypeMethodReferences, TypeParameters, and Types.
+ */
+class T<T1, T2, T3> {
+  // TODO(jdd): Add tests for higher language levels.
+
+  T f(int x) throws Exception {
+    class TT {}
+    if (x == 0 || T.class == null) {
+      return this;
+    }
+    try (AutoCloseable y = null) {
+      throw new RuntimeException();
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/TabularMixedSignInitializer.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TabularMixedSignInitializer.input
new file mode 100644
index 0000000..2715158
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TabularMixedSignInitializer.input
@@ -0,0 +1,17 @@
+public class T {
+  private static final double[] f = {
+    95.0, 75.0, -95.0, 75.0,
+    -95.0, 75.0, +95.0, 75.0
+  };
+
+  private static final int[] g = {
+    x++, y, ++z,
+    x, y, ~z,
+    --x, ++y, z--
+  };
+
+  private static final bool[] h = {
+    a, b, c, d,
+    !e, a, b, c
+  };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/TabularMixedSignInitializer.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TabularMixedSignInitializer.output
new file mode 100644
index 0000000..2715158
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TabularMixedSignInitializer.output
@@ -0,0 +1,17 @@
+public class T {
+  private static final double[] f = {
+    95.0, 75.0, -95.0, 75.0,
+    -95.0, 75.0, +95.0, 75.0
+  };
+
+  private static final int[] g = {
+    x++, y, ++z,
+    x, y, ~z,
+    --x, ++y, z--
+  };
+
+  private static final bool[] h = {
+    a, b, c, d,
+    !e, a, b, c
+  };
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/TryWithResources.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TryWithResources.input
new file mode 100644
index 0000000..5d294f9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TryWithResources.input
@@ -0,0 +1,21 @@
+class TryWtihResources {
+  {
+    try (@A C c = c(); ) {}
+    try (final @A C c = c(); ) {}
+    try (@A final C c = c(); ) {}
+    try (@A final @B C c = c(); ) {}
+
+    try (final BufferedWriter writer =
+        new BufferedWriter(new OutputStreamWriter(testFile, Charset.defaultCharset()))) {
+      writer.append("tom cruise\n").append("avatar\n");
+      writer.flush();
+    }
+
+    try (@SuppressWarnings("resource")
+        Scanner inputScanner = new Scanner(inputStream).useDelimiter("\\s+|,")) {
+      while (inputScanner.hasNextLong()) {
+        placementIds.add(inputScanner.nextLong());
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/TryWithResources.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TryWithResources.output
new file mode 100644
index 0000000..5d294f9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TryWithResources.output
@@ -0,0 +1,21 @@
+class TryWtihResources {
+  {
+    try (@A C c = c(); ) {}
+    try (final @A C c = c(); ) {}
+    try (@A final C c = c(); ) {}
+    try (@A final @B C c = c(); ) {}
+
+    try (final BufferedWriter writer =
+        new BufferedWriter(new OutputStreamWriter(testFile, Charset.defaultCharset()))) {
+      writer.append("tom cruise\n").append("avatar\n");
+      writer.flush();
+    }
+
+    try (@SuppressWarnings("resource")
+        Scanner inputScanner = new Scanner(inputStream).useDelimiter("\\s+|,")) {
+      while (inputScanner.hasNextLong()) {
+        placementIds.add(inputScanner.nextLong());
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/U.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/U.input
new file mode 100644
index 0000000..555cbf8
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/U.input
@@ -0,0 +1,47 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for UnionTypes. */
+class U {
+  void f() {
+    class Exception0 extends Exception {}
+    class Exception1 extends Exception {}
+    class Exception2 extends Exception {}
+    class Exception3 extends Exception {}
+    class Exception4 extends Exception {}
+    class Exception5 extends Exception {}
+    class Exception6 extends Exception {}
+    class Exception7 extends Exception {}
+    class Exception8 extends Exception {}
+    class Exception9 extends Exception {}
+    try {
+      char c = '\123';
+      switch (c) {
+        case '0':
+          throw new Exception0();
+        case '1':
+          throw new Exception1();
+        case '2':
+          throw new Exception2();
+        case '3':
+          throw new Exception3();
+        case '4':
+          throw new Exception4();
+        case '5':
+          throw new Exception5();
+        case '6':
+          throw new Exception6();
+        case '7':
+          throw new Exception7();
+        case '8':
+          throw new Exception8();
+        case '9':
+        default:
+          throw new Exception9();
+      }
+    } catch (
+        Exception0 | Exception1 | Exception2 | Exception3 | Exception4 | Exception5 | Exception6
+                | Exception7 | Exception8 | Exception9
+            e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/U.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/U.output
new file mode 100644
index 0000000..928d0f4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/U.output
@@ -0,0 +1,53 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for UnionTypes. */
+class U {
+  void f() {
+    class Exception0 extends Exception {}
+    class Exception1 extends Exception {}
+    class Exception2 extends Exception {}
+    class Exception3 extends Exception {}
+    class Exception4 extends Exception {}
+    class Exception5 extends Exception {}
+    class Exception6 extends Exception {}
+    class Exception7 extends Exception {}
+    class Exception8 extends Exception {}
+    class Exception9 extends Exception {}
+    try {
+      char c = '\123';
+      switch (c) {
+        case '0':
+          throw new Exception0();
+        case '1':
+          throw new Exception1();
+        case '2':
+          throw new Exception2();
+        case '3':
+          throw new Exception3();
+        case '4':
+          throw new Exception4();
+        case '5':
+          throw new Exception5();
+        case '6':
+          throw new Exception6();
+        case '7':
+          throw new Exception7();
+        case '8':
+          throw new Exception8();
+        case '9':
+        default:
+          throw new Exception9();
+      }
+    } catch (Exception0
+        | Exception1
+        | Exception2
+        | Exception3
+        | Exception4
+        | Exception5
+        | Exception6
+        | Exception7
+        | Exception8
+        | Exception9 e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/UnaryMinus.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/UnaryMinus.input
new file mode 100644
index 0000000..a512fe9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/UnaryMinus.input
@@ -0,0 +1,5 @@
+class UnaryMinus {
+  {
+    int b = - 1;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/UnaryMinus.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/UnaryMinus.output
new file mode 100644
index 0000000..f6329a4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/UnaryMinus.output
@@ -0,0 +1,5 @@
+class UnaryMinus {
+  {
+    int b = -1;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted.input
new file mode 100644
index 0000000..c936d9e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted.input
@@ -0,0 +1,43 @@
+package com . google . googlejavaformat . java . javatests ;
+
+import com . google . common . base . Charsets ; import com . google . common
+. collect . Range ; import com . google . common . collect . RangeSet ; import
+com . google . common . collect . TreeRangeSet ; import com . google .
+googlejavaformat . java . Formatter ; import com . google . googlejavaformat .
+java . JavaCommentsHelper ; import com . google . googlejavaformat . java .
+JavaInput ; import com . google . googlejavaformat . java . JavaOutput ;
+import junit . framework . TestCase ;
+
+import java . io . File ; import java . io . StringWriter ; import java . io .
+Writer ; import java . nio . file . FileSystems ; import java . nio . file .
+Files ; import java . nio . file . Path ; import java . util . ArrayList ;
+import java . util . List ;
+
+/**
+* Integration test for google-java-format. Format each file
+* in the input directory, and confirm that the result is
+* the same as the file in the output directory.
+*/
+public final class Unformatted extends TestCase { private static final int
+MAX_WIDTH = 100 ; public void testFormatter ( ) throws Exception { Path
+inputPath = FileSystems . getDefault ( ) . getPath ( TestPath . getDir ( ) ,
+"input" ) ; Path outputPath = FileSystems . getDefault ( ) . getPath (
+TestPath . getDir ( ) , "output" ) ; File inputDirectory = new File (
+inputPath . toString ( ) ) ; File outputDirectory = new File ( outputPath .
+toString ( ) ) ; for ( File file : inputDirectory . listFiles ( ) ) {
+assertTrue ( file . isFile ( ) ) ; String fileName = file . getName ( ) ;
+assertTrue ( fileName . endsWith ( ".java" ) ) ; byte [ ] inputBytes = Files .
+readAllBytes ( FileSystems . getDefault ( ) . getPath ( inputDirectory .
+toString ( ) , fileName ) ) ; String inputString = new String ( inputBytes ,
+Charsets . UTF_8 ) ; byte [ ] expectedOutputBytes = Files . readAllBytes (
+FileSystems . getDefault ( ) . getPath ( outputDirectory . toString ( ) ,
+fileName ) ) ; String expectedOutputString = new String ( expectedOutputBytes
+, Charsets . UTF_8 ) ; JavaInput javaInput = new JavaInput ( inputString ) ;
+JavaOutput javaOutput = new JavaOutput ( javaInput , new JavaCommentsHelper (
+) , false ) ; List < String > errors = new ArrayList < > ( ) ; Formatter .
+format ( javaInput , javaOutput , MAX_WIDTH , errors , 1 ) ; assertTrue (
+errors . isEmpty ( ) ) ; Writer stringWriter = new StringWriter ( ) ; RangeSet
+< Integer > linesFlag = TreeRangeSet . create ( ) ; linesFlag . add ( Range .<
+Integer > all ( ) ) ; javaOutput . writeMerged ( stringWriter , linesFlag ,
+MAX_WIDTH , errors ) ; String outputString = stringWriter . toString ( ) ;
+assertEquals ( outputString , expectedOutputString ) ; } } }
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted.output
new file mode 100644
index 0000000..29f7963
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted.output
@@ -0,0 +1,58 @@
+package com.google.googlejavaformat.java.javatests;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.JavaCommentsHelper;
+import com.google.googlejavaformat.java.JavaInput;
+import com.google.googlejavaformat.java.JavaOutput;
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Integration test for google-java-format. Format each file in the input directory, and confirm
+ * that the result is the same as the file in the output directory.
+ */
+public final class Unformatted extends TestCase {
+  private static final int MAX_WIDTH = 100;
+
+  public void testFormatter() throws Exception {
+    Path inputPath = FileSystems.getDefault().getPath(TestPath.getDir(), "input");
+    Path outputPath = FileSystems.getDefault().getPath(TestPath.getDir(), "output");
+    File inputDirectory = new File(inputPath.toString());
+    File outputDirectory = new File(outputPath.toString());
+    for (File file : inputDirectory.listFiles()) {
+      assertTrue(file.isFile());
+      String fileName = file.getName();
+      assertTrue(fileName.endsWith(".java"));
+      byte[] inputBytes =
+          Files.readAllBytes(FileSystems.getDefault().getPath(inputDirectory.toString(), fileName));
+      String inputString = new String(inputBytes, Charsets.UTF_8);
+      byte[] expectedOutputBytes =
+          Files.readAllBytes(
+              FileSystems.getDefault().getPath(outputDirectory.toString(), fileName));
+      String expectedOutputString = new String(expectedOutputBytes, Charsets.UTF_8);
+      JavaInput javaInput = new JavaInput(inputString);
+      JavaOutput javaOutput = new JavaOutput(javaInput, new JavaCommentsHelper(), false);
+      List<String> errors = new ArrayList<>();
+      Formatter.format(javaInput, javaOutput, MAX_WIDTH, errors, 1);
+      assertTrue(errors.isEmpty());
+      Writer stringWriter = new StringWriter();
+      RangeSet<Integer> linesFlag = TreeRangeSet.create();
+      linesFlag.add(Range.<Integer>all());
+      javaOutput.writeMerged(stringWriter, linesFlag, MAX_WIDTH, errors);
+      String outputString = stringWriter.toString();
+      assertEquals(outputString, expectedOutputString);
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted2.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted2.input
new file mode 100644
index 0000000..d968aeb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted2.input
@@ -0,0 +1,16 @@
+package com . google . googlejavaformat ;
+
+/**
+* Rewrite comments. This interface is implemented by
+* {@link com.google.googlejavaformat.java.JavaCommentsHelper JavaCommentsHelper}.
+*/
+public interface CommentsHelper {
+  /**
+   * Try to rewrite comments, returning rewritten text.
+   * @param text0 the literal comment text to rewrite
+   * @param maxWidth the line length for the output
+   * @param column0 the current column
+   * @return the rewritten comment
+   */
+  String rewrite ( String text0 , int maxWidth , int column0 ) ;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted2.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted2.output
new file mode 100644
index 0000000..bc150a5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted2.output
@@ -0,0 +1,17 @@
+package com.google.googlejavaformat;
+
+/**
+ * Rewrite comments. This interface is implemented by {@link
+ * com.google.googlejavaformat.java.JavaCommentsHelper JavaCommentsHelper}.
+ */
+public interface CommentsHelper {
+  /**
+   * Try to rewrite comments, returning rewritten text.
+   *
+   * @param text0 the literal comment text to rewrite
+   * @param maxWidth the line length for the output
+   * @param column0 the current column
+   * @return the rewritten comment
+   */
+  String rewrite(String text0, int maxWidth, int column0);
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted3.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted3.input
new file mode 100644
index 0000000..a2649f0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted3.input
@@ -0,0 +1,9 @@
+package com . google . googlejavaformat ;
+
+class Unformatted3 {
+    /**
+     * This comment is not reflowed
+     * because it contains <pre>preformatted</pre>
+     * content.
+     */
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted3.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted3.output
new file mode 100644
index 0000000..b99debe
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Unformatted3.output
@@ -0,0 +1,11 @@
+package com.google.googlejavaformat;
+
+class Unformatted3 {
+  /**
+   * This comment is not reflowed because it contains
+   *
+   * <pre>preformatted</pre>
+   *
+   * content.
+   */
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/V.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/V.input
new file mode 100644
index 0000000..b3e9d1b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/V.input
@@ -0,0 +1,13 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for VariableDeclarationExpressions, VariableDeclarationFragments, and
+ * VariableDeclarationStatements.
+ */
+class V {
+  int x = 0, y = 1;
+
+  void f() {
+    for (int a = 0, b = 1; a < b;) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/V.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/V.output
new file mode 100644
index 0000000..f61e97b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/V.output
@@ -0,0 +1,13 @@
+package com.google.googlejavaformat.java.test;
+
+/**
+ * Tests for VariableDeclarationExpressions, VariableDeclarationFragments, and
+ * VariableDeclarationStatements.
+ */
+class V {
+  int x = 0, y = 1;
+
+  void f() {
+    for (int a = 0, b = 1; a < b; ) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/W.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/W.input
new file mode 100644
index 0000000..53f50d4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/W.input
@@ -0,0 +1,9 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for WhileStatements and WildcardTypes. */
+class W {
+  <T extends Object> T f() {
+    while (true == true == true == true == true == true == true == true == true == true == true
+        == true == true) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/W.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/W.output
new file mode 100644
index 0000000..53f50d4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/W.output
@@ -0,0 +1,9 @@
+package com.google.googlejavaformat.java.test;
+
+/** Tests for WhileStatements and WildcardTypes. */
+class W {
+  <T extends Object> T f() {
+    while (true == true == true == true == true == true == true == true == true == true == true
+        == true == true) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/WildBound.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/WildBound.input
new file mode 100644
index 0000000..fa61386
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/WildBound.input
@@ -0,0 +1,5 @@
+class WildBound {
+  <T> T f() {
+    return (T) X.class.newInstance((F<?>) f, t);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/WildBound.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/WildBound.output
new file mode 100644
index 0000000..fa61386
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/WildBound.output
@@ -0,0 +1,5 @@
+class WildBound {
+  <T> T f() {
+    return (T) X.class.newInstance((F<?>) f, t);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i100.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i100.input
new file mode 100644
index 0000000..b7c88f8
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i100.input
@@ -0,0 +1,7 @@
+public class I100 {
+  void f() {
+    Object o;
+    o = new String @A [] {"foo", "bar"};
+    o = new String @A @B [] @C [] {"foo", "bar"};
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i100.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i100.output
new file mode 100644
index 0000000..b7c88f8
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i100.output
@@ -0,0 +1,7 @@
+public class I100 {
+  void f() {
+    Object o;
+    o = new String @A [] {"foo", "bar"};
+    o = new String @A @B [] @C [] {"foo", "bar"};
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i101.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i101.input
new file mode 100644
index 0000000..97c321e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i101.input
@@ -0,0 +1 @@
+public @interface kHz {} // No prefix defined in the annotation itself
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i101.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i101.output
new file mode 100644
index 0000000..97c321e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i101.output
@@ -0,0 +1 @@
+public @interface kHz {} // No prefix defined in the annotation itself
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i155.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i155.input
new file mode 100644
index 0000000..1a525de
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i155.input
@@ -0,0 +1,5 @@
+class I155 {
+  void walkAndClose(Stream<?> stream) {
+    try (stream) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i155.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i155.output
new file mode 100644
index 0000000..1a525de
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i155.output
@@ -0,0 +1,5 @@
+class I155 {
+  void walkAndClose(Stream<?> stream) {
+    try (stream) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i176.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i176.input
new file mode 100644
index 0000000..78aabff
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i176.input
@@ -0,0 +1,6 @@
+interface
+InterfaceWithPrivateStaticMethod
+{
+  private static void bar() { }
+  default void foo() { bar(); }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i176.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i176.output
new file mode 100644
index 0000000..0e33b90
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i176.output
@@ -0,0 +1,7 @@
+interface InterfaceWithPrivateStaticMethod {
+  private static void bar() {}
+
+  default void foo() {
+    bar();
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i221.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i221.input
new file mode 100644
index 0000000..6320a38
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i221.input
@@ -0,0 +1,8 @@
+class I221 {
+  {
+    logger.log(
+        Level.WARNING,
+        "Unable to obtain jst.web facet version from selected project", //$NON-NLS-1$
+        ex);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i221.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i221.output
new file mode 100644
index 0000000..6320a38
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i221.output
@@ -0,0 +1,8 @@
+class I221 {
+  {
+    logger.log(
+        Level.WARNING,
+        "Unable to obtain jst.web facet version from selected project", //$NON-NLS-1$
+        ex);
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i281.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i281.input
new file mode 100644
index 0000000..0b42d08
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i281.input
@@ -0,0 +1,8 @@
+public enum Empty {}
+public enum Empty {;}
+public enum Empty {;;;}
+public enum Empty {; // comment
+;;}
+public enum Empty {;
+;; // comment
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i281.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i281.output
new file mode 100644
index 0000000..ca3861a
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i281.output
@@ -0,0 +1,23 @@
+public enum Empty {}
+
+public enum Empty {
+  ;
+}
+
+public enum Empty {
+  ;
+  ;
+  ;
+}
+
+public enum Empty {
+  ; // comment
+  ;
+  ;
+}
+
+public enum Empty {
+  ;
+  ;
+  ; // comment
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i282.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i282.input
new file mode 100644
index 0000000..a8bf796
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i282.input
@@ -0,0 +1,11 @@
+public class ArrayVsVarargs {
+
+  void f(String[] arg) {}
+  void g(String... arg) {}
+  void h(String @A [] arg) {}
+  void i(String @B... arg) {}
+  void j(String @C [] @D [] arg) {}
+  void k(String @E [] @F... arg) {}
+
+  Class<?> c = byte[].class;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i282.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i282.output
new file mode 100644
index 0000000..6c92e51
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i282.output
@@ -0,0 +1,16 @@
+public class ArrayVsVarargs {
+
+  void f(String[] arg) {}
+
+  void g(String... arg) {}
+
+  void h(String @A [] arg) {}
+
+  void i(String @B ... arg) {}
+
+  void j(String @C [] @D [] arg) {}
+
+  void k(String @E [] @F ... arg) {}
+
+  Class<?> c = byte[].class;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i55.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i55.input
new file mode 100644
index 0000000..425f8eb
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i55.input
@@ -0,0 +1,13 @@
+import java.lang.annotation.*;
+
+@Target(ElementType.TYPE_USE)
+@interface Interned {}
+
+public class GjfTypeAnnotationOnArrayTest {
+
+  public static void test() {
+    String @Interned [] iarray1 = new String @Interned [2];
+  }
+
+}
+
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i55.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i55.output
new file mode 100644
index 0000000..f7710c9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i55.output
@@ -0,0 +1,11 @@
+import java.lang.annotation.*;
+
+@Target(ElementType.TYPE_USE)
+@interface Interned {}
+
+public class GjfTypeAnnotationOnArrayTest {
+
+  public static void test() {
+    String @Interned [] iarray1 = new String @Interned [2];
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i59.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i59.input
new file mode 100644
index 0000000..937f40f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i59.input
@@ -0,0 +1,10 @@
+import org.checkerframework.checker.tainting.qual.*;
+class Outer {
+  class Nested {
+    class InnerMost {
+      @A Outer context(@B Outer.@C Nested.@D InnerMost this) {
+        return Outer.this;
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i59.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i59.output
new file mode 100644
index 0000000..600b300
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i59.output
@@ -0,0 +1,12 @@
+import org.checkerframework.checker.tainting.qual.*;
+
+class Outer {
+  class Nested {
+    class InnerMost {
+      @A
+      Outer context(@B Outer.@C Nested.@D InnerMost this) {
+        return Outer.this;
+      }
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i60.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i60.input
new file mode 100644
index 0000000..8b0a378
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i60.input
@@ -0,0 +1,9 @@
+import org.checkerframework.checker.nullness.qual.*;
+
+public class FullyQualified {
+
+  public void format1(java.lang.String a1, java.lang. @Nullable Object... a2) {
+    // body
+  }
+
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i60.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i60.output
new file mode 100644
index 0000000..85cdb40
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i60.output
@@ -0,0 +1,8 @@
+import org.checkerframework.checker.nullness.qual.*;
+
+public class FullyQualified {
+
+  public void format1(java.lang.String a1, java.lang.@Nullable Object... a2) {
+    // body
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i65.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i65.input
new file mode 100644
index 0000000..d4a4593
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i65.input
@@ -0,0 +1,5 @@
+import org.checkerframework.checker.nullness.qual.*;
+
+class UnannoPrimitives {
+  Object ar = new @Nullable byte[] {4};
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i65.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i65.output
new file mode 100644
index 0000000..d4a4593
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i65.output
@@ -0,0 +1,5 @@
+import org.checkerframework.checker.nullness.qual.*;
+
+class UnannoPrimitives {
+  Object ar = new @Nullable byte[] {4};
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i66.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i66.input
new file mode 100644
index 0000000..54e3cce
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i66.input
@@ -0,0 +1,7 @@
+public class AnnotatedCast {
+
+  public static @Sibling1 int convertSibling2ToSibling1(@Sibling2 int a) {
+    return (@Sibling1 int) 1;
+  }
+
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i66.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i66.output
new file mode 100644
index 0000000..00cbea7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i66.output
@@ -0,0 +1,6 @@
+public class AnnotatedCast {
+
+  public static @Sibling1 int convertSibling2ToSibling1(@Sibling2 int a) {
+    return (@Sibling1 int) 1;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i71.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i71.input
new file mode 100644
index 0000000..324c3d5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i71.input
@@ -0,0 +1,16 @@
+import java.util.Arrays;
+
+class Test {
+  Wrapper w = new Wrapper(
+      Arrays.asList(
+        "a", "b",
+        "c", "d",
+        "e"
+        ), 2);
+  Wrapper w = new Wrapper(
+      Arrays.asList(
+        "a", "b",
+        "c", "d",
+        "e"
+        ));
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i71.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i71.output
new file mode 100644
index 0000000..ad0bece
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i71.output
@@ -0,0 +1,6 @@
+import java.util.Arrays;
+
+class Test {
+  Wrapper w = new Wrapper(Arrays.asList("a", "b", "c", "d", "e"), 2);
+  Wrapper w = new Wrapper(Arrays.asList("a", "b", "c", "d", "e"));
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i95.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i95.input
new file mode 100644
index 0000000..624664e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i95.input
@@ -0,0 +1,3 @@
+class I95 {
+  public void format2(Object @Nullable ... a2) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i95.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i95.output
new file mode 100644
index 0000000..624664e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i95.output
@@ -0,0 +1,3 @@
+class I95 {
+  public void format2(Object @Nullable ... a2) {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i98.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i98.input
new file mode 100644
index 0000000..abc1698
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i98.input
@@ -0,0 +1,8 @@
+public class AnonInnerDefaults {
+
+  public void tryStuff(final UIElement e) {
+    PolyIface p =
+      new @UI PolyIface() {
+      };
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/i98.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i98.output
new file mode 100644
index 0000000..c96813e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/i98.output
@@ -0,0 +1,6 @@
+public class AnonInnerDefaults {
+
+  public void tryStuff(final UIElement e) {
+    PolyIface p = new @UI PolyIface() {};
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/java11.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/java11.input
new file mode 100644
index 0000000..97bd4ac
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/java11.input
@@ -0,0 +1,9 @@
+class Java11 {
+  interface I {
+    private default void f() {}
+  }
+
+  public static void main(String[] args) {
+    var x = 42;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/java11.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/java11.output
new file mode 100644
index 0000000..97bd4ac
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/java11.output
@@ -0,0 +1,9 @@
+class Java11 {
+  interface I {
+    private default void f() {}
+  }
+
+  public static void main(String[] args) {
+    var x = 42;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/java14.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/java14.input
new file mode 100644
index 0000000..10d4522
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/java14.input
@@ -0,0 +1,50 @@
+class Java14 {
+  void f() {
+    if (obj instanceof String s) {
+    } else {
+    }
+  }
+
+  record Range<T extends Number>(T lo, T hi) implements Comparable<Range<T>> {
+
+    public Range {}
+
+    Range(T lo) {
+      this(lo, lo);
+    }
+
+    @Override
+    public int compareTo(Range<T> other) {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  record Foo(int id) {}
+
+  record Rcv(int id) {
+    public Rcv(Rcv this, int id) {
+      this.id = id;
+    }
+  }
+
+  void g() {
+    var block = """
+    hello
+      text
+        blocks
+    """.indent(6);
+  }
+
+  void h() {
+    int numLetters = switch (day) {
+        case MONDAY, FRIDAY, SUNDAY -> 6;
+        case TUESDAY                -> 7;
+        case THURSDAY, SATURDAY     -> 8;
+        case WEDNESDAY              -> 9;
+    };
+  }
+
+  {
+    for (var arg : List.of()) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/java14.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/java14.output
new file mode 100644
index 0000000..9779198
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/java14.output
@@ -0,0 +1,51 @@
+class Java14 {
+  void f() {
+    if (obj instanceof String s) {
+    } else {
+    }
+  }
+
+  record Range<T extends Number>(T lo, T hi) implements Comparable<Range<T>> {
+
+    public Range {}
+
+    Range(T lo) {
+      this(lo, lo);
+    }
+
+    @Override
+    public int compareTo(Range<T> other) {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  record Foo(int id) {}
+
+  record Rcv(int id) {
+    public Rcv(Rcv this, int id) {
+      this.id = id;
+    }
+  }
+
+  void g() {
+    var block = """
+    hello
+      text
+        blocks
+    """.indent(6);
+  }
+
+  void h() {
+    int numLetters =
+        switch (day) {
+          case MONDAY, FRIDAY, SUNDAY -> 6;
+          case TUESDAY -> 7;
+          case THURSDAY, SATURDAY -> 8;
+          case WEDNESDAY -> 9;
+        };
+  }
+
+  {
+    for (var arg : List.of()) {}
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-and-import-sorting b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-and-import-sorting
new file mode 100644
index 0000000..e6994f7
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-and-import-sorting
@@ -0,0 +1,29 @@
+package com.google.example;
+
+import static com.google.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Preconditions;
+import java.util.List;
+import java.util.Set;
+import javax.annotations.Nullable;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SomeTest {
+
+  <T> void check(@Nullable List<T> x) {
+    Preconditions.checkNodeNull(x);
+  }
+
+  void f() {
+    List<String> xs = null;
+    assertThat(xs).isNull();
+    try {
+      check(xs);
+      fail();
+    } catch (NullPointerException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-and-unused-import-removal b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-and-unused-import-removal
new file mode 100644
index 0000000..7d5df53
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-and-unused-import-removal
@@ -0,0 +1,31 @@
+package com.google.example;
+
+import com.google.common.base.Preconditions;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+import javax.annotations.Nullable;
+
+import static org.junit.Assert.fail;
+import static com.google.truth.Truth.assertThat;
+
+@RunWith(JUnit4.class)
+public class SomeTest {
+
+  <T> void check(@Nullable List<T> x) {
+    Preconditions.checkNodeNull(x);
+  }
+
+  void f() {
+    List<String> xs = null;
+    assertThat(xs).isNull();
+    try {
+      check(xs);
+      fail();
+    } catch (NullPointerException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-only b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-only
new file mode 100644
index 0000000..7d5df53
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.formatting-only
@@ -0,0 +1,31 @@
+package com.google.example;
+
+import com.google.common.base.Preconditions;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+import javax.annotations.Nullable;
+
+import static org.junit.Assert.fail;
+import static com.google.truth.Truth.assertThat;
+
+@RunWith(JUnit4.class)
+public class SomeTest {
+
+  <T> void check(@Nullable List<T> x) {
+    Preconditions.checkNodeNull(x);
+  }
+
+  void f() {
+    List<String> xs = null;
+    assertThat(xs).isNull();
+    try {
+      check(xs);
+      fail();
+    } catch (NullPointerException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.imports-and-formatting b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.imports-and-formatting
new file mode 100644
index 0000000..887d43c
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.imports-and-formatting
@@ -0,0 +1,28 @@
+package com.google.example;
+
+import static com.google.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Preconditions;
+import java.util.List;
+import javax.annotations.Nullable;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SomeTest {
+
+  <T> void check(@Nullable List<T> x) {
+    Preconditions.checkNodeNull(x);
+  }
+
+  void f() {
+    List<String> xs = null;
+    assertThat(xs).isNull();
+    try {
+      check(xs);
+      fail();
+    } catch (NullPointerException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.imports-only b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.imports-only
new file mode 100644
index 0000000..88f83f1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.imports-only
@@ -0,0 +1,27 @@
+package com.google.example;
+
+import static com.google.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Preconditions;
+import java.util.List;
+import javax.annotations.Nullable;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith( JUnit4.class ) public class SomeTest  {
+
+  <T> void check(@Nullable List<T> x) {
+    Preconditions.checkNodeNull(x);
+  }
+
+  void f() {
+    List<String> xs = null;
+    assertThat(xs).isNull();
+    try {
+      check(xs);
+      fail();
+    } catch (NullPointerException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.input b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.input
new file mode 100644
index 0000000..be1eacd
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.input
@@ -0,0 +1,31 @@
+package com.google.example;
+
+import com.google.common.base.Preconditions;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.annotations.Nullable;
+
+import static org.junit.Assert.fail;
+import static com.google.truth.Truth.assertThat;
+
+@RunWith( JUnit4.class ) public class SomeTest  {
+
+  <T> void check(@Nullable List<T> x) {
+    Preconditions.checkNodeNull(x);
+  }
+
+  void f() {
+    List<String> xs = null;
+    assertThat(xs).isNull();
+    try {
+      check(xs);
+      fail();
+    } catch (NullPointerException e) {
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B28750242.input b/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B28750242.input
new file mode 100644
index 0000000..022e0ea
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B28750242.input
@@ -0,0 +1,70 @@
+class Test {
+  /**
+   * Lorum Ipsum.
+   *
+   * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
+   * ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
+   * laboris nisi ut aliquip ex ea commodo consequat.
+   *
+   * <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
+   * nulla pariatur.
+   *
+   * <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+   * anim id est laborum."
+   */
+  void f() {}
+
+  /**
+   * Lorum Ipsum.
+   * <p>
+   * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
+   * ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
+   * laboris nisi ut aliquip ex ea commodo consequat.
+   * <p>
+   * Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
+   * nulla pariatur.
+   * <p>
+   * Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+   * anim id est laborum."
+   */
+  void f() {}
+
+  /**
+   * Lorum Ipsum.
+   *
+   *
+   * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
+   * ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
+   * laboris nisi ut aliquip ex ea commodo consequat.
+   *
+   *
+   * <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
+   * nulla pariatur.
+   *
+   *
+   * <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+   * anim id est laborum."
+   */
+  void f() {}
+
+  /**
+   * Lorum Ipsum.
+   *
+   * <p>
+   *
+   * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
+   * ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
+   * laboris nisi ut aliquip ex ea commodo consequat.
+   *
+   * <p>
+   *
+   * Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
+   * nulla pariatur.
+   *
+   * <p>
+   *
+   * Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+   * anim id est laborum."
+   */
+  void f() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B28750242.output b/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B28750242.output
new file mode 100644
index 0000000..23901a9
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B28750242.output
@@ -0,0 +1,61 @@
+class Test {
+  /**
+   * Lorum Ipsum.
+   *
+   * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
+   * labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
+   * laboris nisi ut aliquip ex ea commodo consequat.
+   *
+   * <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+   * pariatur.
+   *
+   * <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+   * anim id est laborum."
+   */
+  void f() {}
+
+  /**
+   * Lorum Ipsum.
+   *
+   * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
+   * labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
+   * laboris nisi ut aliquip ex ea commodo consequat.
+   *
+   * <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+   * pariatur.
+   *
+   * <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+   * anim id est laborum."
+   */
+  void f() {}
+
+  /**
+   * Lorum Ipsum.
+   *
+   * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
+   * labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
+   * laboris nisi ut aliquip ex ea commodo consequat.
+   *
+   * <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+   * pariatur.
+   *
+   * <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+   * anim id est laborum."
+   */
+  void f() {}
+
+  /**
+   * Lorum Ipsum.
+   *
+   * <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
+   * labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
+   * laboris nisi ut aliquip ex ea commodo consequat.
+   *
+   * <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+   * pariatur.
+   *
+   * <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+   * anim id est laborum."
+   */
+  void f() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B31404367.input b/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B31404367.input
new file mode 100644
index 0000000..5bf96da
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B31404367.input
@@ -0,0 +1,8 @@
+/**
+ * Foo.
+ *
+ * <ul>
+ *   <li> a
+ * </ul>
+ */
+class Foo {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B31404367.output b/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B31404367.output
new file mode 100644
index 0000000..39d08bc
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testjavadoc/B31404367.output
@@ -0,0 +1,8 @@
+/**
+ * Foo.
+ *
+ * <ul>
+ *   <li>a
+ * </ul>
+ */
+class Foo {}
diff --git a/eclipse_plugin/META-INF/MANIFEST.MF b/eclipse_plugin/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..27613e9
--- /dev/null
+++ b/eclipse_plugin/META-INF/MANIFEST.MF
@@ -0,0 +1,15 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: google-java-format
+Bundle-SymbolicName: google-java-format-eclipse-plugin;singleton:=true
+Bundle-Version: 1.6.0
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Require-Bundle: org.eclipse.jdt.core;bundle-version="3.10.0",
+ org.eclipse.jface,
+ org.eclipse.text,
+ org.eclipse.ui,
+ org.eclipse.equinox.common
+Bundle-ClassPath: .,
+ lib/guava-22.0.jar,
+ lib/javac-shaded-9+181-r4173-1.jar,
+ lib/google-java-format-1.6.jar
diff --git a/eclipse_plugin/README.md b/eclipse_plugin/README.md
new file mode 100644
index 0000000..332468d
--- /dev/null
+++ b/eclipse_plugin/README.md
@@ -0,0 +1,26 @@
+# google-java-format Eclipse Plugin
+
+## Enabling
+
+See https://github.com/google/google-java-format#eclipse
+
+## Development
+
+1) Uncomment `<module>eclipse_plugin</module>` in the parent `pom.xml`
+
+2) Run `mvn install`, which will copy the dependences of the plugin to
+`eclipse_plugin/lib`.
+
+2) If you are using Java 9, add
+
+    ```
+    -vm
+    /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/bin/java
+    ```
+
+    to `/Applications/Eclipse.app/Contents/Eclipse/eclipse.ini`.
+
+3) Open the `eclipse_plugin` project in a recent Eclipse SDK build.
+
+4) From `File > Export` select `Plugin-in Development > Deployable plugin-ins
+and fragments` and follow the wizard to export a plugin jar.
diff --git a/eclipse_plugin/build.properties b/eclipse_plugin/build.properties
new file mode 100644
index 0000000..dc5ae7c
--- /dev/null
+++ b/eclipse_plugin/build.properties
@@ -0,0 +1,8 @@
+source.. = src/
+output.. = target/classes
+bin.includes = META-INF/,\
+               .,\
+               plugin.xml,\
+               lib/javac-shaded-9+181-r4173-1.jar,\
+               lib/guava-22.0.jar,\
+               lib/google-java-format-1.6.jar
diff --git a/eclipse_plugin/plugin.xml b/eclipse_plugin/plugin.xml
new file mode 100644
index 0000000..5d0bbf9
--- /dev/null
+++ b/eclipse_plugin/plugin.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.4"?>
+<plugin>
+   <extension point="org.eclipse.jdt.core.javaFormatter">
+   <javaFormatter
+         class="com.google.googlejavaformat.java.GoogleJavaFormatter"
+         id="com.google.googlejavaformat.java.GoogleJavaFormatter"
+         name="google-java-format">
+   </javaFormatter>
+   </extension>
+</plugin>
diff --git a/eclipse_plugin/pom.xml b/eclipse_plugin/pom.xml
new file mode 100644
index 0000000..bb581cd
--- /dev/null
+++ b/eclipse_plugin/pom.xml
@@ -0,0 +1,103 @@
+<!--
+  ~ Copyright 2015 Google Inc.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>com.google.googlejavaformat</groupId>
+    <artifactId>google-java-format-parent</artifactId>
+    <version>1.6</version>
+  </parent>
+
+  <artifactId>google-java-format-eclipse-plugin</artifactId>
+  <version>1.6.0</version>
+  <packaging>eclipse-plugin</packaging>
+  <name>google-java-format Plugin for Eclipse 4.5+</name>
+
+  <description>
+    A Java source code formatter that follows Google Java Style.
+  </description>
+
+  <properties>
+    <tycho-version>0.26.0</tycho-version>
+  </properties>
+
+  <repositories>
+    <repository>
+      <id>mars</id>
+      <layout>p2</layout>
+      <url>https://download.eclipse.org/releases/mars</url>
+    </repository>
+  </repositories>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.googlejavaformat</groupId>
+      <artifactId>google-java-format</artifactId>
+      <version>1.6</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+
+      <plugin>
+        <groupId>org.eclipse.tycho</groupId>
+        <artifactId>tycho-maven-plugin</artifactId>
+        <version>${tycho-version}</version>
+        <extensions>true</extensions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.eclipse.tycho</groupId>
+        <artifactId>target-platform-configuration</artifactId>
+        <version>${tycho-version}</version>
+        <configuration>
+          <environments>
+            <environment>
+              <os>linux</os>
+              <ws>gtk</ws>
+              <arch>x86</arch>
+            </environment>
+            <environment>
+              <os>linux</os>
+              <ws>gtk</ws>
+              <arch>x86_64</arch>
+            </environment>
+            <environment>
+              <os>win32</os>
+              <ws>win32</ws>
+              <arch>x86</arch>
+            </environment>
+            <environment>
+              <os>win32</os>
+              <ws>win32</ws>
+              <arch>x86_64</arch>
+            </environment>
+            <environment>
+              <os>macosx</os>
+              <ws>cocoa</ws>
+              <arch>x86_64</arch>
+            </environment>
+          </environments>
+        </configuration>
+      </plugin>
+
+    </plugins>
+  </build>
+
+</project>
diff --git a/eclipse_plugin/src/com/google/googlejavaformat/java/GoogleJavaFormatter.java b/eclipse_plugin/src/com/google/googlejavaformat/java/GoogleJavaFormatter.java
new file mode 100644
index 0000000..c300514
--- /dev/null
+++ b/eclipse_plugin/src/com/google/googlejavaformat/java/GoogleJavaFormatter.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.googlejavaformat.java;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Range;
+import com.google.googlejavaformat.java.SnippetFormatter.SnippetKind;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jdt.core.dom.ASTParser;
+import org.eclipse.jdt.core.formatter.CodeFormatter;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+
+/** Runs the Google Java formatter on the given code. */
+public class GoogleJavaFormatter extends CodeFormatter {
+
+  private static final int INDENTATION_SIZE = 2;
+
+  @Override
+  public TextEdit format(
+      int kind, String source, int offset, int length, int indentationLevel, String lineSeparator) {
+    IRegion[] regions = new IRegion[] {new Region(offset, length)};
+    return formatInternal(kind, source, regions, indentationLevel);
+  }
+
+  @Override
+  public TextEdit format(
+      int kind, String source, IRegion[] regions, int indentationLevel, String lineSeparator) {
+    return formatInternal(kind, source, regions, indentationLevel);
+  }
+
+  @Override
+  public String createIndentationString(int indentationLevel) {
+    Preconditions.checkArgument(
+        indentationLevel >= 0,
+        "Indentation level cannot be less than zero. Given: %s",
+        indentationLevel);
+    int spaces = indentationLevel * INDENTATION_SIZE;
+    StringBuilder buf = new StringBuilder(spaces);
+    for (int i = 0; i < spaces; i++) {
+      buf.append(' ');
+    }
+    return buf.toString();
+  }
+
+  /** Runs the Google Java formatter on the given source, with only the given ranges specified. */
+  private TextEdit formatInternal(int kind, String source, IRegion[] regions, int initialIndent) {
+    try {
+      boolean includeComments =
+          (kind & CodeFormatter.F_INCLUDE_COMMENTS) == CodeFormatter.F_INCLUDE_COMMENTS;
+      kind &= ~CodeFormatter.F_INCLUDE_COMMENTS;
+      SnippetKind snippetKind;
+      switch (kind) {
+        case ASTParser.K_EXPRESSION:
+          snippetKind = SnippetKind.EXPRESSION;
+          break;
+        case ASTParser.K_STATEMENTS:
+          snippetKind = SnippetKind.STATEMENTS;
+          break;
+        case ASTParser.K_CLASS_BODY_DECLARATIONS:
+          snippetKind = SnippetKind.CLASS_BODY_DECLARATIONS;
+          break;
+        case ASTParser.K_COMPILATION_UNIT:
+          snippetKind = SnippetKind.COMPILATION_UNIT;
+          break;
+        default:
+          throw new IllegalArgumentException(String.format("Unknown snippet kind: %d", kind));
+      }
+      List<Replacement> replacements =
+          new SnippetFormatter()
+              .format(
+                  snippetKind, source, rangesFromRegions(regions), initialIndent, includeComments);
+      if (idempotent(source, regions, replacements)) {
+        // Do not create edits if there's no diff.
+        return null;
+      }
+      // Convert replacements to text edits.
+      return editFromReplacements(replacements);
+    } catch (IllegalArgumentException | FormatterException exception) {
+      // Do not format on errors.
+      return null;
+    }
+  }
+
+  private List<Range<Integer>> rangesFromRegions(IRegion[] regions) {
+    List<Range<Integer>> ranges = new ArrayList<>();
+    for (IRegion region : regions) {
+      ranges.add(Range.closedOpen(region.getOffset(), region.getOffset() + region.getLength()));
+    }
+    return ranges;
+  }
+
+  /** @return {@code true} if input and output texts are equal, else {@code false}. */
+  private boolean idempotent(String source, IRegion[] regions, List<Replacement> replacements) {
+    // This implementation only checks for single replacement.
+    if (replacements.size() == 1) {
+      Replacement replacement = replacements.get(0);
+      String output = replacement.getReplacementString();
+      // Entire source case: input = output, nothing changed.
+      if (output.equals(source)) {
+        return true;
+      }
+      // Single region and single replacement case: if they are equal, nothing changed.
+      if (regions.length == 1) {
+        Range<Integer> range = replacement.getReplaceRange();
+        String snippet = source.substring(range.lowerEndpoint(), range.upperEndpoint());
+        if (output.equals(snippet)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private TextEdit editFromReplacements(List<Replacement> replacements) {
+    // Split the replacements that cross line boundaries.
+    TextEdit edit = new MultiTextEdit();
+    for (Replacement replacement : replacements) {
+      Range<Integer> replaceRange = replacement.getReplaceRange();
+      edit.addChild(
+          new ReplaceEdit(
+              replaceRange.lowerEndpoint(),
+              replaceRange.upperEndpoint() - replaceRange.lowerEndpoint(),
+              replacement.getReplacementString()));
+    }
+    return edit;
+  }
+}
diff --git a/idea_plugin/build.gradle b/idea_plugin/build.gradle
new file mode 100644
index 0000000..7b2f389
--- /dev/null
+++ b/idea_plugin/build.gradle
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+  id "org.jetbrains.intellij" version "0.4.18"
+}
+
+repositories {
+  mavenCentral()
+}
+
+ext {
+  googleJavaFormatVersion = '1.7'
+}
+
+apply plugin: 'org.jetbrains.intellij'
+apply plugin: 'java'
+
+intellij {
+  pluginName = "google-java-format"
+  version = "2020.1"
+}
+
+patchPluginXml {
+  pluginDescription = "Formats source code using the google-java-format tool. This version of " +
+                      "the plugin uses version ${googleJavaFormatVersion} of the tool."
+  version = "${googleJavaFormatVersion}.0.5"
+  sinceBuild = '201'
+  untilBuild = ''
+}
+
+publishPlugin {
+  token = project.ext.properties.jetbrainsPluginRepoToken
+}
+
+sourceSets {
+  main {
+    java.srcDir 'src'
+    resources.srcDir 'resources'
+  }
+}
+
+dependencies {
+  compile "com.google.googlejavaformat:google-java-format:${googleJavaFormatVersion}"
+}
diff --git a/idea_plugin/google-java-format.iml b/idea_plugin/google-java-format.iml
new file mode 100644
index 0000000..568c04f
--- /dev/null
+++ b/idea_plugin/google-java-format.iml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PLUGIN_MODULE" version="4">
+  <component name="DevKit.ModuleBuildProperties" url="file://$MODULE_DIR$/resources/META-INF/plugin.xml" />
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/com/google/googlejavaformat/intellij/v2017_2" isTestSource="false" packagePrefix="com.google.googlejavaformat.intellij" />
+      <excludeFolder url="file://$MODULE_DIR$/src/com/google/googlejavaformat/intellij/v2016_2" />
+      <excludeFolder url="file://$MODULE_DIR$/src/com/google/googlejavaformat/intellij/v2017_1" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="google-java-format-1.4-all-deps" level="project" />
+  </component>
+</module>
diff --git a/idea_plugin/resources/META-INF/plugin.xml b/idea_plugin/resources/META-INF/plugin.xml
new file mode 100644
index 0000000..d633b40
--- /dev/null
+++ b/idea_plugin/resources/META-INF/plugin.xml
@@ -0,0 +1,44 @@
+<idea-plugin url="https://github.com/google/google-java-format/tree/master/idea_plugin">
+  <id>google-java-format</id>
+  <name>google-java-format</name>
+  <vendor url="https://github.com/google/google-java-format">
+    Google
+  </vendor>
+
+  <!-- Mark it as available on all JetBrains IDEs. It's really only useful in
+       IDEA and Android Studio, but there's no way to specify that for some
+       reason. It won't crash PyCharm or anything, so whatever. -->
+  <depends>com.intellij.modules.lang</depends>
+
+  <change-notes><![CDATA[
+    <dl>
+      <dt>1.7.0.5</dt>
+      <dd>Added a version for 2020.1+ IDEs.</dd>
+      <dt>1.7.0.4</dt>
+      <dd>Marked the plugin as being incompatible with 2020.1+ IDEs.</dd>
+      <dt>1.7.0.3</dt>
+      <dd>Fixed the plugin on 2019.3 IDEs.</dd>
+      <dt>1.7.0.2</dt>
+      <dd>Added support for all IDEs after 2017.3.</dd>
+      <dt>1.7.0.1</dt>
+      <dd>Added support for 2019.1 IDEs.</dd>
+      <dt>1.7.0.0</dt>
+      <dd>Upgraded to google-java-format 1.7.</dd>
+    </dl>
+  ]]></change-notes>
+
+  <applicationListeners>
+    <listener class="com.google.googlejavaformat.intellij.InitialConfigurationProjectManagerListener"
+              topic="com.intellij.openapi.project.ProjectManagerListener"/>
+    <listener class="com.google.googlejavaformat.intellij.GoogleJavaFormatInstaller"
+              topic="com.intellij.openapi.project.ProjectManagerListener"/>
+  </applicationListeners>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <projectConfigurable instance="com.google.googlejavaformat.intellij.GoogleJavaFormatConfigurable"
+                         id="google-java-format.settings"
+                         displayName="google-java-format Settings"/>
+    <projectService serviceImplementation="com.google.googlejavaformat.intellij.GoogleJavaFormatSettings"/>
+  </extensions>
+
+</idea-plugin>
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java b/idea_plugin/src/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java
new file mode 100644
index 0000000..ee28e1b
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.intellij;
+
+import com.intellij.formatting.FormattingMode;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.codeStyle.ChangedRangesInfo;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.codeStyle.DocCommentSettings;
+import com.intellij.psi.codeStyle.FormattingModeAwareIndentAdjuster;
+import com.intellij.psi.codeStyle.Indent;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.ThrowableRunnable;
+import java.util.Collection;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Decorates the {@link CodeStyleManager} abstract class by delegating to a concrete implementation
+ * instance (likely IJ's default instance).
+ */
+@SuppressWarnings("deprecation")
+class CodeStyleManagerDecorator extends CodeStyleManager
+    implements FormattingModeAwareIndentAdjuster {
+
+  private final CodeStyleManager delegate;
+
+  CodeStyleManagerDecorator(CodeStyleManager delegate) {
+    this.delegate = delegate;
+  }
+
+  CodeStyleManager getDelegate() {
+    return delegate;
+  }
+
+  @Override
+  public Project getProject() {
+    return delegate.getProject();
+  }
+
+  @Override
+  public PsiElement reformat(PsiElement element) throws IncorrectOperationException {
+    return delegate.reformat(element);
+  }
+
+  @Override
+  public PsiElement reformat(PsiElement element, boolean canChangeWhiteSpacesOnly)
+      throws IncorrectOperationException {
+    return delegate.reformat(element, canChangeWhiteSpacesOnly);
+  }
+
+  @Override
+  public PsiElement reformatRange(PsiElement element, int startOffset, int endOffset)
+      throws IncorrectOperationException {
+    return delegate.reformatRange(element, startOffset, endOffset);
+  }
+
+  @Override
+  public PsiElement reformatRange(
+      PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly)
+      throws IncorrectOperationException {
+    return delegate.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly);
+  }
+
+  @Override
+  public void reformatText(PsiFile file, int startOffset, int endOffset)
+      throws IncorrectOperationException {
+    delegate.reformatText(file, startOffset, endOffset);
+  }
+
+  @Override
+  public void reformatText(PsiFile file, Collection<TextRange> ranges)
+      throws IncorrectOperationException {
+    delegate.reformatText(file, ranges);
+  }
+
+  @Override
+  public void reformatTextWithContext(PsiFile psiFile, ChangedRangesInfo changedRangesInfo)
+      throws IncorrectOperationException {
+    delegate.reformatTextWithContext(psiFile, changedRangesInfo);
+  }
+
+  @Override
+  public void reformatTextWithContext(PsiFile file, Collection<TextRange> ranges)
+      throws IncorrectOperationException {
+    delegate.reformatTextWithContext(file, ranges);
+  }
+
+  @Override
+  public void adjustLineIndent(PsiFile file, TextRange rangeToAdjust)
+      throws IncorrectOperationException {
+    delegate.adjustLineIndent(file, rangeToAdjust);
+  }
+
+  @Override
+  public int adjustLineIndent(PsiFile file, int offset) throws IncorrectOperationException {
+    return delegate.adjustLineIndent(file, offset);
+  }
+
+  @Override
+  public int adjustLineIndent(Document document, int offset) {
+    return delegate.adjustLineIndent(document, offset);
+  }
+
+  public void scheduleIndentAdjustment(Document document, int offset) {
+    delegate.scheduleIndentAdjustment(document, offset);
+  }
+
+  @Override
+  public boolean isLineToBeIndented(PsiFile file, int offset) {
+    return delegate.isLineToBeIndented(file, offset);
+  }
+
+  @Override
+  @Nullable
+  public String getLineIndent(PsiFile file, int offset) {
+    return delegate.getLineIndent(file, offset);
+  }
+
+  @Override
+  @Nullable
+  public String getLineIndent(PsiFile file, int offset, FormattingMode mode) {
+    return delegate.getLineIndent(file, offset, mode);
+  }
+
+  @Override
+  @Nullable
+  public String getLineIndent(Document document, int offset) {
+    return delegate.getLineIndent(document, offset);
+  }
+
+  @Override
+  public Indent getIndent(String text, FileType fileType) {
+    return delegate.getIndent(text, fileType);
+  }
+
+  @Override
+  public String fillIndent(Indent indent, FileType fileType) {
+    return delegate.fillIndent(indent, fileType);
+  }
+
+  @Override
+  public Indent zeroIndent() {
+    return delegate.zeroIndent();
+  }
+
+  @Override
+  public void reformatNewlyAddedElement(ASTNode block, ASTNode addedElement)
+      throws IncorrectOperationException {
+    delegate.reformatNewlyAddedElement(block, addedElement);
+  }
+
+  @Override
+  public boolean isSequentialProcessingAllowed() {
+    return delegate.isSequentialProcessingAllowed();
+  }
+
+  @Override
+  public void performActionWithFormatterDisabled(Runnable r) {
+    delegate.performActionWithFormatterDisabled(r);
+  }
+
+  @Override
+  public <T extends Throwable> void performActionWithFormatterDisabled(ThrowableRunnable<T> r)
+      throws T {
+    delegate.performActionWithFormatterDisabled(r);
+  }
+
+  @Override
+  public <T> T performActionWithFormatterDisabled(Computable<T> r) {
+    return delegate.performActionWithFormatterDisabled(r);
+  }
+
+  @Override
+  public int getSpacing(PsiFile file, int offset) {
+    return delegate.getSpacing(file, offset);
+  }
+
+  @Override
+  public int getMinLineFeeds(PsiFile file, int offset) {
+    return delegate.getMinLineFeeds(file, offset);
+  }
+
+  @Override
+  public void runWithDocCommentFormattingDisabled(PsiFile file, Runnable runnable) {
+    delegate.runWithDocCommentFormattingDisabled(file, runnable);
+  }
+
+  @Override
+  public DocCommentSettings getDocCommentSettings(PsiFile file) {
+    return delegate.getDocCommentSettings(file);
+  }
+
+  // From FormattingModeAwareIndentAdjuster
+
+  /** Uses same fallback as {@link CodeStyleManager#getCurrentFormattingMode}. */
+  @Override
+  public FormattingMode getCurrentFormattingMode() {
+    if (delegate instanceof FormattingModeAwareIndentAdjuster) {
+      return ((FormattingModeAwareIndentAdjuster) delegate).getCurrentFormattingMode();
+    }
+    return FormattingMode.REFORMAT;
+  }
+
+  @Override
+  public int adjustLineIndent(final Document document, final int offset, FormattingMode mode)
+      throws IncorrectOperationException {
+    if (delegate instanceof FormattingModeAwareIndentAdjuster) {
+      return ((FormattingModeAwareIndentAdjuster) delegate)
+          .adjustLineIndent(document, offset, mode);
+    }
+    return offset;
+  }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java b/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java
new file mode 100644
index 0000000..b6e21f7
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.intellij;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.BoundType;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Range;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.FormatterException;
+import com.intellij.openapi.util.TextRange;
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+final class FormatterUtil {
+
+  private FormatterUtil() {}
+
+  static Map<TextRange, String> getReplacements(
+      Formatter formatter, String text, Collection<TextRange> ranges) {
+    try {
+      ImmutableMap.Builder<TextRange, String> replacements = ImmutableMap.builder();
+      formatter
+          .getFormatReplacements(text, toRanges(ranges))
+          .forEach(
+              replacement ->
+                  replacements.put(
+                      toTextRange(replacement.getReplaceRange()),
+                      replacement.getReplacementString()));
+      return replacements.build();
+    } catch (FormatterException e) {
+      return ImmutableMap.of();
+    }
+  }
+
+  private static Collection<Range<Integer>> toRanges(Collection<TextRange> textRanges) {
+    return textRanges
+        .stream()
+        .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset()))
+        .collect(Collectors.toList());
+  }
+
+  private static TextRange toTextRange(Range<Integer> range) {
+    checkState(
+        range.lowerBoundType().equals(BoundType.CLOSED)
+            && range.upperBoundType().equals(BoundType.OPEN));
+    return new TextRange(range.lowerEndpoint(), range.upperEndpoint());
+  }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java
new file mode 100644
index 0000000..52424c2
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.intellij;
+
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.JavaFormatterOptions;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileTypes.StdFileTypes;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.impl.CheckUtil;
+import com.intellij.util.IncorrectOperationException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A {@link CodeStyleManager} implementation which formats .java files with google-java-format.
+ * Formatting of all other types of files is delegated to IJ's default implementation.
+ */
+class GoogleJavaFormatCodeStyleManager extends CodeStyleManagerDecorator {
+
+  public GoogleJavaFormatCodeStyleManager(@NotNull CodeStyleManager original) {
+    super(original);
+  }
+
+  @Override
+  public void reformatText(PsiFile file, int startOffset, int endOffset)
+      throws IncorrectOperationException {
+    if (overrideFormatterForFile(file)) {
+      formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset)));
+    } else {
+      super.reformatText(file, startOffset, endOffset);
+    }
+  }
+
+  @Override
+  public void reformatText(PsiFile file, Collection<TextRange> ranges)
+      throws IncorrectOperationException {
+    if (overrideFormatterForFile(file)) {
+      formatInternal(file, ranges);
+    } else {
+      super.reformatText(file, ranges);
+    }
+  }
+
+  @Override
+  public void reformatTextWithContext(PsiFile file, Collection<TextRange> ranges) {
+    if (overrideFormatterForFile(file)) {
+      formatInternal(file, ranges);
+    } else {
+      super.reformatTextWithContext(file, ranges);
+    }
+  }
+
+  @Override
+  public PsiElement reformatRange(
+      PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly) {
+    // Only handle elements that are PsiFile for now -- otherwise we need to search for some
+    // element within the file at new locations given the original startOffset and endOffsets
+    // to serve as the return value.
+    PsiFile file = element instanceof PsiFile ? (PsiFile) element : null;
+    if (file != null && canChangeWhiteSpacesOnly && overrideFormatterForFile(file)) {
+      formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset)));
+      return file;
+    } else {
+      return super.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly);
+    }
+  }
+
+  /** Return whether or not this formatter can handle formatting the given file. */
+  private boolean overrideFormatterForFile(PsiFile file) {
+    return StdFileTypes.JAVA.equals(file.getFileType())
+        && GoogleJavaFormatSettings.getInstance(getProject()).isEnabled();
+  }
+
+  private void formatInternal(PsiFile file, Collection<TextRange> ranges) {
+    ApplicationManager.getApplication().assertWriteAccessAllowed();
+    PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
+    documentManager.commitAllDocuments();
+    CheckUtil.checkWritable(file);
+
+    Document document = documentManager.getDocument(file);
+
+    if (document == null) {
+      return;
+    }
+    // If there are postponed PSI changes (e.g., during a refactoring), just abort.
+    // If we apply them now, then the incoming text ranges may no longer be valid.
+    if (documentManager.isDocumentBlockedByPsi(document)) {
+      return;
+    }
+
+    format(document, ranges);
+  }
+
+  /**
+   * Format the ranges of the given document.
+   *
+   * <p>Overriding methods will need to modify the document with the result of the external
+   * formatter (usually using {@link #performReplacements(Document, Map)}.
+   */
+  private void format(Document document, Collection<TextRange> ranges) {
+    Style style = GoogleJavaFormatSettings.getInstance(getProject()).getStyle();
+    Formatter formatter = new Formatter(JavaFormatterOptions.builder().style(style).build());
+    performReplacements(
+        document, FormatterUtil.getReplacements(formatter, document.getText(), ranges));
+  }
+
+  private void performReplacements(
+      final Document document, final Map<TextRange, String> replacements) {
+
+    if (replacements.isEmpty()) {
+      return;
+    }
+
+    TreeMap<TextRange, String> sorted = new TreeMap<>(comparing(TextRange::getStartOffset));
+    sorted.putAll(replacements);
+    WriteCommandAction.runWriteCommandAction(
+        getProject(),
+        () -> {
+          for (Entry<TextRange, String> entry : sorted.descendingMap().entrySet()) {
+            document.replaceString(
+                entry.getKey().getStartOffset(), entry.getKey().getEndOffset(), entry.getValue());
+          }
+          PsiDocumentManager.getInstance(getProject()).commitDocument(document);
+        });
+  }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form
new file mode 100644
index 0000000..1db1d79
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.google.googlejavaformat.intellij.GoogleJavaFormatConfigurable">
+  <grid id="27dc6" binding="panel" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+    <margin top="0" left="0" bottom="0" right="0"/>
+    <constraints>
+      <xy x="20" y="20" width="500" height="400"/>
+    </constraints>
+    <properties/>
+    <border type="none"/>
+    <children>
+      <component id="4a87f" class="javax.swing.JCheckBox" binding="enable" default-binding="true">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Enable google-java-format"/>
+        </properties>
+      </component>
+      <vspacer id="19e83">
+        <constraints>
+          <grid row="2" column="0" row-span="1" col-span="2" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+        </constraints>
+      </vspacer>
+      <component id="c93e1" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Code style"/>
+        </properties>
+      </component>
+      <component id="31761" class="javax.swing.JComboBox" binding="styleComboBox" custom-create="true">
+        <constraints>
+          <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="1" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+      </component>
+    </children>
+  </grid>
+</form>
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java
new file mode 100644
index 0000000..2f34b74
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.intellij;
+
+import com.google.googlejavaformat.intellij.GoogleJavaFormatSettings.EnabledState;
+import com.intellij.openapi.options.BaseConfigurable;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.options.SearchableConfigurable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.uiDesigner.core.GridConstraints;
+import com.intellij.uiDesigner.core.GridLayoutManager;
+import com.intellij.uiDesigner.core.Spacer;
+import java.awt.Insets;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+
+class GoogleJavaFormatConfigurable extends BaseConfigurable implements SearchableConfigurable {
+
+  private final Project project;
+  private JPanel panel;
+  private JCheckBox enable;
+  private JComboBox styleComboBox;
+
+  public GoogleJavaFormatConfigurable(Project project) {
+    this.project = project;
+  }
+
+  @NotNull
+  @Override
+  public String getId() {
+    return "google-java-format.settings";
+  }
+
+  @Nullable
+  @Override
+  public Runnable enableSearch(String option) {
+    return null;
+  }
+
+  @Nls
+  @Override
+  public String getDisplayName() {
+    return "google-java-format Settings";
+  }
+
+  @Nullable
+  @Override
+  public String getHelpTopic() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public JComponent createComponent() {
+    return panel;
+  }
+
+  @Override
+  public void apply() throws ConfigurationException {
+    GoogleJavaFormatSettings settings = GoogleJavaFormatSettings.getInstance(project);
+    settings.setEnabled(enable.isSelected() ? EnabledState.ENABLED : getDisabledState());
+    settings.setStyle(((UiFormatterStyle) styleComboBox.getSelectedItem()).convert());
+  }
+
+  private EnabledState getDisabledState() {
+    // The default settings (inherited by new projects) are either 'enabled' or
+    // 'show notification'. There's no way to default new projects to disabled. If someone wants
+    // that, we can add another checkbox, I suppose.
+    return project.isDefault() ? EnabledState.UNKNOWN : EnabledState.DISABLED;
+  }
+
+  @Override
+  public void reset() {
+    GoogleJavaFormatSettings settings = GoogleJavaFormatSettings.getInstance(project);
+    enable.setSelected(settings.isEnabled());
+    styleComboBox.setSelectedItem(UiFormatterStyle.convert(settings.getStyle()));
+  }
+
+  @Override
+  public boolean isModified() {
+    GoogleJavaFormatSettings settings = GoogleJavaFormatSettings.getInstance(project);
+    return enable.isSelected() != settings.isEnabled()
+        || !styleComboBox.getSelectedItem().equals(UiFormatterStyle.convert(settings.getStyle()));
+  }
+
+  @Override
+  public void disposeUIResources() {}
+
+  private void createUIComponents() {
+    styleComboBox = new ComboBox<>(UiFormatterStyle.values());
+  }
+
+  {
+    // GUI initializer generated by IntelliJ IDEA GUI Designer
+    // >>> IMPORTANT!! <<<
+    // DO NOT EDIT OR ADD ANY CODE HERE!
+    $$$setupUI$$$();
+  }
+
+  /**
+   * Method generated by IntelliJ IDEA GUI Designer >>> IMPORTANT!! <<< DO NOT edit this method OR
+   * call it in your code!
+   *
+   * @noinspection ALL
+   */
+  private void $$$setupUI$$$() {
+    createUIComponents();
+    panel = new JPanel();
+    panel.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1));
+    enable = new JCheckBox();
+    enable.setText("Enable google-java-format");
+    panel.add(
+        enable,
+        new GridConstraints(
+            0,
+            0,
+            1,
+            2,
+            GridConstraints.ANCHOR_WEST,
+            GridConstraints.FILL_NONE,
+            GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+            GridConstraints.SIZEPOLICY_FIXED,
+            null,
+            null,
+            null,
+            0,
+            false));
+    final Spacer spacer1 = new Spacer();
+    panel.add(
+        spacer1,
+        new GridConstraints(
+            2,
+            0,
+            1,
+            2,
+            GridConstraints.ANCHOR_CENTER,
+            GridConstraints.FILL_VERTICAL,
+            1,
+            GridConstraints.SIZEPOLICY_WANT_GROW,
+            null,
+            null,
+            null,
+            0,
+            false));
+    final JLabel label1 = new JLabel();
+    label1.setText("Code style");
+    panel.add(
+        label1,
+        new GridConstraints(
+            1,
+            0,
+            1,
+            1,
+            GridConstraints.ANCHOR_WEST,
+            GridConstraints.FILL_NONE,
+            GridConstraints.SIZEPOLICY_FIXED,
+            GridConstraints.SIZEPOLICY_FIXED,
+            null,
+            null,
+            null,
+            0,
+            false));
+    panel.add(
+        styleComboBox,
+        new GridConstraints(
+            1,
+            1,
+            1,
+            1,
+            GridConstraints.ANCHOR_WEST,
+            GridConstraints.FILL_HORIZONTAL,
+            GridConstraints.SIZEPOLICY_CAN_GROW,
+            GridConstraints.SIZEPOLICY_FIXED,
+            null,
+            null,
+            null,
+            1,
+            false));
+  }
+
+  /** @noinspection ALL */
+  public JComponent $$$getRootComponent$$$() {
+    return panel;
+  }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java
new file mode 100644
index 0000000..c606736
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.intellij;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManagerCore;
+import com.intellij.openapi.extensions.PluginId;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManagerListener;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.serviceContainer.ComponentManagerImpl;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A component that replaces the default IntelliJ {@link CodeStyleManager} with one that formats via
+ * google-java-format.
+ */
+final class GoogleJavaFormatInstaller implements ProjectManagerListener {
+
+  @Override
+  public void projectOpened(@NotNull Project project) {
+    installFormatter(project);
+  }
+
+  private static void installFormatter(Project project) {
+    CodeStyleManager currentManager = CodeStyleManager.getInstance(project);
+
+    if (currentManager instanceof GoogleJavaFormatCodeStyleManager) {
+      currentManager = ((GoogleJavaFormatCodeStyleManager) currentManager).getDelegate();
+    }
+
+    setManager(project, new GoogleJavaFormatCodeStyleManager(currentManager));
+  }
+
+  private static void setManager(Project project, CodeStyleManager newManager) {
+    ComponentManagerImpl platformComponentManager = (ComponentManagerImpl) project;
+    IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PluginId.getId("google-java-format"));
+    checkState(plugin != null, "Couldn't locate our own PluginDescriptor.");
+    platformComponentManager.registerServiceInstance(CodeStyleManager.class, newManager, plugin);
+  }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
new file mode 100644
index 0000000..f6d9b5f
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.intellij;
+
+import com.google.googlejavaformat.java.JavaFormatterOptions;
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.openapi.project.Project;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+@State(
+    name = "GoogleJavaFormatSettings",
+    storages = {@Storage("google-java-format.xml")})
+class GoogleJavaFormatSettings implements PersistentStateComponent<GoogleJavaFormatSettings.State> {
+
+  private State state = new State();
+
+  static GoogleJavaFormatSettings getInstance(Project project) {
+    return ServiceManager.getService(project, GoogleJavaFormatSettings.class);
+  }
+
+  @Nullable
+  @Override
+  public State getState() {
+    return state;
+  }
+
+  @Override
+  public void loadState(State state) {
+    this.state = state;
+  }
+
+  boolean isEnabled() {
+    return state.enabled.equals(EnabledState.ENABLED);
+  }
+
+  void setEnabled(boolean enabled) {
+    setEnabled(enabled ? EnabledState.ENABLED : EnabledState.DISABLED);
+  }
+
+  void setEnabled(EnabledState enabled) {
+    state.enabled = enabled;
+  }
+
+  boolean isUninitialized() {
+    return state.enabled.equals(EnabledState.UNKNOWN);
+  }
+
+  JavaFormatterOptions.Style getStyle() {
+    return state.style;
+  }
+
+  void setStyle(JavaFormatterOptions.Style style) {
+    state.style = style;
+  }
+
+  enum EnabledState {
+    UNKNOWN,
+    ENABLED,
+    DISABLED;
+  }
+
+  static class State {
+
+    private EnabledState enabled = EnabledState.UNKNOWN;
+    public JavaFormatterOptions.Style style = JavaFormatterOptions.Style.GOOGLE;
+
+    // enabled used to be a boolean so we use bean property methods for backwards compatibility
+    public void setEnabled(@Nullable String enabledStr) {
+      if (enabledStr == null) {
+        enabled = EnabledState.UNKNOWN;
+      } else if (Boolean.valueOf(enabledStr)) {
+        enabled = EnabledState.ENABLED;
+      } else {
+        enabled = EnabledState.DISABLED;
+      }
+    }
+
+    public String getEnabled() {
+      switch (enabled) {
+        case ENABLED:
+          return "true";
+        case DISABLED:
+          return "false";
+        default:
+          return null;
+      }
+    }
+  }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java b/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java
new file mode 100644
index 0000000..da02310
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.intellij;
+
+import com.intellij.notification.Notification;
+import com.intellij.notification.NotificationDisplayType;
+import com.intellij.notification.NotificationGroup;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManagerListener;
+import org.jetbrains.annotations.NotNull;
+
+final class InitialConfigurationProjectManagerListener implements ProjectManagerListener {
+
+  private static final String NOTIFICATION_TITLE = "Enable google-java-format";
+  private static final NotificationGroup NOTIFICATION_GROUP =
+      new NotificationGroup(NOTIFICATION_TITLE, NotificationDisplayType.STICKY_BALLOON, true);
+
+  @Override
+  public void projectOpened(@NotNull Project project) {
+
+    GoogleJavaFormatSettings settings = GoogleJavaFormatSettings.getInstance(project);
+
+    if (settings.isUninitialized()) {
+      settings.setEnabled(false);
+      displayNewUserNotification(project, settings);
+    }
+  }
+
+  private void displayNewUserNotification(Project project, GoogleJavaFormatSettings settings) {
+    Notification notification =
+        new Notification(
+            NOTIFICATION_GROUP.getDisplayId(),
+            NOTIFICATION_TITLE,
+            "The google-java-format plugin is disabled by default. "
+                + "<a href=\"enable\">Enable for this project</a>.",
+            NotificationType.INFORMATION,
+            (n, e) -> {
+              settings.setEnabled(true);
+              n.expire();
+            });
+    notification.notify(project);
+  }
+}
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/UiFormatterStyle.java b/idea_plugin/src/com/google/googlejavaformat/intellij/UiFormatterStyle.java
new file mode 100644
index 0000000..24ed6f6
--- /dev/null
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/UiFormatterStyle.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.googlejavaformat.intellij;
+
+import com.google.googlejavaformat.java.JavaFormatterOptions;
+import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import java.util.Arrays;
+import java.util.Objects;
+
+/** Configuration options for the formatting style. */
+enum UiFormatterStyle {
+  GOOGLE("Default Google Java style", Style.GOOGLE),
+  AOSP("Android Open Source Project (AOSP) style", Style.AOSP);
+
+  private final String description;
+  private final JavaFormatterOptions.Style style;
+
+  UiFormatterStyle(String description, JavaFormatterOptions.Style style) {
+    this.description = description;
+    this.style = style;
+  }
+
+  @Override
+  public String toString() {
+    return description;
+  }
+
+  public JavaFormatterOptions.Style convert() {
+    return style;
+  }
+
+  static UiFormatterStyle convert(JavaFormatterOptions.Style style) {
+    return Arrays.stream(UiFormatterStyle.values())
+        .filter(value -> Objects.equals(value.style, style))
+        .findFirst()
+        .get();
+  }
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..2ece22d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,253 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Google Inc.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.sonatype.oss</groupId>
+    <artifactId>oss-parent</artifactId>
+    <version>7</version>
+  </parent>
+
+  <groupId>com.google.googlejavaformat</groupId>
+  <artifactId>google-java-format-parent</artifactId>
+  <packaging>pom</packaging>
+  <version>1.8-SNAPSHOT</version>
+
+  <modules>
+    <module>core</module>
+    <!-- google-java-format#24
+    <module>idea_plugin</module>
+    <module>eclipse_plugin</module>
+    -->
+  </modules>
+
+  <name>Google Java Format Parent</name>
+
+  <description>
+    A Java source code formatter that follows Google Java Style.
+  </description>
+
+  <url>https://github.com/google/google-java-format</url>
+
+  <inceptionYear>2015</inceptionYear>
+
+  <organization>
+    <name>Google Inc.</name>
+    <url>http://www.google.com/</url>
+  </organization>
+
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <developers>
+    <developer>
+      <id>jdd</id>
+      <name>John DeTreville</name>
+      <email>jdd@google.com</email>
+      <organization>Google Inc.</organization>
+      <organizationUrl>http://www.google.com/</organizationUrl>
+      <roles>
+        <role>owner</role>
+        <role>developer</role>
+      </roles>
+      <timezone>-8</timezone>
+    </developer>
+  </developers>
+
+  <scm>
+    <url>http://github.com/google/google-java-format/</url>
+    <connection>scm:git:git://github.com/google/google-java-format.git</connection>
+    <developerConnection>scm:git:ssh://git@github.com/google/google-java-format.git</developerConnection>
+    <tag>HEAD</tag>
+  </scm>
+
+  <issueManagement>
+    <system>GitHub Issues</system>
+    <url>http://github.com/google/google-java-format/issues</url>
+  </issueManagement>
+
+  <prerequisites>
+    <maven>3.0.3</maven>
+  </prerequisites>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <java.version>1.8</java.version>
+    <guava.version>28.1-jre</guava.version>
+    <truth.version>1.0</truth.version>
+    <checker.version>2.0.0</checker.version>
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <!-- Required runtime dependencies -->
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>${guava.version}</version>
+      </dependency>
+
+      <!-- Compile-time dependencies -->
+      <dependency>
+        <groupId>org.checkerframework</groupId>
+        <artifactId>checker-qual</artifactId>
+        <version>${checker.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.google.errorprone</groupId>
+        <artifactId>error_prone_annotations</artifactId>
+        <version>2.0.8</version>
+      </dependency>
+
+      <!-- Test dependencies -->
+      <dependency>
+        <groupId>junit</groupId>
+        <artifactId>junit</artifactId>
+        <version>4.12</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava-testlib</artifactId>
+        <version>${guava.version}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>com.google.truth</groupId>
+        <artifactId>truth</artifactId>
+        <version>${truth.version}</version>
+        <scope>test</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.7.0</version>
+        </plugin>
+        <plugin>
+          <artifactId>maven-jar-plugin</artifactId>
+          <version>3.0.2</version>
+        </plugin>
+        <plugin>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>2.1.2</version>
+        </plugin>
+        <plugin>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>3.2.0</version>
+        </plugin>
+        <plugin>
+          <artifactId>maven-gpg-plugin</artifactId>
+          <version>1.4</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.felix</groupId>
+          <artifactId>maven-bundle-plugin</artifactId>
+          <version>2.4.0</version>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>${java.version}</source>
+          <target>${java.version}</target>
+          <compilerArgs>
+            <arg>-XDcompilePolicy=simple</arg>
+            <arg>-Xplugin:ErrorProne</arg>
+            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
+            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
+            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
+            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
+            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
+            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
+            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
+          </compilerArgs>
+          <annotationProcessorPaths>
+            <path>
+              <groupId>com.google.errorprone</groupId>
+              <artifactId>error_prone_core</artifactId>
+              <version>2.3.2</version>
+            </path>
+          </annotationProcessorPaths>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifest>
+              <mainClass>com.google.googlejavaformat.java.Main</mainClass>
+            </manifest>
+          </archive>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-sources</id>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <version>3.2.0</version>
+        <configuration>
+          <doclint>none</doclint>
+        </configuration>
+        <executions>
+          <execution>
+          <id>attach-javadocs</id>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>2.18</version>
+        <configuration>
+          <!-- set heap size to work around http://github.com/travis-ci/travis-ci/issues/3396 -->
+          <argLine>-Xmx1024m</argLine>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/scripts/google-java-format-diff.py b/scripts/google-java-format-diff.py
new file mode 100755
index 0000000..4c6724b
--- /dev/null
+++ b/scripts/google-java-format-diff.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python2.7
+#
+#===- google-java-format-diff.py - google-java-format Diff Reformatter -----===#
+#
+#                     The LLVM Compiler Infrastructure
+#
+# This file is distributed under the University of Illinois Open Source
+# License. See LICENSE.TXT for details.
+#
+#===------------------------------------------------------------------------===#
+
+"""
+google-java-format Diff Reformatter
+============================
+
+This script reads input from a unified diff and reformats all the changed
+lines. This is useful to reformat all the lines touched by a specific patch.
+Example usage for git/svn users:
+
+  git diff -U0 HEAD^ | google-java-format-diff.py -p1 -i
+  svn diff --diff-cmd=diff -x-U0 | google-java-format-diff.py -i
+
+For perforce users:
+
+  P4DIFF="git --no-pager diff --no-index" p4 diff | ./google-java-format-diff.py -i -p7
+
+"""
+
+import argparse
+import difflib
+import re
+import string
+import subprocess
+import StringIO
+import sys
+from distutils.spawn import find_executable
+
+def main():
+  parser = argparse.ArgumentParser(description=
+                                   'Reformat changed lines in diff. Without -i '
+                                   'option just output the diff that would be '
+                                   'introduced.')
+  parser.add_argument('-i', action='store_true', default=False,
+                      help='apply edits to files instead of displaying a diff')
+
+  parser.add_argument('-p', metavar='NUM', default=0,
+                      help='strip the smallest prefix containing P slashes')
+  parser.add_argument('-regex', metavar='PATTERN', default=None,
+                      help='custom pattern selecting file paths to reformat '
+                      '(case sensitive, overrides -iregex)')
+  parser.add_argument('-iregex', metavar='PATTERN', default=r'.*\.java',
+                      help='custom pattern selecting file paths to reformat '
+                      '(case insensitive, overridden by -regex)')
+  parser.add_argument('-v', '--verbose', action='store_true',
+                      help='be more verbose, ineffective without -i')
+  parser.add_argument('-a', '--aosp', action='store_true',
+                      help='use AOSP style instead of Google Style (4-space indentation)')
+  parser.add_argument('--skip-sorting-imports', action='store_true',
+                      help='do not fix the import order')
+  parser.add_argument('-b', '--binary', help='path to google-java-format binary')
+  parser.add_argument('--google-java-format-jar', metavar='ABSOLUTE_PATH', default=None,
+                      help='use a custom google-java-format jar')
+
+  args = parser.parse_args()
+
+  # Extract changed lines for each file.
+  filename = None
+  lines_by_file = {}
+
+  for line in sys.stdin:
+    match = re.search('^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line)
+    if match:
+      filename = match.group(2)
+    if filename == None:
+      continue
+
+    if args.regex is not None:
+      if not re.match('^%s$' % args.regex, filename):
+        continue
+    else:
+      if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
+        continue
+
+    match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
+    if match:
+      start_line = int(match.group(1))
+      line_count = 1
+      if match.group(3):
+        line_count = int(match.group(3))
+      if line_count == 0:
+        continue
+      end_line = start_line + line_count - 1;
+      lines_by_file.setdefault(filename, []).extend(
+          ['-lines', str(start_line) + ':' + str(end_line)])
+
+  if args.binary:
+    base_command = [args.binary]
+  elif args.google_java_format_jar:
+    base_command = ['java', '-jar', args.google_java_format_jar]
+  else:
+    binary = find_executable('google-java-format') or '/usr/bin/google-java-format'
+    base_command = [binary]
+
+  # Reformat files containing changes in place.
+  for filename, lines in lines_by_file.iteritems():
+    if args.i and args.verbose:
+      print 'Formatting', filename
+    command = base_command[:]
+    if args.i:
+      command.append('-i')
+    if args.aosp:
+      command.append('--aosp')
+    if args.skip_sorting_imports:
+      command.append('--skip-sorting-imports')
+    command.extend(lines)
+    command.append(filename)
+    p = subprocess.Popen(command, stdout=subprocess.PIPE,
+                         stderr=None, stdin=subprocess.PIPE)
+    stdout, stderr = p.communicate()
+    if p.returncode != 0:
+      sys.exit(p.returncode);
+
+    if not args.i:
+      with open(filename) as f:
+        code = f.readlines()
+      formatted_code = StringIO.StringIO(stdout).readlines()
+      diff = difflib.unified_diff(code, formatted_code,
+                                  filename, filename,
+                                  '(before formatting)', '(after formatting)')
+      diff_string = string.join(diff, '')
+      if len(diff_string) > 0:
+        sys.stdout.write(diff_string)
+
+if __name__ == '__main__':
+  main()
diff --git a/scripts/mvn-deploy.sh b/scripts/mvn-deploy.sh
new file mode 100755
index 0000000..f6db093
--- /dev/null
+++ b/scripts/mvn-deploy.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+if [ $# -lt 1 ]; then
+  echo "usage $0 <ssl-key> [<param> ...]"
+  exit 1;
+fi
+key=${1}
+shift
+params=${@}
+
+#validate key
+keystatus=$(gpg --list-keys | grep ${key} | awk '{print $1}')
+if [ "${keystatus}" != "pub" ]; then
+  echo "Could not find public key with label ${key}"
+  echo -n "Available keys from: "
+  gpg --list-keys | grep --invert-match '^sub'
+
+  exit 1
+fi
+
+mvn ${params} -P sonatype-oss-release -Dgpg.keyname=${key} deploy
diff --git a/util/publish-snapshot-on-commit.sh b/util/publish-snapshot-on-commit.sh
new file mode 100755
index 0000000..6da8da4
--- /dev/null
+++ b/util/publish-snapshot-on-commit.sh
@@ -0,0 +1,12 @@
+# https://github.com/google/dagger/blob/master/util/publish-snapshot-on-commit.sh
+
+if [ "$TRAVIS_REPO_SLUG" == "google/google-java-format" ] && \
+   [ "$TRAVIS_JDK_VERSION" == "oraclejdk8" ] && \
+   [ "$TRAVIS_PULL_REQUEST" == "false" ] && \
+   [ "$TRAVIS_BRANCH" == "master" ]; then
+  echo -e "Publishing maven snapshot...\n"
+
+  mvn clean source:jar deploy --settings="util/settings.xml" -DskipTests=true -Dinvoker.skip=true -Dmaven.javadoc.skip=true
+
+  echo -e "Published maven snapshot"
+fi
diff --git a/util/settings.xml b/util/settings.xml
new file mode 100644
index 0000000..91f444b
--- /dev/null
+++ b/util/settings.xml
@@ -0,0 +1,9 @@
+<settings>
+  <servers>
+    <server>
+      <id>sonatype-nexus-snapshots</id>
+      <username>${env.CI_DEPLOY_USERNAME}</username>
+      <password>${env.CI_DEPLOY_PASSWORD}</password>
+    </server>
+  </servers>
+</settings>