Snap for 8564071 from 4e47fd3e7ddc9c0468fb64803280c6f25e42c5a0 to mainline-os-statsd-release

Change-Id: I812e2ed23da224d846650fb7c9143d0a3b8cbed2
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..daec318
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+  - package-ecosystem: "maven"
+    directory: "/"
+    schedule:
+      interval: "daily"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..016b8e7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,89 @@
+# Copyright 2020 The Error Prone Authors.
+#
+# 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.
+
+name: CI
+
+on:
+  push:
+    branches:
+    - master
+  pull_request:
+    branches:
+    - master
+
+jobs:
+  test:
+    name: "JDK ${{ matrix.java }} on ${{ matrix.os }}"
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ ubuntu-latest ]
+        java: [ 17, 11 ]
+        experimental: [ false ]
+        include:
+          # Only test on macos and windows with a single recent JDK to avoid a
+          # combinatorial explosion of test configurations.
+          - os: macos-latest
+            java: 17
+            experimental: false
+          - os: windows-latest
+            java: 17
+            experimental: false
+          - os: ubuntu-latest
+            java: 18-ea
+            experimental: true
+    runs-on: ${{ matrix.os }}
+    continue-on-error: ${{ matrix.experimental }}
+    steps:
+      - name: Cancel previous
+        uses: styfle/cancel-workflow-action@0.9.1
+        with:
+          access_token: ${{ github.token }}
+      - name: 'Check out repository'
+        uses: actions/checkout@v2
+      - name: 'Set up JDK ${{ matrix.java }}'
+        uses: actions/setup-java@v2
+        with:
+          java-version: ${{ matrix.java }}
+          distribution: 'zulu'
+          cache: 'maven'
+      - name: 'Install'
+        shell: bash
+        run: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
+      - name: 'Test'
+        shell: bash
+        run: mvn test -B
+
+  publish_snapshot:
+    name: 'Publish snapshot'
+    needs: test
+    if: github.event_name == 'push' && github.repository == 'google/google-java-format' && github.ref == 'refs/heads/master'
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Check out repository'
+        uses: actions/checkout@v2
+      - name: 'Set up JDK 17'
+        uses: actions/setup-java@v2
+        with:
+          java-version: 17
+          distribution: 'zulu'
+          cache: 'maven'
+          server-id: sonatype-nexus-snapshots
+          server-username: CI_DEPLOY_USERNAME
+          server-password: CI_DEPLOY_PASSWORD
+      - name: 'Publish'
+        env:
+          CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }}
+          CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }}
+        run: mvn -pl '!eclipse_plugin' source:jar deploy -B -DskipTests=true -Dinvoker.skip=true -Dmaven.javadoc.skip=true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..a3f066d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,74 @@
+name: Release google-java-format
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "version number for this release."
+        required: true
+
+jobs:
+  build-maven-jars:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+    steps:
+      - name: Setup Signing Key
+        run: |
+          gpg-agent --daemon --default-cache-ttl 7200
+          echo -e "${{ secrets.GPG_SIGNING_KEY }}" | gpg --batch --import --no-tty
+          echo "hello world" > temp.txt
+          gpg --detach-sig --yes -v --output=/dev/null --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" temp.txt
+          rm temp.txt
+          gpg --list-secret-keys --keyid-format LONG
+          
+      - name: Checkout
+        uses: actions/checkout@v2.4.0
+
+      - name: Set up JDK
+        uses: actions/setup-java@v2.5.0
+        with:
+          java-version: 17
+          distribution: 'zulu'
+          cache: 'maven'
+          server-id: sonatype-nexus-staging
+          server-username: CI_DEPLOY_USERNAME
+          server-password: CI_DEPLOY_PASSWORD
+     
+      - name: Bump Version Number
+        run: |
+          mvn --no-transfer-progress versions:set versions:commit -DnewVersion="${{ github.event.inputs.version }}"
+          mvn tycho-versions:update-eclipse-metadata -pl eclipse_plugin
+          git ls-files | grep 'pom.xml$' | xargs git add
+          git config --global user.email "${{ github.actor }}@users.noreply.github.com"
+          git config --global user.name "${{ github.actor }}"
+          git commit -m "Release google-java-format ${{ github.event.inputs.version }}"
+          git tag "v${{ github.event.inputs.version }}"
+          echo "TARGET_COMMITISH=$(git rev-parse HEAD)" >> $GITHUB_ENV
+          git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/google/google-java-format.git
+          
+      - name: Deploy to Sonatype staging
+        env:
+          CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }}
+          CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }}
+        run:
+          mvn --no-transfer-progress -pl '!eclipse_plugin' -P sonatype-oss-release clean deploy -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}"
+
+      - name: Build Eclipse plugin
+        run:
+          mvn --no-transfer-progress -pl 'eclipse_plugin' verify gpg:sign -DskipTests=true -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}"
+
+      - name: Push tag
+        run: |
+          git push origin "v${{ github.event.inputs.version }}"
+          
+      - name: Add Jars to Release Entry
+        uses: softprops/action-gh-release@v0.1.14
+        with:
+          draft: true
+          name: ${{ github.event.input.version }} 
+          tag_name: "v${{ github.event.inputs.version }}"
+          target_commitish: ${{ env.TARGET_COMMITISH }}
+          files: |
+            core/target/google-java-format-*.jar
+            eclipse_plugin/target/google-java-format-eclipse-plugin-*.jar
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 0000000..504456f
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1,10 @@
+--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
+--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b8a7c33..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-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/Android.bp b/Android.bp
index 7ef50b0..8302441 100644
--- a/Android.bp
+++ b/Android.bp
@@ -49,31 +49,19 @@
     exclude_srcs: [
         ":google_java_format_main_srcs",
         "core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java",
+        "core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProvider.java",
     ],
     libs: [
         "error_prone_annotations",
         "google_java_format_android_annotation_stubs",
         "guava",
+        "auto_service_annotations",
+        "auto_value_annotations",
     ],
-    javacflags: [
-        "--add-modules=jdk.compiler",
-        "--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
-        "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
-        "--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
-        "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
-        "--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
-        "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
-        "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
-    ],
-}
-
-java_library_host {
-    name: "google_java_format_filer",
-    srcs: ["core/src/main/java/filer/*.java"],
-    libs: [
-        "error_prone_annotations",
-        "google_java_format_android_annotation_stubs",
-        "guava",
+    plugins: [
+        "auto_oneof_plugin",
+        "auto_service_plugin",
+        "auto_value_plugin",
     ],
     javacflags: [
         "--add-modules=jdk.compiler",
diff --git a/README.md b/README.md
index 9ce3998..5d1d7c7 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
 and run it with:
 
 ```
-java -jar /path/to/google-java-format-1.8-all-deps.jar <options> [files...]
+java -jar /path/to/google-java-format-${GJF_VERSION?}-all-deps.jar <options> [files...]
 ```
 
 The formatter can act on whole files, on limited lines (`--lines`), on specific
@@ -27,6 +27,21 @@
 formatting. This is a deliberate design decision to unify our code formatting on
 a single format.*
 
+#### JDK 16
+
+The following flags are required when running on JDK 16, due to
+[JEP 396: Strongly Encapsulate JDK Internals by Default](https://openjdk.java.net/jeps/396):
+
+```
+java \
+  --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
+  --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
+  --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
+  --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
+  --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
+  -jar google-java-format-${GJF_VERSION?}-all-deps.jar <options> [files...]
+```
+
 ### IntelliJ, Android Studio, and other JetBrains IDEs
 
 A
@@ -55,9 +70,9 @@
 
 ### Eclipse
 
-Version 1.6 of the
-[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
+The latest version of the `google-java-format` Eclipse plugin can be downloaded
+from the [releases page](https://github.com/google/google-java-format/releases).
+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.
 
@@ -98,7 +113,7 @@
 <dependency>
   <groupId>com.google.googlejavaformat</groupId>
   <artifactId>google-java-format</artifactId>
-  <version>1.8</version>
+  <version>${google-java-format.version}</version>
 </dependency>
 ```
 
@@ -106,7 +121,7 @@
 
 ```groovy
 dependencies {
-  compile 'com.google.googlejavaformat:google-java-format:1.8'
+  implementation 'com.google.googlejavaformat:google-java-format:$googleJavaFormatVersion'
 }
 ```
 
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index ac535c9..0000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-# Don't build branches that are not PRs, to avoid double builds.
-branches:
-  only:
-    - master
-
-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
index e5eab9c..1d47159 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.googlejavaformat</groupId>
     <artifactId>google-java-format-parent</artifactId>
-    <version>1.9</version>
+  <version>HEAD-SNAPSHOT</version>
   </parent>
 
   <artifactId>google-java-format</artifactId>
@@ -51,7 +51,16 @@
       <artifactId>error_prone_annotations</artifactId>
       <optional>true</optional>
     </dependency>
-
+    <dependency>
+      <groupId>com.google.auto.value</groupId>
+      <artifactId>auto-value-annotations</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>com.google.auto.service</groupId>
+      <artifactId>auto-service-annotations</artifactId>
+      <optional>true</optional>
+    </dependency>
 
     <!-- Test dependencies -->
     <dependency>
@@ -67,9 +76,14 @@
       <artifactId>truth</artifactId>
     </dependency>
     <dependency>
+      <groupId>com.google.truth.extensions</groupId>
+      <artifactId>truth-java8-extension</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>com.google.testing.compile</groupId>
       <artifactId>compile-testing</artifactId>
-      <version>0.15</version>
+      <version>0.19</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
@@ -110,7 +124,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-shade-plugin</artifactId>
-        <version>2.4.3</version>
+        <version>3.2.4</version>
         <executions>
           <execution>
             <id>shade-all-deps</id>
@@ -157,51 +171,6 @@
         </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>
@@ -227,7 +196,7 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>build-helper-maven-plugin</artifactId>
-        <version>3.0.0</version>
+        <version>3.3.0</version>
         <executions>
           <execution>
             <id>add-source</id>
@@ -280,5 +249,45 @@
         </plugins>
       </build>
     </profile>
+    <profile>
+      <id>native</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.graalvm.buildtools</groupId>
+            <artifactId>native-maven-plugin</artifactId>
+            <version>0.9.9</version>
+            <extensions>true</extensions>
+            <executions>
+              <execution>
+                <id>build-native</id>
+                <goals>
+                  <goal>build</goal>
+                </goals>
+                <phase>package</phase>
+              </execution>
+              <execution>
+                <id>test-native</id>
+                <goals>
+                  <goal>test</goal>
+                </goals>
+                <phase>test</phase>
+              </execution>
+            </executions>
+            <configuration>
+              <imageName>google-java-format</imageName>
+              <classpath>
+                <param>${project.build.directory}/${project.artifactId}-${project.version}-all-deps.jar</param>
+              </classpath>
+              <buildArgs>
+                <buildArg>-H:IncludeResourceBundles=com.sun.tools.javac.resources.compiler</buildArg>
+                <buildArg>--no-fallback</buildArg>
+                <buildArg>--initialize-at-build-time=com.sun.tools.javac.file.Locations</buildArg>
+              </buildArgs>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
   </profiles>
 </project>
diff --git a/core/src/main/java/com/google/googlejavaformat/Doc.java b/core/src/main/java/com/google/googlejavaformat/Doc.java
index e663c96..35acca3 100644
--- a/core/src/main/java/com/google/googlejavaformat/Doc.java
+++ b/core/src/main/java/com/google/googlejavaformat/Doc.java
@@ -15,6 +15,7 @@
 package com.google.googlejavaformat;
 
 import static com.google.common.collect.Iterables.getLast;
+import static java.lang.Math.max;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.DiscreteDomain;
@@ -653,7 +654,7 @@
 
       if (broken) {
         this.broken = true;
-        this.newIndent = Math.max(lastIndent + plusIndent.eval(), 0);
+        this.newIndent = max(lastIndent + plusIndent.eval(), 0);
         return state.withColumn(newIndent);
       } else {
         this.broken = false;
diff --git a/core/src/main/java/com/google/googlejavaformat/Input.java b/core/src/main/java/com/google/googlejavaformat/Input.java
index 9e17c2b..66a3921 100644
--- a/core/src/main/java/com/google/googlejavaformat/Input.java
+++ b/core/src/main/java/com/google/googlejavaformat/Input.java
@@ -63,7 +63,7 @@
     /** Is the {@code Tok} a "//" comment? */
     boolean isSlashSlashComment();
 
-    /** Is the {@code Tok} a "//" comment? */
+    /** Is the {@code Tok} a "/*" comment? */
     boolean isSlashStarComment();
 
     /** Is the {@code Tok} a javadoc comment? */
@@ -114,14 +114,14 @@
   /**
    * Get the number of toks.
    *
-   * @return the number of toks, including the EOF tok
+   * @return the number of toks, excluding the EOF tok
    */
   public abstract int getkN();
 
   /**
    * Get the Token by index.
    *
-   * @param k the token index
+   * @param k the Tok index
    */
   public abstract Token getToken(int k);
 
diff --git a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java
index e8e100f..db431c0 100644
--- a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java
+++ b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java
@@ -14,7 +14,12 @@
 
 package com.google.googlejavaformat;
 
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -39,14 +44,14 @@
     int start = startToken.getTok().getPosition();
     for (Tok tok : startToken.getToksBefore()) {
       if (tok.isComment()) {
-        start = Math.min(start, tok.getPosition());
+        start = 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());
+        end = max(end, tok.getPosition() + tok.length());
       }
     }
     return end - start;
@@ -62,7 +67,7 @@
         return start;
       }
       if (tok.isComment()) {
-        start = Math.min(start, tok.getPosition());
+        start = min(start, tok.getPosition());
       }
     }
     return start;
@@ -279,6 +284,28 @@
   }
 
   /**
+   * Returns the {@link Input.Tok}s starting at the current source position, which are satisfied by
+   * the given predicate.
+   */
+  public ImmutableList<Tok> peekTokens(int startPosition, Predicate<Input.Tok> predicate) {
+    ImmutableList<? extends Input.Token> tokens = input.getTokens();
+    Preconditions.checkState(
+        tokens.get(tokenI).getTok().getPosition() == startPosition,
+        "Expected the current token to be at position %s, found: %s",
+        startPosition,
+        tokens.get(tokenI));
+    ImmutableList.Builder<Tok> result = ImmutableList.builder();
+    for (int idx = tokenI; idx < tokens.size(); idx++) {
+      Tok tok = tokens.get(idx).getTok();
+      if (!predicate.apply(tok)) {
+        break;
+      }
+      result.add(tok);
+    }
+    return result.build();
+  }
+
+  /**
    * Emit an optional token iff it exists on the input. This is used to emit tokens whose existence
    * has been lost in the AST.
    *
diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
index 2023826..f7c3dec 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java
@@ -54,7 +54,7 @@
       int idx = option.indexOf('=');
       if (idx >= 0) {
         flag = option.substring(0, idx);
-        value = option.substring(idx + 1, option.length());
+        value = option.substring(idx + 1);
       } else {
         flag = option;
         value = null;
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
index 3e97395..aac829d 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
@@ -14,8 +14,6 @@
 
 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;
@@ -42,7 +40,6 @@
 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;
@@ -154,7 +151,7 @@
     OpsBuilder builder = new OpsBuilder(javaInput, javaOutput);
     // Output the compilation unit.
     JavaInputAstVisitor visitor;
-    if (getMajor() >= 14) {
+    if (Runtime.version().feature() >= 14) {
       try {
         visitor =
             Class.forName("com.google.googlejavaformat.java.java14.Java14InputAstVisitor")
@@ -176,23 +173,6 @@
     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;
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java
index 3ccb44a..808916c 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java
@@ -26,7 +26,7 @@
 /** Checked exception class for formatter errors. */
 public final class FormatterException extends Exception {
 
-  private ImmutableList<FormatterDiagnostic> diagnostics;
+  private final ImmutableList<FormatterDiagnostic> diagnostics;
 
   public FormatterException(String message) {
     this(FormatterDiagnostic.create(message));
@@ -47,7 +47,8 @@
 
   public static FormatterException fromJavacDiagnostics(
       Iterable<Diagnostic<? extends JavaFileObject>> diagnostics) {
-    return new FormatterException(Iterables.transform(diagnostics, d -> toFormatterDiagnostic(d)));
+    return new FormatterException(
+        Iterables.transform(diagnostics, FormatterException::toFormatterDiagnostic));
   }
 
   private static FormatterDiagnostic toFormatterDiagnostic(Diagnostic<?> input) {
diff --git a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProvider.java b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProvider.java
new file mode 100644
index 0000000..7bcad4c
--- /dev/null
+++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 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.auto.service.AutoService;
+import java.io.PrintWriter;
+import java.util.spi.ToolProvider;
+
+/** Provide a way to be invoked without necessarily starting a new VM. */
+@AutoService(ToolProvider.class)
+public class GoogleJavaFormatToolProvider implements ToolProvider {
+  @Override
+  public String name() {
+    return "google-java-format";
+  }
+
+  @Override
+  public int run(PrintWriter out, PrintWriter err, String... args) {
+    try {
+      return Main.main(out, err, args);
+    } catch (RuntimeException e) {
+      err.print(e.getMessage());
+      return -1; // pass non-zero value back indicating an error has happened
+    }
+  }
+}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java
index a82715e..dcbaea1 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java
@@ -346,6 +346,10 @@
           i++;
         }
       }
+      while (tokenAt(i).equals(";")) {
+        // Extra semicolons are not allowed by the JLS but are accepted by javac.
+        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
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java
index 4d3d30d..fbb6fe7 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java
@@ -60,7 +60,7 @@
     return style.indentationMultiplier();
   }
 
-  boolean formatJavadoc() {
+  public boolean formatJavadoc() {
     return formatJavadoc;
   }
 
@@ -91,7 +91,7 @@
       return this;
     }
 
-    Builder formatJavadoc(boolean formatJavadoc) {
+    public Builder formatJavadoc(boolean formatJavadoc) {
       this.formatJavadoc = formatJavadoc;
       return this;
     }
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
index 999c8fb..165bdeb 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
@@ -311,7 +311,7 @@
     for (Tok tok : toks) {
       builder.put(tok.getPosition(), tok.getColumn());
     }
-    return builder.build();
+    return builder.buildOrThrow();
   }
 
   /**
@@ -557,33 +557,28 @@
   }
 
   /**
-   * Convert from an offset and length flag pair to a token range.
+   * Convert from a character range to a token range.
    *
-   * @param offset the {@code 0}-based offset in characters
-   * @param length the length in characters
+   * @param characterRange the {@code 0}-based {@link Range} of characters
    * @return the {@code 0}-based {@link Range} of tokens
-   * @throws FormatterException if offset + length is outside the file
+   * @throws FormatterException if the upper endpoint of the range is outside the file
    */
-  Range<Integer> characterRangeToTokenRange(int offset, int length) throws FormatterException {
-    int requiredLength = offset + length;
-    if (requiredLength > text.length()) {
+  Range<Integer> characterRangeToTokenRange(Range<Integer> characterRange)
+      throws FormatterException {
+    if (characterRange.upperEndpoint() > text.length()) {
       throw new FormatterException(
           String.format(
               "error: invalid length %d, offset + length (%d) is outside the file",
-              length, requiredLength));
+              characterRange.upperEndpoint() - characterRange.lowerEndpoint(),
+              characterRange.upperEndpoint()));
     }
-    if (length < 0) {
-      return EMPTY_RANGE;
-    }
-    if (length == 0) {
-      // 0 stands for "format the line under the cursor"
-      length = 1;
-    }
+    // empty range stands for "format the line under the cursor"
+    Range<Integer> nonEmptyRange =
+        characterRange.isEmpty()
+            ? Range.closedOpen(characterRange.lowerEndpoint(), characterRange.lowerEndpoint() + 1)
+            : characterRange;
     ImmutableCollection<Token> enclosed =
-        getPositionTokenMap()
-            .subRangeMap(Range.closedOpen(offset, offset + length))
-            .asMapOfRanges()
-            .values();
+        getPositionTokenMap().subRangeMap(nonEmptyRange).asMapOfRanges().values();
     if (enclosed.isEmpty()) {
       return EMPTY_RANGE;
     }
@@ -594,7 +589,7 @@
   /**
    * Get the number of toks.
    *
-   * @return the number of toks, including the EOF tok
+   * @return the number of toks, excluding the EOF tok
    */
   @Override
   public int getkN() {
@@ -604,7 +599,7 @@
   /**
    * Get the Token by index.
    *
-   * @param k the token index
+   * @param k the Tok index
    */
   @Override
   public Token getToken(int k) {
@@ -664,12 +659,9 @@
   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());
+    for (Range<Integer> characterRange : characterRanges) {
       tokenRangeSet.add(
-          characterRangeToTokenRange(
-              characterRange.lowerEndpoint(),
-              characterRange.upperEndpoint() - characterRange.lowerEndpoint()));
+          characterRangeToTokenRange(characterRange.canonical(DiscreteDomain.integers())));
     }
     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
index 6ce0f66..daed250 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java
@@ -43,19 +43,27 @@
 import static com.sun.source.tree.Tree.Kind.VARIABLE;
 import static java.util.stream.Collectors.toList;
 
+import com.google.auto.value.AutoOneOf;
+import com.google.auto.value.AutoValue;
 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.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
 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.Range;
+import com.google.common.collect.RangeSet;
 import com.google.common.collect.Streams;
+import com.google.common.collect.TreeRangeSet;
+import com.google.errorprone.annotations.CheckReturnValue;
 import com.google.googlejavaformat.CloseOp;
 import com.google.googlejavaformat.Doc;
 import com.google.googlejavaformat.Doc.FillMode;
@@ -139,8 +147,9 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
+import java.util.Comparator;
 import java.util.Deque;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -168,7 +177,7 @@
   }
 
   /** Whether to break or not. */
-  enum BreakOrNot {
+  protected enum BreakOrNot {
     YES,
     NO;
 
@@ -178,7 +187,7 @@
   }
 
   /** Whether to collapse empty blocks. */
-  enum CollapseEmptyOrNot {
+  protected enum CollapseEmptyOrNot {
     YES,
     NO;
 
@@ -192,7 +201,7 @@
   }
 
   /** Whether to allow leading blank lines in blocks. */
-  enum AllowLeadingBlankLine {
+  protected enum AllowLeadingBlankLine {
     YES,
     NO;
 
@@ -202,7 +211,7 @@
   }
 
   /** Whether to allow trailing blank lines in blocks. */
-  enum AllowTrailingBlankLine {
+  protected enum AllowTrailingBlankLine {
     YES,
     NO;
 
@@ -269,6 +278,21 @@
     }
   }
 
+  // TODO(cushon): generalize this
+  private static final ImmutableMultimap<String, String> TYPE_ANNOTATIONS = typeAnnotations();
+
+  private static ImmutableSetMultimap<String, String> typeAnnotations() {
+    ImmutableSetMultimap.Builder<String, String> result = ImmutableSetMultimap.builder();
+    for (String annotation :
+        ImmutableList.of(
+            "org.jspecify.nullness.Nullable",
+            "org.checkerframework.checker.nullness.qual.Nullable")) {
+      String simpleName = annotation.substring(annotation.lastIndexOf('.') + 1);
+      result.put(simpleName, annotation);
+    }
+    return result.build();
+  }
+
   protected final OpsBuilder builder;
 
   protected static final Indent.Const ZERO = Indent.Const.ZERO;
@@ -278,6 +302,8 @@
   protected final Indent.Const plusTwo;
   protected final Indent.Const plusFour;
 
+  private final Set<Name> typeAnnotationSimpleNames = new HashSet<>();
+
   private static final ImmutableList<Op> breakList(Optional<BreakTag> breakTag) {
     return ImmutableList.of(Doc.Break.make(Doc.FillMode.UNIFIED, " ", ZERO, breakTag));
   }
@@ -293,8 +319,6 @@
     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.
@@ -377,11 +401,14 @@
       first = false;
       dropEmptyDeclarations();
     }
+    handleModule(first, node);
     // set a partial format marker at EOF to make sure we can format the entire file
     markForPartialFormat();
     return null;
   }
 
+  protected void handleModule(boolean first, CompilationUnitTree node) {}
+
   /** 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(";"))) {
@@ -415,10 +442,7 @@
   public void visitAnnotationType(ClassTree node) {
     sync(node);
     builder.open(ZERO);
-    visitAndBreakModifiers(
-        node.getModifiers(),
-        Direction.VERTICAL,
-        /* declarationAnnotationBreak= */ Optional.empty());
+    typeDeclarationModifiers(node.getModifiers());
     builder.open(ZERO);
     token("@");
     token("interface");
@@ -677,9 +701,10 @@
     builder.space();
     addTypeArguments(node.getTypeArguments(), plusFour);
     if (node.getClassBody() != null) {
-      builder.addAll(
+      List<AnnotationTree> annotations =
           visitModifiers(
-              node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.empty()));
+              node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.empty());
+      visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES);
     }
     scan(node.getIdentifier(), null);
     addArguments(node.getArguments(), plusFour);
@@ -800,10 +825,7 @@
   public boolean visitEnumDeclaration(ClassTree node) {
     sync(node);
     builder.open(ZERO);
-    visitAndBreakModifiers(
-        node.getModifiers(),
-        Direction.VERTICAL,
-        /* declarationAnnotationBreak= */ Optional.empty());
+    typeDeclarationModifiers(node.getModifiers());
     builder.open(plusFour);
     token("enum");
     builder.breakOp(" ");
@@ -967,7 +989,7 @@
     }
   }
 
-  private TypeWithDims variableFragmentDims(boolean first, int leadingDims, Tree type) {
+  private static TypeWithDims variableFragmentDims(boolean first, int leadingDims, Tree type) {
     if (type == null) {
       return null;
     }
@@ -1112,6 +1134,7 @@
 
   @Override
   public Void visitImport(ImportTree node, Void unused) {
+    checkForTypeAnnotation(node);
     sync(node);
     token("import");
     builder.space();
@@ -1126,6 +1149,21 @@
     return null;
   }
 
+  private void checkForTypeAnnotation(ImportTree node) {
+    Name simpleName = getSimpleName(node);
+    Collection<String> wellKnownAnnotations = TYPE_ANNOTATIONS.get(simpleName.toString());
+    if (!wellKnownAnnotations.isEmpty()
+        && wellKnownAnnotations.contains(node.getQualifiedIdentifier().toString())) {
+      typeAnnotationSimpleNames.add(simpleName);
+    }
+  }
+
+  private static Name getSimpleName(ImportTree importTree) {
+    return importTree.getQualifiedIdentifier() instanceof IdentifierTree
+        ? ((IdentifierTree) importTree.getQualifiedIdentifier()).getName()
+        : ((MemberSelectTree) importTree.getQualifiedIdentifier()).getIdentifier();
+  }
+
   @Override
   public Void visitBinary(BinaryTree node, Void unused) {
     sync(node);
@@ -1358,9 +1396,12 @@
         }
       }
     }
-    builder.addAll(
+    List<AnnotationTree> typeAnnotations =
         visitModifiers(
-            annotations, Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty()));
+            node.getModifiers(),
+            annotations,
+            Direction.VERTICAL,
+            /* declarationAnnotationBreak= */ Optional.empty());
 
     Tree baseReturnType = null;
     Deque<List<? extends AnnotationTree>> dims = null;
@@ -1369,6 +1410,9 @@
           DimensionHelpers.extractDims(node.getReturnType(), SortedDims.YES);
       baseReturnType = extractedDims.node;
       dims = new ArrayDeque<>(extractedDims.dims);
+    } else {
+      verticalAnnotations(typeAnnotations);
+      typeAnnotations = ImmutableList.of();
     }
 
     builder.open(plusFour);
@@ -1377,7 +1421,14 @@
     builder.open(ZERO);
     {
       boolean first = true;
+      if (!typeAnnotations.isEmpty()) {
+        visitAnnotations(typeAnnotations, BreakOrNot.NO, BreakOrNot.NO);
+        first = false;
+      }
       if (!node.getTypeParameters().isEmpty()) {
+        if (!first) {
+          builder.breakToFill(" ");
+        }
         token("<");
         typeParametersRest(node.getTypeParameters(), plusFour);
         if (!returnTypeAnnotations.isEmpty()) {
@@ -1600,11 +1651,7 @@
   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("-")) {
+    if (isUnaryMinusLiteral(sourceForNode)) {
       token("-");
       sourceForNode = sourceForNode.substring(1).trim();
     }
@@ -1612,6 +1659,14 @@
     return null;
   }
 
+  // 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.
+  private static boolean isUnaryMinusLiteral(String literalTreeSource) {
+    return literalTreeSource.startsWith("-");
+  }
+
   private void visitPackage(
       ExpressionTree packageName, List<? extends AnnotationTree> packageAnnotations) {
     if (!packageAnnotations.isEmpty()) {
@@ -1697,10 +1752,10 @@
       default:
         return false;
     }
-    if (!(node.getExpression() instanceof UnaryTree)) {
+    JCTree.Tag tag = unaryTag(node.getExpression());
+    if (tag == null) {
       return false;
     }
-    JCTree.Tag tag = ((JCTree) node.getExpression()).getTag();
     if (tag.isPostUnaryOp()) {
       return false;
     }
@@ -1710,6 +1765,17 @@
     return true;
   }
 
+  private JCTree.Tag unaryTag(ExpressionTree expression) {
+    if (expression instanceof UnaryTree) {
+      return ((JCTree) expression).getTag();
+    }
+    if (expression instanceof LiteralTree
+        && isUnaryMinusLiteral(getSourceForNode(expression, getCurrentPath()))) {
+      return JCTree.Tag.MINUS;
+    }
+    return null;
+  }
+
   @Override
   public Void visitPrimitiveType(PrimitiveTypeTree node, Void unused) {
     sync(node);
@@ -1939,14 +2005,11 @@
 
   public void visitClassDeclaration(ClassTree node) {
     sync(node);
-    List<Op> breaks =
-        visitModifiers(
-            node.getModifiers(),
-            Direction.VERTICAL,
-            /* declarationAnnotationBreak= */ Optional.empty());
+    typeDeclarationModifiers(node.getModifiers());
+    List<? extends Tree> permitsTypes = getPermitsClause(node);
     boolean hasSuperclassType = node.getExtendsClause() != null;
     boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty();
-    builder.addAll(breaks);
+    boolean hasPermitsTypes = !permitsTypes.isEmpty();
     token(node.getKind() == Tree.Kind.INTERFACE ? "interface" : "class");
     builder.space();
     visit(node.getSimpleName());
@@ -1958,7 +2021,7 @@
       if (!node.getTypeParameters().isEmpty()) {
         typeParametersRest(
             node.getTypeParameters(),
-            hasSuperclassType || hasSuperInterfaceTypes ? plusFour : ZERO);
+            hasSuperclassType || hasSuperInterfaceTypes || hasPermitsTypes ? plusFour : ZERO);
       }
       if (hasSuperclassType) {
         builder.breakToFill(" ");
@@ -1966,22 +2029,10 @@
         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();
-      }
+      classDeclarationTypeList(
+          node.getKind() == Tree.Kind.INTERFACE ? "extends" : "implements",
+          node.getImplementsClause());
+      classDeclarationTypeList("permits", permitsTypes);
     }
     builder.close();
     if (node.getMembers() == null) {
@@ -2062,7 +2113,7 @@
   // Helper methods.
 
   /** Helper method for annotations. */
-  void visitAnnotations(
+  protected void visitAnnotations(
       List<? extends AnnotationTree> annotations, BreakOrNot breakBefore, BreakOrNot breakAfter) {
     if (!annotations.isEmpty()) {
       if (breakBefore.isYes()) {
@@ -2082,8 +2133,16 @@
     }
   }
 
+  void verticalAnnotations(List<AnnotationTree> annotations) {
+    for (AnnotationTree annotation : annotations) {
+      builder.forcedBreak();
+      scan(annotation, null);
+      builder.forcedBreak();
+    }
+  }
+
   /** Helper method for blocks. */
-  private void visitBlock(
+  protected void visitBlock(
       BlockTree node,
       CollapseEmptyOrNot collapseEmptyOrNot,
       AllowLeadingBlankLine allowLeadingBlankLine,
@@ -2169,12 +2228,21 @@
     }
   }
 
+  protected void typeDeclarationModifiers(ModifiersTree modifiers) {
+    List<AnnotationTree> typeAnnotations =
+        visitModifiers(
+            modifiers, Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty());
+    verticalAnnotations(typeAnnotations);
+  }
+
   /** Output combined modifiers and annotations and the trailing break. */
   void visitAndBreakModifiers(
       ModifiersTree modifiers,
       Direction annotationDirection,
       Optional<BreakTag> declarationAnnotationBreak) {
-    builder.addAll(visitModifiers(modifiers, annotationDirection, declarationAnnotationBreak));
+    List<AnnotationTree> typeAnnotations =
+        visitModifiers(modifiers, annotationDirection, declarationAnnotationBreak);
+    visitAnnotations(typeAnnotations, BreakOrNot.NO, BreakOrNot.YES);
   }
 
   @Override
@@ -2183,36 +2251,50 @@
   }
 
   /** Output combined modifiers and annotations and returns the trailing break. */
-  protected List<Op> visitModifiers(
+  @CheckReturnValue
+  protected ImmutableList<AnnotationTree> visitModifiers(
       ModifiersTree modifiersTree,
       Direction annotationsDirection,
       Optional<BreakTag> declarationAnnotationBreak) {
     return visitModifiers(
-        modifiersTree.getAnnotations(), annotationsDirection, declarationAnnotationBreak);
+        modifiersTree,
+        modifiersTree.getAnnotations(),
+        annotationsDirection,
+        declarationAnnotationBreak);
   }
 
-  protected List<Op> visitModifiers(
+  @CheckReturnValue
+  protected ImmutableList<AnnotationTree> visitModifiers(
+      ModifiersTree modifiersTree,
       List<? extends AnnotationTree> annotationTrees,
       Direction annotationsDirection,
       Optional<BreakTag> declarationAnnotationBreak) {
-    if (annotationTrees.isEmpty() && !nextIsModifier()) {
-      return EMPTY_LIST;
+    DeclarationModifiersAndTypeAnnotations splitModifiers =
+        splitModifiers(modifiersTree, annotationTrees);
+    return visitModifiers(splitModifiers, annotationsDirection, declarationAnnotationBreak);
+  }
+
+  @CheckReturnValue
+  private ImmutableList<AnnotationTree> visitModifiers(
+      DeclarationModifiersAndTypeAnnotations splitModifiers,
+      Direction annotationsDirection,
+      Optional<BreakTag> declarationAnnotationBreak) {
+    if (splitModifiers.declarationModifiers().isEmpty()) {
+      return splitModifiers.typeAnnotations();
     }
-    Deque<AnnotationTree> annotations = new ArrayDeque<>(annotationTrees);
+    Deque<AnnotationOrModifier> declarationModifiers =
+        new ArrayDeque<>(splitModifiers.declarationModifiers());
     builder.open(ZERO);
     boolean first = true;
     boolean lastWasAnnotation = false;
-    while (!annotations.isEmpty()) {
-      if (nextIsModifier()) {
-        break;
-      }
+    while (!declarationModifiers.isEmpty() && !declarationModifiers.peekFirst().isModifier()) {
       if (!first) {
         builder.addAll(
             annotationsDirection.isVertical()
                 ? forceBreakList(declarationAnnotationBreak)
                 : breakList(declarationAnnotationBreak));
       }
-      scan(annotations.removeFirst(), null);
+      formatAnnotationOrModifier(declarationModifiers);
       first = false;
       lastWasAnnotation = true;
     }
@@ -2221,8 +2303,9 @@
         annotationsDirection.isVertical()
             ? forceBreakList(declarationAnnotationBreak)
             : breakList(declarationAnnotationBreak);
-    if (annotations.isEmpty() && !nextIsModifier()) {
-      return trailingBreak;
+    if (declarationModifiers.isEmpty()) {
+      builder.addAll(trailingBreak);
+      return splitModifiers.typeAnnotations();
     }
     if (lastWasAnnotation) {
       builder.addAll(trailingBreak);
@@ -2230,24 +2313,171 @@
 
     builder.open(ZERO);
     first = true;
-    while (nextIsModifier() || !annotations.isEmpty()) {
+    while (!declarationModifiers.isEmpty()) {
       if (!first) {
         builder.addAll(breakFillList(Optional.empty()));
       }
-      if (nextIsModifier()) {
-        token(builder.peekToken().get());
-      } else {
-        scan(annotations.removeFirst(), null);
-        lastWasAnnotation = true;
-      }
+      formatAnnotationOrModifier(declarationModifiers);
       first = false;
     }
     builder.close();
-    return breakFillList(Optional.empty());
+    builder.addAll(breakFillList(Optional.empty()));
+    return splitModifiers.typeAnnotations();
   }
 
-  boolean nextIsModifier() {
-    switch (builder.peekToken().get()) {
+  /** Represents an annotation or a modifier in a {@link ModifiersTree}. */
+  @AutoOneOf(AnnotationOrModifier.Kind.class)
+  abstract static class AnnotationOrModifier implements Comparable<AnnotationOrModifier> {
+    enum Kind {
+      MODIFIER,
+      ANNOTATION
+    }
+
+    abstract Kind getKind();
+
+    abstract AnnotationTree annotation();
+
+    abstract Input.Tok modifier();
+
+    static AnnotationOrModifier ofModifier(Input.Tok m) {
+      return AutoOneOf_JavaInputAstVisitor_AnnotationOrModifier.modifier(m);
+    }
+
+    static AnnotationOrModifier ofAnnotation(AnnotationTree a) {
+      return AutoOneOf_JavaInputAstVisitor_AnnotationOrModifier.annotation(a);
+    }
+
+    boolean isModifier() {
+      return getKind().equals(Kind.MODIFIER);
+    }
+
+    boolean isAnnotation() {
+      return getKind().equals(Kind.ANNOTATION);
+    }
+
+    int position() {
+      switch (getKind()) {
+        case MODIFIER:
+          return modifier().getPosition();
+        case ANNOTATION:
+          return getStartPosition(annotation());
+      }
+      throw new AssertionError();
+    }
+
+    private static final Comparator<AnnotationOrModifier> COMPARATOR =
+        Comparator.comparingInt(AnnotationOrModifier::position);
+
+    @Override
+    public int compareTo(AnnotationOrModifier o) {
+      return COMPARATOR.compare(this, o);
+    }
+  }
+
+  /**
+   * The modifiers annotations for a declaration, grouped in to a prefix that contains all of the
+   * declaration annotations and modifiers, and a suffix of type annotations.
+   *
+   * <p>For examples like {@code @Deprecated public @Nullable Foo foo();}, this allows us to format
+   * {@code @Deprecated public} as declaration modifiers, and {@code @Nullable} as a type annotation
+   * on the return type.
+   */
+  @AutoValue
+  abstract static class DeclarationModifiersAndTypeAnnotations {
+    abstract ImmutableList<AnnotationOrModifier> declarationModifiers();
+
+    abstract ImmutableList<AnnotationTree> typeAnnotations();
+
+    static DeclarationModifiersAndTypeAnnotations create(
+        ImmutableList<AnnotationOrModifier> declarationModifiers,
+        ImmutableList<AnnotationTree> typeAnnotations) {
+      return new AutoValue_JavaInputAstVisitor_DeclarationModifiersAndTypeAnnotations(
+          declarationModifiers, typeAnnotations);
+    }
+
+    static DeclarationModifiersAndTypeAnnotations empty() {
+      return create(ImmutableList.of(), ImmutableList.of());
+    }
+
+    boolean hasDeclarationAnnotation() {
+      return declarationModifiers().stream().anyMatch(AnnotationOrModifier::isAnnotation);
+    }
+  }
+
+  /**
+   * Examines the token stream to convert the modifiers for a declaration into a {@link
+   * DeclarationModifiersAndTypeAnnotations}.
+   */
+  DeclarationModifiersAndTypeAnnotations splitModifiers(
+      ModifiersTree modifiersTree, List<? extends AnnotationTree> annotations) {
+    if (annotations.isEmpty() && !isModifier(builder.peekToken().get())) {
+      return DeclarationModifiersAndTypeAnnotations.empty();
+    }
+    RangeSet<Integer> annotationRanges = TreeRangeSet.create();
+    for (AnnotationTree annotationTree : annotations) {
+      annotationRanges.add(
+          Range.closedOpen(
+              getStartPosition(annotationTree), getEndPosition(annotationTree, getCurrentPath())));
+    }
+    ImmutableList<Input.Tok> toks =
+        builder.peekTokens(
+            getStartPosition(modifiersTree),
+            (Input.Tok tok) ->
+                // ModifiersTree end position information isn't reliable, so scan tokens as long as
+                // we're seeing annotations or modifiers
+                annotationRanges.contains(tok.getPosition()) || isModifier(tok.getText()));
+    ImmutableList<AnnotationOrModifier> modifiers =
+        ImmutableList.copyOf(
+            Streams.concat(
+                    toks.stream()
+                        // reject tokens from inside AnnotationTrees, we only want modifiers
+                        .filter(t -> !annotationRanges.contains(t.getPosition()))
+                        .map(AnnotationOrModifier::ofModifier),
+                    annotations.stream().map(AnnotationOrModifier::ofAnnotation))
+                .sorted()
+                .collect(toList()));
+    // Take a suffix of annotations that are well-known type annotations, and which appear after any
+    // declaration annotations or modifiers
+    ImmutableList.Builder<AnnotationTree> typeAnnotations = ImmutableList.builder();
+    int idx = modifiers.size() - 1;
+    while (idx >= 0) {
+      AnnotationOrModifier modifier = modifiers.get(idx);
+      if (!modifier.isAnnotation() || !isTypeAnnotation(modifier.annotation())) {
+        break;
+      }
+      typeAnnotations.add(modifier.annotation());
+      idx--;
+    }
+    return DeclarationModifiersAndTypeAnnotations.create(
+        modifiers.subList(0, idx + 1), typeAnnotations.build().reverse());
+  }
+
+  private void formatAnnotationOrModifier(Deque<AnnotationOrModifier> modifiers) {
+    AnnotationOrModifier modifier = modifiers.removeFirst();
+    switch (modifier.getKind()) {
+      case MODIFIER:
+        token(modifier.modifier().getText());
+        if (modifier.modifier().getText().equals("non")) {
+          token(modifiers.removeFirst().modifier().getText());
+          token(modifiers.removeFirst().modifier().getText());
+        }
+        break;
+      case ANNOTATION:
+        scan(modifier.annotation(), null);
+        break;
+    }
+  }
+
+  boolean isTypeAnnotation(AnnotationTree annotationTree) {
+    Tree annotationType = annotationTree.getAnnotationType();
+    if (!(annotationType instanceof IdentifierTree)) {
+      return false;
+    }
+    return typeAnnotationSimpleNames.contains(((IdentifierTree) annotationType).getName());
+  }
+
+  private static boolean isModifier(String token) {
+    switch (token) {
       case "public":
       case "protected":
       case "private":
@@ -2260,6 +2490,9 @@
       case "native":
       case "strictfp":
       case "default":
+      case "sealed":
+      case "non":
+      case "-":
         return true;
       default:
         return false;
@@ -2366,7 +2599,7 @@
     builder.open(ZERO);
     boolean first = true;
     if (receiver.isPresent()) {
-      // TODO(jdd): Use builders.
+      // TODO(user): Use builders.
       declareOne(
           DeclarationKind.PARAMETER,
           Direction.HORIZONTAL,
@@ -2868,7 +3101,7 @@
   }
 
   /** Returns the simple names of expressions in a "." chain. */
-  private List<String> simpleNames(Deque<ExpressionTree> stack) {
+  private static ImmutableList<String> simpleNames(Deque<ExpressionTree> stack) {
     ImmutableList.Builder<String> simpleNames = ImmutableList.builder();
     OUTER:
     for (ExpressionTree expression : stack) {
@@ -2906,7 +3139,7 @@
         if (!methodInvocation.getTypeArguments().isEmpty()) {
           builder.open(plusFour);
           addTypeArguments(methodInvocation.getTypeArguments(), ZERO);
-          // TODO(jdd): Should indent the name -4.
+          // TODO(user): Should indent the name -4.
           builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, tyargTag);
           builder.close();
         }
@@ -2925,14 +3158,14 @@
    * Returns the base expression of an erray access, e.g. given {@code foo[0][0]} returns {@code
    * foo}.
    */
-  private ExpressionTree getArrayBase(ExpressionTree node) {
+  private static ExpressionTree getArrayBase(ExpressionTree node) {
     while (node instanceof ArrayAccessTree) {
       node = ((ArrayAccessTree) node).getExpression();
     }
     return node;
   }
 
-  private ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) {
+  private static ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) {
     ExpressionTree select = methodInvocation.getMethodSelect();
     return select instanceof MemberSelectTree ? ((MemberSelectTree) select).getExpression() : null;
   }
@@ -2973,7 +3206,7 @@
    * 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) {
+  private static Deque<ExpressionTree> getArrayIndices(ExpressionTree expression) {
     Deque<ExpressionTree> indices = new ArrayDeque<>();
     while (expression instanceof ArrayAccessTree) {
       ArrayAccessTree array = (ArrayAccessTree) expression;
@@ -3270,20 +3503,25 @@
     }
 
     Deque<List<? extends AnnotationTree>> dims =
-        new ArrayDeque<>(
-            typeWithDims.isPresent() ? typeWithDims.get().dims : Collections.emptyList());
+        new ArrayDeque<>(typeWithDims.isPresent() ? typeWithDims.get().dims : ImmutableList.of());
     int baseDims = 0;
 
+    // preprocess to separate declaration annotations + modifiers, type annotations
+
+    DeclarationModifiersAndTypeAnnotations declarationAndTypeModifiers =
+        modifiers
+            .map(m -> splitModifiers(m, m.getAnnotations()))
+            .orElse(DeclarationModifiersAndTypeAnnotations.empty());
     builder.open(
-        kind == DeclarationKind.PARAMETER
-                && (modifiers.isPresent() && !modifiers.get().getAnnotations().isEmpty())
+        kind == DeclarationKind.PARAMETER && declarationAndTypeModifiers.hasDeclarationAnnotation()
             ? plusFour
             : ZERO);
     {
-      if (modifiers.isPresent()) {
-        visitAndBreakModifiers(
-            modifiers.get(), annotationsDirection, Optional.of(verticalAnnotationBreak));
-      }
+      List<AnnotationTree> annotations =
+          visitModifiers(
+              declarationAndTypeModifiers,
+              annotationsDirection,
+              Optional.of(verticalAnnotationBreak));
       boolean isVar =
           builder.peekToken().get().equals("var")
               && (!name.contentEquals("var") || builder.peekToken(1).get().equals("var"));
@@ -3294,6 +3532,7 @@
         {
           builder.open(ZERO);
           {
+            visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES);
             if (typeWithDims.isPresent() && typeWithDims.get().node != null) {
               scan(typeWithDims.get().node, null);
               int totalDims = dims.size();
@@ -3533,6 +3772,31 @@
     }
   }
 
+  /** Gets the permits clause for the given node. This is only available in Java 15 and later. */
+  protected List<? extends Tree> getPermitsClause(ClassTree node) {
+    return ImmutableList.of();
+  }
+
+  private void classDeclarationTypeList(String token, List<? extends Tree> types) {
+    if (types.isEmpty()) {
+      return;
+    }
+    builder.breakToFill(" ");
+    builder.open(types.size() > 1 ? plusFour : ZERO);
+    token(token);
+    builder.space();
+    boolean first = true;
+    for (Tree type : types) {
+      if (!first) {
+        token(",");
+        builder.breakOp(" ");
+      }
+      scan(type, null);
+      first = false;
+    }
+    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
@@ -3540,7 +3804,8 @@
    *
    * <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) {
+  private static List<VariableTree> variableFragments(
+      PeekingIterator<? extends Tree> it, Tree first) {
     List<VariableTree> fragments = new ArrayList<>();
     if (first.getKind() == VARIABLE) {
       int start = getStartPosition(first);
@@ -3590,7 +3855,7 @@
    * @param modifiers the list of {@link ModifiersTree}s
    * @return whether the local can be declared with horizontal annotations
    */
-  private Direction canLocalHaveHorizontalAnnotations(ModifiersTree modifiers) {
+  private static Direction canLocalHaveHorizontalAnnotations(ModifiersTree modifiers) {
     int parameterlessAnnotations = 0;
     for (AnnotationTree annotation : modifiers.getAnnotations()) {
       if (annotation.getArguments().isEmpty()) {
@@ -3607,7 +3872,7 @@
    * 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) {
+  private static Direction fieldAnnotationDirection(ModifiersTree modifiers) {
     for (AnnotationTree annotation : modifiers.getAnnotations()) {
       if (!annotation.getArguments().isEmpty()) {
         return Direction.VERTICAL;
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
index c059318..c43a91a 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
@@ -14,6 +14,7 @@
 
 package com.google.googlejavaformat.java;
 
+import static java.lang.Math.min;
 import static java.util.Comparator.comparing;
 
 import com.google.common.base.CharMatcher;
@@ -89,7 +90,7 @@
     partialFormatRanges.add(Range.closed(lo, hi));
   }
 
-  // TODO(jdd): Add invariant.
+  // TODO(user): Add invariant.
   @Override
   public void append(String text, Range<Integer> range) {
     if (!range.isEmpty()) {
@@ -261,8 +262,7 @@
         }
       }
 
-      int replaceTo =
-          Math.min(endTok.getPosition() + endTok.length(), javaInput.getText().length());
+      int replaceTo = 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) {
@@ -304,7 +304,7 @@
         } else {
           if (newline == -1) {
             // If there wasn't a trailing newline in the input, indent the next line.
-            replacement.append(after.substring(0, idx));
+            replacement.append(after, 0, idx);
           }
           break;
         }
@@ -352,7 +352,7 @@
   public static int startPosition(Token token) {
     int min = token.getTok().getPosition();
     for (Input.Tok tok : token.getToksBefore()) {
-      min = Math.min(min, tok.getPosition());
+      min = min(min, tok.getPosition());
     }
     return min;
   }
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java
index a8c9efd..ba7e3b7 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java
@@ -128,10 +128,28 @@
 
     @Override
     protected Comment processComment(int pos, int endPos, CommentStyle style) {
-      char[] buf = reader.getRawCharacters(pos, endPos);
+      char[] buf = getRawCharactersReflectively(pos, endPos);
       return new CommentWithTextAndPosition(
           pos, endPos, new AccessibleReader(fac, buf, buf.length), style);
     }
+
+    private char[] getRawCharactersReflectively(int beginIndex, int endIndex) {
+      Object instance;
+      try {
+        instance = JavaTokenizer.class.getDeclaredField("reader").get(this);
+      } catch (ReflectiveOperationException e) {
+        instance = this;
+      }
+      try {
+        return (char[])
+            instance
+                .getClass()
+                .getMethod("getRawCharacters", int.class, int.class)
+                .invoke(instance, beginIndex, endIndex);
+      } catch (ReflectiveOperationException e) {
+        throw new LinkageError(e.getMessage(), e);
+      }
+    }
   }
 
   /** A {@link Comment} that saves its text and start position. */
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Main.java b/core/src/main/java/com/google/googlejavaformat/java/Main.java
index 9231bda..953ca58 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/Main.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/Main.java
@@ -14,6 +14,7 @@
 
 package com.google.googlejavaformat.java;
 
+import static java.lang.Math.min;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.io.ByteStreams;
@@ -40,7 +41,7 @@
   private static final int MAX_THREADS = 20;
   private static final String STDIN_FILENAME = "<stdin>";
 
-  static final String versionString() {
+  static String versionString() {
     return "google-java-format: Version " + GoogleJavaFormatVersion.version();
   }
 
@@ -62,20 +63,27 @@
    * @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));
+    int result = main(out, err, args);
+    System.exit(result);
+  }
+
+  /**
+   * Package-private main entry point used this CLI program and the java.util.spi.ToolProvider
+   * implementation in the same package as this Main class.
+   */
+  static int main(PrintWriter out, PrintWriter err, String... args) {
     try {
       Main formatter = new Main(out, err, System.in);
-      result = formatter.format(args);
+      return formatter.format(args);
     } catch (UsageException e) {
       err.print(e.getMessage());
-      result = 0;
+      return 0;
     } finally {
       err.flush();
       out.flush();
     }
-    System.exit(result);
   }
 
   /**
@@ -109,7 +117,7 @@
   }
 
   private int formatFiles(CommandLineOptions parameters, JavaFormatterOptions options) {
-    int numThreads = Math.min(MAX_THREADS, parameters.files().size());
+    int numThreads = min(MAX_THREADS, parameters.files().size());
     ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
 
     Map<Path, String> inputs = new LinkedHashMap<>();
@@ -146,7 +154,7 @@
       } catch (ExecutionException e) {
         if (e.getCause() instanceof FormatterException) {
           for (FormatterDiagnostic diagnostic : ((FormatterException) e.getCause()).diagnostics()) {
-            errWriter.println(path + ":" + diagnostic.toString());
+            errWriter.println(path + ":" + diagnostic);
           }
         } else {
           errWriter.println(path + ": error: " + e.getCause().getMessage());
@@ -205,7 +213,7 @@
       }
     } catch (FormatterException e) {
       for (FormatterDiagnostic diagnostic : e.diagnostics()) {
-        errWriter.println(stdinFilename + ":" + diagnostic.toString());
+        errWriter.println(stdinFilename + ":" + diagnostic);
       }
       ok = false;
       // TODO(cpovirk): Catch other types of exception (as we do in the formatFiles case).
diff --git a/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java
index f7f610b..e14b290 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java
@@ -36,44 +36,6 @@
 /** 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(
@@ -130,7 +92,7 @@
           if (i > 0) {
             addTrivia(replacement, modifierTokens.get(i).getToksBefore());
           }
-          replacement.append(mods.get(i).toString());
+          replacement.append(mods.get(i));
           if (i < (modifierTokens.size() - 1)) {
             addTrivia(replacement, modifierTokens.get(i).getToksAfter());
           }
@@ -152,7 +114,44 @@
    * is not a modifier.
    */
   private static Modifier asModifier(Token token) {
-    return getModifier(((JavaInput.Tok) token.getTok()).kind());
+    TokenKind kind = ((JavaInput.Tok) token.getTok()).kind();
+    if (kind != 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: // fall out
+      }
+    }
+    switch (token.getTok().getText()) {
+      case "non-sealed":
+        return Modifier.valueOf("NON_SEALED");
+      case "sealed":
+        return Modifier.valueOf("SEALED");
+      default:
+        return null;
+    }
   }
 
   /** Applies replacements to the given string. */
diff --git a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java
index d939480..20e55e9 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java
@@ -16,6 +16,7 @@
 
 package com.google.googlejavaformat.java;
 
+import static java.lang.Math.max;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.CharMatcher;
@@ -31,6 +32,7 @@
 import com.google.googlejavaformat.Newlines;
 import com.sun.source.doctree.DocCommentTree;
 import com.sun.source.doctree.ReferenceTree;
+import com.sun.source.tree.CaseTree;
 import com.sun.source.tree.IdentifierTree;
 import com.sun.source.tree.ImportTree;
 import com.sun.source.tree.Tree;
@@ -54,8 +56,10 @@
 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.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import javax.tools.Diagnostic;
@@ -114,6 +118,31 @@
       return null;
     }
 
+    // TODO(cushon): remove this override when pattern matching in switch is no longer a preview
+    // feature, and TreePathScanner visits CaseTree#getLabels instead of CaseTree#getExpressions
+    @SuppressWarnings("unchecked") // reflection
+    @Override
+    public Void visitCase(CaseTree tree, Void unused) {
+      if (CASE_TREE_GET_LABELS != null) {
+        try {
+          scan((List<? extends Tree>) CASE_TREE_GET_LABELS.invoke(tree), null);
+        } catch (ReflectiveOperationException e) {
+          throw new LinkageError(e.getMessage(), e);
+        }
+      }
+      return super.visitCase(tree, null);
+    }
+
+    private static final Method CASE_TREE_GET_LABELS = caseTreeGetLabels();
+
+    private static Method caseTreeGetLabels() {
+      try {
+        return CaseTree.class.getMethod("getLabels");
+      } catch (NoSuchMethodException e) {
+        return null;
+      }
+    }
+
     @Override
     public Void scan(Tree tree, Void unused) {
       if (tree == null) {
@@ -145,7 +174,9 @@
       public Void visitReference(ReferenceTree referenceTree, Void unused) {
         DCReference reference = (DCReference) referenceTree;
         long basePos =
-            reference.getSourcePosition((DCTree.DCDocComment) getCurrentPath().getDocComment());
+            reference
+                .pos((DCTree.DCDocComment) getCurrentPath().getDocComment())
+                .getStartPosition();
         // 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) {
@@ -247,7 +278,7 @@
       }
       // delete the import
       int endPosition = importTree.getEndPosition(unit.endPositions);
-      endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
+      endPosition = 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)) {
diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
index e41bb66..c0f16e9 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java
@@ -239,9 +239,10 @@
    * @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
+   * @param trailing extra space to leave after the last line, to accommodate a ; or )
+   * @param components the text to reflow. This is a list of “words” of a single literal. Its first
+   *     and last quotes have been stripped
+   * @param first0 true if the text includes the beginning of its enclosing concat chain
    */
   private static String reflow(
       String separator,
@@ -251,7 +252,7 @@
       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.
+    // Reserve two spaces for the start and end quotes.
     int width = columnLimit - startColumn - 2;
     Deque<String> input = new ArrayDeque<>(components);
     List<String> lines = new ArrayList<>();
@@ -259,10 +260,13 @@
     while (!input.isEmpty()) {
       int length = 0;
       List<String> line = new ArrayList<>();
-      if (input.stream().mapToInt(x -> x.length()).sum() <= width) {
+      // If we know this is going to be the last line, then remove a bit of width to account for the
+      // trailing characters.
+      if (input.stream().mapToInt(String::length).sum() <= width) {
+        // This isn’t quite optimal, but arguably good enough. See b/179561701
         width -= trailing;
       }
-      while (!input.isEmpty() && (length <= 4 || (length + input.peekFirst().length()) < width)) {
+      while (!input.isEmpty() && (length <= 4 || (length + input.peekFirst().length()) <= width)) {
         String text = input.removeFirst();
         line.add(text);
         length += text.length();
diff --git a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java
index 4e871a6..21fae5f 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java
@@ -164,7 +164,7 @@
         hasLowercase |= Character.isLowerCase(c);
       }
       if (firstUppercase) {
-        return hasLowercase ? UPPER_CAMEL : UPPERCASE;
+        return (hasLowercase || name.length() == 1) ? 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
index 82c0843..a10f2d0 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java
@@ -46,9 +46,9 @@
     "    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",
+    "  --skip-reflowing-long-strings",
     "    Do not reflow string literals that exceed the column limit.",
-    " . --skip-javadoc-formatting",
+    "  --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"
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
index 78cfd66..890687f 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/java14/Java14InputAstVisitor.java
@@ -15,48 +15,113 @@
 package com.google.googlejavaformat.java.java14;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.collect.MoreCollectors.toOptional;
+import static com.google.common.collect.Iterables.getOnlyElement;
 
 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.OpsBuilder.BlankLineWanted;
 import com.google.googlejavaformat.java.JavaInputAstVisitor;
+import com.sun.source.tree.AnnotationTree;
 import com.sun.source.tree.BindingPatternTree;
+import com.sun.source.tree.BlockTree;
 import com.sun.source.tree.CaseTree;
 import com.sun.source.tree.ClassTree;
-import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.CompilationUnitTree;
 import com.sun.source.tree.InstanceOfTree;
+import com.sun.source.tree.ModifiersTree;
+import com.sun.source.tree.ModuleTree;
 import com.sun.source.tree.SwitchExpressionTree;
 import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
 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.lang.reflect.Method;
 import java.util.List;
 import java.util.Optional;
+import javax.lang.model.element.Name;
 
 /**
  * Extends {@link JavaInputAstVisitor} with support for AST nodes that were added or modified for
  * Java 14.
  */
 public class Java14InputAstVisitor extends JavaInputAstVisitor {
+  private static final Method COMPILATION_UNIT_TREE_GET_MODULE =
+      maybeGetMethod(CompilationUnitTree.class, "getModule");
+  private static final Method CLASS_TREE_GET_PERMITS_CLAUSE =
+      maybeGetMethod(ClassTree.class, "getPermitsClause");
+  private static final Method BINDING_PATTERN_TREE_GET_VARIABLE =
+      maybeGetMethod(BindingPatternTree.class, "getVariable");
+  private static final Method BINDING_PATTERN_TREE_GET_TYPE =
+      maybeGetMethod(BindingPatternTree.class, "getType");
+  private static final Method BINDING_PATTERN_TREE_GET_BINDING =
+      maybeGetMethod(BindingPatternTree.class, "getBinding");
+  private static final Method CASE_TREE_GET_LABELS = maybeGetMethod(CaseTree.class, "getLabels");
 
   public Java14InputAstVisitor(OpsBuilder builder, int indentMultiplier) {
     super(builder, indentMultiplier);
   }
 
   @Override
+  protected void handleModule(boolean first, CompilationUnitTree node) {
+    if (COMPILATION_UNIT_TREE_GET_MODULE == null) {
+      // Java < 17, see https://bugs.openjdk.java.net/browse/JDK-8255464
+      return;
+    }
+    ModuleTree module = (ModuleTree) invoke(COMPILATION_UNIT_TREE_GET_MODULE, node);
+    if (module != null) {
+      if (!first) {
+        builder.blankLineWanted(BlankLineWanted.YES);
+      }
+      markForPartialFormat();
+      visitModule(module, null);
+      builder.forcedBreak();
+    }
+  }
+
+  @Override
+  protected List<? extends Tree> getPermitsClause(ClassTree node) {
+    if (CLASS_TREE_GET_PERMITS_CLAUSE != null) {
+      return (List<? extends Tree>) invoke(CLASS_TREE_GET_PERMITS_CLAUSE, node);
+    } else {
+      // Java < 15
+      return super.getPermitsClause(node);
+    }
+  }
+
+  @Override
   public Void visitBindingPattern(BindingPatternTree node, Void unused) {
     sync(node);
-    scan(node.getType(), null);
-    builder.breakOp(" ");
-    visit(node.getBinding());
+    if (BINDING_PATTERN_TREE_GET_VARIABLE != null) {
+      VariableTree variableTree = (VariableTree) invoke(BINDING_PATTERN_TREE_GET_VARIABLE, node);
+      visitBindingPattern(
+          variableTree.getModifiers(), variableTree.getType(), variableTree.getName());
+    } else if (BINDING_PATTERN_TREE_GET_TYPE != null && BINDING_PATTERN_TREE_GET_BINDING != null) {
+      Tree type = (Tree) invoke(BINDING_PATTERN_TREE_GET_TYPE, node);
+      Name name = (Name) invoke(BINDING_PATTERN_TREE_GET_BINDING, node);
+      visitBindingPattern(/* modifiers= */ null, type, name);
+    } else {
+      throw new LinkageError(
+          "BindingPatternTree must have either getVariable() or both getType() and getBinding(),"
+              + " but does not");
+    }
     return null;
   }
 
+  private void visitBindingPattern(ModifiersTree modifiers, Tree type, Name name) {
+    if (modifiers != null) {
+      List<AnnotationTree> annotations =
+          visitModifiers(modifiers, Direction.HORIZONTAL, Optional.empty());
+      visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES);
+    }
+    scan(type, null);
+    builder.breakOp(" ");
+    visit(name);
+  }
+
   @Override
   public Void visitYield(YieldTree node, Void aVoid) {
     sync(node);
@@ -98,14 +163,9 @@
 
   public void visitRecordDeclaration(ClassTree node) {
     sync(node);
-    List<Op> breaks =
-        visitModifiers(
-            node.getModifiers(),
-            Direction.VERTICAL,
-            /* declarationAnnotationBreak= */ Optional.empty());
+    typeDeclarationModifiers(node.getModifiers());
     Verify.verify(node.getExtendsClause() == null);
     boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty();
-    builder.addAll(breaks);
     token("record");
     builder.space();
     visit(node.getSimpleName());
@@ -117,10 +177,7 @@
       if (!node.getTypeParameters().isEmpty()) {
         typeParametersRest(node.getTypeParameters(), hasSuperInterfaceTypes ? plusFour : ZERO);
       }
-      ImmutableList<JCVariableDecl> parameters =
-          compactRecordConstructor(node)
-              .map(m -> ImmutableList.copyOf(m.getParameters()))
-              .orElseGet(() -> recordVariables(node));
+      ImmutableList<JCVariableDecl> parameters = recordVariables(node);
       token("(");
       if (!parameters.isEmpty()) {
         // Break before args.
@@ -159,14 +216,6 @@
     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)
@@ -199,20 +248,33 @@
     sync(node);
     markForPartialFormat();
     builder.forcedBreak();
-    if (node.getExpressions().isEmpty()) {
+    List<? extends Tree> labels;
+    boolean isDefault;
+    if (CASE_TREE_GET_LABELS != null) {
+      labels = (List<? extends Tree>) invoke(CASE_TREE_GET_LABELS, node);
+      isDefault =
+          labels.size() == 1
+              && getOnlyElement(labels).getKind().name().equals("DEFAULT_CASE_LABEL");
+    } else {
+      labels = node.getExpressions();
+      isDefault = labels.isEmpty();
+    }
+    if (isDefault) {
       token("default", plusTwo);
     } else {
       token("case", plusTwo);
+      builder.open(labels.size() > 1 ? plusFour : ZERO);
       builder.space();
       boolean first = true;
-      for (ExpressionTree expression : node.getExpressions()) {
+      for (Tree expression : labels) {
         if (!first) {
           token(",");
-          builder.space();
+          builder.breakOp(" ");
         }
         scan(expression, null);
         first = false;
       }
+      builder.close();
     }
     switch (node.getCaseKind()) {
       case STATEMENT:
@@ -226,7 +288,16 @@
         token("-");
         token(">");
         builder.space();
-        scan(node.getBody(), null);
+        if (node.getBody().getKind() == Tree.Kind.BLOCK) {
+          // Explicit call with {@link CollapseEmptyOrNot.YES} to handle empty case blocks.
+          visitBlock(
+              (BlockTree) node.getBody(),
+              CollapseEmptyOrNot.YES,
+              AllowLeadingBlankLine.NO,
+              AllowTrailingBlankLine.NO);
+        } else {
+          scan(node.getBody(), null);
+        }
         builder.guessToken(";");
         break;
       default:
@@ -234,4 +305,20 @@
     }
     return null;
   }
+
+  private static Method maybeGetMethod(Class<?> c, String name) {
+    try {
+      return c.getMethod(name);
+    } catch (ReflectiveOperationException e) {
+      return null;
+    }
+  }
+
+  private static Object invoke(Method m, Object target) {
+    try {
+      return m.invoke(target);
+    } catch (ReflectiveOperationException e) {
+      throw new LinkageError(e.getMessage(), e);
+    }
+  }
 }
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
index 5addc67..03938a6 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java
@@ -166,15 +166,30 @@
    * 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) + " */";
+    if (matcher.matches()) {
+      String line = matcher.group(1);
+      if (line.isEmpty()) {
+        return "/** */";
+      } else if (oneLineJavadoc(line, blockIndent)) {
+        return "/** " + line + " */";
+      }
     }
     return input;
   }
 
+  private static boolean oneLineJavadoc(String line, int blockIndent) {
+    int oneLinerContentLength = MAX_LINE_LENGTH - "/**  */".length() - blockIndent;
+    if (line.length() > oneLinerContentLength) {
+      return false;
+    }
+    // If the javadoc contains only a tag, use multiple lines to encourage writing a summary
+    // fragment, unless it's /* @hide */.
+    if (line.startsWith("@") && !line.equals("@hide")) {
+      return false;
+    }
+    return true;
+  }
+
   private JavadocFormatter() {}
 }
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
index c2431c4..0361415 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java
@@ -15,6 +15,7 @@
 package com.google.googlejavaformat.java.javadoc;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Comparators.max;
 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;
@@ -26,9 +27,7 @@
 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;
 
 /**
@@ -270,8 +269,7 @@
   }
 
   private void requestWhitespace(RequestedWhitespace requestedWhitespace) {
-    this.requestedWhitespace =
-        Ordering.natural().max(requestedWhitespace, this.requestedWhitespace);
+    this.requestedWhitespace = max(requestedWhitespace, this.requestedWhitespace);
   }
 
   /**
@@ -396,7 +394,7 @@
 
   // 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));
+    output.append(" ".repeat(count));
   }
 
   /**
diff --git a/core/src/main/resources/META-INF/native-image/reflect-config.json b/core/src/main/resources/META-INF/native-image/reflect-config.json
new file mode 100644
index 0000000..2c65803
--- /dev/null
+++ b/core/src/main/resources/META-INF/native-image/reflect-config.json
@@ -0,0 +1,6 @@
+[
+  {
+    "name": "com.sun.tools.javac.parser.UnicodeReader",
+    "allDeclaredMethods": true
+  }
+]
diff --git a/core/src/main/scripts/google-java-format.el b/core/src/main/scripts/google-java-format.el
index f9e8d2a..f269ab3 100644
--- a/core/src/main/scripts/google-java-format.el
+++ b/core/src/main/scripts/google-java-format.el
@@ -46,7 +46,7 @@
 
 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)
+  :type '(file :must-match t :match (lambda (widget file) (file-executable-p file)))
   :risky t)
 
 ;;;###autoload
diff --git a/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java b/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
index 8d71f4d..1a4ed09 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/CommandLineOptionsParserTest.java
@@ -19,12 +19,12 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.fail;
 
+import com.google.common.collect.ImmutableList;
 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;
@@ -39,7 +39,7 @@
 
   @Test
   public void defaults() {
-    CommandLineOptions options = CommandLineOptionsParser.parse(Collections.<String>emptyList());
+    CommandLineOptions options = CommandLineOptionsParser.parse(ImmutableList.of());
     assertThat(options.files()).isEmpty();
     assertThat(options.stdin()).isFalse();
     assertThat(options.aosp()).isFalse();
@@ -159,7 +159,7 @@
       CommandLineOptionsParser.parse(Arrays.asList("-lines=1:1", "-lines=1:1"));
       fail();
     } catch (IllegalArgumentException e) {
-      assertThat(e.getMessage()).contains("overlap");
+      assertThat(e).hasMessageThat().contains("overlap");
     }
   }
 
diff --git a/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java
index 0b81ba6..fc966fa 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java
@@ -23,7 +23,6 @@
 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;
@@ -98,8 +97,7 @@
 
     int result = main.format(path.toString());
     assertThat(stdout.toString()).isEmpty();
-    assertThat(stderr.toString())
-        .contains("InvalidSyntax.java:1:35: error: illegal unicode escape");
+    assertThat(stderr.toString()).contains("error: illegal unicode escape");
     assertThat(result).isEqualTo(1);
   }
 
@@ -156,7 +154,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("A.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -173,7 +171,7 @@
   public void parseErrorStdin() throws FormatterException, IOException, UsageException {
     String input = "class Foo { void f() {\n g() } }";
 
-    InputStream inStream = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
+    InputStream inStream = new ByteArrayInputStream(input.getBytes(UTF_8));
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
     Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), inStream);
@@ -190,7 +188,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("A.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -206,7 +204,7 @@
   @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));
+    InputStream inStream = new ByteArrayInputStream(input.getBytes(UTF_8));
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
     Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), inStream);
diff --git a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java
index 44ba639..61a4346 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java
@@ -14,8 +14,7 @@
 
 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.collect.MoreCollectors.toOptional;
 import static com.google.common.io.Files.getFileExtension;
 import static com.google.common.io.Files.getNameWithoutExtension;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -23,7 +22,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.io.CharStreams;
 import com.google.common.reflect.ClassPath;
 import com.google.common.reflect.ClassPath.ResourceInfo;
@@ -31,12 +30,12 @@
 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.Optional;
 import java.util.TreeMap;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -47,8 +46,13 @@
 @RunWith(Parameterized.class)
 public class FormatterIntegrationTest {
 
-  private static final ImmutableSet<String> JAVA14_TESTS =
-      ImmutableSet.of("I477", "Records", "RSLs", "Var", "ExpressionSwitch");
+  private static final ImmutableMultimap<Integer, String> VERSIONED_TESTS =
+      ImmutableMultimap.<Integer, String>builder()
+          .putAll(14, "I477", "Records", "RSLs", "Var", "ExpressionSwitch", "I574", "I594")
+          .putAll(15, "I603")
+          .putAll(16, "I588")
+          .putAll(17, "I683", "I684", "I696")
+          .build();
 
   @Parameters(name = "{index}: {0}")
   public static Iterable<Object[]> data() throws IOException {
@@ -76,7 +80,7 @@
           case "output":
             outputs.put(baseName, contents);
             break;
-          default:
+          default: // fall out
         }
       }
     }
@@ -87,7 +91,9 @@
       String input = inputs.get(fileName);
       assertTrue("unmatched input", outputs.containsKey(fileName));
       String expectedOutput = outputs.get(fileName);
-      if (JAVA14_TESTS.contains(fileName) && getMajor() < 14) {
+      Optional<Integer> version =
+          VERSIONED_TESTS.inverse().get(fileName).stream().collect(toOptional());
+      if (version.isPresent() && Runtime.version().feature() < version.get()) {
         continue;
       }
       testInputs.add(new Object[] {fileName, input, expectedOutput});
@@ -95,21 +101,6 @@
     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;
@@ -125,7 +116,9 @@
   @Test
   public void format() {
     try {
-      String output = new Formatter().formatSource(input);
+      Formatter formatter = new Formatter();
+      String output = formatter.formatSource(input);
+      output = StringWrapper.wrap(output, formatter);
       assertEquals("bad output for " + name, expected, 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
index 3f6e974..1653e56 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java
@@ -16,6 +16,7 @@
 
 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 static org.junit.Assert.fail;
 
 import com.google.common.base.Joiner;
@@ -27,7 +28,6 @@
 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;
@@ -63,7 +63,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("A.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -94,7 +94,7 @@
     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));
+    InputStream in = new ByteArrayInputStream(input.getBytes(UTF_8));
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
 
@@ -115,7 +115,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -132,7 +132,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -238,7 +238,7 @@
               "",
               "import java.util.List;",
               "",
-              "import javax.annotations.Nullable;");
+              "import javax.annotation.Nullable;");
 
   @Test
   public void importsNotReorderedByDefault() throws FormatterException {
@@ -262,7 +262,7 @@
     String expect =
         "package com.google.example;\n\n"
             + "import java.util.List;\n"
-            + "import javax.annotations.Nullable;\n\n"
+            + "import javax.annotation.Nullable;\n\n"
             + "public class ExampleTest {\n"
             + "  @Nullable List<?> xs;\n"
             + "}\n";
@@ -302,7 +302,7 @@
     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));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -315,14 +315,14 @@
 
     assertThat(err.toString()).isEmpty();
     assertThat(out.toString()).isEmpty();
-    String output = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
+    String output = new String(Files.readAllBytes(path), 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));
+      return CharStreams.toString(new InputStreamReader(stream, UTF_8));
     }
   }
 
diff --git a/core/src/test/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProviderTest.java b/core/src/test/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProviderTest.java
new file mode 100644
index 0000000..15e4522
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProviderTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 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 java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ServiceLoader;
+import java.util.spi.ToolProvider;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link GoogleJavaFormatToolProvider}. */
+@RunWith(JUnit4.class)
+public class GoogleJavaFormatToolProviderTest {
+
+  @Test
+  public void testUsageOutputAfterLoadingViaToolName() {
+    String name = "google-java-format";
+
+    assertThat(
+            ServiceLoader.load(ToolProvider.class).stream()
+                .map(ServiceLoader.Provider::get)
+                .map(ToolProvider::name))
+        .contains(name);
+
+    ToolProvider format = ToolProvider.findFirst(name).get();
+
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    int result = format.run(new PrintWriter(out, true), new PrintWriter(err, true), "--help");
+
+    assertThat(result).isEqualTo(0);
+
+    String usage = err.toString();
+
+    // Check that doc links are included.
+    assertThat(usage).containsMatch("http.*/google-java-format");
+    assertThat(usage).contains("Usage: google-java-format");
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java b/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java
index 5a6b1f9..0b9dab2 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java
@@ -313,7 +313,7 @@
             "",
             "import java.util.List;",
             "",
-            "import javax.annotations.Nullable;",
+            "import javax.annotation.Nullable;",
             "",
             "import static org.junit.Assert.fail;",
             "import static com.google.truth.Truth.assertThat;",
@@ -329,7 +329,7 @@
             "",
             "import com.google.common.base.Preconditions;",
             "import java.util.List;",
-            "import javax.annotations.Nullable;",
+            "import javax.annotation.Nullable;",
             "import org.junit.runner.RunWith;",
             "import org.junit.runners.JUnit4;",
             "",
@@ -527,6 +527,27 @@
             "class Test {}",
           }
         },
+        {
+          {
+            "package p;",
+            "",
+            "import java.lang.Bar;",
+            "import java.lang.Baz;",
+            ";",
+            "import java.lang.Foo;",
+            "",
+            "interface Test {}",
+          },
+          {
+            "package p;",
+            "",
+            "import java.lang.Bar;",
+            "import java.lang.Baz;",
+            "import java.lang.Foo;",
+            "",
+            "interface Test {}",
+          }
+        }
       };
 
       ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
@@ -799,7 +820,7 @@
             "",
             "public class Blim {}",
           },
-        },
+        }
       };
       ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
       Arrays.stream(inputsOutputs).forEach(input -> builder.add(createRow(input)));
diff --git a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java
index f5103d9..6849c01 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java
@@ -937,7 +937,9 @@
       "class Test {}",
     };
     String[] expected = {
-      "/** @param this is a param */", //
+      "/**", //
+      " * @param this is a param",
+      " */",
       "class Test {}",
     };
     doFormatTest(input, expected);
@@ -1415,4 +1417,33 @@
     };
     doFormatTest(input, expected);
   }
+
+  @Test
+  public void missingSummaryFragment() {
+    String[] input = {
+      "public class Foo {",
+      "  /**",
+      "   * @return something.",
+      "   */",
+      "  public void setSomething() {}",
+      "",
+      "  /**",
+      "   * @hide",
+      "   */",
+      "  public void setSomething() {}",
+      "}",
+    };
+    String[] expected = {
+      "public class Foo {",
+      "  /**",
+      "   * @return something.",
+      "   */",
+      "  public void setSomething() {}",
+      "",
+      "  /** @hide */",
+      "  public void setSomething() {}",
+      "}",
+    };
+    doFormatTest(input, expected);
+  }
 }
diff --git a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
index 613d391..ac3eb39 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
@@ -14,6 +14,8 @@
 
 package com.google.googlejavaformat.java;
 
+import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH;
+import static com.google.common.base.StandardSystemProperty.JAVA_HOME;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -50,6 +52,16 @@
   // PrintWriter instances used below are hard-coded to use system-default line separator.
   private final Joiner joiner = Joiner.on(System.lineSeparator());
 
+  private static final ImmutableList<String> ADD_EXPORTS =
+      ImmutableList.of(
+          "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
+          "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
+          "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
+          "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+          "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+          "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+          "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED");
+
   @Test
   public void testUsageOutput() {
     StringWriter out = new StringWriter();
@@ -107,11 +119,13 @@
   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()))
+                ImmutableList.<String>builder()
+                    .add(Paths.get(JAVA_HOME.value()).resolve("bin/java").toString())
+                    .addAll(ADD_EXPORTS)
+                    .add("-cp")
+                    .add(JAVA_CLASS_PATH.value())
+                    .add(Main.class.getName())
+                    .build())
             .redirectError(Redirect.PIPE)
             .redirectOutput(Redirect.PIPE)
             .start();
@@ -308,7 +322,7 @@
       "@ParametersAreNonnullByDefault",
       "package com.google.common.labs.base;",
       "",
-      "import javax.annotation.CheckReturnValue;",
+      "import com.google.errorprone.annotations.CheckReturnValue;",
       "import javax.annotation.ParametersAreNonnullByDefault;",
       "",
     };
@@ -388,9 +402,9 @@
 
     assertThat(out.toString())
         .isEqualTo(
-            b.toAbsolutePath().toString()
+            b.toAbsolutePath()
                 + System.lineSeparator()
-                + c.toAbsolutePath().toString()
+                + c.toAbsolutePath()
                 + System.lineSeparator());
     assertThat(err.toString()).isEmpty();
   }
@@ -433,14 +447,16 @@
     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",
-                    "-"))
+                ImmutableList.<String>builder()
+                    .add(Paths.get(JAVA_HOME.value()).resolve("bin/java").toString())
+                    .addAll(ADD_EXPORTS)
+                    .add("-cp")
+                    .add(JAVA_CLASS_PATH.value())
+                    .add(Main.class.getName())
+                    .add("-n")
+                    .add("--set-exit-if-changed")
+                    .add("-")
+                    .build())
             .redirectInput(path.toFile())
             .redirectError(Redirect.PIPE)
             .redirectOutput(Redirect.PIPE)
@@ -459,14 +475,16 @@
     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()))
+                ImmutableList.<String>builder()
+                    .add(Paths.get(JAVA_HOME.value()).resolve("bin/java").toString())
+                    .addAll(ADD_EXPORTS)
+                    .add("-cp")
+                    .add(JAVA_CLASS_PATH.value())
+                    .add(Main.class.getName())
+                    .add("-n")
+                    .add("--set-exit-if-changed")
+                    .add(path.toAbsolutePath().toString())
+                    .build())
             .redirectError(Redirect.PIPE)
             .redirectOutput(Redirect.PIPE)
             .start();
@@ -474,7 +492,7 @@
     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(out).isEqualTo(path.toAbsolutePath() + System.lineSeparator());
     assertThat(process.exitValue()).isEqualTo(1);
   }
 
@@ -523,8 +541,8 @@
       "class T {",
       "  String s =",
       "      \"one long incredibly unbroken sentence moving from topic to topic so that no one had"
-          + " a\"",
-      "          + \" chance to interrupt\";",
+          + " a chance\"",
+      "          + \" to interrupt\";",
       "}",
       "",
     };
diff --git a/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java b/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java
index 57d55d7..b1142b3 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java
@@ -25,7 +25,6 @@
 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;
@@ -397,7 +396,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -429,7 +428,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -475,7 +474,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -521,7 +520,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -567,7 +566,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -714,7 +713,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("FormatterException.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -957,7 +956,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1081,7 +1080,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1108,7 +1107,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1133,7 +1132,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1156,7 +1155,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1177,7 +1176,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, input.getBytes(StandardCharsets.UTF_8));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1429,7 +1428,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, lines(input).getBytes(StandardCharsets.UTF_8));
+    Files.write(path, lines(input).getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1459,7 +1458,7 @@
 
     Path tmpdir = testFolder.newFolder().toPath();
     Path path = tmpdir.resolve("Foo.java");
-    Files.write(path, lines(input).getBytes(StandardCharsets.UTF_8));
+    Files.write(path, lines(input).getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1505,7 +1504,7 @@
   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));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
@@ -1655,7 +1654,7 @@
   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));
+    Files.write(path, input.getBytes(UTF_8));
 
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();
diff --git a/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsCaseLabelsTest.java b/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsCaseLabelsTest.java
new file mode 100644
index 0000000..c0babb0
--- /dev/null
+++ b/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsCaseLabelsTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 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 static org.junit.Assume.assumeTrue;
+
+import com.google.common.base.Joiner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests that unused import removal doesn't remove types used in case labels. */
+@RunWith(JUnit4.class)
+public class RemoveUnusedImportsCaseLabelsTest {
+  @Test
+  public void preserveTypesInCaseLabels() throws FormatterException {
+    assumeTrue(Runtime.version().feature() >= 17);
+    String input =
+        Joiner.on('\n')
+            .join(
+                "package example;",
+                "import example.model.SealedInterface;",
+                "import example.model.TypeA;",
+                "import example.model.TypeB;",
+                "public class Main {",
+                "  public void apply(SealedInterface sealedInterface) {",
+                "    switch(sealedInterface) {",
+                "      case TypeA a -> System.out.println(\"A!\");",
+                "      case TypeB b -> System.out.println(\"B!\");",
+                "    }",
+                "  }",
+                "}");
+    assertThat(removeUnusedImports(input)).isEqualTo(input);
+  }
+}
diff --git a/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java b/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java
index 1965feb..675bc88 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java
@@ -258,7 +258,7 @@
     };
     ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
     for (String[][] inputAndOutput : inputsOutputs) {
-      assertThat(inputAndOutput.length).isEqualTo(2);
+      assertThat(inputAndOutput).hasLength(2);
       String[] input = inputAndOutput[0];
       String[] output = inputAndOutput[1];
       String[] parameters = {
diff --git a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java
index 89c94ea..53fb54d 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java
@@ -395,8 +395,8 @@
 
   @Test
   public void testCR() throws Exception {
-    assertThat(StringWrapper.wrap(40, formatter.formatSource(input.replace("\n", "\r")), formatter))
-        .isEqualTo(output.replace("\n", "\r"));
+    assertThat(StringWrapper.wrap(40, formatter.formatSource(input.replace('\n', '\r')), formatter))
+        .isEqualTo(output.replace('\n', '\r'));
   }
 
   @Test
diff --git a/core/src/test/java/com/google/googlejavaformat/java/TypeNameClassifierTest.java b/core/src/test/java/com/google/googlejavaformat/java/TypeNameClassifierTest.java
index 9d1e00a..3270bc6 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/TypeNameClassifierTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/TypeNameClassifierTest.java
@@ -43,6 +43,7 @@
     assertThat(JavaCaseFormat.from("a_$")).isEqualTo(JavaCaseFormat.LOWERCASE);
     assertThat(JavaCaseFormat.from("_")).isEqualTo(JavaCaseFormat.LOWERCASE);
     assertThat(JavaCaseFormat.from("_A")).isEqualTo(JavaCaseFormat.UPPERCASE);
+    assertThat(JavaCaseFormat.from("A")).isEqualTo(JavaCaseFormat.UPPER_CAMEL);
   }
 
   private static Optional<Integer> getPrefix(String qualifiedName) {
@@ -62,6 +63,7 @@
     assertThat(getPrefix("ClassName.CONST")).hasValue(1);
     assertThat(getPrefix("ClassName.varName")).hasValue(1);
     assertThat(getPrefix("ClassName.Inner.varName")).hasValue(2);
+    assertThat(getPrefix("com.R.foo")).hasValue(2);
   }
 
   @Test
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
index 4fef207..38cac35 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/filer/FormattingFilerTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/filer/FormattingFilerTest.java
@@ -52,7 +52,7 @@
         new Messager() {
           @Override
           public void printMessage(javax.tools.Diagnostic.Kind kind, CharSequence msg) {
-            logMessages.add(kind.toString() + ";" + msg);
+            logMessages.add(kind + ";" + msg);
           }
 
           @Override
@@ -73,9 +73,9 @@
 
     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();
+    try (Writer writer = formattingFiler.createSourceFile("foo.Bar").openWriter()) {
+      writer.write(file);
+    }
 
     assertThat(logMessages).containsExactly("NOTE;Error formatting foo.Bar");
   }
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
index 81d13aa..c658630 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.input
@@ -17,13 +17,13 @@
   @X(x = 1)
   private @interface Y {}
 
-  // TODO(jdd): Add annotation declaration with empty body.
+  // TODO(user): 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.
+  // TODO(user): Include type annotations once we can include a higher language level.
 
   int[] array1 = new int[5];
   int[] array2 =
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
index 3eff456..5d5d88f 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/A.output
@@ -17,13 +17,13 @@
   @X(x = 1)
   private @interface Y {}
 
-  // TODO(jdd): Add annotation declaration with empty body.
+  // TODO(user): 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.
+  // TODO(user): Include type annotations once we can include a higher language level.
 
   int[] array1 = new int[5];
   int[] array2 =
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B173808510.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B173808510.input
new file mode 100644
index 0000000..e3e8493
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B173808510.input
@@ -0,0 +1,8 @@
+class B173808510 {
+  // b/173808510
+  @FlagSpec(
+      name = "myFlag",
+      help =
+          "areallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyloongword word1 word2")
+  Flag<Integer> dummy = null;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B173808510.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B173808510.output
new file mode 100644
index 0000000..45a939e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B173808510.output
@@ -0,0 +1,9 @@
+class B173808510 {
+  // b/173808510
+  @FlagSpec(
+      name = "myFlag",
+      help =
+          "areallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyloongword word1"
+              + " word2")
+  Flag<Integer> dummy = null;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B183431894.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B183431894.input
new file mode 100644
index 0000000..7c220d5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B183431894.input
@@ -0,0 +1,4 @@
+class B183431894 {
+  int a = - -1;
+  int d = + +1;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B183431894.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B183431894.output
new file mode 100644
index 0000000..7c220d5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B183431894.output
@@ -0,0 +1,4 @@
+class B183431894 {
+  int a = - -1;
+  int d = + +1;
+}
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
index 30c232b..9408235 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.input
@@ -5,7 +5,7 @@
     if (!metadata.ignoreOutputTransformations()
         && Producers.isListenableFutureMapKey(outputKey)) {
       ImmutableList<ProducerNode<?>> nodes = createMapNodes((ProducerNode) node);
-      checkCollectionNodesAgainstWhitelist(nodes, whitelist);
+      checkCollectionNodesAgainstAllowlist(nodes, allowlist);
       return nodes;
 
     } else if (!metadata.ignoreOutputTransformations()
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
index 950f4eb..aeb36b8 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20535125.output
@@ -4,7 +4,7 @@
   void m() {
     if (!metadata.ignoreOutputTransformations() && Producers.isListenableFutureMapKey(outputKey)) {
       ImmutableList<ProducerNode<?>> nodes = createMapNodes((ProducerNode) node);
-      checkCollectionNodesAgainstWhitelist(nodes, whitelist);
+      checkCollectionNodesAgainstAllowlist(nodes, allowlist);
       return nodes;
 
     } else if (!metadata.ignoreOutputTransformations()
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
index 957c2df..de746bb 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.input
@@ -1,8 +1,8 @@
 class B20701054 {
   void m() {
     ImmutableList<String> x = ImmutableList.builder().add(1).build();
-    OptionalBinder.<ASD>newOptionalBinder(binder(), InputWhitelist.class).setBinding().to(
-        AllInputWhitelist.class);
+    OptionalBinder.<ASD>newOptionalBinder(binder(), InputAllowlist.class).setBinding().to(
+        AllInputAllowlist.class);
 
     Foo z = Foo.INSTANCE.field;
     Foo z = Foo.INSTANCE.field.field;
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
index 7ce6fda..2fd9a9a 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20701054.output
@@ -1,9 +1,9 @@
 class B20701054 {
   void m() {
     ImmutableList<String> x = ImmutableList.builder().add(1).build();
-    OptionalBinder.<ASD>newOptionalBinder(binder(), InputWhitelist.class)
+    OptionalBinder.<ASD>newOptionalBinder(binder(), InputAllowlist.class)
         .setBinding()
-        .to(AllInputWhitelist.class);
+        .to(AllInputAllowlist.class);
 
     Foo z = Foo.INSTANCE.field;
     Foo z = Foo.INSTANCE.field.field;
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
index 86e46d5..7317f17 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input
@@ -1,6 +1,6 @@
 public class B20844369 {
   private static final String ID_PATTERN =
-  // TODO(daw): add min/max lengths for the numbers here, e.g. android ID
+  // TODO(user): add min/max lengths for the numbers here, e.g. android ID
   "(?:(?<androidId>\\d+)\\+)?" // optional Android ID
       + "(?<type>\\d+)" // type
       + ":"
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
index 982dc2b..62f9721 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output
@@ -1,6 +1,6 @@
 public class B20844369 {
   private static final String ID_PATTERN =
-      // TODO(daw): add min/max lengths for the numbers here, e.g. android ID
+      // TODO(user): add min/max lengths for the numbers here, e.g. android ID
       "(?:(?<androidId>\\d+)\\+)?" // optional Android ID
           + "(?<type>\\d+)" // type
           + ":"
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
index 7baed6c..31bf3b8 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.input
@@ -6,7 +6,7 @@
  * CreationReferences.
  */
 class C<T> {
-  // TODO(jdd): Test higher-language-level constructs.
+  // TODO(user): Test higher-language-level constructs.
 
   C() {
     this(
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
index fcf773e..c62c7ae 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/C.output
@@ -6,7 +6,7 @@
  * CreationReferences.
  */
 class C<T> {
-  // TODO(jdd): Test higher-language-level constructs.
+  // TODO(user): Test higher-language-level constructs.
 
   C() {
     this(
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
index daca973..d69ed1e 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.input
@@ -2,7 +2,7 @@
 
 /** Tests for Dimensions and DoStatements. */
 class D {
-  // TODO(jdd): Test higher-language-level features.
+  // TODO(user): Test higher-language-level features.
 
   int[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
           [][][][]
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
index daca973..d69ed1e 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/D.output
@@ -2,7 +2,7 @@
 
 /** Tests for Dimensions and DoStatements. */
 class D {
-  // TODO(jdd): Test higher-language-level features.
+  // TODO(user): Test higher-language-level features.
 
   int[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
           [][][][]
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
index 0e98139..479466a 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.input
@@ -9,7 +9,7 @@
  */
 @MarkerAnnotation
 class E<T> {
-  // TODO(jdd): Test higher language-level features.
+  // TODO(user): Test higher language-level features.
 
   enum Enum1 {
     A, B, C, D;
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
index 4dd603a..fb4f2fa 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/E.output
@@ -9,7 +9,7 @@
  */
 @MarkerAnnotation
 class E<T> {
-  // TODO(jdd): Test higher language-level features.
+  // TODO(user): Test higher language-level features.
 
   enum Enum1 {
     A,
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/ExpressionSwitch.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/ExpressionSwitch.input
index 1e4db16..5590f74 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/ExpressionSwitch.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/ExpressionSwitch.input
@@ -25,4 +25,17 @@
       default -> System.out.println("default");
     }
   }
+
+  String breakLongCaseArgs(MyEnum e) {
+    return switch (e) {
+      case SOME_RATHER_LONG_NAME_1, SOME_RATHER_LONG_NAME_2, SOME_RATHER_LONG_NAME_3, SOME_RATHER_LONG_NAME_4, SOME_RATHER_LONG_NAME_5, SOME_RATHER_LONG_NAME_6, SOME_RATHER_LONG_NAME_7 -> {}
+      case SOME_RATHER_LONG_NAME_8 -> {}
+    };
+  }
+
+  String dontBreakShortCaseArgs(MyEnum e) {
+    return switch (e) {
+      case CASE_A, CASE_B -> {}
+    };
+  }
 }
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/ExpressionSwitch.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/ExpressionSwitch.output
index 6458aa0..00ae892 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/ExpressionSwitch.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/ExpressionSwitch.output
@@ -31,4 +31,23 @@
       default -> System.out.println("default");
     }
   }
+
+  String breakLongCaseArgs(MyEnum e) {
+    return switch (e) {
+      case SOME_RATHER_LONG_NAME_1,
+          SOME_RATHER_LONG_NAME_2,
+          SOME_RATHER_LONG_NAME_3,
+          SOME_RATHER_LONG_NAME_4,
+          SOME_RATHER_LONG_NAME_5,
+          SOME_RATHER_LONG_NAME_6,
+          SOME_RATHER_LONG_NAME_7 -> {}
+      case SOME_RATHER_LONG_NAME_8 -> {}
+    };
+  }
+
+  String dontBreakShortCaseArgs(MyEnum e) {
+    return switch (e) {
+      case CASE_A, CASE_B -> {}
+    };
+  }
 }
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I574.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I574.input
new file mode 100644
index 0000000..27d23d0
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I574.input
@@ -0,0 +1,6 @@
+public record Record(@NotNull Object o) {
+
+  public Record {
+    this.o = o;
+  }
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I574.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I574.output
new file mode 100644
index 0000000..b0deb2d
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I574.output
@@ -0,0 +1,6 @@
+public record Record(@NotNull Object o) {
+
+  public Record {
+    this.o = o;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I588.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I588.input
new file mode 100644
index 0000000..9c8f992
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I588.input
@@ -0,0 +1,8 @@
+class T {
+  int f(Object x) {
+    if (x instanceof final Integer i) {
+      return i;
+    }
+    return -1;
+  }
+}
\ No newline at end of file
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I588.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I588.output
new file mode 100644
index 0000000..37ff2f5
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I588.output
@@ -0,0 +1,8 @@
+class T {
+  int f(Object x) {
+    if (x instanceof final Integer i) {
+      return i;
+    }
+    return -1;
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I594.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I594.input
new file mode 100644
index 0000000..98f667e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I594.input
@@ -0,0 +1,7 @@
+public class I594 {
+  public void thisIsNotFormattedCorrectly(Object something){
+    if(something instanceof String somethingAsString){
+      return;
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I594.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I594.output
new file mode 100644
index 0000000..7c519a2
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I594.output
@@ -0,0 +1,7 @@
+public class I594 {
+  public void thisIsNotFormattedCorrectly(Object something) {
+    if (something instanceof String somethingAsString) {
+      return;
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I603.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I603.input
new file mode 100644
index 0000000..6cedc53
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I603.input
@@ -0,0 +1,16 @@
+class I603 {
+  sealed abstract class T1 {}
+
+  sealed class T2 extends X implements Y permits Z {}
+
+  sealed class T3
+      permits
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  sealed class T4
+      implements
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      permits
+          Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+          Yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I603.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I603.output
new file mode 100644
index 0000000..22153b6
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I603.output
@@ -0,0 +1,13 @@
+class I603 {
+  abstract sealed class T1 {}
+
+  sealed class T2 extends X implements Y permits Z {}
+
+  sealed class T3
+      permits Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx {}
+
+  sealed class T4
+      implements Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+      permits Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+          Yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I643.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I643.input
new file mode 100644
index 0000000..a31a230
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I643.input
@@ -0,0 +1,14 @@
+public class Foo {
+    static final int VERBOSE_WORDY_AND_LENGTHY_ONE = 1;
+    static final int VERBOSE_WORDY_AND_LENGTHY_TWO = 2;
+    static final int VERBOSE_WORDY_AND_LENGTHY_FOUR = 4;
+
+    public static int fn(int x) {
+        switch (x) {
+        case VERBOSE_WORDY_AND_LENGTHY_ONE | VERBOSE_WORDY_AND_LENGTHY_TWO | VERBOSE_WORDY_AND_LENGTHY_FOUR:
+            return 0;
+        default:
+            return 1;
+        }
+    }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I643.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I643.output
new file mode 100644
index 0000000..945cbea
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I643.output
@@ -0,0 +1,16 @@
+public class Foo {
+  static final int VERBOSE_WORDY_AND_LENGTHY_ONE = 1;
+  static final int VERBOSE_WORDY_AND_LENGTHY_TWO = 2;
+  static final int VERBOSE_WORDY_AND_LENGTHY_FOUR = 4;
+
+  public static int fn(int x) {
+    switch (x) {
+      case VERBOSE_WORDY_AND_LENGTHY_ONE
+          | VERBOSE_WORDY_AND_LENGTHY_TWO
+          | VERBOSE_WORDY_AND_LENGTHY_FOUR:
+        return 0;
+      default:
+        return 1;
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I683.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I683.input
new file mode 100644
index 0000000..9104f19
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I683.input
@@ -0,0 +1,14 @@
+interface Test {
+
+  static class Test1 implements Test{}
+  static class Test2 implements Test{}
+  
+  public static void main(String[] args) {
+    Test test = new Test1();
+    switch (test) {
+      case Test1 test1 -> {}
+      case Test2 test2 -> {}
+      default -> throw new IllegalStateException("Unexpected value: " + test);
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I683.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I683.output
new file mode 100644
index 0000000..5b9c466
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I683.output
@@ -0,0 +1,15 @@
+interface Test {
+
+  static class Test1 implements Test {}
+
+  static class Test2 implements Test {}
+
+  public static void main(String[] args) {
+    Test test = new Test1();
+    switch (test) {
+      case Test1 test1 -> {}
+      case Test2 test2 -> {}
+      default -> throw new IllegalStateException("Unexpected value: " + test);
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I684.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I684.input
new file mode 100644
index 0000000..cbce0dd
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I684.input
@@ -0,0 +1,14 @@
+package example;
+
+import example.model.SealedInterface;
+import example.model.TypeA;
+import example.model.TypeB;
+
+public class Main {
+  public void apply(SealedInterface sealedInterface) {
+    switch(sealedInterface) {
+      case TypeA a -> System.out.println("A!");
+      case TypeB b -> System.out.println("B!");
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I684.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I684.output
new file mode 100644
index 0000000..4e5e9b4
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I684.output
@@ -0,0 +1,14 @@
+package example;
+
+import example.model.SealedInterface;
+import example.model.TypeA;
+import example.model.TypeB;
+
+public class Main {
+  public void apply(SealedInterface sealedInterface) {
+    switch (sealedInterface) {
+      case TypeA a -> System.out.println("A!");
+      case TypeB b -> System.out.println("B!");
+    }
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I696.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I696.input
new file mode 100644
index 0000000..156e6ef
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I696.input
@@ -0,0 +1,11 @@
+public abstract non-sealed class A extends SealedClass {
+}
+
+non-sealed class B extends SealedClass {
+}
+
+non-sealed @A class B extends SealedClass {
+}
+
+@A non-sealed class B extends SealedClass {
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I696.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I696.output
new file mode 100644
index 0000000..14721c3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I696.output
@@ -0,0 +1,8 @@
+public abstract non-sealed class A extends SealedClass {}
+
+non-sealed class B extends SealedClass {}
+
+non-sealed @A class B extends SealedClass {}
+
+@A
+non-sealed class B extends SealedClass {}
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
index f0b3e66..eda543d 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.input
@@ -2,7 +2,7 @@
 
 /** Tests for LabeledStatements and LambdaExpressions. */
 class L {
-  // TODO(jdd): Include high language-level tests.
+  // TODO(user): Include high language-level tests.
 
   void f() {
     LABEL:
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
index f0b3e66..eda543d 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/L.output
@@ -2,7 +2,7 @@
 
 /** Tests for LabeledStatements and LambdaExpressions. */
 class L {
-  // TODO(jdd): Include high language-level tests.
+  // TODO(user): Include high language-level tests.
 
   void f() {
     LABEL:
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/LiteralReflow.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/LiteralReflow.input
new file mode 100644
index 0000000..2aa4de3
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/LiteralReflow.input
@@ -0,0 +1,44 @@
+class LiteralReflow {
+  static class TestLineBreak {
+    String doesNotBreakAt100 = "A very long long long long long long long long long loong sentence";
+    String breaksAt101 = "A very long long long long long long long long long long loooong sentence";
+  }
+
+  static class TestReflowLimit {
+    String doesNotReflowAt100 =
+        "A very long long long long long long long long long long long long long looooong sentence";
+    String reflowsWhenLongerThan100 =
+        "A very long long long long long long long long long long long long long long long sentence";
+  }
+
+  static class TestReflowLocation {
+    String accommodatesWordsUpTo100 =
+        "A very long long long long long long long long long long long long long long long looooong sentence";
+    String breaksBeforeWordsReach101 =
+        "A very long long long long long long long long long long long long long long long loooooong sentence";
+  }
+
+  static class Test2LineReflowLimit {
+    String doesNotReflowEitherLinesAt100 =
+        "A very long long long long long long long long long long long long long looooong sentence. And a second very long long long long long long long long long long loong sentence";
+    String reflowsLastLineAt101 =
+        "A very long long long long long long long long long long long long long looooong sentence. And a second very long long long long long long long long long long looong sentence";
+  }
+
+  static class TestWithTrailingCharacters {
+    String fitsLastLineUpTo100WithTrailingCharacters =
+        f(
+            f(
+                "A very long long long long long long long long long long long long loong sentence. And a second very long long long long long long long long loong sentence"));
+    String reflowsLastLineToAccommodateTrailingCharacters =
+        f(
+            f(
+                "A very long long long long long long long long long long long long loong sentence. And a second very long long long long long long long long looong sentence"));
+    // Tests an off-by-one issue, but see b/179561701 for a similar issue that is not yet fixed
+    String doesNotOverTriggerLastLineReflow =
+        f(
+            f(
+                "A very long long long long long long long long long long long long loong sentence."
+                    + " And a second very loong sentence with trailing a a a a a a a a a a a a a a a"));
+  }
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/LiteralReflow.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/LiteralReflow.output
new file mode 100644
index 0000000..50ed7bd
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/LiteralReflow.output
@@ -0,0 +1,55 @@
+class LiteralReflow {
+  static class TestLineBreak {
+    String doesNotBreakAt100 = "A very long long long long long long long long long loong sentence";
+    String breaksAt101 =
+        "A very long long long long long long long long long long loooong sentence";
+  }
+
+  static class TestReflowLimit {
+    String doesNotReflowAt100 =
+        "A very long long long long long long long long long long long long long looooong sentence";
+    String reflowsWhenLongerThan100 =
+        "A very long long long long long long long long long long long long long long long"
+            + " sentence";
+  }
+
+  static class TestReflowLocation {
+    String accommodatesWordsUpTo100 =
+        "A very long long long long long long long long long long long long long long long looooong"
+            + " sentence";
+    String breaksBeforeWordsReach101 =
+        "A very long long long long long long long long long long long long long long long"
+            + " loooooong sentence";
+  }
+
+  static class Test2LineReflowLimit {
+    String doesNotReflowEitherLinesAt100 =
+        "A very long long long long long long long long long long long long long looooong sentence."
+            + " And a second very long long long long long long long long long long loong sentence";
+    String reflowsLastLineAt101 =
+        "A very long long long long long long long long long long long long long looooong sentence."
+            + " And a second very long long long long long long long long long long looong"
+            + " sentence";
+  }
+
+  static class TestWithTrailingCharacters {
+    String fitsLastLineUpTo100WithTrailingCharacters =
+        f(
+            f(
+                "A very long long long long long long long long long long long long loong sentence."
+                    + " And a second very long long long long long long long long loong sentence"));
+    String reflowsLastLineToAccommodateTrailingCharacters =
+        f(
+            f(
+                "A very long long long long long long long long long long long long loong sentence."
+                    + " And a second very long long long long long long long long looong"
+                    + " sentence"));
+    // Tests an off-by-one issue, but see b/179561701 for a similar issue that is not yet fixed
+    String doesNotOverTriggerLastLineReflow =
+        f(
+            f(
+                "A very long long long long long long long long long long long long loong sentence."
+                    + " And a second very loong sentence with trailing a a a a a a a a a a a a a a"
+                    + " a"));
+  }
+}
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
index a1e07d1..15fc1b2 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.input
@@ -7,7 +7,7 @@
  * SynchronizedStatements.
  */
 class S {
-  // TODO(jdd): Add tests for higher language levels.
+  // TODO(user): Add tests for higher language levels.
 
   int x = 0;
 
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
index a1e07d1..15fc1b2 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/S.output
@@ -7,7 +7,7 @@
  * SynchronizedStatements.
  */
 class S {
-  // TODO(jdd): Add tests for higher language levels.
+  // TODO(user): Add tests for higher language levels.
 
   int x = 0;
 
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
index c8cd293..fc9bc09 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.input
@@ -5,7 +5,7 @@
  * TypeDeclarations, TypeLiterals, TypeMethodReferences, TypeParameters, and Types.
  */
 class T<T1, T2, T3> {
-  // TODO(jdd): Add tests for higher language levels.
+  // TODO(user): Add tests for higher language levels.
 
   T f(int x) throws Exception {
     class TT {}
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
index c8cd293..fc9bc09 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.output
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/T.output
@@ -5,7 +5,7 @@
  * TypeDeclarations, TypeLiterals, TypeMethodReferences, TypeParameters, and Types.
  */
 class T<T1, T2, T3> {
-  // TODO(jdd): Add tests for higher language levels.
+  // TODO(user): Add tests for higher language levels.
 
   T f(int x) throws Exception {
     class TT {}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/TypeAnnotations.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TypeAnnotations.input
new file mode 100644
index 0000000..ddaa8f1
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TypeAnnotations.input
@@ -0,0 +1,33 @@
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+class TypeAnnotations {
+
+  @Deprecated
+  public @Nullable Object foo() {}
+
+  public @Deprecated Object foo() {}
+
+  @Nullable Foo handle() {
+    @Nullable Bar bar = bar();
+    try (@Nullable Baz baz = baz()) {}
+  }
+
+  Foo(
+      @Nullable Bar //
+          param1, //
+      Baz //
+          param2) {}
+
+  void g(
+      @Deprecated @Nullable ImmutableList<String> veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      @Deprecated @Nullable ImmutableList<String> veryVeryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooong) {}
+
+  @Deprecated @Nullable TypeAnnotations() {}
+
+  enum Foo {
+    @Nullable
+    BAR;
+  }
+
+  @Nullable @Nullable Object doubleTrouble() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/TypeAnnotations.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TypeAnnotations.output
new file mode 100644
index 0000000..8dd5d4e
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/TypeAnnotations.output
@@ -0,0 +1,39 @@
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+class TypeAnnotations {
+
+  @Deprecated
+  public @Nullable Object foo() {}
+
+  public @Deprecated Object foo() {}
+
+  @Nullable Foo handle() {
+    @Nullable Bar bar = bar();
+    try (@Nullable Baz baz = baz()) {}
+  }
+
+  Foo(
+      @Nullable Bar //
+          param1, //
+      Baz //
+          param2) {}
+
+  void g(
+      @Deprecated
+          @Nullable ImmutableList<String>
+              veryVeryLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong,
+      @Deprecated
+          @Nullable ImmutableList<String>
+              veryVeryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooong) {}
+
+  @Deprecated
+  @Nullable
+  TypeAnnotations() {}
+
+  enum Foo {
+    @Nullable
+    BAR;
+  }
+
+  @Nullable @Nullable Object doubleTrouble() {}
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/b26306390.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/b26306390.input
new file mode 100644
index 0000000..da6c01b
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/b26306390.input
@@ -0,0 +1,3 @@
+class B26306390 {
+  int resourceId = com.some.extremely.verbose.pkg.name.R.string.some_extremely_long_resource_identifier_that_exceeds_the_column_limit;
+}
diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/b26306390.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/b26306390.output
new file mode 100644
index 0000000..a21772f
--- /dev/null
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/b26306390.output
@@ -0,0 +1,5 @@
+class B26306390 {
+  int resourceId =
+      com.some.extremely.verbose.pkg.name.R.string
+          .some_extremely_long_resource_identifier_that_exceeds_the_column_limit;
+}
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
index e6994f7..8d144c2 100644
--- 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
@@ -6,7 +6,7 @@
 import com.google.common.base.Preconditions;
 import java.util.List;
 import java.util.Set;
-import javax.annotations.Nullable;
+import javax.annotation.Nullable;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
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
index 7d5df53..9b1d01f 100644
--- 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
@@ -7,7 +7,7 @@
 
 import java.util.List;
 
-import javax.annotations.Nullable;
+import javax.annotation.Nullable;
 
 import static org.junit.Assert.fail;
 import static com.google.truth.Truth.assertThat;
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
index 7d5df53..9b1d01f 100644
--- 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
@@ -7,7 +7,7 @@
 
 import java.util.List;
 
-import javax.annotations.Nullable;
+import javax.annotation.Nullable;
 
 import static org.junit.Assert.fail;
 import static com.google.truth.Truth.assertThat;
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
index 887d43c..d7bcd6d 100644
--- 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
@@ -5,7 +5,7 @@
 
 import com.google.common.base.Preconditions;
 import java.util.List;
-import javax.annotations.Nullable;
+import javax.annotation.Nullable;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
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
index 88f83f1..a50a83e 100644
--- 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
@@ -5,7 +5,7 @@
 
 import com.google.common.base.Preconditions;
 import java.util.List;
-import javax.annotations.Nullable;
+import javax.annotation.Nullable;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
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
index be1eacd..dd992a3 100644
--- a/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.input
+++ b/core/src/test/resources/com/google/googlejavaformat/java/testimports/A.input
@@ -8,7 +8,7 @@
 import java.util.List;
 import java.util.Set;
 
-import javax.annotations.Nullable;
+import javax.annotation.Nullable;
 
 import static org.junit.Assert.fail;
 import static com.google.truth.Truth.assertThat;
diff --git a/eclipse_plugin/META-INF/MANIFEST.MF b/eclipse_plugin/META-INF/MANIFEST.MF
index 27613e9..9132453 100644
--- a/eclipse_plugin/META-INF/MANIFEST.MF
+++ b/eclipse_plugin/META-INF/MANIFEST.MF
@@ -2,14 +2,14 @@
 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
+Bundle-Vendor: Google
+Bundle-Version: 1.13.0
+Bundle-RequiredExecutionEnvironment: JavaSE-11
 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
+ lib/guava.jar,
+ lib/google-java-format.jar
diff --git a/eclipse_plugin/README.md b/eclipse_plugin/README.md
index 332468d..395a368 100644
--- a/eclipse_plugin/README.md
+++ b/eclipse_plugin/README.md
@@ -1,4 +1,4 @@
-# google-java-format Eclipse Plugin
+# Google Java Format Eclipse Plugin
 
 ## Enabling
 
@@ -6,21 +6,38 @@
 
 ## Development
 
-1) Uncomment `<module>eclipse_plugin</module>` in the parent `pom.xml`
+### Prerequisites
 
-2) Run `mvn install`, which will copy the dependences of the plugin to
-`eclipse_plugin/lib`.
+Before building the plugin, make sure to run `mvn
+tycho-versions:update-eclipse-metadata` to update the bundle version in
+`META-INF/MANIFEST.MF`.
 
-2) If you are using Java 9, add
+### Building the Plugin
 
-    ```
-    -vm
-    /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/bin/java
-    ```
+1) Run `mvn clean package` in the `eclipse_plugin` directory. This will first copy the dependencies
+of the plugin to `eclipse_plugin/lib/` and then triggers the tycho build that uses these
+dependencies (as declared in `build.properties`) for the actual Eclipse plugin build.<br><br>
+If you also want to add the build artifact to the local maven repository, you can use
+`mvn clean install -Dtycho.localArtifacts=ignore` instead. Note, however, that you then must use
+this build command for every build with that specific version number until you clear the build
+artifact (or the
+[p2-local-metadata.properties](https://wiki.eclipse.org/Tycho/Target_Platform#Locally_built_artifacts))
+from your local repository. Otherwise, you might run into issues caused by the build using an
+outdated build artifact created by a previous build instead of re-building the plugin. More
+information on this issue is given
+[in this thread](https://www.eclipse.org/lists/tycho-user/msg00952.html) and
+[this bug tracker entry](https://bugs.eclipse.org/bugs/show_bug.cgi?id=355367).
 
-    to `/Applications/Eclipse.app/Contents/Eclipse/eclipse.ini`.
+2) You can find the built plugin in
+`eclipse_plugin/target/google-java-format-eclipse-plugin-<version>.jar`
 
-3) Open the `eclipse_plugin` project in a recent Eclipse SDK build.
+#### Building against a local (snapshot) release of the core
 
-4) From `File > Export` select `Plugin-in Development > Deployable plugin-ins
-and fragments` and follow the wizard to export a plugin jar.
+With the current build setup, the Eclipse plugin build pulls the needed build
+artifacts of the google java format core from the maven repository and copies it
+into the `eclipse_plugin/lib/` directory.
+
+If you instead want to build against a local (snapshot) build of the core which
+is not available in a maven repository (local or otherwise), you will have to
+place the appropriate version into the `eclipse_plugin/lib/` directory yourself
+before the build.
diff --git a/eclipse_plugin/build.properties b/eclipse_plugin/build.properties
index dc5ae7c..dd1d835 100644
--- a/eclipse_plugin/build.properties
+++ b/eclipse_plugin/build.properties
@@ -3,6 +3,5 @@
 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
+               lib/guava.jar,\
+               lib/google-java-format.jar
diff --git a/eclipse_plugin/plugin.xml b/eclipse_plugin/plugin.xml
index b832a1e..1fdfb6e 100644
--- a/eclipse_plugin/plugin.xml
+++ b/eclipse_plugin/plugin.xml
@@ -1,3 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
 <!--
   Copyright 2020 Google Inc.
 
@@ -14,7 +16,6 @@
   limitations under the License.
 -->
 
-<?xml version="1.0" encoding="UTF-8"?>
 <?eclipse version="3.4"?>
 <plugin>
    <extension point="org.eclipse.jdt.core.javaFormatter">
diff --git a/eclipse_plugin/pom.xml b/eclipse_plugin/pom.xml
index bb581cd..534e685 100644
--- a/eclipse_plugin/pom.xml
+++ b/eclipse_plugin/pom.xml
@@ -14,28 +14,35 @@
   ~ limitations under the License.
   -->
 
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<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>
 
+  <groupId>com.google.googlejavaformat</groupId>
   <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>
+  <version>1.13.0</version>
+
+  <name>Google Java Format Plugin for Eclipse 4.5+</name>
 
   <description>
-    A Java source code formatter that follows Google Java Style.
+    A Java source code formatter plugin for Eclipse that follows Google Java Style.
   </description>
 
   <properties>
-    <tycho-version>0.26.0</tycho-version>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <tycho-version>2.6.0</tycho-version>
   </properties>
 
+  <dependencies>
+    <dependency>
+      <groupId>com.google.googlejavaformat</groupId>
+      <artifactId>google-java-format</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
   <repositories>
     <repository>
       <id>mars</id>
@@ -44,16 +51,30 @@
     </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.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>3.2.0</version>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <phase>initialize</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <outputDirectory>lib</outputDirectory>
+          <includeScope>runtime</includeScope>
+          <stripVersion>true</stripVersion>
+          <overWriteReleases>true</overWriteReleases>
+          <overWriteSnapshots>true</overWriteSnapshots>
+          <includeArtifactIds>guava,google-java-format</includeArtifactIds>
+        </configuration>
+      </plugin>
 
       <plugin>
         <groupId>org.eclipse.tycho</groupId>
@@ -64,6 +85,15 @@
 
       <plugin>
         <groupId>org.eclipse.tycho</groupId>
+        <artifactId>tycho-versions-plugin</artifactId>
+        <version>${tycho-version}</version>
+        <configuration>
+          <newVersion>${project.version}</newVersion>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.eclipse.tycho</groupId>
         <artifactId>target-platform-configuration</artifactId>
         <version>${tycho-version}</version>
         <configuration>
@@ -97,7 +127,15 @@
         </configuration>
       </plugin>
 
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.7.0</version>
+        <configuration>
+          <source>11</source>
+          <target>11</target>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
-
 </project>
diff --git a/idea_plugin/.gitignore b/idea_plugin/.gitignore
new file mode 100644
index 0000000..16bc65a
--- /dev/null
+++ b/idea_plugin/.gitignore
@@ -0,0 +1,5 @@
+build
+.gradle
+gradle
+gradlew
+gradlew.bat
\ No newline at end of file
diff --git a/idea_plugin/build.gradle b/idea_plugin/build.gradle
index d74957d..d9f769d 100644
--- a/idea_plugin/build.gradle
+++ b/idea_plugin/build.gradle
@@ -15,7 +15,7 @@
  */
 
 plugins {
-  id "org.jetbrains.intellij" version "0.4.21"
+  id "org.jetbrains.intellij" version "1.3.1"
 }
 
 repositories {
@@ -23,23 +23,27 @@
 }
 
 ext {
-  googleJavaFormatVersion = '1.8'
+  googleJavaFormatVersion = "1.13.0"
 }
 
-apply plugin: 'org.jetbrains.intellij'
-apply plugin: 'java'
+apply plugin: "org.jetbrains.intellij"
+apply plugin: "java"
+
+sourceCompatibility = JavaVersion.VERSION_11
+targetCompatibility = JavaVersion.VERSION_11
 
 intellij {
   pluginName = "google-java-format"
-  version = "202.6250.13-EAP-SNAPSHOT"
+  plugins = ["java"]
+  version = "221.3427-EAP-CANDIDATE-SNAPSHOT"
 }
 
 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.1"
-  sinceBuild = '201'
-  untilBuild = ''
+  version.set("${googleJavaFormatVersion}.0")
+  sinceBuild = "203"
+  untilBuild = ""
 }
 
 publishPlugin {
@@ -48,11 +52,11 @@
 
 sourceSets {
   main {
-    java.srcDir 'src'
-    resources.srcDir 'resources'
+    java.srcDir "src"
+    resources.srcDir "resources"
   }
 }
 
 dependencies {
-  compile "com.google.googlejavaformat:google-java-format:${googleJavaFormatVersion}"
+  implementation "com.google.googlejavaformat:google-java-format:${googleJavaFormatVersion}"
 }
diff --git a/idea_plugin/google-java-format.iml b/idea_plugin/google-java-format.iml
deleted file mode 100644
index 568c04f..0000000
--- a/idea_plugin/google-java-format.iml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?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
index 5b313cf..f10cde5 100644
--- a/idea_plugin/resources/META-INF/plugin.xml
+++ b/idea_plugin/resources/META-INF/plugin.xml
@@ -14,7 +14,8 @@
   limitations under the License.
 -->
 
-<idea-plugin url="https://github.com/google/google-java-format/tree/master/idea_plugin">
+<idea-plugin url="https://github.com/google/google-java-format/tree/master/idea_plugin"
+  require-restart="true">
   <id>google-java-format</id>
   <name>google-java-format</name>
   <vendor url="https://github.com/google/google-java-format">
@@ -24,14 +25,24 @@
   <!-- 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>
+  <depends>com.intellij.java</depends>
 
   <change-notes><![CDATA[
     <dl>
+      <dt>1.13.0.0</dt>
+      <dd>Updated to use google-java-format 1.13.</dd>
+      <dt>1.12.0.0</dt>
+      <dd>Updated to use google-java-format 1.12.</dd>
+      <dt>1.11.0.0</dt>
+      <dd>Updated to use google-java-format 1.11.</dd>
+      <dt>1.10.0.0</dt>
+      <dd>Updated to use google-java-format 1.10.</dd>
+      <dt>1.9.0.0</dt>
+      <dd>Updated to use google-java-format 1.9.</dd>
       <dt>1.8.0.1</dt>
-      <dd>Fixed support for 2020.2 IDEs.<dd>
+      <dd>Fixed support for 2020.2 IDEs.</dd>
       <dt>1.8.0.0</dt>
-      <dd>Updated to use google-java-format 1.8.<dd>
+      <dd>Updated to use google-java-format 1.8.</dd>
       <dt>1.7.0.5</dt>
       <dd>Added a version for 2020.1+ IDEs.</dd>
       <dt>1.7.0.4</dt>
@@ -48,17 +59,22 @@
   ]]></change-notes>
 
   <applicationListeners>
-    <listener class="com.google.googlejavaformat.intellij.InitialConfigurationProjectManagerListener"
-              topic="com.intellij.openapi.project.ProjectManagerListener"/>
+    <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"/>
+      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"/>
+    <projectConfigurable
+      instance="com.google.googlejavaformat.intellij.GoogleJavaFormatConfigurable"
+      id="google-java-format.settings"
+      displayName="google-java-format Settings"/>
+    <projectService
+      serviceImplementation="com.google.googlejavaformat.intellij.GoogleJavaFormatSettings"/>
+    <notificationGroup displayType="STICKY_BALLOON" id="Enable google-java-format"
+      isLogByDefault="true"/>
   </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
index c70ba17..af5da95 100644
--- a/idea_plugin/src/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java
@@ -34,10 +34,11 @@
 import com.intellij.util.ThrowableRunnable;
 import java.util.Collection;
 import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * Decorates the {@link CodeStyleManager} abstract class by delegating to a concrete implementation
- * instance (likely IJ's default instance).
+ * instance (likely IntelliJ's default instance).
  */
 @SuppressWarnings("deprecation")
 class CodeStyleManagerDecorator extends CodeStyleManager
@@ -54,98 +55,102 @@
   }
 
   @Override
-  public Project getProject() {
+  public @NotNull Project getProject() {
     return delegate.getProject();
   }
 
   @Override
-  public PsiElement reformat(PsiElement element) throws IncorrectOperationException {
+  public @NotNull PsiElement reformat(@NotNull PsiElement element)
+      throws IncorrectOperationException {
     return delegate.reformat(element);
   }
 
   @Override
-  public PsiElement reformat(PsiElement element, boolean canChangeWhiteSpacesOnly)
+  public @NotNull PsiElement reformat(@NotNull PsiElement element, boolean canChangeWhiteSpacesOnly)
       throws IncorrectOperationException {
     return delegate.reformat(element, canChangeWhiteSpacesOnly);
   }
 
   @Override
-  public PsiElement reformatRange(PsiElement element, int startOffset, int endOffset)
+  public PsiElement reformatRange(@NotNull 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)
+      @NotNull 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)
+  public void reformatText(@NotNull PsiFile file, int startOffset, int endOffset)
       throws IncorrectOperationException {
     delegate.reformatText(file, startOffset, endOffset);
   }
 
   @Override
-  public void reformatText(PsiFile file, Collection<TextRange> ranges)
+  public void reformatText(@NotNull PsiFile file, @NotNull Collection<? extends TextRange> ranges)
       throws IncorrectOperationException {
     delegate.reformatText(file, ranges);
   }
 
   @Override
-  public void reformatTextWithContext(PsiFile psiFile, ChangedRangesInfo changedRangesInfo)
+  public void reformatTextWithContext(
+      @NotNull PsiFile psiFile, @NotNull ChangedRangesInfo changedRangesInfo)
       throws IncorrectOperationException {
     delegate.reformatTextWithContext(psiFile, changedRangesInfo);
   }
 
   @Override
-  public void reformatTextWithContext(PsiFile file, Collection<TextRange> ranges)
+  public void reformatTextWithContext(
+      @NotNull PsiFile file, @NotNull Collection<? extends TextRange> ranges)
       throws IncorrectOperationException {
     delegate.reformatTextWithContext(file, ranges);
   }
 
   @Override
-  public void adjustLineIndent(PsiFile file, TextRange rangeToAdjust)
+  public void adjustLineIndent(@NotNull PsiFile file, TextRange rangeToAdjust)
       throws IncorrectOperationException {
     delegate.adjustLineIndent(file, rangeToAdjust);
   }
 
   @Override
-  public int adjustLineIndent(PsiFile file, int offset) throws IncorrectOperationException {
+  public int adjustLineIndent(@NotNull PsiFile file, int offset)
+      throws IncorrectOperationException {
     return delegate.adjustLineIndent(file, offset);
   }
 
   @Override
-  public int adjustLineIndent(Document document, int offset) {
+  public int adjustLineIndent(@NotNull Document document, int offset) {
     return delegate.adjustLineIndent(document, offset);
   }
 
-  public void scheduleIndentAdjustment(Document document, int offset) {
+  public void scheduleIndentAdjustment(@NotNull Document document, int offset) {
     delegate.scheduleIndentAdjustment(document, offset);
   }
 
   @Override
-  public boolean isLineToBeIndented(PsiFile file, int offset) {
+  public boolean isLineToBeIndented(@NotNull PsiFile file, int offset) {
     return delegate.isLineToBeIndented(file, offset);
   }
 
   @Override
   @Nullable
-  public String getLineIndent(PsiFile file, int offset) {
+  public String getLineIndent(@NotNull PsiFile file, int offset) {
     return delegate.getLineIndent(file, offset);
   }
 
   @Override
   @Nullable
-  public String getLineIndent(PsiFile file, int offset, FormattingMode mode) {
+  public String getLineIndent(@NotNull PsiFile file, int offset, FormattingMode mode) {
     return delegate.getLineIndent(file, offset, mode);
   }
 
   @Override
   @Nullable
-  public String getLineIndent(Document document, int offset) {
+  public String getLineIndent(@NotNull Document document, int offset) {
     return delegate.getLineIndent(document, offset);
   }
 
@@ -165,7 +170,7 @@
   }
 
   @Override
-  public void reformatNewlyAddedElement(ASTNode block, ASTNode addedElement)
+  public void reformatNewlyAddedElement(@NotNull ASTNode block, @NotNull ASTNode addedElement)
       throws IncorrectOperationException {
     delegate.reformatNewlyAddedElement(block, addedElement);
   }
@@ -192,22 +197,23 @@
   }
 
   @Override
-  public int getSpacing(PsiFile file, int offset) {
+  public int getSpacing(@NotNull PsiFile file, int offset) {
     return delegate.getSpacing(file, offset);
   }
 
   @Override
-  public int getMinLineFeeds(PsiFile file, int offset) {
+  public int getMinLineFeeds(@NotNull PsiFile file, int offset) {
     return delegate.getMinLineFeeds(file, offset);
   }
 
   @Override
-  public void runWithDocCommentFormattingDisabled(PsiFile file, Runnable runnable) {
+  public void runWithDocCommentFormattingDisabled(
+      @NotNull PsiFile file, @NotNull Runnable runnable) {
     delegate.runWithDocCommentFormattingDisabled(file, runnable);
   }
 
   @Override
-  public DocCommentSettings getDocCommentSettings(PsiFile file) {
+  public @NotNull DocCommentSettings getDocCommentSettings(@NotNull PsiFile file) {
     return delegate.getDocCommentSettings(file);
   }
 
@@ -223,7 +229,8 @@
   }
 
   @Override
-  public int adjustLineIndent(final Document document, final int offset, FormattingMode mode)
+  public int adjustLineIndent(
+      final @NotNull Document document, final int offset, FormattingMode mode)
       throws IncorrectOperationException {
     if (delegate instanceof FormattingModeAwareIndentAdjuster) {
       return ((FormattingModeAwareIndentAdjuster) delegate)
@@ -233,7 +240,7 @@
   }
 
   @Override
-  public void scheduleReformatWhenSettingsComputed(PsiFile file) {
+  public void scheduleReformatWhenSettingsComputed(@NotNull PsiFile file) {
     delegate.scheduleReformatWhenSettingsComputed(file);
   }
 }
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java b/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java
index b6e21f7..a5e69c9 100644
--- a/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java
@@ -33,7 +33,7 @@
   private FormatterUtil() {}
 
   static Map<TextRange, String> getReplacements(
-      Formatter formatter, String text, Collection<TextRange> ranges) {
+      Formatter formatter, String text, Collection<? extends TextRange> ranges) {
     try {
       ImmutableMap.Builder<TextRange, String> replacements = ImmutableMap.builder();
       formatter
@@ -49,9 +49,8 @@
     }
   }
 
-  private static Collection<Range<Integer>> toRanges(Collection<TextRange> textRanges) {
-    return textRanges
-        .stream()
+  private static Collection<Range<Integer>> toRanges(Collection<? extends TextRange> textRanges) {
+    return textRanges.stream()
         .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset()))
         .collect(Collectors.toList());
   }
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java
index 52424c2..3d56743 100644
--- a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java
@@ -22,18 +22,21 @@
 import com.google.googlejavaformat.java.Formatter;
 import com.google.googlejavaformat.java.JavaFormatterOptions;
 import com.google.googlejavaformat.java.JavaFormatterOptions.Style;
+import com.intellij.ide.highlighter.JavaFileType;
 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.ChangedRangesInfo;
 import com.intellij.psi.codeStyle.CodeStyleManager;
 import com.intellij.psi.impl.CheckUtil;
 import com.intellij.util.IncorrectOperationException;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.TreeMap;
@@ -41,7 +44,7 @@
 
 /**
  * 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.
+ * Formatting of all other types of files is delegated to IntelliJ's default implementation.
  */
 class GoogleJavaFormatCodeStyleManager extends CodeStyleManagerDecorator {
 
@@ -50,7 +53,7 @@
   }
 
   @Override
-  public void reformatText(PsiFile file, int startOffset, int endOffset)
+  public void reformatText(@NotNull PsiFile file, int startOffset, int endOffset)
       throws IncorrectOperationException {
     if (overrideFormatterForFile(file)) {
       formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset)));
@@ -60,7 +63,7 @@
   }
 
   @Override
-  public void reformatText(PsiFile file, Collection<TextRange> ranges)
+  public void reformatText(@NotNull PsiFile file, @NotNull Collection<? extends TextRange> ranges)
       throws IncorrectOperationException {
     if (overrideFormatterForFile(file)) {
       formatInternal(file, ranges);
@@ -70,7 +73,19 @@
   }
 
   @Override
-  public void reformatTextWithContext(PsiFile file, Collection<TextRange> ranges) {
+  public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info)
+      throws IncorrectOperationException {
+    List<TextRange> ranges = new ArrayList<>();
+    if (info.insertedRanges != null) {
+      ranges.addAll(info.insertedRanges);
+    }
+    ranges.addAll(info.allChangedRanges);
+    reformatTextWithContext(file, ranges);
+  }
+
+  @Override
+  public void reformatTextWithContext(
+      @NotNull PsiFile file, @NotNull Collection<? extends TextRange> ranges) {
     if (overrideFormatterForFile(file)) {
       formatInternal(file, ranges);
     } else {
@@ -80,7 +95,10 @@
 
   @Override
   public PsiElement reformatRange(
-      PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly) {
+      @NotNull 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.
@@ -93,13 +111,13 @@
     }
   }
 
-  /** Return whether or not this formatter can handle formatting the given file. */
+  /** Return whether this formatter can handle formatting the given file. */
   private boolean overrideFormatterForFile(PsiFile file) {
-    return StdFileTypes.JAVA.equals(file.getFileType())
+    return JavaFileType.INSTANCE.equals(file.getFileType())
         && GoogleJavaFormatSettings.getInstance(getProject()).isEnabled();
   }
 
-  private void formatInternal(PsiFile file, Collection<TextRange> ranges) {
+  private void formatInternal(PsiFile file, Collection<? extends TextRange> ranges) {
     ApplicationManager.getApplication().assertWriteAccessAllowed();
     PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
     documentManager.commitAllDocuments();
@@ -123,9 +141,9 @@
    * 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)}.
+   * formatter (usually using {@link #performReplacements(Document, Map)}).
    */
-  private void format(Document document, Collection<TextRange> ranges) {
+  private void format(Document document, Collection<? extends TextRange> ranges) {
     Style style = GoogleJavaFormatSettings.getInstance(getProject()).getStyle();
     Formatter formatter = new Formatter(JavaFormatterOptions.builder().style(style).build());
     performReplacements(
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
index f6d9b5f..1e92a4b 100644
--- a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java
@@ -23,6 +23,7 @@
 import com.intellij.openapi.components.Storage;
 import com.intellij.openapi.project.Project;
 import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jetbrains.annotations.NotNull;
 
 @State(
     name = "GoogleJavaFormatSettings",
@@ -42,7 +43,7 @@
   }
 
   @Override
-  public void loadState(State state) {
+  public void loadState(@NotNull State state) {
     this.state = state;
   }
 
@@ -73,7 +74,7 @@
   enum EnabledState {
     UNKNOWN,
     ENABLED,
-    DISABLED;
+    DISABLED
   }
 
   static class State {
@@ -85,7 +86,7 @@
     public void setEnabled(@Nullable String enabledStr) {
       if (enabledStr == null) {
         enabled = EnabledState.UNKNOWN;
-      } else if (Boolean.valueOf(enabledStr)) {
+      } else if (Boolean.parseBoolean(enabledStr)) {
         enabled = EnabledState.ENABLED;
       } else {
         enabled = EnabledState.DISABLED;
diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java b/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java
index da02310..1906347 100644
--- a/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java
+++ b/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java
@@ -17,8 +17,8 @@
 package com.google.googlejavaformat.intellij;
 
 import com.intellij.notification.Notification;
-import com.intellij.notification.NotificationDisplayType;
 import com.intellij.notification.NotificationGroup;
+import com.intellij.notification.NotificationGroupManager;
 import com.intellij.notification.NotificationType;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.project.ProjectManagerListener;
@@ -28,11 +28,10 @@
 
   private static final String NOTIFICATION_TITLE = "Enable google-java-format";
   private static final NotificationGroup NOTIFICATION_GROUP =
-      new NotificationGroup(NOTIFICATION_TITLE, NotificationDisplayType.STICKY_BALLOON, true);
+      NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_TITLE);
 
   @Override
   public void projectOpened(@NotNull Project project) {
-
     GoogleJavaFormatSettings settings = GoogleJavaFormatSettings.getInstance(project);
 
     if (settings.isUninitialized()) {
diff --git a/pom.xml b/pom.xml
index 35bba26..c7f6856 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,23 +19,15 @@
          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.9</version>
+  <version>HEAD-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>
@@ -95,9 +87,14 @@
   <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>
+    <guava.version>31.0.1-jre</guava.version>
+    <truth.version>1.1.3</truth.version>
+    <checker.version>3.21.2</checker.version>
+    <errorprone.version>2.11.0</errorprone.version>
+    <auto-value.version>1.9</auto-value.version>
+    <auto-service.version>1.0.1</auto-service.version>
+    <maven-javadoc-plugin.version>3.3.1</maven-javadoc-plugin.version>
+    <maven-source-plugin.version>3.2.1</maven-source-plugin.version>
   </properties>
 
   <dependencyManagement>
@@ -118,14 +115,24 @@
       <dependency>
         <groupId>com.google.errorprone</groupId>
         <artifactId>error_prone_annotations</artifactId>
-        <version>2.0.8</version>
+        <version>${errorprone.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.google.auto.value</groupId>
+        <artifactId>auto-value-annotations</artifactId>
+        <version>${auto-value.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.google.auto.service</groupId>
+        <artifactId>auto-service-annotations</artifactId>
+        <version>${auto-service.version}</version>
       </dependency>
 
       <!-- Test dependencies -->
       <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
-        <version>4.12</version>
+        <version>4.13.2</version>
         <scope>test</scope>
       </dependency>
       <dependency>
@@ -140,6 +147,12 @@
         <version>${truth.version}</version>
         <scope>test</scope>
       </dependency>
+      <dependency>
+        <groupId>com.google.truth.extensions</groupId>
+        <artifactId>truth-java8-extension</artifactId>
+        <version>${truth.version}</version>
+        <scope>test</scope>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
@@ -148,28 +161,28 @@
       <plugins>
         <plugin>
           <artifactId>maven-compiler-plugin</artifactId>
-          <version>3.7.0</version>
+          <version>3.9.0</version>
         </plugin>
         <plugin>
           <artifactId>maven-jar-plugin</artifactId>
-          <version>3.0.2</version>
+          <version>3.2.2</version>
         </plugin>
         <plugin>
           <artifactId>maven-source-plugin</artifactId>
-          <version>2.1.2</version>
+          <version>3.2.1</version>
         </plugin>
         <plugin>
           <artifactId>maven-javadoc-plugin</artifactId>
-          <version>3.2.0</version>
+          <version>3.3.1</version>
         </plugin>
         <plugin>
           <artifactId>maven-gpg-plugin</artifactId>
-          <version>1.4</version>
+          <version>3.0.1</version>
         </plugin>
         <plugin>
           <groupId>org.apache.felix</groupId>
           <artifactId>maven-bundle-plugin</artifactId>
-          <version>2.4.0</version>
+          <version>5.1.4</version>
         </plugin>
       </plugins>
     </pluginManagement>
@@ -180,7 +193,9 @@
         <configuration>
           <source>${java.version}</source>
           <target>${java.version}</target>
+          <encoding>UTF-8</encoding>
           <compilerArgs>
+            <!-- compile-time arguments for google-java-format -->
             <arg>-XDcompilePolicy=simple</arg>
             <arg>-Xplugin:ErrorProne</arg>
             <arg>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
@@ -195,7 +210,17 @@
             <path>
               <groupId>com.google.errorprone</groupId>
               <artifactId>error_prone_core</artifactId>
-              <version>2.3.2</version>
+              <version>${errorprone.version}</version>
+            </path>
+            <path>
+              <groupId>com.google.auto.value</groupId>
+              <artifactId>auto-value</artifactId>
+              <version>${auto-value.version}</version>
+            </path>
+            <path>
+              <groupId>com.google.auto.service</groupId>
+              <artifactId>auto-service</artifactId>
+              <version>${auto-service.version}</version>
             </path>
           </annotationProcessorPaths>
         </configuration>
@@ -226,7 +251,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-javadoc-plugin</artifactId>
-        <version>3.2.0</version>
+        <version>3.3.1</version>
         <configuration>
           <doclint>none</doclint>
         </configuration>
@@ -242,12 +267,87 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
-        <version>2.18</version>
+        <version>2.22.2</version>
         <configuration>
           <!-- set heap size to work around http://github.com/travis-ci/travis-ci/issues/3396 -->
-          <argLine>-Xmx1024m</argLine>
+          <argLine>
+            -Xmx1024m
+            --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
+            --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
+            --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+            --add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
+            --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
+            --add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
+            --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+            --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
+            --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+            --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
+          </argLine>
         </configuration>
       </plugin>
     </plugins>
   </build>
+
+  <distributionManagement>
+    <snapshotRepository>
+      <id>sonatype-nexus-snapshots</id>
+      <name>Sonatype Nexus Snapshots</name>
+      <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
+    </snapshotRepository>
+    <repository>
+      <id>sonatype-nexus-staging</id>
+      <name>Nexus Release Repository</name>
+      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+    </repository>
+  </distributionManagement>
+
+  <profiles>
+    <profile>
+      <id>sonatype-oss-release</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-source-plugin</artifactId>
+            <version>${maven-source-plugin.version}</version>
+            <executions>
+              <execution>
+                <id>attach-sources</id>
+                <goals>
+                  <goal>jar-no-fork</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <version>${maven-javadoc-plugin.version}</version>
+            <executions>
+              <execution>
+                <id>attach-javadocs</id>
+                <goals>
+                  <goal>jar</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-gpg-plugin</artifactId>
+            <version>3.0.1</version>
+            <executions>
+              <execution>
+                <id>sign-artifacts</id>
+                <phase>verify</phase>
+                <goals>
+                  <goal>sign</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
 </project>
diff --git a/scripts/google-java-format-diff.py b/scripts/google-java-format-diff.py
index 1abd037..151ae33 100755
--- a/scripts/google-java-format-diff.py
+++ b/scripts/google-java-format-diff.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2.7
+#!/usr/bin/env python3
 #
 #===- google-java-format-diff.py - google-java-format Diff Reformatter -----===#
 #
@@ -31,9 +31,9 @@
 import re
 import string
 import subprocess
-import StringIO
+import io
 import sys
-from distutils.spawn import find_executable
+from shutil import which
 
 def main():
   parser = argparse.ArgumentParser(description=
@@ -59,6 +59,11 @@
                       help='do not fix the import order')
   parser.add_argument('--skip-removing-unused-imports', action='store_true',
                       help='do not remove ununsed imports')
+  parser.add_argument(
+      '--skip-javadoc-formatting',
+      action='store_true',
+      default=False,
+      help='do not reformat javadoc')
   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')
@@ -100,13 +105,13 @@
   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'
+    binary = which('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():
+  for filename, lines in lines_by_file.items():
     if args.i and args.verbose:
-      print 'Formatting', filename
+      print('Formatting', filename)
     command = base_command[:]
     if args.i:
       command.append('-i')
@@ -116,6 +121,8 @@
       command.append('--skip-sorting-imports')
     if args.skip_removing_unused_imports:
       command.append('--skip-removing-unused-imports')
+    if args.skip_javadoc_formatting:
+      command.append('--skip-javadoc-formatting')
     command.extend(lines)
     command.append(filename)
     p = subprocess.Popen(command, stdout=subprocess.PIPE,
@@ -127,11 +134,11 @@
     if not args.i:
       with open(filename) as f:
         code = f.readlines()
-      formatted_code = StringIO.StringIO(stdout).readlines()
+      formatted_code = io.StringIO(stdout.decode('utf-8')).readlines()
       diff = difflib.unified_diff(code, formatted_code,
                                   filename, filename,
                                   '(before formatting)', '(after formatting)')
-      diff_string = string.join(diff, '')
+      diff_string = ''.join(diff)
       if len(diff_string) > 0:
         sys.stdout.write(diff_string)