diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..735cccaf7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,130 @@ +# Copyright 2020 The Google Java Format 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-OpenJDK: + name: "JDK ${{ matrix.java }} on ${{ matrix.os }}" + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + java: [25, 21] + 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: 25 + experimental: false + - os: windows-latest + java: 25 + experimental: false + - os: ubuntu-latest + java: 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@v4 + - name: "Set up JDK ${{ matrix.java }} from jdk.java.net" + if: ${{ matrix.java == 'EA' }} + uses: oracle-actions/setup-java@v1 + with: + website: jdk.java.net + release: ${{ matrix.java }} + - name: "Set up JDK ${{ matrix.java }} from Zulu" + if: ${{ matrix.java != 'EA' && matrix.java != 'GraalVM' }} + uses: actions/setup-java@v4 + 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 + + test-GraalVM: + name: "GraalVM on ${{ matrix.os }}" + strategy: + fail-fast: false + matrix: + # Use "oldest" available ubuntu-* instead of -latest, + # see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories; + # due to https://github.com/google/google-java-format/issues/1072. + os: [ubuntu-22.04, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + continue-on-error: true + steps: + - name: Cancel previous + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + - name: "Check out repository" + uses: actions/checkout@v4 + - name: "Set up GraalVM ${{ matrix.java }}" + uses: graalvm/setup-graalvm@v1 + with: + java-version: "25" + distribution: "graalvm-community" + github-token: ${{ secrets.GITHUB_TOKEN }} + native-image-job-reports: "true" + cache: "maven" + - name: "Native Build" + run: mvn -Pnative -DskipTests package -pl core -am + - name: "Native Test" + # Bash script for testing won't work on Windows + # TODO: Anyone reading this wants to write a *.bat or *.ps1 equivalent? + if: ${{ matrix.os != 'windows-latest' }} + run: util/test-native.sh + + publish_snapshot: + name: "Publish snapshot" + needs: test-OpenJDK + 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@v4 + - name: "Set up JDK 21" + uses: actions/setup-java@v4 + with: + java-version: 21 + 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 000000000..43dd0ed29 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,120 @@ +# Copyright 2020 The Google Java Format 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: 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: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: "zulu" + cache: "maven" + server-id: central + server-username: CI_DEPLOY_USERNAME + server-password: CI_DEPLOY_PASSWORD + gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Bump Version Number + run: | + mvn --no-transfer-progress versions:set versions:commit -DnewVersion="${{ github.event.inputs.version }}" + mvn --no-transfer-progress versions:set versions:commit -DnewVersion="${{ github.event.inputs.version }}" -pl eclipse_plugin + mvn tycho-versions:update-eclipse-metadata -pl eclipse_plugin + git ls-files | grep -E '(pom.xml|MANIFEST.MF)$' | 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 }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + 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 Artifacts 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 + + build-native-image: + name: "Build GraalVM native-image on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + permissions: + contents: write + needs: build-maven-jars + strategy: + matrix: + # Use "oldest" available ubuntu-* instead of -latest, + # see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories; + # due to https://github.com/google/google-java-format/issues/1072. + os: [ubuntu-22.04, ubuntu-22.04-arm, macos-latest, windows-latest] + env: + # NB: Must keep the keys in this inline JSON below in line with the os: above! + SUFFIX: ${{fromJson('{"ubuntu-22.04":"linux-x86-64", "ubuntu-22.04-arm":"linux-arm64", "macos-latest":"darwin-arm64", "windows-latest":"windows-x86-64"}')[matrix.os]}} + EXTENSION: ${{ matrix.os == 'windows-latest' && '.exe' || '' }} + steps: + - name: "Check out repository" + uses: actions/checkout@v4 + - name: "Set up GraalVM" + uses: graalvm/setup-graalvm@v1 + with: + java-version: "25" + distribution: "graalvm-community" + github-token: ${{ secrets.GITHUB_TOKEN }} + native-image-job-reports: "true" + cache: "maven" + - name: Bump Version Number + run: mvn --no-transfer-progress versions:set versions:commit -DnewVersion="${{ github.event.inputs.version }}" + - name: "Native" + run: mvn -Pnative -DskipTests package -pl core -am + - name: "Move outputs" + run: cp core/target/google-java-format${{ env.EXTENSION }} google-java-format_${{ env.SUFFIX }}${{ env.EXTENSION }} + - name: "Upload native-image" + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release upload "v${{ github.event.inputs.version }}" "google-java-format_${{ env.SUFFIX }}${{ env.EXTENSION }}" diff --git a/.gitignore b/.gitignore index db4325627..235f69d3d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ target/ bin/ out/ +eclipse_plugin/lib/ diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 000000000..2e8f1d41f --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,13 @@ +--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 + +-Djdk.xml.maxGeneralEntitySizeLimit=0 +-Djdk.xml.totalEntitySizeLimit=0 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 03bf5124f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: java - -notifications: - email: - recipients: - - google-java-format-dev+ci@google.com - on_success: change - on_failure: always - -jdk: - - oraclejdk8 - -# use travis-ci docker based infrastructure -sudo: false - -cache: - directories: - - $HOME/.m2 - -install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V - -script: mvn test -B - -env: - global: - - secure: KkUX74NDDk95WR60zwN6x6pz49KAfR0zUu1thxl8Kke0+WVoIv1EBo7/e4ZXTdBKxlzQCX9Aa0OlIyUlhGJeuNIGtX16lcNyZNmKSacfrT68MpZqi+nAiYp8tnOyW/zuI+shSKHkGQOFq6c9KTtR9vG8kjr1Q9dNl/H5QjGaG1ZMiU/mGH9ompf+0nQTMDLKaEWV+SpKGjK5U1Zs2p08I9KKePbZoi9L2oAw5cH9wW8Q3pQJds6Rkwy9aecxRd4xmTza7Lb04dmByjqY8gsIjrTN0onOndBmLKTHiH5NVLKf0ilEVGiMQ1x4eCQolcRpGzxdTTKI0ahiWS59UABVoy1sXYqkIbZjpmMuGhHvbRir7YEXaG8LRUAxdWd9drJfvKQeBphQlIJKwajHSiMAdc9zisQg1UW75HSGKoPDHpzq+P7YBil2PUjk+5yUy5OytX6IebFT4KdeCO2ayu338yqb2t8q98elMoD5jwFVD0tpkLQ6xsYodClSGfMCVfP2zTkB7c4sHZV7tJS68CiNt7sCwz9CTNApFiSWMBxLKkKQ7VSBTy9bAn+phvW0u/maGsrRnehmsV3PVPtEsMlrqeMGwaPqIwx1l6otVQCnGRt3e8z3HoxY6AaBPaX0Z8lH2y+BxYhWTYzGhRxyyV666u/9yekTXmH53c7at7mau6Q= - - secure: VWnZcPA4esdaMJgh0Mui7K5O++AGZY3AYswufd0UUbAmnK60O6cDBOSelnr7hImDgZ09L2RWMXIVCt4b+UFXoIhqrvZKVitUXPldS6uNJeGT9p6quFf36o8Wf0ppKWnPd66AY6ECnE75Ujn1Maw899kb3zY2SvIvzA7HlXqtmowHCVGoJ4ou6LQxJpVEJ4hjvS2gQMF9W31uOzRzMI1JhdZioYmqe6eq9sGmRZZiYON7jBqX8f4XW0tTZoK+dVRNwYQcwyqcvQpxeI15VWDq5cqjBw3ps5XSEYTNIFUXREnEEi+vLdCuw/YRZp1ij7LiQKp6bcb2KROXaWii4VpUNWxIAflm4Nvn/8pa/3CUwqIbxTSAL+Qkb2iEzuYuNPGLr72mQgGEnlSpaqzUx0miwjJ41x3Q8mf72ihIME7YQGMDJL7TA7/GjXFeSxroPk65tbssAGmbjwGGJX67NHUzeQPW2QPA2cohCHyopKB9GqhKgKwKjenkCUaBGCaZReZz9XkSkHTXlxxSakMTmgJnA9To9d2lPOy0nppUvrd/0uAbPuxxCZqXElRvOvHKzpV1ZpKpqSxvjh63mCQRTi2rFiPn8uFaajai9mHaPoGmNwQwIUbAviNqifuIEPpc6cOuyV0MWJwdFLo1SKamJya/MwQz+IwXuY2TX7Fmv9HovdM= - -after_success: -- util/publish-snapshot-on-commit.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ba853922..0f5b8cf48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,11 @@ -Want to contribute? Great! First, read this page (including the small print at the end). +Want to contribute? Great! First, read this page (including the small print at +the end). ### Before you contribute -Before we can use your code, you must sign the -[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) + +Before we can use your code, you must sign the [Google Individual Contributor +License +Agreement](https://developers.google.com/open-source/cla/individual?csw=1) (CLA), which you can do online. The CLA is necessary mainly because you own the copyright to your changes, even after your contribution becomes part of our codebase, so we need your permission to use and distribute your code. We also @@ -16,9 +19,11 @@ possibly guide you. Coordinating up front makes it much easier to avoid frustration later on. ### Code reviews + All submissions, including submissions by project members, require review. We -use Github pull requests for this purpose. +use GitHub pull requests for this purpose. ### The small print -Contributions made by corporations are covered by a different agreement than -the one above, the Software Grant and Corporate Contributor License Agreement. + +Contributions made by corporations are covered by a different agreement than the +one above, the Software Grant and Corporate Contributor License Agreement. diff --git a/README.md b/README.md index 209a34bff..a5c400122 100644 --- a/README.md +++ b/README.md @@ -7,48 +7,134 @@ ## Using the formatter -### from the command-line +### From the command-line [Download the formatter](https://github.com/google/google-java-format/releases) and run it with: ``` -java -jar /path/to/google-java-format-1.3-all-deps.jar [files...] +java -jar /path/to/google-java-format-${GJF_VERSION?}-all-deps.jar [files...] ``` +Note that it uses the `jdk.compiler` module to parse the Java source code. The +`java` binary version used must therefore be from a JDK (not JRE) with a version +equal to or newer than the Java language version of the files being formatted. +The minimum Java version can be found in `core/pom.xml` (currently Java 17). An +alternative is to use the available GraalVM based native binaries instead. + The formatter can act on whole files, on limited lines (`--lines`), on specific -ofsets (`--offset`), passing through to standard-out (default) or altered +offsets (`--offset`), passing through to standard-out (default) or altered in-place (`--replace`). +Option `--help` will print full usage details; including built-in documentation +about other flags, such as `--aosp`, `--fix-imports-only`, +`--skip-sorting-imports`, `--skip-removing-unused-import`, +`--skip-reflowing-long-strings`, `--skip-javadoc-formatting`, or the `--dry-run` +and `--set-exit-if-changed`. + +Using `@` reads options and filenames from a file, instead of +arguments. + To reformat changed lines in a specific patch, use -[`google-java-format-diff.py`] -(https://github.com/google/google-java-format/blob/master/scripts/google-java-format-diff.py) +[`google-java-format-diff.py`](https://github.com/google/google-java-format/blob/master/scripts/google-java-format-diff.py). ***Note:*** *There is no configurability as to the formatter's algorithm for formatting. This is a deliberate design decision to unify our code formatting on a single format.* -### IntelliJ +### IntelliJ, Android Studio, and other JetBrains IDEs + +A +[google-java-format IntelliJ plugin](https://plugins.jetbrains.com/plugin/8527) +is available from the plugin repository. To install it, go to your IDE's +settings and select the `Plugins` category. Click the `Marketplace` tab, search +for the `google-java-format` plugin, and click the `Install` button. -A [google-java-format IntelliJ plugin](https://plugins.jetbrains.com/plugin/8527) -is available from the plugin repository. +The plugin will be disabled by default. To enable, +[open the Project settings](https://www.jetbrains.com/help/idea/configure-project-settings.html), +then click "google-java-format Settings" and check the "Enable +google-java-format" checkbox. -The plugin will not be enabled by default. To enable it in the current project, -go to "File→Settings...→google-java-format Settings" and check the "Enable" -checkbox. +To enable it by default in new projects, +[open the default settings for new projects](https://www.jetbrains.com/help/idea/configure-project-settings.html#new-default-settings) +and configure it under "Other Settings/google-java-format Settings". -To enable it by default in new projects, use "File→Other Settings→Default -Settings...". +When enabled, it will replace the normal `Reformat Code` and `Optimize Imports` +actions. + +#### IntelliJ JRE Config + +The google-java-format plugin uses some internal classes that aren't available +without extra configuration. To use the plugin, you need to +[add some options to your IDE's Java runtime](https://www.jetbrains.com/help/idea/tuning-the-ide.html#procedure-jvm-options). +To do that, go to `Help→Edit Custom VM Options...` and paste in these lines: + +``` +--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.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 +``` + +Once you've done that, restart the IDE. ### Eclipse -A [google-java-format Eclipse -plugin](https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-eclipse-plugin-1.3.0.jar) -can be downloaded from the releases page. +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. + +The plugin adds two formatter implementations: + +* `google-java-format`: using 2 spaces indent +* `aosp-java-format`: using 4 spaces indent + +These that can be selected in "Window" > "Preferences" > "Java" > "Code Style" > +"Formatter" > "Formatter Implementation". -The plugin adds a `google-java-format` formatter implementation that can be -configured in `Window > Preferences > Java > Code Style > Formatter > Formatter -Implementation`. +#### Eclipse JRE Config + +The plugin uses some internal classes that aren't available without extra +configuration. To use the plugin, you will need to edit the +[`eclipse.ini`](https://wiki.eclipse.org/Eclipse.ini) file. + +Open the `eclipse.ini` file in any editor and paste in these lines towards the +end (but anywhere after `-vmargs` will do): + +``` +--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.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 +``` + +Once you've done that, restart the IDE. + +### Third-party integrations + +* Visual Studio Code + * [google-java-format-for-vs-code](https://marketplace.visualstudio.com/items?itemName=JoseVSeb.google-java-format-for-vs-code) +* Gradle plugins + * [spotless](https://github.com/diffplug/spotless/tree/main/plugin-gradle#google-java-format) + * [sherter/google-java-format-gradle-plugin](https://github.com/sherter/google-java-format-gradle-plugin) +* Apache Maven plugins + * [spotless](https://github.com/diffplug/spotless/tree/main/plugin-maven#google-java-format) + * [spotify/fmt-maven-plugin](https://github.com/spotify/fmt-maven-plugin) + * [talios/googleformatter-maven-plugin](https://github.com/talios/googleformatter-maven-plugin) + * [Cosium/maven-git-code-format](https://github.com/Cosium/maven-git-code-format): + A maven plugin that automatically deploys google-java-format as a + pre-commit git hook. +* SBT plugins + * [sbt/sbt-java-formatter](https://github.com/sbt/sbt-java-formatter) +* [Github Actions](https://github.com/features/actions) + * [googlejavaformat-action](https://github.com/axel-op/googlejavaformat-action): + Automatically format your Java files when you push on github ### as a library @@ -56,13 +142,26 @@ The formatter can be used in software which generates java to output more legible java code. Just include the library in your maven/gradle/etc. configuration. +`google-java-format` uses internal javac APIs for parsing Java source. The +following JVM flags are required when running on JDK 16 and newer, due to +[JEP 396: Strongly Encapsulate JDK Internals by Default](https://openjdk.java.net/jeps/396): + +``` +--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.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 +``` + #### Maven ```xml com.google.googlejavaformat google-java-format - 1.3 + ${google-java-format.version} ``` @@ -70,7 +169,7 @@ configuration. ```groovy dependencies { - compile 'com.google.googlejavaformat:google-java-format:1.3' + implementation 'com.google.googlejavaformat:google-java-format:$googleJavaFormatVersion' } ``` @@ -93,7 +192,9 @@ Your starting point should be the instance methods of ## Building from source - mvn install +``` +mvn install +``` ## Contributing diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index a4c7ee9cb..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -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 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 9e154ea0a..3b106cf71 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -22,7 +22,7 @@ com.google.googlejavaformat google-java-format-parent - 1.4-SNAPSHOT + HEAD-SNAPSHOT google-java-format @@ -39,15 +39,11 @@ com.google.guava guava - - com.google.errorprone - javac - - com.google.code.findbugs - jsr305 + org.jspecify + jspecify true @@ -55,7 +51,16 @@ error_prone_annotations true - + + com.google.auto.value + auto-value-annotations + true + + + com.google.auto.service + auto-service-annotations + true + @@ -69,12 +74,16 @@ com.google.truth truth - 0.26 + + + com.google.truth.extensions + truth-java8-extension + test com.google.testing.compile compile-testing - 0.8 + 0.19 test @@ -84,14 +93,22 @@ maven-javadoc-plugin + 17 UTF-8 UTF-8 UTF-8 - http://google.github.io/guava/releases/${guava.version}/api/docs/ - http://jsr-305.googlecode.com/svn/trunk/javadoc/ - http://docs.oracle.com/javase/7/docs/api/ + https://guava.dev/releases/${guava.version}/api/docs/ + https://docs.oracle.com/en/java/javase/11/docs/api + + --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED,com.google.googlejavaformat + @@ -106,7 +123,7 @@ org.apache.maven.plugins maven-shade-plugin - 2.4.3 + 3.2.4 shade-all-deps @@ -144,54 +161,118 @@ true + + + com.google.googlejavaformat + + - maven-resources-plugin - 3.0.1 + com.google.code.maven-replacer-plugin + replacer + 1.5.3 - copy-resources - package + generate-sources - copy-resources + replace - - ../eclipse_plugin/lib - - - target - ${project.artifactId}-${project.version}.jar - - - + + ${project.basedir}/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template + ${project.build.directory}/generated-sources/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java + + + %VERSION% + ${project.version} + + + - org.apache.maven.plugins - maven-dependency-plugin - 2.10 + org.codehaus.mojo + build-helper-maven-plugin + 3.3.0 - copy-dependencies - package + add-source + generate-sources - copy-dependencies + add-source - ../eclipse_plugin/lib - true - true - true - org.eclipse.jdt.core - compile - provided + + ${project.build.directory}/generated-sources/java/ + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + + native + + + + org.graalvm.buildtools + native-maven-plugin + 0.10.0 + true + + + build-native + + compile-no-fork + + package + + + test-native + + test + + test + + + + google-java-format + + ${project.build.directory}/${project.artifactId}-${project.version}-all-deps.jar + + + -H:+UnlockExperimentalVMOptions + -H:IncludeResourceBundles=com.sun.tools.javac.resources.compiler + -H:IncludeResourceBundles=com.sun.tools.javac.resources.javac + --no-fallback + --initialize-at-build-time=com.sun.tools.javac.file.Locations + -H:+ReportExceptionStackTraces + -H:-UseContainerSupport + -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + -march=compatibility + + + + + + + diff --git a/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java index 45e507bbd..1e33003b0 100644 --- a/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java +++ b/core/src/main/java/com/google/googlejavaformat/CommentsHelper.java @@ -14,6 +14,10 @@ package com.google.googlejavaformat; +import com.google.googlejavaformat.Input.Tok; +import java.util.Optional; +import java.util.regex.Pattern; + /** * Rewrite comments. This interface is implemented by {@link * com.google.googlejavaformat.java.JavaCommentsHelper JavaCommentsHelper}. @@ -28,4 +32,19 @@ public interface CommentsHelper { * @return the rewritten comment */ String rewrite(Input.Tok tok, int maxWidth, int column0); + + static Optional reformatParameterComment(Tok tok) { + if (!tok.isSlashStarComment()) { + return Optional.empty(); + } + var match = PARAMETER_COMMENT.matcher(tok.getOriginalText()); + if (!match.matches()) { + return Optional.empty(); + } + return Optional.of(String.format("/* %s= */", match.group(1))); + } + + Pattern PARAMETER_COMMENT = + Pattern.compile( + "/\\*\\s*(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\Q...\\E)?)\\s*=\\s*\\*/"); } diff --git a/core/src/main/java/com/google/googlejavaformat/Doc.java b/core/src/main/java/com/google/googlejavaformat/Doc.java index 168e03f87..6414a3fb1 100644 --- a/core/src/main/java/com/google/googlejavaformat/Doc.java +++ b/core/src/main/java/com/google/googlejavaformat/Doc.java @@ -15,15 +15,19 @@ package com.google.googlejavaformat; import static com.google.common.collect.Iterables.getLast; +import static com.google.googlejavaformat.CommentsHelper.reformatParameterComment; +import static java.lang.Math.max; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.Iterators; import com.google.common.collect.Range; import com.google.googlejavaformat.Output.BreakTag; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * {@link com.google.googlejavaformat.java.JavaInputAstVisitor JavaInputAstVisitor} outputs a @@ -59,6 +63,16 @@ public enum FillMode { FORCED } + /** + * The maximum supported line width. + * + *

This can be used as a sentinel/threshold for {@code Doc}s that break unconditionally. + * + *

The value was selected to be obviously too large for any practical line, but small enough to + * prevent accidental overflow. + */ + public static final int MAX_LINE_WIDTH = 1000; + /** State for writing. */ public static final class State { final int lastIndent; @@ -99,43 +113,31 @@ public String toString() { private static final Range EMPTY_RANGE = Range.closedOpen(-1, -1); private static final DiscreteDomain INTEGERS = DiscreteDomain.integers(); - // Memoized width; Float.POSITIVE_INFINITY if contains forced breaks. - private boolean widthComputed = false; - private float width = 0.0F; + private final Supplier width = Suppliers.memoize(this::computeWidth); // Memoized flat; not defined (and never computed) if contains forced breaks. - private boolean flatComputed = false; - private String flat = ""; + private final Supplier flat = Suppliers.memoize(this::computeFlat); // Memoized Range. - private boolean rangeComputed = false; - private Range range = EMPTY_RANGE; + private final Supplier> range = Suppliers.memoize(this::computeRange); /** - * Return the width of a {@code Doc}, or {@code Float.POSITIVE_INFINITY} if it must be broken. + * Return the width of a {@code Doc}. * * @return the width */ - final float getWidth() { - if (!widthComputed) { - width = computeWidth(); - widthComputed = true; - } - return width; + final int getWidth() { + return width.get(); } /** - * Return a {@code Doc}'s flat-string value; not defined (and never called) if the (@code Doc} + * Return a {@code Doc}'s flat-string value; not defined (and never called) if the {@code Doc} * contains forced breaks. * * @return the flat-string value */ final String getFlat() { - if (!flatComputed) { - flat = computeFlat(); - flatComputed = true; - } - return flat; + return flat.get(); } /** @@ -144,19 +146,15 @@ final String getFlat() { * @return the {@code Doc}'s {@link Range} */ final Range range() { - if (!rangeComputed) { - range = computeRange(); - rangeComputed = true; - } - return range; + return range.get(); } /** * Compute the {@code Doc}'s width. * - * @return the width, or {@code Float.POSITIVE_INFINITY} if it must be broken + * @return the width */ - abstract float computeWidth(); + abstract int computeWidth(); /** * Compute the {@code Doc}'s flat value. Not defined (and never called) if contains forced breaks. @@ -213,12 +211,8 @@ void add(Doc doc) { } @Override - float computeWidth() { - float thisWidth = 0.0F; - for (Doc doc : docs) { - thisWidth += doc.getWidth(); - } - return thisWidth; + int computeWidth() { + return getWidth(docs); } @Override @@ -257,10 +251,10 @@ Range computeRange() { @Override public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) { - float thisWidth = getWidth(); + int thisWidth = getWidth(); if (state.column + thisWidth <= maxWidth) { oneLine = true; - return state.withColumn(state.column + (int) thisWidth); + return state.withColumn(state.column + thisWidth); } State broken = computeBroken( @@ -271,11 +265,11 @@ public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State st private static void splitByBreaks(List docs, List> splits, List breaks) { splits.clear(); breaks.clear(); - splits.add(new ArrayList()); + splits.add(new ArrayList<>()); for (Doc doc : docs) { if (doc instanceof Break) { breaks.add((Break) doc); - splits.add(new ArrayList()); + splits.add(new ArrayList<>()); } else { getLast(splits).add(doc); } @@ -288,7 +282,7 @@ private State computeBroken(CommentsHelper commentsHelper, int maxWidth, State s state = computeBreakAndSplit( - commentsHelper, maxWidth, state, Optional.absent(), splits.get(0)); + commentsHelper, maxWidth, state, /* optBreakDoc= */ Optional.empty(), splits.get(0)); // Handle following breaks and split. for (int i = 0; i < breaks.size(); i++) { @@ -306,8 +300,8 @@ private static State computeBreakAndSplit( State state, Optional optBreakDoc, List split) { - float breakWidth = optBreakDoc.isPresent() ? optBreakDoc.get().getWidth() : 0.0F; - float splitWidth = getWidth(split); + int breakWidth = optBreakDoc.isPresent() ? optBreakDoc.get().getWidth() : 0; + int splitWidth = getWidth(split); boolean shouldBreak = (optBreakDoc.isPresent() && optBreakDoc.get().fillMode == FillMode.UNIFIED) || state.mustBreak @@ -359,12 +353,16 @@ private void writeFilled(Output output) { * Get the width of a sequence of {@link Doc}s. * * @param docs the {@link Doc}s - * @return the width, or {@code Float.POSITIVE_INFINITY} if any {@link Doc} must be broken + * @return the width */ - static float getWidth(List docs) { - float width = 0.0F; + static int getWidth(List docs) { + int width = 0; for (Doc doc : docs) { width += doc.getWidth(); + + if (width >= MAX_LINE_WIDTH) { + return MAX_LINE_WIDTH; // Paranoid overflow protection + } } return width; } @@ -399,6 +397,10 @@ boolean isReal() { private final Indent plusIndentCommentsBefore; private final Optional breakAndIndentTrailingComment; + private Input.Tok tok() { + return token.getTok(); + } + private Token( Input.Token token, RealOrImaginary realOrImaginary, @@ -466,8 +468,9 @@ public void add(DocBuilder builder) { } @Override - float computeWidth() { - return token.getTok().length(); + int computeWidth() { + int idx = Newlines.firstBreak(tok().getOriginalText()); + return (idx >= 0) ? MAX_LINE_WIDTH : tok().length(); } @Override @@ -482,8 +485,7 @@ Range computeRange() { @Override public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State state) { - String text = token.getTok().getOriginalText(); - return state.withColumn(state.column + text.length()); + return state.withColumn(state.column + computeWidth()); } @Override @@ -523,8 +525,8 @@ public void add(DocBuilder builder) { } @Override - float computeWidth() { - return 1.0F; + int computeWidth() { + return 1; } @Override @@ -571,19 +573,19 @@ private Break(FillMode fillMode, String flat, Indent plusIndent, Optionalabsent()); + return new Break(fillMode, flat, plusIndent, /* optTag= */ Optional.empty()); } /** * Make a {@code Break}. * * @param fillMode the {@link FillMode} - * @param flat the the text when not broken + * @param flat the text when not broken * @param plusIndent extra indent if taken * @param optTag an optional tag for remembering whether the break was taken * @return the new {@code Break} @@ -626,8 +628,8 @@ public void add(DocBuilder builder) { } @Override - float computeWidth() { - return isForced() ? Float.POSITIVE_INFINITY : (float) flat.length(); + int computeWidth() { + return isForced() ? MAX_LINE_WIDTH : flat.length(); } @Override @@ -653,7 +655,7 @@ public State computeBreaks(State state, int lastIndent, boolean broken) { 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; @@ -716,22 +718,31 @@ public void add(DocBuilder builder) { } @Override - float computeWidth() { + int computeWidth() { int idx = Newlines.firstBreak(tok.getOriginalText()); // only count the first line of multi-line block comments if (tok.isComment()) { if (idx > 0) { return idx; + } else if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) { + // Account for line comments with missing spaces, see computeFlat. + return tok.length() + 1; } else { - return tok.length(); + return reformatParameterComment(tok).map(String::length).orElse(tok.length()); } } - return idx != -1 ? Float.POSITIVE_INFINITY : (float) tok.length(); + return idx != -1 ? MAX_LINE_WIDTH : tok.length(); } @Override String computeFlat() { - return tok.getOriginalText(); + // TODO(cushon): commentsHelper.rewrite doesn't get called for spans that fit in a single + // line. That's fine for multi-line comment reflowing, but problematic for adding missing + // spaces in line comments. + if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) { + return "// " + tok.getOriginalText().substring("//".length()); + } + return reformatParameterComment(tok).orElse(tok.getOriginalText()); } @Override diff --git a/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java b/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java index be7f8a6ca..252da5bdf 100644 --- a/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java +++ b/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java @@ -49,7 +49,7 @@ public int line() { } /** - * Returns the 0-indexed column number on which the error occurred, or {@code -1} if the error + * Returns the 1-indexed column number on which the error occurred, or {@code -1} if the error * does not have a column. */ public int column() { @@ -61,14 +61,14 @@ public String message() { return message; } + @Override public String toString() { StringBuilder sb = new StringBuilder(); if (lineNumber >= 0) { sb.append(lineNumber).append(':'); } if (column >= 0) { - // internal column numbers are 0-based, but diagnostics use 1-based indexing by convention - sb.append(column + 1).append(':'); + sb.append(column).append(':'); } if (lineNumber >= 0 || column >= 0) { sb.append(' '); diff --git a/core/src/main/java/com/google/googlejavaformat/FormattingError.java b/core/src/main/java/com/google/googlejavaformat/FormattingError.java index 09c7a97af..50381d0cd 100644 --- a/core/src/main/java/com/google/googlejavaformat/FormattingError.java +++ b/core/src/main/java/com/google/googlejavaformat/FormattingError.java @@ -14,14 +14,8 @@ package com.google.googlejavaformat; -import static java.util.Locale.ENGLISH; - -import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import javax.tools.Diagnostic; -import javax.tools.JavaFileObject; /** An unchecked formatting error. */ public class FormattingError extends Error { @@ -40,20 +34,4 @@ public FormattingError(Iterable diagnostics) { public ImmutableList diagnostics() { return diagnostics; } - - public static FormattingError fromJavacDiagnostics( - Iterable> diagnostics) { - return new FormattingError(Iterables.transform(diagnostics, TO_FORMATTER_DIAGNOSTIC)); - } - - private static final Function, FormatterDiagnostic> TO_FORMATTER_DIAGNOSTIC = - new Function, FormatterDiagnostic>() { - @Override - public FormatterDiagnostic apply(Diagnostic input) { - return FormatterDiagnostic.create( - (int) input.getLineNumber(), - (int) input.getColumnNumber(), - input.getMessage(ENGLISH)); - } - }; } diff --git a/core/src/main/java/com/google/googlejavaformat/Input.java b/core/src/main/java/com/google/googlejavaformat/Input.java index c2af02b7f..66a392190 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 @@ public interface Tok { /** 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? */ @@ -111,6 +111,20 @@ public interface Token { public abstract String getText(); + /** + * Get the number of toks. + * + * @return the number of toks, excluding the EOF tok + */ + public abstract int getkN(); + + /** + * Get the Token by index. + * + * @param k the Tok index + */ + public abstract Token getToken(int k); + @Override public String toString() { return MoreObjects.toStringHelper(this).add("super", super.toString()).toString(); diff --git a/core/src/main/java/com/google/googlejavaformat/InputOutput.java b/core/src/main/java/com/google/googlejavaformat/InputOutput.java index ea5831610..46dc70bbc 100644 --- a/core/src/main/java/com/google/googlejavaformat/InputOutput.java +++ b/core/src/main/java/com/google/googlejavaformat/InputOutput.java @@ -53,15 +53,9 @@ public final String getLine(int lineI) { return lines.get(lineI); } - /** The {@link Range}s of the tokens or comments beginning on each line. */ - protected final List> range0s = new ArrayList<>(); - /** The {@link Range}s of the tokens or comments lying on each line, in any part. */ protected final List> ranges = new ArrayList<>(); - /** The {@link Range}s of the tokens or comments ending on each line. */ - protected final List> range1s = new ArrayList<>(); - private static void addToRanges(List> ranges, int i, int k) { while (ranges.size() <= i) { ranges.add(EMPTY_RANGE); @@ -78,11 +72,9 @@ protected final void computeRanges(List toks) { lineI += Newlines.count(txt); int k = tok.getIndex(); if (k >= 0) { - addToRanges(range0s, lineI0, k); for (int i = lineI0; i <= lineI; i++) { addToRanges(ranges, i, k); } - addToRanges(range1s, lineI0, k); } } } @@ -91,11 +83,10 @@ protected final void computeRanges(List toks) { * Given an {@code InputOutput}, compute the map from tok indices to line ranges. * * @param put the {@code InputOutput} - * @param kN the number of tokens - * @return the map from {@link com.google.googlejavaformat.java.JavaInput.Tok} indices to line + * @return the map from {@code com.google.googlejavaformat.java.JavaInput.Tok} indices to line * ranges in this {@code put} */ - public static Map> makeKToIJ(InputOutput put, int kN) { + public static Map> makeKToIJ(InputOutput put) { Map> map = new HashMap<>(); int ijN = put.getLineCount(); for (int ij = 0; ij <= ijN; ij++) { @@ -111,16 +102,6 @@ public static Map> makeKToIJ(InputOutput put, int kN) { return map; } - /** - * Get the {@link Range} of {@link Input.Tok}s beginning on a line. - * - * @param lineI the line number - * @return the {@link Range} of {@link Input.Tok}s beginning on the specified line - */ - public final Range getRange0s(int lineI) { - return 0 <= lineI && lineI < range0s.size() ? range0s.get(lineI) : EMPTY_RANGE; - } - /** * Get the {@link Range} of {@link Input.Tok}s lying in any part on a line. * @@ -131,27 +112,8 @@ public final Range getRanges(int lineI) { return 0 <= lineI && lineI < ranges.size() ? ranges.get(lineI) : EMPTY_RANGE; } - /** - * Get the {@link Range} of {@link Input.Tok}s ending on a line. - * - * @param lineI the line number - * @return the {@link Range} of {@link Input.Tok}s ending on the specified line - */ - public final Range getRange1s(int lineI) { - return 0 <= lineI && lineI < range1s.size() ? range1s.get(lineI) : EMPTY_RANGE; - } - @Override public String toString() { - return "InputOutput{" - + "lines=" - + lines - + ", range0s=" - + range0s - + ", ranges=" - + ranges - + ", range1s=" - + range1s - + '}'; + return "InputOutput{" + "lines=" + lines + ", ranges=" + ranges + '}'; } } diff --git a/core/src/main/java/com/google/googlejavaformat/Newlines.java b/core/src/main/java/com/google/googlejavaformat/Newlines.java index 49b328240..6a1241c36 100644 --- a/core/src/main/java/com/google/googlejavaformat/Newlines.java +++ b/core/src/main/java/com/google/googlejavaformat/Newlines.java @@ -42,6 +42,16 @@ public static boolean isNewline(String input) { return BREAKS.contains(input); } + /** Returns the length of the newline sequence at the current offset, or {@code -1}. */ + public static int hasNewlineAt(String input, int idx) { + for (String b : BREAKS) { + if (input.startsWith(b, idx)) { + return b.length(); + } + } + return -1; + } + /** * Returns the terminating line break in the input, or {@code null} if the input does not end in a * break. @@ -63,15 +73,16 @@ public static String guessLineSeparator(String text) { for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); switch (c) { - case '\r': + case '\r' -> { if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { return "\r\n"; } return "\r"; - case '\n': + } + case '\n' -> { return "\n"; - default: - break; + } + default -> {} } } return "\n"; @@ -125,7 +136,7 @@ private void advance() { if (idx + 1 < input.length() && input.charAt(idx + 1) == '\n') { idx++; } - // falls through + // falls through case '\n': idx++; curr = idx; diff --git a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java index 80aa02277..7f0fabb34 100644 --- a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java +++ b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java @@ -14,8 +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.Optional; +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; @@ -26,6 +30,7 @@ import com.google.googlejavaformat.Output.BreakTag; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * An {@code OpsBuilder} creates a list of {@link Op}s, which is turned into a {@link Doc} by {@link @@ -33,26 +38,26 @@ */ public final class OpsBuilder { - /** @return the actual size of the AST node at position, including comments. */ + /** Returns the actual size of the AST node at position, including comments. */ public int actualSize(int position, int length) { Token startToken = input.getPositionTokenMap().get(position); int start = startToken.getTok().getPosition(); for (Tok tok : startToken.getToksBefore()) { if (tok.isComment()) { - start = Math.min(start, tok.getPosition()); + 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; } - /** @return the start column of the token at {@code position}, including leading comments. */ + /** Returns the start column of the token at {@code position}, including leading comments. */ public Integer actualStartColumn(int position) { Token startToken = input.getPositionTokenMap().get(position); int start = startToken.getTok().getPosition(); @@ -62,7 +67,7 @@ public Integer actualStartColumn(int position) { return start; } if (tok.isComment()) { - start = Math.min(start, tok.getPosition()); + start = min(start, tok.getPosition()); } } return start; @@ -81,7 +86,8 @@ public abstract static class BlankLineWanted { * Explicitly preserve blank lines from the input (e.g. before the first member in a class * declaration). Overrides conditional blank lines. */ - public static final BlankLineWanted PRESERVE = new SimpleBlankLine(Optional.absent()); + public static final BlankLineWanted PRESERVE = + new SimpleBlankLine(/* wanted= */ Optional.empty()); /** Is the blank line wanted? */ public abstract Optional wanted(); @@ -127,7 +133,7 @@ public Optional wanted() { return Optional.of(true); } } - return Optional.absent(); + return Optional.empty(); } @Override @@ -153,7 +159,7 @@ public BlankLineWanted merge(BlankLineWanted other) { int depth = 0; /** Add an {@link Op}, and record open/close ops for later validation of unclosed levels. */ - private void add(Op op) { + public final void add(Op op) { if (op instanceof OpenOp) { depth++; } else if (op instanceof CloseOp) { @@ -206,7 +212,6 @@ public void checkClosed(int previous) { /** Create a {@link FormatterDiagnostic} at the current position. */ public FormatterDiagnostic diagnostic(String message) { - System.err.printf(">>>> %d: %s\n", inputPosition, message); return input.createDiagnostic(inputPosition, message); } @@ -240,7 +245,10 @@ public final void drain() { Input.Token token = tokens.get(tokenI++); add( Doc.Token.make( - token, Doc.Token.RealOrImaginary.IMAGINARY, ZERO, Optional.absent())); + token, + Doc.Token.RealOrImaginary.IMAGINARY, + ZERO, + /* breakAndIndentTrailingComment= */ Optional.empty())); } } this.inputPosition = inputPosition; @@ -263,10 +271,38 @@ public final void close() { /** Return the text of the next {@link Input.Token}, or absent if there is none. */ public final Optional peekToken() { + return peekToken(0); + } + + /** Return the text of an upcoming {@link Input.Token}, or absent if there is none. */ + public final Optional peekToken(int skip) { + ImmutableList tokens = input.getTokens(); + int idx = tokenI + skip; + return idx < tokens.size() + ? Optional.of(tokens.get(idx).getTok().getOriginalText()) + : Optional.empty(); + } + + /** + * Returns the {@link Input.Tok}s starting at the current source position, which are satisfied by + * the given predicate. + */ + public ImmutableList peekTokens(int startPosition, Predicate predicate) { ImmutableList tokens = input.getTokens(); - return tokenI < tokens.size() - ? Optional.of(tokens.get(tokenI).getTok().getOriginalText()) - : Optional.absent(); + 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 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(); } /** @@ -276,7 +312,11 @@ public final Optional peekToken() { * @param token the optional token */ public final void guessToken(String token) { - token(token, Doc.Token.RealOrImaginary.IMAGINARY, ZERO, Optional.absent()); + token( + token, + Doc.Token.RealOrImaginary.IMAGINARY, + ZERO, + /* breakAndIndentTrailingComment= */ Optional.empty()); } public final void token( @@ -285,7 +325,7 @@ public final void token( Indent plusIndentCommentsBefore, Optional breakAndIndentTrailingComment) { ImmutableList tokens = input.getTokens(); - if (token.equals(peekToken().orNull())) { // Found the input token. Output it. + if (token.equals(peekToken().orElse(null))) { // Found the input token. Output it. add( Doc.Token.make( tokens.get(tokenI++), @@ -301,7 +341,8 @@ public final void token( throw new FormattingError( diagnostic( String.format( - "expected token: '%s'; generated %s instead", peekToken().orNull(), token))); + "expected token: '%s'; generated %s instead", + peekToken().orElse(null), token))); } } } @@ -315,7 +356,10 @@ public final void op(String op) { int opN = op.length(); for (int i = 0; i < opN; i++) { token( - op.substring(i, i + 1), Doc.Token.RealOrImaginary.REAL, ZERO, Optional.absent()); + op.substring(i, i + 1), + Doc.Token.RealOrImaginary.REAL, + ZERO, + /* breakAndIndentTrailingComment= */ Optional.empty()); } } @@ -383,7 +427,7 @@ public final void breakToFill(String flat) { * @param plusIndent extra indent if taken */ public final void breakOp(Doc.FillMode fillMode, String flat, Indent plusIndent) { - breakOp(fillMode, flat, plusIndent, Optional.absent()); + breakOp(fillMode, flat, plusIndent, /* optionalTag= */ Optional.empty()); } /** @@ -520,7 +564,7 @@ public final ImmutableList build() { Doc.Break.make( Doc.FillMode.FORCED, "", - tokenOp.breakAndIndentTrailingComment().or(Const.ZERO))); + tokenOp.breakAndIndentTrailingComment().orElse(Const.ZERO))); } else { tokOps.put(k + 1, SPACE); } @@ -536,7 +580,18 @@ public final ImmutableList build() { * were generated (presumably), copy all input non-tokens literally, even spaces and * newlines. */ + int newlines = 0; + boolean lastWasComment = false; for (Input.Tok tokBefore : token.getToksBefore()) { + if (tokBefore.isNewline()) { + newlines++; + } else if (tokBefore.isComment()) { + newlines = 0; + lastWasComment = tokBefore.isComment(); + } + if (lastWasComment && newlines > 0) { + tokOps.put(j, Doc.Break.makeForced()); + } tokOps.put(j, Doc.Tok.make(tokBefore)); } for (Input.Tok tokAfter : token.getToksAfter()) { @@ -586,8 +641,8 @@ private static boolean isForcedBreak(Op op) { private static List makeComment(Input.Tok comment) { return comment.isSlashStarComment() - ? ImmutableList.of(Doc.Tok.make(comment)) - : ImmutableList.of(Doc.Tok.make(comment), Doc.Break.makeForced()); + ? ImmutableList.of(Doc.Tok.make(comment)) + : ImmutableList.of(Doc.Tok.make(comment), Doc.Break.makeForced()); } @Override diff --git a/core/src/main/java/com/google/googlejavaformat/Output.java b/core/src/main/java/com/google/googlejavaformat/Output.java index cbf6a9f35..ea039fa83 100644 --- a/core/src/main/java/com/google/googlejavaformat/Output.java +++ b/core/src/main/java/com/google/googlejavaformat/Output.java @@ -15,16 +15,16 @@ package com.google.googlejavaformat; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; import com.google.common.collect.Range; import com.google.googlejavaformat.OpsBuilder.BlankLineWanted; +import java.util.Optional; /** An output from the formatter. */ public abstract class Output extends InputOutput { /** Unique identifier for a break. */ public static final class BreakTag { - Optional taken = Optional.absent(); + Optional taken = Optional.empty(); public void recordBroken(boolean broken) { // TODO(cushon): enforce invariants. @@ -36,7 +36,7 @@ public void recordBroken(boolean broken) { } public boolean wasBreakTaken() { - return taken.or(false); + return taken.orElse(false); } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java index 47a6cfd21..e794a1815 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java +++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java @@ -14,127 +14,50 @@ package com.google.googlejavaformat.java; +import com.google.auto.value.AutoBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableRangeSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.Optional; /** * Command line options for google-java-format. * - *

google-java-format doesn't depend on AutoValue, to allow AutoValue to depend on - * google-java-format. + * @param files The files to format. + * @param inPlace Format files in place. + * @param lines Line ranges to format. + * @param offsets Character offsets for partial formatting, paired with {@code lengths}. + * @param lengths Partial formatting region lengths, paired with {@code offsets}. + * @param aosp Use AOSP style instead of Google Style (4-space indentation). + * @param version Print the version. + * @param help Print usage information. + * @param stdin Format input from stdin. + * @param fixImportsOnly Fix imports, but do no formatting. + * @param sortImports Sort imports. + * @param removeUnusedImports Remove unused imports. + * @param dryRun Print the paths of the files whose contents would change if the formatter were run + * normally. + * @param setExitIfChanged Return exit code 1 if there are any formatting changes. + * @param assumeFilename Return the name to use for diagnostics when formatting standard input. */ -final class CommandLineOptions { - - private final ImmutableList files; - private final boolean inPlace; - private final ImmutableRangeSet lines; - private final ImmutableList offsets; - private final ImmutableList lengths; - private final boolean aosp; - private final boolean version; - private final boolean help; - private final boolean stdin; - private final boolean fixImportsOnly; - private final boolean removeJavadocOnlyImports; - private final boolean sortImports; - private final boolean removeUnusedImports; - - CommandLineOptions( - ImmutableList files, - boolean inPlace, - ImmutableRangeSet lines, - ImmutableList offsets, - ImmutableList lengths, - boolean aosp, - boolean version, - boolean help, - boolean stdin, - boolean fixImportsOnly, - boolean removeJavadocOnlyImports, - boolean sortImports, - boolean removeUnusedImports) { - this.files = files; - this.inPlace = inPlace; - this.lines = lines; - this.offsets = offsets; - this.lengths = lengths; - this.aosp = aosp; - this.version = version; - this.help = help; - this.stdin = stdin; - this.fixImportsOnly = fixImportsOnly; - this.removeJavadocOnlyImports = removeJavadocOnlyImports; - this.sortImports = sortImports; - this.removeUnusedImports = removeUnusedImports; - } - - /** The files to format. */ - ImmutableList files() { - return files; - } - - /** Format files in place. */ - boolean inPlace() { - return inPlace; - } - - /** Line ranges to format. */ - ImmutableRangeSet lines() { - return lines; - } - - /** Character offsets for partial formatting, paired with {@code lengths}. */ - ImmutableList offsets() { - return offsets; - } - - /** Partial formatting region lengths, paired with {@code offsets}. */ - ImmutableList lengths() { - return lengths; - } - - /** Use AOSP style instead of Google Style (4-space indentation). */ - boolean aosp() { - return aosp; - } - - /** Print the version. */ - boolean version() { - return version; - } - - /** Print usage information. */ - boolean help() { - return help; - } - - /** Format input from stdin. */ - boolean stdin() { - return stdin; - } - - /** Fix imports, but do no formatting. */ - boolean fixImportsOnly() { - return fixImportsOnly; - } - - /** - * When fixing imports, remove imports that are used only in javadoc and fully-qualify any - * {@code @link} tags referring to the imported types. - */ - boolean removeJavadocOnlyImports() { - return removeJavadocOnlyImports; - } - - /** Sort imports. */ - boolean sortImports() { - return sortImports; - } - - /** Remove unused imports. */ - boolean removeUnusedImports() { - return removeUnusedImports; - } +record CommandLineOptions( + ImmutableList files, + boolean inPlace, + ImmutableRangeSet lines, + ImmutableList offsets, + ImmutableList lengths, + boolean aosp, + boolean version, + boolean help, + boolean stdin, + boolean fixImportsOnly, + boolean sortImports, + boolean removeUnusedImports, + boolean dryRun, + boolean setExitIfChanged, + Optional assumeFilename, + boolean reflowLongStrings, + boolean formatJavadoc) { /** Returns true if partial formatting was selected. */ boolean isSelection() { @@ -142,103 +65,70 @@ boolean isSelection() { } static Builder builder() { - return new Builder(); + return new AutoBuilder_CommandLineOptions_Builder() + .sortImports(true) + .removeUnusedImports(true) + .reflowLongStrings(true) + .formatJavadoc(true) + .aosp(false) + .version(false) + .help(false) + .stdin(false) + .fixImportsOnly(false) + .dryRun(false) + .setExitIfChanged(false) + .inPlace(false); } - static class Builder { - - private final ImmutableList.Builder files = ImmutableList.builder(); - private final ImmutableRangeSet.Builder lines = ImmutableRangeSet.builder(); - private final ImmutableList.Builder offsets = ImmutableList.builder(); - private final ImmutableList.Builder lengths = ImmutableList.builder(); - private Boolean inPlace = false; - private Boolean aosp = false; - private Boolean version = false; - private Boolean help = false; - private Boolean stdin = false; - private Boolean fixImportsOnly = false; - private Boolean removeJavadocOnlyImports = false; - private Boolean sortImports = true; - private Boolean removeUnusedImports = true; - - ImmutableList.Builder filesBuilder() { - return files; - } + @AutoBuilder + interface Builder { - Builder inPlace(boolean inPlace) { - this.inPlace = inPlace; - return this; - } + ImmutableList.Builder filesBuilder(); - ImmutableRangeSet.Builder linesBuilder() { - return lines; - } + Builder inPlace(boolean inPlace); - Builder addOffset(Integer offset) { - offsets.add(offset); - return this; - } + Builder lines(ImmutableRangeSet lines); - Builder addLength(Integer length) { - lengths.add(length); - return this; - } + ImmutableList.Builder offsetsBuilder(); - Builder aosp(boolean aosp) { - this.aosp = aosp; + @CanIgnoreReturnValue + default Builder addOffset(Integer offset) { + offsetsBuilder().add(offset); return this; } - Builder version(boolean version) { - this.version = version; - return this; - } + ImmutableList.Builder lengthsBuilder(); - Builder help(boolean help) { - this.help = help; + @CanIgnoreReturnValue + default Builder addLength(Integer length) { + lengthsBuilder().add(length); return this; } - Builder stdin(boolean stdin) { - this.stdin = stdin; - return this; - } + Builder aosp(boolean aosp); - Builder fixImportsOnly(boolean fixImportsOnly) { - this.fixImportsOnly = fixImportsOnly; - return this; - } + Builder version(boolean version); - Builder removeJavadocOnlyImports(boolean removeJavadocOnlyImports) { - this.removeJavadocOnlyImports = removeJavadocOnlyImports; - return this; - } + Builder help(boolean help); - Builder sortImports(boolean sortImports) { - this.sortImports = sortImports; - return this; - } + Builder stdin(boolean stdin); - Builder removeUnusedImports(boolean removeUnusedImports) { - this.removeUnusedImports = removeUnusedImports; - return this; - } + Builder fixImportsOnly(boolean fixImportsOnly); - CommandLineOptions build() { - return new CommandLineOptions( - this.files.build(), - this.inPlace, - this.lines.build(), - this.offsets.build(), - this.lengths.build(), - this.aosp, - this.version, - this.help, - this.stdin, - this.fixImportsOnly, - this.removeJavadocOnlyImports, - this.sortImports, - this.removeUnusedImports); - } + Builder sortImports(boolean sortImports); + + Builder removeUnusedImports(boolean removeUnusedImports); + + Builder dryRun(boolean dryRun); + + Builder setExitIfChanged(boolean setExitIfChanged); + + Builder assumeFilename(String assumeFilename); + + Builder reflowLongStrings(boolean reflowLongStrings); + + Builder formatJavadoc(boolean formatJavadoc); + + CommandLineOptions build(); } } 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 b12e2bfd2..f5ce703e8 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java +++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java @@ -14,9 +14,20 @@ package com.google.googlejavaformat.java; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -25,11 +36,18 @@ final class CommandLineOptionsParser { private static final Splitter COMMA_SPLITTER = Splitter.on(','); private static final Splitter COLON_SPLITTER = Splitter.on(':'); + private static final Splitter ARG_SPLITTER = + Splitter.on(CharMatcher.breakingWhitespace()).omitEmptyStrings().trimResults(); /** Parses {@link CommandLineOptions}. */ static CommandLineOptions parse(Iterable options) { CommandLineOptions.Builder optionsBuilder = CommandLineOptions.builder(); - Iterator it = options.iterator(); + List expandedOptions = new ArrayList<>(); + expandParamsFiles(options, expandedOptions); + Iterator it = expandedOptions.iterator(); + // Accumulate the ranges in a mutable builder to merge overlapping ranges, + // which ImmutableRangeSet doesn't support. + RangeSet linesBuilder = TreeRangeSet.create(); while (it.hasNext()) { String option = it.next(); if (!option.startsWith("-")) { @@ -41,67 +59,35 @@ static CommandLineOptions parse(Iterable options) { 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; } // NOTE: update usage information in UsageException when new flags are added switch (flag) { - case "-i": - case "-r": - case "-replace": - case "--replace": - optionsBuilder.inPlace(true); - break; - case "--lines": - case "-lines": - case "--line": - case "-line": - parseRangeSet(optionsBuilder.linesBuilder(), getValue(flag, it, value)); - break; - case "--offset": - case "-offset": - optionsBuilder.addOffset(parseInteger(it, flag, value)); - break; - case "--length": - case "-length": - optionsBuilder.addLength(parseInteger(it, flag, value)); - break; - case "--aosp": - case "-aosp": - case "-a": - optionsBuilder.aosp(true); - break; - case "--version": - case "-version": - case "-v": - optionsBuilder.version(true); - break; - case "--help": - case "-help": - case "-h": - optionsBuilder.help(true); - break; - case "--fix-imports-only": - optionsBuilder.fixImportsOnly(true); - break; - case "--experimental-remove-javadoc-only-imports": - optionsBuilder.removeJavadocOnlyImports(true); - break; - case "--skip-sorting-imports": - optionsBuilder.sortImports(false); - break; - case "--skip-removing-unused-imports": - optionsBuilder.removeUnusedImports(false); - break; - case "-": - optionsBuilder.stdin(true); - break; - default: - throw new IllegalArgumentException("unexpected flag: " + flag); + case "-i", "-r", "-replace", "--replace" -> optionsBuilder.inPlace(true); + case "--lines", "-lines", "--line", "-line" -> + parseRangeSet(linesBuilder, getValue(flag, it, value)); + case "--offset", "-offset" -> optionsBuilder.addOffset(parseInteger(it, flag, value)); + case "--length", "-length" -> optionsBuilder.addLength(parseInteger(it, flag, value)); + case "--aosp", "-aosp", "-a" -> optionsBuilder.aosp(true); + case "--version", "-version", "-v" -> optionsBuilder.version(true); + case "--help", "-help", "-h" -> optionsBuilder.help(true); + case "--fix-imports-only" -> optionsBuilder.fixImportsOnly(true); + case "--skip-sorting-imports" -> optionsBuilder.sortImports(false); + case "--skip-removing-unused-imports" -> optionsBuilder.removeUnusedImports(false); + case "--skip-reflowing-long-strings" -> optionsBuilder.reflowLongStrings(false); + case "--skip-javadoc-formatting" -> optionsBuilder.formatJavadoc(false); + case "-" -> optionsBuilder.stdin(true); + case "-n", "--dry-run" -> optionsBuilder.dryRun(true); + case "--set-exit-if-changed" -> optionsBuilder.setExitIfChanged(true); + case "-assume-filename", "--assume-filename" -> + optionsBuilder.assumeFilename(getValue(flag, it, value)); + default -> throw new IllegalArgumentException("unexpected flag: " + flag); } } + optionsBuilder.lines(ImmutableRangeSet.copyOf(linesBuilder)); return optionsBuilder.build(); } @@ -130,7 +116,7 @@ private static String getValue(String flag, Iterator it, String value) { * number. Line numbers are {@code 1}-based, but are converted to the {@code 0}-based numbering * used internally by google-java-format. */ - private static void parseRangeSet(ImmutableRangeSet.Builder result, String ranges) { + private static void parseRangeSet(RangeSet result, String ranges) { for (String range : COMMA_SPLITTER.split(ranges)) { result.add(parseRange(range)); } @@ -142,16 +128,42 @@ private static void parseRangeSet(ImmutableRangeSet.Builder result, Str */ private static Range parseRange(String arg) { List args = COLON_SPLITTER.splitToList(arg); - switch (args.size()) { - case 1: + return switch (args.size()) { + case 1 -> { int line = Integer.parseInt(args.get(0)) - 1; - return Range.closedOpen(line, line + 1); - case 2: + yield Range.closedOpen(line, line + 1); + } + case 2 -> { int line0 = Integer.parseInt(args.get(0)) - 1; int line1 = Integer.parseInt(args.get(1)) - 1; - return Range.closedOpen(line0, line1 + 1); - default: - throw new IllegalArgumentException(arg); + yield Range.closedOpen(line0, line1 + 1); + } + default -> throw new IllegalArgumentException(arg); + }; + } + + /** + * Pre-processes an argument list, expanding arguments of the form {@code @filename} by reading + * the content of the file and appending whitespace-delimited options to {@code arguments}. + */ + private static void expandParamsFiles(Iterable args, List expanded) { + for (String arg : args) { + if (arg.isEmpty()) { + continue; + } + if (!arg.startsWith("@")) { + expanded.add(arg); + } else if (arg.startsWith("@@")) { + expanded.add(arg.substring(1)); + } else { + Path path = Paths.get(arg.substring(1)); + try { + String sequence = new String(Files.readAllBytes(path), UTF_8); + expandParamsFiles(ARG_SPLITTER.split(sequence), expanded); + } catch (IOException e) { + throw new UncheckedIOException(path + ": could not read file: " + e.getMessage(), e); + } + } } } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java b/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java index bb4ab5f1c..3bf4793c3 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java +++ b/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java @@ -15,16 +15,16 @@ package com.google.googlejavaformat.java; import com.google.common.collect.ImmutableList; +import com.sun.source.tree.AnnotatedTypeTree; +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ArrayTypeTree; +import com.sun.source.tree.Tree; +import com.sun.tools.javac.tree.JCTree; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.List; -import org.openjdk.source.tree.AnnotatedTypeTree; -import org.openjdk.source.tree.AnnotationTree; -import org.openjdk.source.tree.ArrayTypeTree; -import org.openjdk.source.tree.Tree; -import org.openjdk.tools.javac.tree.JCTree; /** * Utilities for working with array dimensions. @@ -34,7 +34,7 @@ * *

For example, {@code int [] a;} cannot be distinguished from {@code int [] a [];} in the AST. */ -public class DimensionHelpers { +class DimensionHelpers { /** The array dimension specifiers (including any type annotations) associated with a type. */ static class TypeWithDims { @@ -106,19 +106,18 @@ private static Iterable> reorderBySourcePosition( * int}. */ private static Tree extractDims(Deque> dims, Tree node) { - switch (node.getKind()) { - case ARRAY_TYPE: - return extractDims(dims, ((ArrayTypeTree) node).getType()); - case ANNOTATED_TYPE: + return switch (node.getKind()) { + case ARRAY_TYPE -> extractDims(dims, ((ArrayTypeTree) node).getType()); + case ANNOTATED_TYPE -> { AnnotatedTypeTree annotatedTypeTree = (AnnotatedTypeTree) node; if (annotatedTypeTree.getUnderlyingType().getKind() != Tree.Kind.ARRAY_TYPE) { - return node; + yield node; } node = extractDims(dims, annotatedTypeTree.getUnderlyingType()); - dims.addFirst(ImmutableList.copyOf(annotatedTypeTree.getAnnotations())); - return node; - default: - return node; - } + dims.addFirst(ImmutableList.copyOf(annotatedTypeTree.getAnnotations())); + yield node; + } + default -> node; + }; } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java index 54016bdd0..7cec1fca8 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java +++ b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java @@ -14,51 +14,81 @@ package com.google.googlejavaformat.java; +import com.google.auto.value.AutoValue; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeSet; -import com.google.googlejavaformat.java.RemoveUnusedImports.JavadocOnlyImports; +import java.nio.file.Path; import java.util.concurrent.Callable; +import org.jspecify.annotations.Nullable; /** * Encapsulates information about a file to be formatted, including which parts of the file to * format. */ -public class FormatFileCallable implements Callable { +class FormatFileCallable implements Callable { + + @AutoValue + abstract static class Result { + abstract @Nullable Path path(); + + abstract String input(); + + abstract @Nullable String output(); + + boolean changed() { + return !input().equals(output()); + } + + abstract @Nullable FormatterException exception(); + + static Result create( + @Nullable Path path, + String input, + @Nullable String output, + @Nullable FormatterException exception) { + return new AutoValue_FormatFileCallable_Result(path, input, output, exception); + } + } + + private final Path path; private final String input; private final CommandLineOptions parameters; private final JavaFormatterOptions options; public FormatFileCallable( - CommandLineOptions parameters, String input, JavaFormatterOptions options) { + CommandLineOptions parameters, Path path, String input, JavaFormatterOptions options) { + this.path = path; this.input = input; this.parameters = parameters; this.options = options; } @Override - public String call() throws FormatterException { - if (parameters.fixImportsOnly()) { - return fixImports(input); - } + public Result call() { + try { + if (parameters.fixImportsOnly()) { + return Result.create(path, input, fixImports(input), /* exception= */ null); + } - String formatted = - new Formatter(options).formatSource(input, characterRanges(input).asRanges()); - formatted = fixImports(formatted); - return formatted; + Formatter formatter = new Formatter(options); + String formatted = formatter.formatSource(input, characterRanges(input).asRanges()); + formatted = fixImports(formatted); + if (parameters.reflowLongStrings()) { + formatted = StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, formatted, formatter); + } + return Result.create(path, input, formatted, /* exception= */ null); + } catch (FormatterException e) { + return Result.create(path, input, /* output= */ null, e); + } } private String fixImports(String input) throws FormatterException { if (parameters.removeUnusedImports()) { - input = - RemoveUnusedImports.removeUnusedImports( - input, - parameters.removeJavadocOnlyImports() - ? JavadocOnlyImports.REMOVE - : JavadocOnlyImports.KEEP); + input = RemoveUnusedImports.removeUnusedImports(input); } if (parameters.sortImports()) { - input = ImportOrderer.reorderImports(input); + input = ImportOrderer.reorderImports(input, options.style()); } return input; } 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 90d0f4b23..ff87eaab7 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java @@ -14,11 +14,8 @@ package com.google.googlejavaformat.java; -import static java.nio.charset.StandardCharsets.UTF_8; -import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; @@ -32,36 +29,23 @@ import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.Op; import com.google.googlejavaformat.OpsBuilder; -import java.io.File; -import java.io.IOError; +import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; +import com.sun.tools.javac.util.Context; import java.io.IOException; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import javax.tools.Diagnostic; -import javax.tools.DiagnosticCollector; -import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; -import javax.tools.SimpleJavaFileObject; -import javax.tools.StandardLocation; -import org.openjdk.tools.javac.file.JavacFileManager; -import org.openjdk.tools.javac.parser.JavacParser; -import org.openjdk.tools.javac.parser.ParserFactory; -import org.openjdk.tools.javac.tree.JCTree.JCCompilationUnit; -import org.openjdk.tools.javac.util.Context; -import org.openjdk.tools.javac.util.Log; -import org.openjdk.tools.javac.util.Options; /** * This is google-java-format, a new Java formatter that follows the Google Java Style Guide quite * precisely---to the letter and to the spirit. * - *

This formatter uses the Eclipse parser to generate an AST. Because the Eclipse AST loses - * information about the non-tokens in the input (including newlines, comments, etc.), and even some - * tokens (e.g., optional commas or semicolons), this formatter lexes the input again and follows - * along in the resulting list of tokens. Its lexer splits all multi-character operators (like ">>") + *

This formatter uses the javac parser to generate an AST. Because the AST loses information + * about the non-tokens in the input (including newlines, comments, etc.), and even some tokens + * (e.g., optional commas or semicolons), this formatter lexes the input again and follows along in + * the resulting list of tokens. Its lexer splits all multi-character operators (like ">>") * into multiple single-character operators. Each non-token is assigned to a token---non-tokens * following a token on the same line go with that token; those following go with the next token--- * and there is a final EOF token to hold final comments. @@ -89,26 +73,9 @@ @Immutable public final class Formatter { - static final Range EMPTY_RANGE = Range.closedOpen(-1, -1); + public static final int MAX_LINE_LENGTH = 100; - static final Predicate> ERROR_DIAGNOSTIC = - new Predicate>() { - @Override - public boolean apply(Diagnostic input) { - if (input.getKind() != Diagnostic.Kind.ERROR) { - return false; - } - switch (input.getCode()) { - case "compiler.err.invalid.meth.decl.ret.type.req": - // accept constructor-like method declarations that don't match the name of their - // enclosing class - return false; - default: - break; - } - return true; - } - }; + static final Range EMPTY_RANGE = Range.closedOpen(-1, -1); private final JavaFormatterOptions options; @@ -129,56 +96,39 @@ public Formatter(JavaFormatterOptions options) { * @param javaOutput the {@link JavaOutput} * @param options the {@link JavaFormatterOptions} */ - static void format( - final JavaInput javaInput, JavaOutput javaOutput, JavaFormatterOptions options) { + static void format(final JavaInput javaInput, JavaOutput javaOutput, JavaFormatterOptions options) + throws FormatterException { Context context = new Context(); - DiagnosticCollector diagnostics = new DiagnosticCollector<>(); - context.put(DiagnosticListener.class, diagnostics); - Options.instance(context).put("allowStringFolding", "false"); - JCCompilationUnit unit; - JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8); - try { - fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of()); - } catch (IOException e) { - // impossible - throw new IOError(e); - } - SimpleJavaFileObject source = - new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) { - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - return javaInput.getText(); - } - }; - Log.instance(context).useSource(source); - ParserFactory parserFactory = ParserFactory.instance(context); - JavacParser parser = - parserFactory.newParser( - javaInput.getText(), - /*keepDocComments=*/ true, - /*keepEndPos=*/ true, - /*keepLineMap=*/ true); - unit = parser.parseCompilationUnit(); - unit.sourcefile = source; + List> errorDiagnostics = new ArrayList<>(); + JCCompilationUnit unit = + Trees.parse( + context, errorDiagnostics, /* allowStringFolding= */ false, javaInput.getText()); javaInput.setCompilationUnit(unit); - Iterable> errorDiagnostics = - Iterables.filter(diagnostics.getDiagnostics(), ERROR_DIAGNOSTIC); - if (!Iterables.isEmpty(errorDiagnostics)) { - throw FormattingError.fromJavacDiagnostics(errorDiagnostics); + if (!errorDiagnostics.isEmpty()) { + throw FormatterException.fromJavacDiagnostics(errorDiagnostics); } OpsBuilder builder = new OpsBuilder(javaInput, javaOutput); // Output the compilation unit. - new JavaInputAstVisitor(builder, options.indentationMultiplier()).scan(unit, null); + JavaInputAstVisitor visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier()); + visitor.scan(unit, null); builder.sync(javaInput.getText().length()); builder.drain(); Doc doc = new DocBuilder().withOps(builder.build()).build(); - doc.computeBreaks( - javaOutput.getCommentsHelper(), options.maxLineLength(), new Doc.State(+0, 0)); + doc.computeBreaks(javaOutput.getCommentsHelper(), MAX_LINE_LENGTH, new Doc.State(+0, 0)); doc.write(javaOutput); javaOutput.flush(); } + static boolean errorDiagnostic(Diagnostic input) { + if (input.getKind() != Diagnostic.Kind.ERROR) { + return false; + } + // accept constructor-like method declarations that don't match the name of their + // enclosing class + return !input.getCode().equals("compiler.err.invalid.meth.decl.ret.type.req"); + } + /** * Format the given input (a Java compilation unit) into the output stream. * @@ -194,12 +144,34 @@ public void formatSource(CharSource input, CharSink output) /** * Format an input string (a Java compilation unit) into an output string. * + *

Leaves import statements untouched. + * * @param input the input string * @return the output string * @throws FormatterException if the input string cannot be parsed */ public String formatSource(String input) throws FormatterException { - return formatSource(input, Collections.singleton(Range.closedOpen(0, input.length()))); + return formatSource(input, ImmutableList.of(Range.closedOpen(0, input.length()))); + } + + /** + * Formats an input string (a Java compilation unit) and fixes imports. + * + *

Fixing imports includes ordering, spacing, and removal of unused import statements. + * + * @param input the input string + * @return the output string + * @throws FormatterException if the input string cannot be parsed + * @see + * Google Java Style Guide - 3.3.3 Import ordering and spacing + */ + public String formatSourceAndFixImports(String input) throws FormatterException { + input = ImportOrderer.reorderImports(input, options.style()); + input = RemoveUnusedImports.removeUnusedImports(input); + String formatted = formatSource(input); + formatted = StringWrapper.wrap(formatted, this); + return formatted; } /** @@ -231,7 +203,9 @@ public ImmutableList getFormatReplacements( // TODO(cushon): this is only safe because the modifier ordering doesn't affect whitespace, // and doesn't change the replacements that are output. This is not true in general for // 'de-linting' changes (e.g. import ordering). - javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges); + if (options.reorderModifiers()) { + javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges); + } String lineSeparator = Newlines.guessLineSeparator(input); JavaOutput javaOutput = 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 2cf567d8b..7fe67073a 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java +++ b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java @@ -14,14 +14,22 @@ package com.google.googlejavaformat.java; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.Locale.ENGLISH; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.googlejavaformat.FormatterDiagnostic; import java.util.List; +import java.util.regex.Pattern; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; /** Checked exception class for formatter errors. */ public final class FormatterException extends Exception { - private ImmutableList diagnostics; + private final ImmutableList diagnostics; public FormatterException(String message) { this(FormatterDiagnostic.create(message)); @@ -39,4 +47,35 @@ public FormatterException(Iterable diagnostics) { public List diagnostics() { return diagnostics; } + + public static FormatterException fromJavacDiagnostics( + List> diagnostics) { + return new FormatterException( + diagnostics.stream() + .map(FormatterException::toFormatterDiagnostic) + .collect(toImmutableList())); + } + + private static FormatterDiagnostic toFormatterDiagnostic(Diagnostic input) { + return FormatterDiagnostic.create( + (int) input.getLineNumber(), (int) input.getColumnNumber(), input.getMessage(ENGLISH)); + } + + public String formatDiagnostics(String path, String input) { + List lines = Splitter.on(NEWLINE_PATTERN).splitToList(input); + StringBuilder sb = new StringBuilder(); + for (FormatterDiagnostic diagnostic : diagnostics()) { + sb.append(path).append(":").append(diagnostic).append(System.lineSeparator()); + int line = diagnostic.line(); + int column = diagnostic.column(); + if (line != -1 && column != -1) { + sb.append(CharMatcher.breakingWhitespace().trimTrailingFrom(lines.get(line - 1))) + .append(System.lineSeparator()); + sb.append(" ".repeat(column - 1)).append('^').append(System.lineSeparator()); + } + } + return sb.toString(); + } + + private static final Pattern NEWLINE_PATTERN = Pattern.compile("\\R"); } diff --git a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatTool.java b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatTool.java new file mode 100644 index 000000000..3c315aaf1 --- /dev/null +++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatTool.java @@ -0,0 +1,53 @@ +/* + * 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.collect.Sets.toImmutableEnumSet; + +import com.google.auto.service.AutoService; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Set; +import javax.lang.model.SourceVersion; +import javax.tools.Tool; + +/** Provide a way to be invoked without necessarily starting a new VM. */ +@AutoService(Tool.class) +public class GoogleJavaFormatTool implements Tool { + @Override + public String name() { + return "google-java-format"; + } + + @Override + public Set getSourceVersions() { + return Arrays.stream(SourceVersion.values()).collect(toImmutableEnumSet()); + } + + @Override + public int run(InputStream in, OutputStream out, OutputStream err, String... args) { + PrintStream outStream = new PrintStream(out); + PrintStream errStream = new PrintStream(err); + try { + return Main.main(in, outStream, errStream, args); + } catch (RuntimeException e) { + errStream.print(e.getMessage()); + errStream.flush(); + return 1; // pass non-zero value back indicating an error has happened + } + } +} 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 000000000..438eac596 --- /dev/null +++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProvider.java @@ -0,0 +1,39 @@ +/* + * 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(System.in, out, err, args); + } catch (RuntimeException e) { + err.print(e.getMessage()); + err.flush(); + return 1; // pass non-zero value back indicating an error has happened + } + } +} diff --git a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template similarity index 87% rename from core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java rename to core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template index e858d9e46..88706fbf1 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java +++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template @@ -14,6 +14,9 @@ package com.google.googlejavaformat.java; -public class GoogleJavaFormatVersion { - public static final String VERSION = "1.0"; +class GoogleJavaFormatVersion { + + static String version() { + return "%VERSION%"; + } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java index abfdde4ad..375a3289b 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java @@ -14,26 +14,94 @@ package com.google.googlejavaformat.java; import static com.google.common.collect.Iterables.getLast; +import static com.google.common.primitives.Booleans.trueFirst; -import com.google.common.base.Optional; +import com.google.common.base.CharMatcher; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.googlejavaformat.Newlines; +import com.google.googlejavaformat.java.JavaFormatterOptions.Style; import com.google.googlejavaformat.java.JavaInput.Tok; -import org.openjdk.tools.javac.parser.Tokens.TokenKind; +import com.sun.tools.javac.parser.Tokens.TokenKind; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.stream.Stream; /** Orders imports in Java source code. */ public class ImportOrderer { + + private static final Splitter DOT_SPLITTER = Splitter.on('.'); + /** * Reorder the inputs in {@code text}, a complete Java program. On success, another complete Java * program is returned, which is the same as the original except the imports are in order. * * @throws FormatterException if the input could not be parsed. */ - public static String reorderImports(String text) throws FormatterException { + public static String reorderImports(String text, Style style) throws FormatterException { ImmutableList toks = JavaInput.buildToks(text, CLASS_START); - return new ImportOrderer(text, toks).reorderImports(); + return new ImportOrderer(text, toks, style).reorderImports(); + } + + /** + * Reorder the inputs in {@code text}, a complete Java program, in Google style. On success, + * another complete Java program is returned, which is the same as the original except the imports + * are in order. + * + * @deprecated Use {@link #reorderImports(String, Style)} instead + * @throws FormatterException if the input could not be parsed. + */ + @Deprecated + public static String reorderImports(String text) throws FormatterException { + return reorderImports(text, Style.GOOGLE); + } + + private String reorderImports() throws FormatterException { + int firstImportStart; + Optional maybeFirstImport = findIdentifier(0, IMPORT_OR_CLASS_START); + if (!maybeFirstImport.isPresent() || !tokenAt(maybeFirstImport.get()).equals("import")) { + // No imports, so nothing to do. + return text; + } + firstImportStart = maybeFirstImport.get(); + int unindentedFirstImportStart = unindent(firstImportStart); + + ImportsAndIndex imports = scanImports(firstImportStart); + int afterLastImport = imports.index; + + // Make sure there are no more imports before the next class (etc) definition. + Optional maybeLaterImport = findIdentifier(afterLastImport, IMPORT_OR_CLASS_START); + if (maybeLaterImport.isPresent() && tokenAt(maybeLaterImport.get()).equals("import")) { + throw new FormatterException("Imports not contiguous (perhaps a comment separates them?)"); + } + + StringBuilder result = new StringBuilder(); + String prefix = tokString(0, unindentedFirstImportStart); + result.append(prefix); + if (!prefix.isEmpty() && Newlines.getLineEnding(prefix) == null) { + result.append(lineSeparator).append(lineSeparator); + } + result.append(reorderedImportsString(imports.imports)); + + List tail = new ArrayList<>(); + tail.add(CharMatcher.whitespace().trimLeadingFrom(tokString(afterLastImport, toks.size()))); + if (!toks.isEmpty()) { + Tok lastTok = getLast(toks); + int tailStart = lastTok.getPosition() + lastTok.length(); + tail.add(text.substring(tailStart)); + } + if (tail.stream().anyMatch(s -> !s.isEmpty())) { + result.append(lineSeparator); + tail.forEach(result::append); + } + + return result.toString(); } /** @@ -51,83 +119,157 @@ public static String reorderImports(String text) throws FormatterException { private static final ImmutableSet IMPORT_OR_CLASS_START = ImmutableSet.of("import", "class", "interface", "enum"); + /** + * A {@link Comparator} that orders {@link Import}s by Google Style, defined at + * https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing. + * + *

Module imports are not allowed by Google Style, so we make an arbitrary choice about where + * to include them if they are present. + */ + private static final Comparator GOOGLE_IMPORT_COMPARATOR = + Comparator.comparing(Import::importType).thenComparing(Import::imported); + + /** + * A {@link Comparator} that orders {@link Import}s by AOSP Style, defined at + * https://source.android.com/setup/contribute/code-style#order-import-statements and implemented + * in IntelliJ at + * https://android.googlesource.com/platform/development/+/master/ide/intellij/codestyles/AndroidStyle.xml. + * + *

Module imports are not mentioned by Android Style, so we make an arbitrary choice about + * where to include them if they are present. + */ + private static final Comparator AOSP_IMPORT_COMPARATOR = + Comparator.comparing(Import::importType) + .thenComparing(Import::isAndroid, trueFirst()) + .thenComparing(Import::isThirdParty, trueFirst()) + .thenComparing(Import::isJava, trueFirst()) + .thenComparing(Import::imported); + + /** + * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link + * Import}s based on Google style. + */ + private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) { + return !prev.importType().equals(curr.importType()); + } + + /** + * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link + * Import}s based on AOSP style. + */ + private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) { + if (!prev.importType().equals(curr.importType())) { + return true; + } + // insert blank line between "com.android" from "com.anythingelse" + if (prev.isAndroid() && !curr.isAndroid()) { + return true; + } + return !prev.topLevel().equals(curr.topLevel()); + } + private final String text; private final ImmutableList toks; private final String lineSeparator; + private final Comparator importComparator; + private final BiFunction shouldInsertBlankLineFn; - private ImportOrderer(String text, ImmutableList toks) throws FormatterException { + private ImportOrderer(String text, ImmutableList toks, Style style) { this.text = text; this.toks = toks; this.lineSeparator = Newlines.guessLineSeparator(text); + if (style.equals(Style.GOOGLE)) { + this.importComparator = GOOGLE_IMPORT_COMPARATOR; + this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineGoogle; + } else if (style.equals(Style.AOSP)) { + this.importComparator = AOSP_IMPORT_COMPARATOR; + this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineAosp; + } else { + throw new IllegalArgumentException("Unsupported code style: " + style); + } } - /** An import statement. */ - private static class Import implements Comparable { - /** The name being imported, for example {@code java.util.List}. */ - final String imported; - - /** The characters after the final {@code ;}, up to and including the line terminator. */ - final String trailing; + enum ImportType { + STATIC, + MODULE, + NORMAL + } - /** True if this is {@code import static}. */ - final boolean isStatic; + /** An import statement. */ + class Import { + private final String imported; + private final String trailing; + private final ImportType importType; - Import(String imported, String trailing, boolean isStatic) { + Import(String imported, String trailing, ImportType importType) { this.imported = imported; this.trailing = trailing; - this.isStatic = isStatic; + this.importType = importType; } - // This is how the sorting happens, including sorting static imports before non-static ones. - @Override - public int compareTo(Import that) { - if (this.isStatic != that.isStatic) { - return this.isStatic ? -1 : +1; - } - return this.imported.compareTo(that.imported); + /** The name being imported, for example {@code java.util.List}. */ + String imported() { + return imported; } - // This is a complete line to be output for this import, including the line terminator. - @Override - public String toString() { - String staticString = isStatic ? "static " : ""; - return "import " + staticString + imported + ";" + trailing; + /** Returns the {@link ImportType}. */ + ImportType importType() { + return importType; } - } - private String reorderImports() throws FormatterException { - int firstImportStart; - Optional maybeFirstImport = findIdentifier(0, IMPORT_OR_CLASS_START); - if (!maybeFirstImport.isPresent() || !tokenAt(maybeFirstImport.get()).equals("import")) { - // No imports, so nothing to do. - return text; + /** The top-level package of the import. */ + String topLevel() { + return DOT_SPLITTER.split(imported()).iterator().next(); } - firstImportStart = maybeFirstImport.get(); - int unindentedFirstImportStart = unindent(firstImportStart); - ImportsAndIndex imports = scanImports(firstImportStart); - int afterLastImport = imports.index; + /** True if this is an Android import per AOSP style. */ + boolean isAndroid() { + return Stream.of("android.", "androidx.", "dalvik.", "libcore.", "com.android.") + .anyMatch(imported::startsWith); + } - // Make sure there are no more imports before the next class (etc) definition. - Optional maybeLaterImport = findIdentifier(afterLastImport, IMPORT_OR_CLASS_START); - if (maybeLaterImport.isPresent() && tokenAt(maybeLaterImport.get()).equals("import")) { - throw new FormatterException("Imports not contiguous (perhaps a comment separates them?)"); + /** True if this is a Java import per AOSP style. */ + boolean isJava() { + return switch (topLevel()) { + case "java", "javax" -> true; + default -> false; + }; } - // Add back the text from after the point where we stopped tokenizing. - String tail; - if (toks.isEmpty()) { - tail = ""; - } else { - Tok lastTok = getLast(toks); - int tailStart = lastTok.getPosition() + lastTok.length(); - tail = text.substring(tailStart); + /** + * The {@code //} comment lines after the final {@code ;}, up to and including the line + * terminator of the last one. Note: In case two imports were separated by a space (which is + * disallowed by the style guide), the trailing whitespace of the first import does not include + * a line terminator. + */ + String trailing() { + return trailing; + } + + /** True if this is a third-party import per AOSP style. */ + public boolean isThirdParty() { + return !(isAndroid() || isJava()); } - return tokString(0, unindentedFirstImportStart) - + reorderedImportsString(imports.imports) - + tokString(afterLastImport, toks.size()) - + tail; + // One or multiple lines, the import itself and following comments, including the line + // terminator. + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("import "); + switch (importType) { + case STATIC -> sb.append("static "); + case MODULE -> sb.append("module "); + case NORMAL -> {} + } + sb.append(imported()).append(';'); + if (trailing().trim().isEmpty()) { + sb.append(lineSeparator); + } else { + sb.append(trailing()); + } + return sb.toString(); + } } private String tokString(int start, int end) { @@ -155,7 +297,7 @@ private static class ImportsAndIndex { * -> ( | )* * -> "import" ("static" )? * ("." )* ("." "*")? ? ";" - * ? ? + * ? ? ( )* * } * * @param i the index to start parsing at. @@ -164,7 +306,7 @@ private static class ImportsAndIndex { */ private ImportsAndIndex scanImports(int i) throws FormatterException { int afterLastImport = i; - ImmutableSortedSet.Builder imports = ImmutableSortedSet.naturalOrder(); + ImmutableSortedSet.Builder imports = ImmutableSortedSet.orderedBy(importComparator); // JavaInput.buildToks appends a zero-width EOF token after all tokens. It won't match any // of our tests here and protects us from running off the end of the toks list. Since it is // zero-width it doesn't matter if we include it in our string concatenation at the end. @@ -173,8 +315,13 @@ private ImportsAndIndex scanImports(int i) throws FormatterException { if (isSpaceToken(i)) { i++; } - boolean isStatic = tokenAt(i).equals("static"); - if (isStatic) { + ImportType importType = + switch (tokenAt(i)) { + case "static" -> ImportType.STATIC; + case "module" -> ImportType.MODULE; + default -> ImportType.NORMAL; + }; + if (!importType.equals(ImportType.NORMAL)) { i++; if (isSpaceToken(i)) { i++; @@ -201,16 +348,25 @@ private ImportsAndIndex scanImports(int i) throws FormatterException { trailing.append(tokenAt(i)); i++; } - if (isSlashSlashCommentToken(i)) { + if (isNewlineToken(i)) { trailing.append(tokenAt(i)); i++; } - if (!isNewlineToken(i)) { - throw new FormatterException("Extra tokens after import: " + tokenAt(i)); + // Gather (if any) all single line comments and accompanied line terminators following this + // import + while (isSlashSlashCommentToken(i)) { + trailing.append(tokenAt(i)); + i++; + if (isNewlineToken(i)) { + trailing.append(tokenAt(i)); + i++; + } } - trailing.append(tokenAt(i)); - i++; - imports.add(new Import(importedName, trailing.toString(), isStatic)); + 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(), importType)); // Remember the position just after the import we just saw, before skipping blank lines. // If the next thing after the blank lines is not another import then we don't want to // include those blank lines in the text to be replaced. @@ -224,22 +380,20 @@ private ImportsAndIndex scanImports(int i) throws FormatterException { // Produces the sorted output based on the imports we have scanned. private String reorderedImportsString(ImmutableSortedSet imports) { - assert !imports.isEmpty(); - - Import firstImport = imports.iterator().next(); + Preconditions.checkArgument(!imports.isEmpty(), "imports"); - // Pretend that the first import was preceded by another import of the same kind - // (static or non-static), so we don't insert a newline there. - boolean lastWasStatic = firstImport.isStatic; + // Pretend that the first import was preceded by another import of the same kind, so we don't + // insert a newline there. + Import prevImport = imports.iterator().next(); StringBuilder sb = new StringBuilder(); - for (Import thisImport : imports) { - if (lastWasStatic && !thisImport.isStatic) { + for (Import currImport : imports) { + if (shouldInsertBlankLineFn.apply(prevImport, currImport)) { // Blank line between static and non-static imports. sb.append(lineSeparator); } - lastWasStatic = thisImport.isStatic; - sb.append(thisImport); + sb.append(currImport); + prevImport = currImport; } return sb.toString(); } @@ -272,7 +426,7 @@ private StringAndIndex scanImported(int start) throws FormatterException { // At the start of each iteration of this loop, i points to an identifier. // On exit from the loop, i points to a token after an identifier or after *. while (true) { - assert isIdentifierToken(i); + Preconditions.checkState(isIdentifierToken(i)); imported.append(tokenAt(i)); i++; if (!tokenAt(i).equals(".")) { @@ -291,7 +445,7 @@ private StringAndIndex scanImported(int start) throws FormatterException { /** * Returns the index of the first place where one of the given identifiers occurs, or {@code - * Optional.absent()} if there is none. + * Optional.empty()} if there is none. * * @param start the index to start looking at * @param identifiers the identifiers to look for @@ -305,7 +459,7 @@ private Optional findIdentifier(int start, ImmutableSet identif } } } - return Optional.absent(); + return Optional.empty(); } /** Returns the given token, or the preceding token if it is a whitespace token. */ diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java index 33c6ef470..9526b892c 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java @@ -23,12 +23,14 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** {@code JavaCommentsHelper} extends {@link CommentsHelper} to rewrite Java comments. */ public final class JavaCommentsHelper implements CommentsHelper { - private final JavaFormatterOptions options; private final String lineSeparator; + private final JavaFormatterOptions options; public JavaCommentsHelper(String lineSeparator, JavaFormatterOptions options) { this.lineSeparator = lineSeparator; @@ -41,21 +43,27 @@ public String rewrite(Tok tok, int maxWidth, int column0) { return tok.getOriginalText(); } String text = tok.getOriginalText(); - if (tok.isJavadocComment()) { - text = JavadocFormatter.formatJavadoc(text, column0, options); + if (tok.isJavadocComment() && options.formatJavadoc()) { + text = JavadocFormatter.formatJavadoc(text, column0); } List lines = new ArrayList<>(); Iterator it = Newlines.lineIterator(text); while (it.hasNext()) { - lines.add(CharMatcher.whitespace().trimTrailingFrom(it.next())); + if (tok.isSlashSlashComment()) { + lines.add(CharMatcher.whitespace().trimFrom(it.next())); + } else { + lines.add(CharMatcher.whitespace().trimTrailingFrom(it.next())); + } } if (tok.isSlashSlashComment()) { return indentLineComments(lines, column0); - } else if (javadocShaped(lines)) { - return indentJavadoc(lines, column0); - } else { - return preserveIndentation(lines, column0); } + return CommentsHelper.reformatParameterComment(tok) + .orElseGet( + () -> + javadocShaped(lines) + ? indentJavadoc(lines, column0) + : preserveIndentation(lines, column0)); } // For non-javadoc-shaped block comments, shift the entire block to the correct @@ -88,9 +96,9 @@ private String preserveIndentation(List lines, int column0) { return builder.toString(); } - // Remove leading whitespace (trailing was already removed), wrap if necessary, and re-indent. + // Wraps and re-indents line comments. private String indentLineComments(List lines, int column0) { - lines = wrapLineComments(lines, column0, options); + lines = wrapLineComments(lines, column0); StringBuilder builder = new StringBuilder(); builder.append(lines.get(0).trim()); String indentString = Strings.repeat(" ", column0); @@ -100,12 +108,27 @@ private String indentLineComments(List lines, int column0) { return builder.toString(); } - private List wrapLineComments( - List lines, int column0, JavaFormatterOptions options) { + // Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot + // contain leading spaces. + private static final Pattern LINE_COMMENT_MISSING_SPACE_PREFIX = + Pattern.compile("^(//+)(?!noinspection|\\$NON-NLS-\\d+\\$)[^\\s/]"); + + private List wrapLineComments(List lines, int column0) { List result = new ArrayList<>(); for (String line : lines) { - while (line.length() + column0 > options.maxLineLength()) { - int idx = options.maxLineLength() - column0; + // Add missing leading spaces to line comments: `//foo` -> `// foo`. + Matcher matcher = LINE_COMMENT_MISSING_SPACE_PREFIX.matcher(line); + if (matcher.find()) { + int length = matcher.group(1).length(); + line = Strings.repeat("/", length) + " " + line.substring(length); + } + if (line.startsWith("// MOE:")) { + // don't wrap comments for https://github.com/google/MOE + result.add(line); + continue; + } + while (line.length() + column0 > Formatter.MAX_LINE_LENGTH) { + int idx = Formatter.MAX_LINE_LENGTH - column0; // only break on whitespace characters, and ignore the leading `// ` while (idx >= 2 && !CharMatcher.whitespace().matches(line.charAt(idx))) { idx--; @@ -162,4 +185,3 @@ private static boolean javadocShaped(List lines) { return true; } } - diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java index 28abbd05d..67c13d0b3 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java @@ -14,6 +14,7 @@ package com.google.googlejavaformat.java; +import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; /** @@ -27,12 +28,10 @@ * preferences, and in fact it would work directly against our primary goals. */ @Immutable -public class JavaFormatterOptions { - - static final int DEFAULT_MAX_LINE_LENGTH = 100; +@AutoValue +public abstract class JavaFormatterOptions { public enum Style { - /** The default Google Java Style configuration. */ GOOGLE(1), @@ -50,21 +49,17 @@ int indentationMultiplier() { } } - private final Style style; - - private JavaFormatterOptions(Style style) { - this.style = style; + /** Returns the multiplier for the unit of indent. */ + public int indentationMultiplier() { + return style().indentationMultiplier(); } - /** Returns the maximum formatted width */ - public int maxLineLength() { - return DEFAULT_MAX_LINE_LENGTH; - } + public abstract boolean formatJavadoc(); - /** Returns the multiplier for the unit of indent */ - public int indentationMultiplier() { - return style.indentationMultiplier(); - } + public abstract boolean reorderModifiers(); + + /** Returns the code style. */ + public abstract Style style(); /** Returns the default formatting options. */ public static JavaFormatterOptions defaultOptions() { @@ -73,22 +68,22 @@ public static JavaFormatterOptions defaultOptions() { /** Returns a builder for {@link JavaFormatterOptions}. */ public static Builder builder() { - return new Builder(); + return new AutoValue_JavaFormatterOptions.Builder() + .style(Style.GOOGLE) + .formatJavadoc(true) + .reorderModifiers(true); } /** A builder for {@link JavaFormatterOptions}. */ - public static class Builder { - private Style style = Style.GOOGLE; + @AutoValue.Builder + public abstract static class Builder { - private Builder() {} + public abstract Builder style(Style style); - public Builder style(Style style) { - this.style = style; - return this; - } + public abstract Builder formatJavadoc(boolean formatJavadoc); - public JavaFormatterOptions build() { - return new JavaFormatterOptions(style); - } + public abstract Builder reorderModifiers(boolean reorderModifiers); + + public abstract JavaFormatterOptions build(); } } 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 35ec4de57..cf525e86f 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java @@ -33,7 +33,17 @@ import com.google.googlejavaformat.Input; import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.java.JavacTokens.RawTok; +import com.sun.tools.javac.file.JavacFileManager; +import com.sun.tools.javac.parser.Tokens.TokenKind; +import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.JCDiagnostic; +import com.sun.tools.javac.util.Log; +import com.sun.tools.javac.util.Log.DeferredDiagnosticHandler; +import com.sun.tools.javac.util.Options; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -42,15 +52,11 @@ import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; import javax.tools.DiagnosticListener; +import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.SimpleJavaFileObject; -import org.openjdk.tools.javac.file.JavacFileManager; -import org.openjdk.tools.javac.parser.Tokens.TokenKind; -import org.openjdk.tools.javac.tree.JCTree.JCCompilationUnit; -import org.openjdk.tools.javac.util.Context; -import org.openjdk.tools.javac.util.Log; -import org.openjdk.tools.javac.util.Log.DeferredDiagnosticHandler; +import org.jspecify.annotations.Nullable; /** {@code JavaInput} extends {@link Input} to represent a Java input document. */ public final class JavaInput extends Input { @@ -59,10 +65,9 @@ public final class JavaInput extends Input { * either a token (if {@code isToken()}), or a non-token, which is a comment (if {@code * isComment()}) or a newline (if {@code isNewline()}) or a maximal sequence of other whitespace * characters (if {@code isSpaces()}). Each {@link Tok} contains a sequence of characters, an - * index (sequential starting at {@code 0} for tokens and comments, else {@code -1}), and an - * Eclipse-compatible ({@code 0}-origin) position in the input. The concatenation of the texts of - * all the {@link Tok}s equals the input. Each Input ends with a token EOF {@link Tok}, with empty - * text. + * index (sequential starting at {@code 0} for tokens and comments, else {@code -1}), and a + * ({@code 0}-origin) position in the input. The concatenation of the texts of all the {@link + * Tok}s equals the input. Each Input ends with a token EOF {@link Tok}, with empty text. * *

A {@code /*} comment possibly contains newlines; a {@code //} comment does not contain the * terminating newline character, but is followed by a newline {@link Tok}. @@ -155,7 +160,9 @@ public boolean isSlashStarComment() { @Override public boolean isJavadocComment() { - return text.startsWith("/**") && text.length() > 4; + // comments like `/***` are also javadoc, but their formatting probably won't be improved + // by the javadoc formatter + return text.startsWith("/**") && text.charAt("/**".length()) != '*' && text.length() > 4; } @Override @@ -309,7 +316,7 @@ private static ImmutableMap makePositionToColumnMap(List for (Tok tok : toks) { builder.put(tok.getPosition(), tok.getColumn()); } - return builder.build(); + return builder.buildOrThrow(); } /** @@ -329,7 +336,7 @@ public ImmutableMap getPositionToColumnMap() { /** Lex the input and build the list of toks. */ private ImmutableList buildToks(String text) throws FormatterException { - ImmutableList toks = buildToks(text, ImmutableSet.of()); + ImmutableList toks = buildToks(text, ImmutableSet.of()); kN = getLast(toks).getIndex(); computeRanges(toks); return toks; @@ -346,7 +353,9 @@ static ImmutableList buildToks(String text, ImmutableSet stopTok throws FormatterException { stopTokens = ImmutableSet.builder().addAll(stopTokens).add(TokenKind.EOF).build(); Context context = new Context(); - new JavacFileManager(context, true, UTF_8); + Options.instance(context).put("--enable-preview", "true"); + JavaFileManager fileManager = new JavacFileManager(context, false, UTF_8); + context.put(JavaFileManager.class, fileManager); DiagnosticCollector diagnosticCollector = new DiagnosticCollector<>(); context.put(DiagnosticListener.class, diagnosticCollector); Log log = Log.instance(context); @@ -357,9 +366,17 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept return text; } }); - DeferredDiagnosticHandler diagnostics = new DeferredDiagnosticHandler(log); + DeferredDiagnosticHandler diagnostics = deferredDiagnosticHandler(log); ImmutableList rawToks = JavacTokens.getTokens(text, context, stopTokens); - if (diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) { + Collection ds; + try { + @SuppressWarnings("unchecked") + var extraLocalForSuppression = (Collection) GET_DIAGNOSTICS.invoke(diagnostics); + ds = extraLocalForSuppression; + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + if (ds.stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) { return ImmutableList.of(new Tok(0, "", "", 0, 0, true, null)); // EOF } int kN = 0; @@ -466,6 +483,39 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept return ImmutableList.copyOf(toks); } + private static final Constructor + DEFERRED_DIAGNOSTIC_HANDLER_CONSTRUCTOR = getDeferredDiagnosticHandlerConstructor(); + + // Depending on the JDK version, we might have a static class whose constructor has an explicit + // Log parameter, or an inner class whose constructor has an *implicit* Log parameter. They are + // different at the source level, but look the same to reflection. + + private static Constructor getDeferredDiagnosticHandlerConstructor() { + try { + return DeferredDiagnosticHandler.class.getConstructor(Log.class); + } catch (NoSuchMethodException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static DeferredDiagnosticHandler deferredDiagnosticHandler(Log log) { + try { + return DEFERRED_DIAGNOSTIC_HANDLER_CONSTRUCTOR.newInstance(log); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static final Method GET_DIAGNOSTICS = getGetDiagnostics(); + + private static @Nullable Method getGetDiagnostics() { + try { + return DeferredDiagnosticHandler.class.getMethod("getDiagnostics"); + } catch (NoSuchMethodException e) { + throw new LinkageError(e.getMessage(), e); + } + } + private static int updateColumn(int columnI, String originalTokText) { Integer last = Iterators.getLast(Newlines.lineOffsetIterator(originalTokText)); if (last > 0) { @@ -511,20 +561,18 @@ private static ImmutableList buildTokens(List toks) { // TODO(cushon): find a better strategy. if (toks.get(k).isSlashStarComment()) { switch (tok.getText()) { - case "(": - case "<": - case ".": + case "(", "<", "." -> { break OUTER; - default: - break; + } + default -> {} } } if (toks.get(k).isJavadocComment()) { switch (tok.getText()) { - case ";": + case ";" -> { break OUTER; - default: - break; + } + default -> {} } } if (isParamComment(toks.get(k))) { @@ -554,33 +602,30 @@ private static boolean isParamComment(Tok tok) { } /** - * 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 + * @throws FormatterException if the upper endpoint of the range is outside the file */ - Range characterRangeToTokenRange(int offset, int length) throws FormatterException { - int requiredLength = offset + length; - if (requiredLength > text.length()) { + Range characterRangeToTokenRange(Range 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)); - } - if (length < 0) { - return EMPTY_RANGE; - } - if (length == 0) { - // 0 stands for "format the line under the cursor" - length = 1; - } + "error: invalid offset (%d) or length (%d); offset + length (%d) > file length (%d)", + characterRange.lowerEndpoint(), + characterRange.upperEndpoint() - characterRange.lowerEndpoint(), + characterRange.upperEndpoint(), + text.length())); + } + // empty range stands for "format the line under the cursor" + Range nonEmptyRange = + characterRange.isEmpty() + ? Range.closedOpen(characterRange.lowerEndpoint(), characterRange.lowerEndpoint() + 1) + : characterRange; ImmutableCollection enclosed = - getPositionTokenMap() - .subRangeMap(Range.closedOpen(offset, offset + length)) - .asMapOfRanges() - .values(); + getPositionTokenMap().subRangeMap(nonEmptyRange).asMapOfRanges().values(); if (enclosed.isEmpty()) { return EMPTY_RANGE; } @@ -591,18 +636,20 @@ Range characterRangeToTokenRange(int offset, int length) throws Formatt /** * Get the number of toks. * - * @return the number of toks, including the EOF tok + * @return the number of toks, excluding the EOF tok */ - int getkN() { + @Override + public int getkN() { return kN; } /** * Get the Token by index. * - * @param k the token index + * @param k the Tok index */ - Token getToken(int k) { + @Override + public Token getToken(int k) { return kToToken[k]; } @@ -659,12 +706,9 @@ public void setCompilationUnit(JCCompilationUnit unit) { public RangeSet characterRangesToTokenRanges(Collection> characterRanges) throws FormatterException { RangeSet tokenRangeSet = TreeRangeSet.create(); - for (Range characterRange0 : characterRanges) { - Range characterRange = characterRange0.canonical(DiscreteDomain.integers()); + for (Range 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 4a2b5e422..2f0373ec6 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java @@ -14,6 +14,7 @@ package com.google.googlejavaformat.java; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.googlejavaformat.Doc.FillMode.INDEPENDENT; @@ -29,36 +30,49 @@ import static com.google.googlejavaformat.java.Trees.operatorName; import static com.google.googlejavaformat.java.Trees.precedence; import static com.google.googlejavaformat.java.Trees.skipParen; -import static org.openjdk.source.tree.Tree.Kind.ANNOTATION; -import static org.openjdk.source.tree.Tree.Kind.ARRAY_ACCESS; -import static org.openjdk.source.tree.Tree.Kind.ASSIGNMENT; -import static org.openjdk.source.tree.Tree.Kind.BLOCK; -import static org.openjdk.source.tree.Tree.Kind.EXTENDS_WILDCARD; -import static org.openjdk.source.tree.Tree.Kind.IF; -import static org.openjdk.source.tree.Tree.Kind.METHOD_INVOCATION; -import static org.openjdk.source.tree.Tree.Kind.NEW_ARRAY; -import static org.openjdk.source.tree.Tree.Kind.NEW_CLASS; -import static org.openjdk.source.tree.Tree.Kind.STRING_LITERAL; -import static org.openjdk.source.tree.Tree.Kind.UNION_TYPE; -import static org.openjdk.source.tree.Tree.Kind.VARIABLE; - +import static com.sun.source.tree.Tree.Kind.ANNOTATION; +import static com.sun.source.tree.Tree.Kind.ARRAY_ACCESS; +import static com.sun.source.tree.Tree.Kind.ASSIGNMENT; +import static com.sun.source.tree.Tree.Kind.BLOCK; +import static com.sun.source.tree.Tree.Kind.EXTENDS_WILDCARD; +import static com.sun.source.tree.Tree.Kind.IF; +import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION; +import static com.sun.source.tree.Tree.Kind.NEW_ARRAY; +import static com.sun.source.tree.Tree.Kind.NEW_CLASS; +import static com.sun.source.tree.Tree.Kind.STRING_LITERAL; +import static com.sun.source.tree.Tree.Kind.UNION_TYPE; +import static com.sun.source.tree.Tree.Kind.VARIABLE; +import static java.util.stream.Collectors.joining; +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.Optional; 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; import com.google.googlejavaformat.FormattingError; import com.google.googlejavaformat.Indent; import com.google.googlejavaformat.Input; +import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.Op; import com.google.googlejavaformat.OpenOp; import com.google.googlejavaformat.OpsBuilder; @@ -66,83 +80,108 @@ import com.google.googlejavaformat.Output.BreakTag; import com.google.googlejavaformat.java.DimensionHelpers.SortedDims; import com.google.googlejavaformat.java.DimensionHelpers.TypeWithDims; +import com.sun.source.tree.AnnotatedTypeTree; +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ArrayAccessTree; +import com.sun.source.tree.ArrayTypeTree; +import com.sun.source.tree.AssertTree; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.BindingPatternTree; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.BreakTree; +import com.sun.source.tree.CaseLabelTree; +import com.sun.source.tree.CaseTree; +import com.sun.source.tree.CatchTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.ConstantCaseLabelTree; +import com.sun.source.tree.ContinueTree; +import com.sun.source.tree.DeconstructionPatternTree; +import com.sun.source.tree.DefaultCaseLabelTree; +import com.sun.source.tree.DirectiveTree; +import com.sun.source.tree.DoWhileLoopTree; +import com.sun.source.tree.EmptyStatementTree; +import com.sun.source.tree.EnhancedForLoopTree; +import com.sun.source.tree.ExportsTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.ForLoopTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.IfTree; +import com.sun.source.tree.ImportTree; +import com.sun.source.tree.InstanceOfTree; +import com.sun.source.tree.IntersectionTypeTree; +import com.sun.source.tree.LabeledStatementTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ModifiersTree; +import com.sun.source.tree.ModuleTree; +import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.OpensTree; +import com.sun.source.tree.ParameterizedTypeTree; +import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.PatternCaseLabelTree; +import com.sun.source.tree.PatternTree; +import com.sun.source.tree.PrimitiveTypeTree; +import com.sun.source.tree.ProvidesTree; +import com.sun.source.tree.RequiresTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.SwitchExpressionTree; +import com.sun.source.tree.SwitchTree; +import com.sun.source.tree.SynchronizedTree; +import com.sun.source.tree.ThrowTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TryTree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.TypeParameterTree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.UnionTypeTree; +import com.sun.source.tree.UsesTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.tree.WhileLoopTree; +import com.sun.source.tree.WildcardTree; +import com.sun.source.tree.YieldTree; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.TreeInfo; +import com.sun.tools.javac.tree.TreeScanner; +import java.lang.reflect.Method; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; +import java.util.Collection; +import java.util.Comparator; import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Stream; import javax.lang.model.element.Name; -import org.openjdk.source.tree.AnnotatedTypeTree; -import org.openjdk.source.tree.AnnotationTree; -import org.openjdk.source.tree.ArrayAccessTree; -import org.openjdk.source.tree.ArrayTypeTree; -import org.openjdk.source.tree.AssertTree; -import org.openjdk.source.tree.AssignmentTree; -import org.openjdk.source.tree.BinaryTree; -import org.openjdk.source.tree.BlockTree; -import org.openjdk.source.tree.BreakTree; -import org.openjdk.source.tree.CaseTree; -import org.openjdk.source.tree.CatchTree; -import org.openjdk.source.tree.ClassTree; -import org.openjdk.source.tree.CompilationUnitTree; -import org.openjdk.source.tree.CompoundAssignmentTree; -import org.openjdk.source.tree.ConditionalExpressionTree; -import org.openjdk.source.tree.ContinueTree; -import org.openjdk.source.tree.DoWhileLoopTree; -import org.openjdk.source.tree.EmptyStatementTree; -import org.openjdk.source.tree.EnhancedForLoopTree; -import org.openjdk.source.tree.ExpressionStatementTree; -import org.openjdk.source.tree.ExpressionTree; -import org.openjdk.source.tree.ForLoopTree; -import org.openjdk.source.tree.IdentifierTree; -import org.openjdk.source.tree.IfTree; -import org.openjdk.source.tree.ImportTree; -import org.openjdk.source.tree.InstanceOfTree; -import org.openjdk.source.tree.IntersectionTypeTree; -import org.openjdk.source.tree.LabeledStatementTree; -import org.openjdk.source.tree.LambdaExpressionTree; -import org.openjdk.source.tree.LiteralTree; -import org.openjdk.source.tree.MemberReferenceTree; -import org.openjdk.source.tree.MemberSelectTree; -import org.openjdk.source.tree.MethodInvocationTree; -import org.openjdk.source.tree.MethodTree; -import org.openjdk.source.tree.ModifiersTree; -import org.openjdk.source.tree.NewArrayTree; -import org.openjdk.source.tree.NewClassTree; -import org.openjdk.source.tree.ParameterizedTypeTree; -import org.openjdk.source.tree.ParenthesizedTree; -import org.openjdk.source.tree.PrimitiveTypeTree; -import org.openjdk.source.tree.ReturnTree; -import org.openjdk.source.tree.StatementTree; -import org.openjdk.source.tree.SwitchTree; -import org.openjdk.source.tree.SynchronizedTree; -import org.openjdk.source.tree.ThrowTree; -import org.openjdk.source.tree.Tree; -import org.openjdk.source.tree.TryTree; -import org.openjdk.source.tree.TypeCastTree; -import org.openjdk.source.tree.TypeParameterTree; -import org.openjdk.source.tree.UnaryTree; -import org.openjdk.source.tree.UnionTypeTree; -import org.openjdk.source.tree.VariableTree; -import org.openjdk.source.tree.WhileLoopTree; -import org.openjdk.source.tree.WildcardTree; -import org.openjdk.source.util.TreePath; -import org.openjdk.source.util.TreePathScanner; -import org.openjdk.tools.javac.code.Flags; -import org.openjdk.tools.javac.tree.JCTree; -import org.openjdk.tools.javac.tree.TreeScanner; +import org.jspecify.annotations.Nullable; /** * An AST visitor that builds a stream of {@link Op}s to format from the given {@link * CompilationUnitTree}. */ -public final class JavaInputAstVisitor extends TreePathScanner { +public class JavaInputAstVisitor extends TreePathScanner { /** Direction for Annotations (usually VERTICAL). */ - enum Direction { + protected enum Direction { VERTICAL, HORIZONTAL; @@ -152,7 +191,7 @@ boolean isVertical() { } /** Whether to break or not. */ - enum BreakOrNot { + protected enum BreakOrNot { YES, NO; @@ -162,7 +201,7 @@ boolean isYes() { } /** Whether to collapse empty blocks. */ - enum CollapseEmptyOrNot { + protected enum CollapseEmptyOrNot { YES, NO; @@ -176,7 +215,7 @@ boolean isYes() { } /** Whether to allow leading blank lines in blocks. */ - enum AllowLeadingBlankLine { + protected enum AllowLeadingBlankLine { YES, NO; @@ -186,7 +225,7 @@ static AllowLeadingBlankLine valueOf(boolean b) { } /** Whether to allow trailing blank lines in blocks. */ - enum AllowTrailingBlankLine { + protected enum AllowTrailingBlankLine { YES, NO; @@ -196,7 +235,7 @@ static AllowTrailingBlankLine valueOf(boolean b) { } /** Whether to include braces. */ - enum BracesOrNot { + protected enum BracesOrNot { YES, NO; @@ -244,7 +283,7 @@ boolean isYes() { } /** Whether these declarations are the first in the block. */ - enum FirstDeclarationsOrNot { + protected enum FirstDeclarationsOrNot { YES, NO; @@ -253,17 +292,37 @@ boolean isYes() { } } - private final OpsBuilder builder; + // TODO(cushon): generalize this + private static final ImmutableMultimap TYPE_ANNOTATIONS = typeAnnotations(); + + private static ImmutableSetMultimap typeAnnotations() { + ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder(); + for (String annotation : + ImmutableList.of( + "org.jspecify.annotations.NonNull", + "org.jspecify.annotations.Nullable", + "org.jspecify.annotations.Nullable", + "org.checkerframework.checker.nullness.qual.NonNull", + "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; + protected final int indentMultiplier; + protected final Indent.Const minusTwo; + protected final Indent.Const minusFour; + protected final Indent.Const plusTwo; + protected final Indent.Const plusFour; - private static final Indent.Const ZERO = Indent.Const.ZERO; - private final int indentMultiplier; - private final Indent.Const minusTwo; - private final Indent.Const minusFour; - private final Indent.Const plusTwo; - private final Indent.Const plusFour; + private final Set typeAnnotationSimpleNames = new HashSet<>(); private static final ImmutableList breakList(Optional breakTag) { - return ImmutableList.of(Doc.Break.make(Doc.FillMode.UNIFIED, " ", ZERO, breakTag)); + return ImmutableList.of(Doc.Break.make(Doc.FillMode.UNIFIED, " ", ZERO, breakTag)); } private static final ImmutableList breakFillList(Optional breakTag) { @@ -274,11 +333,9 @@ private static final ImmutableList breakFillList(Optional breakTag } private static final ImmutableList forceBreakList(Optional breakTag) { - return ImmutableList.of(Doc.Break.make(FillMode.FORCED, "", Indent.Const.ZERO, breakTag)); + return ImmutableList.of(Doc.Break.make(FillMode.FORCED, "", Indent.Const.ZERO, breakTag)); } - private static final ImmutableList 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. @@ -300,7 +357,7 @@ public JavaInputAstVisitor(OpsBuilder builder, int indentMultiplier) { } /** A record of whether we have visited into an expression. */ - private final Deque inExpression = new ArrayDeque<>(Arrays.asList(false)); + private final Deque inExpression = new ArrayDeque<>(ImmutableList.of(false)); private boolean inExpression() { return inExpression.peekLast(); @@ -308,6 +365,12 @@ private boolean inExpression() { @Override public Void scan(Tree tree, Void unused) { + // Pre-visit AST for preview features, since com.sun.source.tree.AnyPattern can't be + // accessed directly without --enable-preview. + if (tree instanceof JCTree.JCAnyPattern) { + visitJcAnyPattern((JCTree.JCAnyPattern) tree); + return null; + } inExpression.addLast(tree instanceof ExpressionTree || inExpression.peekLast()); int previous = builder.depth(); try { @@ -325,15 +388,16 @@ public Void scan(Tree tree, Void unused) { @Override public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { - boolean first = true; + boolean afterFirstToken = false; if (node.getPackageName() != null) { markForPartialFormat(); visitPackage(node.getPackageName(), node.getPackageAnnotations()); builder.forcedBreak(); - first = false; + afterFirstToken = true; } + dropEmptyDeclarations(); if (!node.getImports().isEmpty()) { - if (!first) { + if (afterFirstToken) { builder.blankLineWanted(BlankLineWanted.YES); } for (ImportTree importDeclaration : node.getImports()) { @@ -342,7 +406,7 @@ public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { scan(importDeclaration, null); builder.forcedBreak(); } - first = false; + afterFirstToken = true; } dropEmptyDeclarations(); for (Tree type : node.getTypeDecls()) { @@ -351,53 +415,73 @@ public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { // TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed continue; } - if (!first) { + if (afterFirstToken) { builder.blankLineWanted(BlankLineWanted.YES); } markForPartialFormat(); scan(type, null); builder.forcedBreak(); - first = false; + afterFirstToken = true; dropEmptyDeclarations(); } + handleModule(afterFirstToken, node); // set a partial format marker at EOF to make sure we can format the entire file markForPartialFormat(); return null; } + protected void handleModule(boolean afterFirstToken, CompilationUnitTree node) { + ModuleTree module = node.getModule(); + if (module != null) { + if (afterFirstToken) { + builder.blankLineWanted(YES); + } + markForPartialFormat(); + visitModule(module, null); + builder.forcedBreak(); + } + } + /** Skips over extra semi-colons at the top-level, or in a class member declaration lists. */ - private void dropEmptyDeclarations() { + protected void dropEmptyDeclarations() { if (builder.peekToken().equals(Optional.of(";"))) { while (builder.peekToken().equals(Optional.of(";"))) { + builder.forcedBreak(); markForPartialFormat(); token(";"); } } } + // Replace with Flags.IMPLICIT_CLASS once JDK 25 is the minimum supported version + private static final int IMPLICIT_CLASS = 1 << 19; + @Override public Void visitClass(ClassTree tree, Void unused) { + if ((TreeInfo.flags((JCTree) tree) & IMPLICIT_CLASS) == IMPLICIT_CLASS) { + visitImplicitClass(tree); + return null; + } switch (tree.getKind()) { - case ANNOTATION_TYPE: - visitAnnotationType(tree); - break; - case CLASS: - case INTERFACE: - visitClassDeclaration(tree); - break; - case ENUM: - visitEnumDeclaration(tree); - break; - default: - throw new AssertionError(tree.getKind()); + case ANNOTATION_TYPE -> visitAnnotationType(tree); + case CLASS, INTERFACE -> visitClassDeclaration(tree); + case ENUM -> visitEnumDeclaration(tree); + case RECORD -> visitRecordDeclaration(tree); + default -> throw new AssertionError(tree.getKind()); } return null; } + private void visitImplicitClass(ClassTree node) { + builder.open(minusTwo); + addBodyDeclarations(node.getMembers(), BracesOrNot.NO, FirstDeclarationsOrNot.YES); + builder.close(); + } + public void visitAnnotationType(ClassTree node) { sync(node); builder.open(ZERO); - visitAndBreakModifiers(node.getModifiers(), Direction.VERTICAL, Optional.absent()); + typeDeclarationModifiers(node.getModifiers()); builder.open(ZERO); token("@"); token("interface"); @@ -434,9 +518,9 @@ public Void visitNewArray(NewArrayTree node, Void unused) { Deque dimExpressions = new ArrayDeque<>(node.getDimensions()); - Deque> annotations = new ArrayDeque<>(); + Deque> annotations = new ArrayDeque<>(); annotations.add(ImmutableList.copyOf(node.getAnnotations())); - annotations.addAll((List>) node.getDimAnnotations()); + annotations.addAll(node.getDimAnnotations()); annotations.addAll(extractedDims.dims); scan(base, null); @@ -466,9 +550,9 @@ public boolean visitArrayInitializer(List expressions) builder.open(plusTwo); token("{"); builder.forcedBreak(); - boolean first = true; + boolean afterFirstToken = false; for (Iterable row : Iterables.partition(expressions, cols)) { - if (!first) { + if (afterFirstToken) { builder.forcedBreak(); } builder.open(row.iterator().next().getKind() == NEW_ARRAY || cols == 1 ? ZERO : plusFour); @@ -483,7 +567,7 @@ public boolean visitArrayInitializer(List expressions) } builder.guessToken(","); builder.close(); - first = false; + afterFirstToken = true; } builder.breakOp(minusTwo); builder.close(); @@ -514,15 +598,15 @@ public boolean visitArrayInitializer(List expressions) if (allowFilledElementsOnOwnLine) { builder.open(ZERO); } - boolean first = true; + boolean afterFirstToken = false; FillMode fillMode = shortItems ? FillMode.INDEPENDENT : FillMode.UNIFIED; for (ExpressionTree expression : expressions) { - if (!first) { + if (afterFirstToken) { token(","); builder.breakOp(fillMode, " ", ZERO); } scan(expression, null); - first = false; + afterFirstToken = true; } builder.guessToken(","); if (allowFilledElementsOnOwnLine) { @@ -558,7 +642,7 @@ private void visitAnnotatedArrayType(Tree node) { TypeWithDims extractedDims = DimensionHelpers.extractDims(node, SortedDims.YES); builder.open(plusFour); scan(extractedDims.node, null); - Deque> dims = new ArrayDeque<>(extractedDims.dims); + Deque> dims = new ArrayDeque<>(extractedDims.dims); maybeAddDims(dims); Verify.verify(dims.isEmpty()); builder.close(); @@ -656,9 +740,10 @@ public Void visitNewClass(NewClassTree node, Void unused) { builder.space(); addTypeArguments(node.getTypeArguments(), plusFour); if (node.getClassBody() != null) { - builder.addAll( + List annotations = visitModifiers( - node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.absent())); + node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.empty()); + visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES); } scan(node.getIdentifier(), null); addArguments(node.getArguments(), plusFour); @@ -745,7 +830,7 @@ public Void visitEnhancedForLoop(EnhancedForLoopTree node, Void unused) { node.getVariable(), Optional.of(node.getExpression()), ":", - Optional.absent()); + /* trailing= */ Optional.empty()); builder.close(); token(")"); builder.close(); @@ -779,7 +864,7 @@ private void visitEnumConstantDeclaration(VariableTree enumConstant) { public boolean visitEnumDeclaration(ClassTree node) { sync(node); builder.open(ZERO); - visitAndBreakModifiers(node.getModifiers(), Direction.VERTICAL, Optional.absent()); + typeDeclarationModifiers(node.getModifiers()); builder.open(plusFour); token("enum"); builder.breakOp(" "); @@ -793,14 +878,14 @@ public boolean visitEnumDeclaration(ClassTree node) { token("implements"); builder.breakOp(" "); builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; for (Tree superInterfaceType : node.getImplementsClause()) { - if (!first) { + if (afterFirstToken) { token(","); builder.breakToFill(" "); } scan(superInterfaceType, null); - first = false; + afterFirstToken = true; } builder.close(); builder.close(); @@ -821,27 +906,41 @@ public boolean visitEnumDeclaration(ClassTree node) { members.add(member); } if (enumConstants.isEmpty() && members.isEmpty()) { - builder.open(ZERO); - builder.blankLineWanted(BlankLineWanted.NO); - token("}"); - builder.close(); + if (builder.peekToken().equals(Optional.of(";"))) { + builder.open(plusTwo); + builder.forcedBreak(); + token(";"); + builder.forcedBreak(); + dropEmptyDeclarations(); + builder.close(); + builder.open(ZERO); + builder.forcedBreak(); + builder.blankLineWanted(BlankLineWanted.NO); + token("}", plusTwo); + builder.close(); + } else { + builder.open(ZERO); + builder.blankLineWanted(BlankLineWanted.NO); + token("}"); + builder.close(); + } } else { builder.open(plusTwo); builder.blankLineWanted(BlankLineWanted.NO); builder.forcedBreak(); builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; for (VariableTree enumConstant : enumConstants) { - if (!first) { + if (afterFirstToken) { token(","); builder.forcedBreak(); builder.blankLineWanted(BlankLineWanted.PRESERVE); } markForPartialFormat(); visitEnumConstantDeclaration(enumConstant); - first = false; + afterFirstToken = true; } - if (builder.peekToken().or("").equals(",")) { + if (builder.peekToken().orElse("").equals(",")) { token(","); builder.forcedBreak(); // The ";" goes on its own line. } @@ -865,23 +964,79 @@ public boolean visitEnumDeclaration(ClassTree node) { return false; } + public void visitRecordDeclaration(ClassTree node) { + sync(node); + typeDeclarationModifiers(node.getModifiers()); + Verify.verify(node.getExtendsClause() == null); + boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty(); + token("record"); + builder.space(); + visit(node.getSimpleName()); + if (!node.getTypeParameters().isEmpty()) { + token("<"); + } + builder.open(plusFour); + { + if (!node.getTypeParameters().isEmpty()) { + typeParametersRest(node.getTypeParameters(), hasSuperInterfaceTypes ? plusFour : ZERO); + } + ImmutableList parameters = JavaInputAstVisitor.recordVariables(node); + token("("); + if (!parameters.isEmpty()) { + // Break before args. + builder.breakToFill(""); + } + // record headers can't declare receiver parameters + visitFormals(/* receiver= */ Optional.empty(), parameters); + token(")"); + if (hasSuperInterfaceTypes) { + builder.breakToFill(" "); + builder.open(node.getImplementsClause().size() > 1 ? plusFour : ZERO); + token("implements"); + builder.space(); + boolean afterFirstToken = false; + for (Tree superInterfaceType : node.getImplementsClause()) { + if (afterFirstToken) { + token(","); + builder.breakOp(" "); + } + scan(superInterfaceType, null); + afterFirstToken = true; + } + builder.close(); + } + } + builder.close(); + if (node.getMembers() == null) { + token(";"); + } else { + ImmutableList members = + node.getMembers().stream() + .filter(t -> (TreeInfo.flags((JCTree) t) & Flags.GENERATED_MEMBER) == 0) + .collect(toImmutableList()); + addBodyDeclarations(members, BracesOrNot.YES, FirstDeclarationsOrNot.YES); + } + dropEmptyDeclarations(); + } + + private static ImmutableList recordVariables(ClassTree node) { + return node.getMembers().stream() + .filter(JCTree.JCVariableDecl.class::isInstance) + .map(JCTree.JCVariableDecl.class::cast) + .filter(m -> (m.mods.flags & RECORD) == RECORD) + .collect(toImmutableList()); + } + @Override public Void visitMemberReference(MemberReferenceTree node, Void unused) { - sync(node); builder.open(plusFour); scan(node.getQualifierExpression(), null); builder.breakOp(); builder.op("::"); addTypeArguments(node.getTypeArguments(), plusFour); switch (node.getMode()) { - case INVOKE: - visit(node.getName()); - break; - case NEW: - token("new"); - break; - default: - throw new AssertionError(node.getMode()); + case INVOKE -> visit(node.getName()); + case NEW -> token("new"); } builder.close(); return null; @@ -916,26 +1071,25 @@ void visitVariables( annotationDirection, Optional.of(fragment.getModifiers()), fragment.getType(), - VarArgsOrNot.fromVariable(fragment), - ImmutableList.of(), - fragment.getName(), + /* name= */ fragment.getName(), "", "=", - Optional.fromNullable(fragment.getInitializer()), + Optional.ofNullable(fragment.getInitializer()), Optional.of(";"), - Optional.absent(), - Optional.fromNullable(variableFragmentDims(true, 0, fragment.getType()))); + /* receiverExpression= */ Optional.empty(), + Optional.ofNullable(variableFragmentDims(false, 0, fragment.getType()))); } else { declareMany(fragments, annotationDirection); } } - private TypeWithDims variableFragmentDims(boolean first, int leadingDims, Tree type) { + private static TypeWithDims variableFragmentDims( + boolean afterFirstToken, int leadingDims, Tree type) { if (type == null) { return null; } - if (first) { + if (!afterFirstToken) { return DimensionHelpers.extractDims(type, SortedDims.YES); } TypeWithDims dims = DimensionHelpers.extractDims(type, SortedDims.NO); @@ -958,19 +1112,19 @@ public Void visitForLoop(ForLoopTree node, Void unused) { if (!node.getInitializer().isEmpty()) { if (node.getInitializer().get(0).getKind() == VARIABLE) { PeekingIterator it = - Iterators.peekingIterator(node.getInitializer().iterator()); + Iterators.peekingIterator(node.getInitializer().iterator()); visitVariables( variableFragments(it, it.next()), DeclarationKind.NONE, Direction.HORIZONTAL); } else { - boolean first = true; + boolean afterFirstToken = false; builder.open(ZERO); for (StatementTree t : node.getInitializer()) { - if (!first) { + if (afterFirstToken) { token(","); builder.breakOp(" "); } scan(((ExpressionStatementTree) t).getExpression(), null); - first = false; + afterFirstToken = true; } token(";"); builder.close(); @@ -1027,11 +1181,11 @@ public Void visitIf(IfTree node, Void unused) { } } builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; boolean followingBlock = false; int expressionsN = expressions.size(); for (int i = 0; i < expressionsN; i++) { - if (!first) { + if (afterFirstToken) { if (followingBlock) { builder.space(); } else { @@ -1055,7 +1209,7 @@ public Void visitIf(IfTree node, Void unused) { AllowLeadingBlankLine.YES, AllowTrailingBlankLine.valueOf(trailingClauses)); followingBlock = statements.get(i).getKind() == BLOCK; - first = false; + afterFirstToken = true; } if (node.getElseStatement() != null) { if (followingBlock) { @@ -1076,9 +1230,14 @@ public Void visitIf(IfTree node, Void unused) { @Override public Void visitImport(ImportTree node, Void unused) { + checkForTypeAnnotation(node); sync(node); token("import"); builder.space(); + if (isModuleImport(node)) { + token("module"); + builder.space(); + } if (node.isStatic()) { token("static"); builder.space(); @@ -1090,13 +1249,47 @@ public Void visitImport(ImportTree node, Void unused) { return null; } + private static final @Nullable Method IS_MODULE_METHOD = getIsModuleMethod(); + + private static @Nullable Method getIsModuleMethod() { + try { + return ImportTree.class.getMethod("isModule"); + } catch (NoSuchMethodException ignored) { + return null; + } + } + + private static boolean isModuleImport(ImportTree importTree) { + if (IS_MODULE_METHOD == null) { + return false; + } + try { + return (boolean) IS_MODULE_METHOD.invoke(importTree); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private void checkForTypeAnnotation(ImportTree node) { + Name simpleName = getSimpleName(node); + Collection 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); /* - * Collect together all operators with same precedence to clean up indentation. Eclipse's - * extended operands help a little (to collect together the same operator), but they're applied - * inconsistently, and don't apply to other operators of the same precedence. + * Collect together all operators with same precedence to clean up indentation. */ List operands = new ArrayList<>(); List operators = new ArrayList<>(); @@ -1124,7 +1317,11 @@ public Void visitInstanceOf(InstanceOfTree node, Void unused) { builder.open(ZERO); token("instanceof"); builder.breakOp(" "); - scan(node.getType(), null); + if (node.getPattern() != null) { + scan(node.getPattern(), null); + } else { + scan(node.getType(), null); + } builder.close(); builder.close(); return null; @@ -1134,15 +1331,15 @@ public Void visitInstanceOf(InstanceOfTree node, Void unused) { public Void visitIntersectionType(IntersectionTypeTree node, Void unused) { sync(node); builder.open(plusFour); - boolean first = true; + boolean afterFirstToken = false; for (Tree type : node.getBounds()) { - if (!first) { + if (afterFirstToken) { builder.breakToFill(" "); token("&"); builder.space(); } scan(type, null); - first = false; + afterFirstToken = true; } builder.close(); return null; @@ -1169,14 +1366,17 @@ public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { if (parens) { token("("); } - boolean first = true; + boolean afterFirstToken = false; for (VariableTree parameter : node.getParameters()) { - if (!first) { + if (afterFirstToken) { token(","); builder.breakOp(" "); } - scan(parameter, null); - first = false; + visitVariables( + ImmutableList.of(parameter), + DeclarationKind.NONE, + fieldAnnotationDirection(parameter.getModifiers())); + afterFirstToken = true; } if (parens) { token(")"); @@ -1215,16 +1415,17 @@ public Void visitAnnotation(AnnotationTree node, Void unused) { token("@"); scan(node.getAnnotationType(), null); if (!node.getArguments().isEmpty()) { - builder.open(plusTwo); + builder.open(plusFour); token("("); builder.breakOp(); - boolean first = true; + boolean afterFirstToken = false; // Format the member value pairs one-per-line if any of them are // initialized with arrays. - boolean hasArrayInitializer = Iterables.any(node.getArguments(), IS_ARRAY_VALUE); + boolean hasArrayInitializer = + Iterables.any(node.getArguments(), JavaInputAstVisitor::isArrayValue); for (ExpressionTree argument : node.getArguments()) { - if (!first) { + if (afterFirstToken) { token(","); if (hasArrayInitializer) { builder.forcedBreak(); @@ -1232,12 +1433,15 @@ public Void visitAnnotation(AnnotationTree node, Void unused) { builder.breakOp(" "); } } - visitAnnotationArgument((AssignmentTree) argument); - first = false; + if (argument instanceof AssignmentTree) { + visitAnnotationArgument((AssignmentTree) argument); + } else { + scan(argument, null); + } + afterFirstToken = true; } - builder.breakOp(UNIFIED, "", minusTwo, Optional.absent()); + token(")"); builder.close(); - token(")", plusTwo); builder.close(); return null; @@ -1249,15 +1453,13 @@ public Void visitAnnotation(AnnotationTree node, Void unused) { return null; } - private static final Predicate IS_ARRAY_VALUE = - new Predicate() { - @Override - public boolean apply(ExpressionTree argument) { - ExpressionTree expression = ((AssignmentTree) argument).getExpression(); - return expression instanceof NewArrayTree - && ((NewArrayTree) expression).getType() == null; - } - }; + private static boolean isArrayValue(ExpressionTree argument) { + if (!(argument instanceof AssignmentTree)) { + return false; + } + ExpressionTree expression = ((AssignmentTree) argument).getExpression(); + return expression instanceof NewArrayTree && ((NewArrayTree) expression).getType() == null; + } public void visitAnnotationArgument(AssignmentTree node) { boolean isArrayInitializer = node.getExpression().getKind() == NEW_ARRAY; @@ -1296,12 +1498,22 @@ public Void visitAnnotatedType(AnnotatedTypeTree node, Void unused) { return null; } + // TODO(cushon): Use Flags if/when we drop support for Java 11 + + protected static final long COMPACT_RECORD_CONSTRUCTOR = 1L << 51; + + protected static final long RECORD = 1L << 61; + @Override public Void visitMethod(MethodTree node, Void unused) { sync(node); List annotations = node.getModifiers().getAnnotations(); List returnTypeAnnotations = ImmutableList.of(); + boolean isRecordConstructor = + (((JCMethodDecl) node).mods.flags & COMPACT_RECORD_CONSTRUCTOR) + == COMPACT_RECORD_CONSTRUCTOR; + if (!node.getTypeParameters().isEmpty() && !annotations.isEmpty()) { int typeParameterStart = getStartPosition(node.getTypeParameters().get(0)); for (int i = 0; i < annotations.size(); i++) { @@ -1312,15 +1524,31 @@ public Void visitMethod(MethodTree node, Void unused) { } } } - builder.addAll(visitModifiers(annotations, Direction.VERTICAL, Optional.absent())); + List typeAnnotations = + visitModifiers( + node.getModifiers(), + annotations, + Direction.VERTICAL, + /* declarationAnnotationBreak= */ Optional.empty()); + if (node.getTypeParameters().isEmpty() && node.getReturnType() != null) { + // If there are type parameters, we use a heuristic above to format annotations after the + // type parameter declarations as type-use annotations. If there are no type parameters, + // use the heuristics in visitModifiers for recognizing well known type-use annotations and + // formatting them as annotations on the return type. + returnTypeAnnotations = typeAnnotations; + typeAnnotations = ImmutableList.of(); + } Tree baseReturnType = null; - Deque> dims = null; + Deque> dims = null; if (node.getReturnType() != null) { TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getReturnType(), SortedDims.YES); baseReturnType = extractedDims.node; dims = new ArrayDeque<>(extractedDims.dims); + } else { + verticalAnnotations(typeAnnotations); + typeAnnotations = ImmutableList.of(); } builder.open(plusFour); @@ -1328,37 +1556,46 @@ public Void visitMethod(MethodTree node, Void unused) { BreakTag breakBeforeType = genSym(); builder.open(ZERO); { - boolean first = true; + boolean afterFirstToken = false; + if (!typeAnnotations.isEmpty()) { + visitAnnotations(typeAnnotations, BreakOrNot.NO, BreakOrNot.NO); + afterFirstToken = true; + } if (!node.getTypeParameters().isEmpty()) { - token("<"); - typeParametersRest(node.getTypeParameters(), plusFour); - if (!returnTypeAnnotations.isEmpty()) { + if (afterFirstToken) { builder.breakToFill(" "); - visitAnnotations(returnTypeAnnotations, BreakOrNot.NO, BreakOrNot.NO); } - first = false; + token("<"); + typeParametersRest(node.getTypeParameters(), plusFour); + afterFirstToken = true; } boolean openedNameAndTypeScope = false; // constructor-like declarations that don't match the name of the enclosing class are // parsed as method declarations with a null return type if (baseReturnType != null) { - if (!first) { + if (afterFirstToken) { builder.breakOp(INDEPENDENT, " ", ZERO, Optional.of(breakBeforeType)); } else { - first = false; + afterFirstToken = true; } if (!openedNameAndTypeScope) { builder.open(make(breakBeforeType, plusFour, ZERO)); openedNameAndTypeScope = true; } + builder.open(ZERO); + if (!returnTypeAnnotations.isEmpty()) { + visitAnnotations(returnTypeAnnotations, BreakOrNot.NO, BreakOrNot.NO); + builder.breakOp(" "); + } scan(baseReturnType, null); maybeAddDims(dims); + builder.close(); } - if (!first) { + if (afterFirstToken) { builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO, Optional.of(breakBeforeName)); } else { - first = false; + afterFirstToken = true; } if (!openedNameAndTypeScope) { builder.open(ZERO); @@ -1369,7 +1606,9 @@ public Void visitMethod(MethodTree node, Void unused) { name = builder.peekToken().get(); } token(name); - token("("); + if (!isRecordConstructor) { + token("("); + } // end of name and type scope builder.close(); } @@ -1379,12 +1618,14 @@ public Void visitMethod(MethodTree node, Void unused) { builder.open(Indent.If.make(breakBeforeType, plusFour, ZERO)); builder.open(ZERO); { - if (!node.getParameters().isEmpty() || node.getReceiverParameter() != null) { - // Break before args. - builder.breakToFill(""); - visitFormals(Optional.fromNullable(node.getReceiverParameter()), node.getParameters()); + if (!isRecordConstructor) { + if (!node.getParameters().isEmpty() || node.getReceiverParameter() != null) { + // Break before args. + builder.breakToFill(""); + visitFormals(Optional.ofNullable(node.getReceiverParameter()), node.getParameters()); + } + token(")"); } - token(")"); if (dims != null) { maybeAddDims(dims); } @@ -1423,7 +1664,7 @@ public Void visitMethod(MethodTree node, Void unused) { token(";"); } else { builder.space(); - builder.token("{", Doc.Token.RealOrImaginary.REAL, plusTwo, Optional.of(plusTwo)); + builder.token("{", Doc.Token.RealOrImaginary.REAL, plusTwo, Optional.of(plusTwo)); } builder.close(); @@ -1453,10 +1694,89 @@ private void methodBody(MethodTree node) { @Override public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { sync(node); + if (handleLogStatement(node)) { + return null; + } visitDot(node); return null; } + /** + * Special-cases log statements, to output: + * + *

{@code
+   * logger.atInfo().log(
+   *     "Number of foos: %d, foos.size());
+   * }
+ * + *

Instead of: + * + *

{@code
+   * logger
+   *     .atInfo()
+   *     .log(
+   *         "Number of foos: %d, foos.size());
+   * }
+ */ + private boolean handleLogStatement(MethodInvocationTree node) { + if (!getMethodName(node).contentEquals("log")) { + return false; + } + Deque parts = new ArrayDeque<>(); + ExpressionTree curr = node; + while (curr instanceof MethodInvocationTree) { + MethodInvocationTree method = (MethodInvocationTree) curr; + parts.addFirst(method); + if (!LOG_METHODS.contains(getMethodName(method).toString())) { + return false; + } + curr = Trees.getMethodReceiver(method); + } + if (!(curr instanceof IdentifierTree)) { + return false; + } + parts.addFirst(curr); + visitDotWithPrefix( + ImmutableList.copyOf(parts), false, ImmutableList.of(parts.size() - 1), INDEPENDENT); + return true; + } + + static final ImmutableSet LOG_METHODS = + ImmutableSet.of( + "at", + "atConfig", + "atDebug", + "atFine", + "atFiner", + "atFinest", + "atInfo", + "atMostEvery", + "atSevere", + "atWarning", + "every", + "log", + "logVarargs", + "perUnique", + "withCause", + "withStackTrace"); + + private static List handleStream(List parts) { + return indexes( + parts.stream(), + p -> { + if (!(p instanceof MethodInvocationTree)) { + return false; + } + Name name = getMethodName((MethodInvocationTree) p); + return Stream.of("stream", "parallelStream", "toBuilder") + .anyMatch(name::contentEquals); + }) + .collect(toList()); + } + + private static Stream indexes(Stream stream, Predicate predicate) { + return Streams.mapWithIndex(stream, (x, i) -> predicate.apply(x) ? i : -1).filter(x -> x != -1); + } @Override public Void visitMemberSelect(MemberSelectTree node, Void unused) { @@ -1469,11 +1789,24 @@ public Void visitMemberSelect(MemberSelectTree node, Void unused) { 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 singed literal (with no - // unary minus), but the lexer still expects two tokens. - if (sourceForNode.startsWith("-")) { + if (sourceForNode.startsWith("\"\"\"")) { + String separator = Newlines.guessLineSeparator(sourceForNode); + ImmutableList initialLines = sourceForNode.lines().collect(toImmutableList()); + String stripped = initialLines.stream().skip(1).collect(joining(separator)).stripIndent(); + // Use the last line of the text block to determine if it is deindented to column 0, by + // comparing the length of the line in the input source with the length after processing + // the text block contents with stripIndent(). + boolean deindent = + getLast(initialLines).stripTrailing().length() + == Streams.findLast(stripped.lines()).orElseThrow().stripTrailing().length(); + if (deindent) { + Indent indent = Indent.Const.make(Integer.MIN_VALUE / indentMultiplier, indentMultiplier); + builder.breakOp(indent); + } + token(sourceForNode); + return null; + } + if (isUnaryMinusLiteral(sourceForNode)) { token("-"); sourceForNode = sourceForNode.substring(1).trim(); } @@ -1481,6 +1814,14 @@ public Void visitLiteral(LiteralTree node, Void unused) { 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 packageAnnotations) { if (!packageAnnotations.isEmpty()) { @@ -1511,15 +1852,14 @@ public Void visitParameterizedType(ParameterizedTypeTree node, Void unused) { token("<"); builder.breakOp(); builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; for (Tree typeArgument : node.getTypeArguments()) { - if (!first) { + if (afterFirstToken) { token(","); - // TODO(cushon): unify breaks - builder.breakToFill(" "); + builder.breakOp(" "); } scan(typeArgument, null); - first = false; + afterFirstToken = true; } builder.close(); builder.close(); @@ -1561,16 +1901,15 @@ private void splitToken(String operatorName) { private boolean ambiguousUnaryOperator(UnaryTree node, String operatorName) { switch (node.getKind()) { - case UNARY_MINUS: - case UNARY_PLUS: - break; - default: + case UNARY_MINUS, UNARY_PLUS -> {} + 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; } @@ -1580,39 +1919,31 @@ private boolean ambiguousUnaryOperator(UnaryTree node, String operatorName) { 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); switch (node.getPrimitiveTypeKind()) { - case BOOLEAN: - token("boolean"); - break; - case BYTE: - token("byte"); - break; - case SHORT: - token("short"); - break; - case INT: - token("int"); - break; - case LONG: - token("long"); - break; - case CHAR: - token("char"); - break; - case FLOAT: - token("float"); - break; - case DOUBLE: - token("double"); - break; - case VOID: - token("void"); - break; - default: - throw new AssertionError(node.getPrimitiveTypeKind()); + case BOOLEAN -> token("boolean"); + case BYTE -> token("byte"); + case SHORT -> token("short"); + case INT -> token("int"); + case LONG -> token("long"); + case CHAR -> token("char"); + case FLOAT -> token("float"); + case DOUBLE -> token("double"); + case VOID -> token("void"); + default -> throw new AssertionError(node.getPrimitiveTypeKind()); } return null; } @@ -1639,7 +1970,7 @@ boolean visitSingleMemberAnnotation(AnnotationTree node) { if (node.getArguments().size() != 1) { return false; } - ExpressionTree value = Iterables.getOnlyElement(node.getArguments()); + ExpressionTree value = getOnlyElement(node.getArguments()); if (value.getKind() == ASSIGNMENT) { return false; } @@ -1662,52 +1993,97 @@ public Void visitCase(CaseTree node, Void unused) { sync(node); markForPartialFormat(); builder.forcedBreak(); - if (node.getExpression() == null) { - token("default", plusTwo); - token(":"); + List labels = node.getLabels(); + boolean isDefault = + labels.size() == 1 && getOnlyElement(labels).getKind().name().equals("DEFAULT_CASE_LABEL"); + builder.open(node.getCaseKind().equals(CaseTree.CaseKind.RULE) ? plusFour : ZERO); + if (isDefault) { + token("default", ZERO); } else { - token("case", plusTwo); + token("case", ZERO); + builder.open(ZERO); builder.space(); - scan(node.getExpression(), null); - token(":"); + boolean afterFirstToken = false; + for (Tree expression : labels) { + if (afterFirstToken) { + token(","); + builder.breakOp(" "); + } + scan(expression, null); + afterFirstToken = true; + } + builder.close(); } - builder.open(plusTwo); - try { - // don't partially format within case groups - inExpression.addLast(true); - visitStatements(node.getStatements()); - } finally { - inExpression.removeLast(); + + final ExpressionTree guard = node.getGuard(); + if (guard != null) { + builder.breakToFill(" "); + token("when"); + builder.space(); + scan(guard, null); + } + + switch (node.getCaseKind()) { + case STATEMENT -> { + token(":"); + builder.open(plusTwo); + visitStatements(node.getStatements()); + builder.close(); + builder.close(); + } + case RULE -> { + builder.space(); + token("-"); + token(">"); + if (node.getBody().getKind() == BLOCK) { + builder.close(); + builder.space(); + // Explicit call with {@link CollapseEmptyOrNot.YES} to handle empty case blocks. + visitBlock( + (BlockTree) node.getBody(), + CollapseEmptyOrNot.YES, + AllowLeadingBlankLine.NO, + AllowTrailingBlankLine.NO); + } else { + builder.breakOp(" "); + scan(node.getBody(), null); + builder.close(); + } + builder.guessToken(";"); + } } - builder.close(); return null; } @Override public Void visitSwitch(SwitchTree node, Void unused) { sync(node); + visitSwitch(node.getExpression(), node.getCases()); + return null; + } + + protected void visitSwitch(ExpressionTree expression, List cases) { token("switch"); builder.space(); token("("); - scan(skipParen(node.getExpression()), null); + scan(skipParen(expression), null); token(")"); builder.space(); tokenBreakTrailingComment("{", plusTwo); builder.blankLineWanted(BlankLineWanted.NO); builder.open(plusTwo); - boolean first = true; - for (CaseTree caseTree : node.getCases()) { - if (!first) { + boolean afterFirstToken = false; + for (CaseTree caseTree : cases) { + if (afterFirstToken) { builder.blankLineWanted(BlankLineWanted.PRESERVE); } scan(caseTree, null); - first = false; + afterFirstToken = true; } builder.close(); builder.forcedBreak(); builder.blankLineWanted(BlankLineWanted.NO); token("}", plusFour); - return null; } @Override @@ -1745,31 +2121,34 @@ public Void visitTry(TryTree node, Void unused) { if (!node.getResources().isEmpty()) { token("("); builder.open(node.getResources().size() > 1 ? plusFour : ZERO); - boolean first = true; + boolean afterFirstToken = false; for (Tree resource : node.getResources()) { - if (!first) { + if (afterFirstToken) { builder.forcedBreak(); } - VariableTree variableTree = (VariableTree) resource; - declareOne( - DeclarationKind.PARAMETER, - fieldAnnotationDirection(variableTree.getModifiers()), - Optional.of(variableTree.getModifiers()), - variableTree.getType(), - VarArgsOrNot.NO, - ImmutableList.of(), - variableTree.getName(), - "", - "=", - Optional.fromNullable(variableTree.getInitializer()), - Optional.absent(), - Optional.absent(), - Optional.absent()); + if (resource instanceof VariableTree) { + VariableTree variableTree = (VariableTree) resource; + declareOne( + DeclarationKind.PARAMETER, + fieldAnnotationDirection(variableTree.getModifiers()), + Optional.of(variableTree.getModifiers()), + variableTree.getType(), + /* name= */ variableTree.getName(), + "", + "=", + Optional.ofNullable(variableTree.getInitializer()), + /* trailing= */ Optional.empty(), + /* receiverExpression= */ Optional.empty(), + /* typeWithDims= */ Optional.empty()); + } else { + // TODO(cushon): think harder about what to do with `try (resource1; resource2) {}` + scan(resource, null); + } if (builder.peekToken().equals(Optional.of(";"))) { token(";"); builder.space(); } - first = false; + afterFirstToken = true; } if (builder.peekToken().equals(Optional.of(";"))) { token(";"); @@ -1808,11 +2187,11 @@ public Void visitTry(TryTree node, Void unused) { public void visitClassDeclaration(ClassTree node) { sync(node); - List breaks = - visitModifiers(node.getModifiers(), Direction.VERTICAL, Optional.absent()); + typeDeclarationModifiers(node.getModifiers()); + List permitsTypes = node.getPermitsClause(); 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()); @@ -1824,7 +2203,7 @@ public void visitClassDeclaration(ClassTree node) { if (!node.getTypeParameters().isEmpty()) { typeParametersRest( node.getTypeParameters(), - hasSuperclassType || hasSuperInterfaceTypes ? plusFour : ZERO); + hasSuperclassType || hasSuperInterfaceTypes || hasPermitsTypes ? plusFour : ZERO); } if (hasSuperclassType) { builder.breakToFill(" "); @@ -1832,22 +2211,10 @@ public void visitClassDeclaration(ClassTree node) { 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) { @@ -1870,15 +2237,15 @@ public Void visitTypeParameter(TypeParameterTree node, Void unused) { builder.open(plusFour); builder.breakOp(" "); builder.open(plusFour); - boolean first = true; + boolean afterFirstToken = false; for (Tree typeBound : node.getBounds()) { - if (!first) { + if (afterFirstToken) { builder.breakToFill(" "); token("&"); builder.space(); } scan(typeBound, null); - first = false; + afterFirstToken = true; } builder.close(); builder.close(); @@ -1928,19 +2295,19 @@ public Void visitWildcard(WildcardTree node, Void unused) { // Helper methods. /** Helper method for annotations. */ - void visitAnnotations( + protected void visitAnnotations( List annotations, BreakOrNot breakBefore, BreakOrNot breakAfter) { if (!annotations.isEmpty()) { if (breakBefore.isYes()) { builder.breakToFill(" "); } - boolean first = true; + boolean afterFirstToken = false; for (AnnotationTree annotation : annotations) { - if (!first) { + if (afterFirstToken) { builder.breakToFill(" "); } scan(annotation, null); - first = false; + afterFirstToken = true; } if (breakAfter.isYes()) { builder.breakToFill(" "); @@ -1948,8 +2315,16 @@ void visitAnnotations( } } - /** Helper method for blocks. */ - private void visitBlock( + void verticalAnnotations(List annotations) { + for (AnnotationTree annotation : annotations) { + builder.forcedBreak(); + scan(annotation, null); + builder.forcedBreak(); + } + } + + /** Helper method for blocks. */ + protected void visitBlock( BlockTree node, CollapseEmptyOrNot collapseEmptyOrNot, AllowLeadingBlankLine allowLeadingBlankLine, @@ -1999,31 +2374,31 @@ private void visitStatement( AllowTrailingBlankLine allowTrailingBlank) { sync(node); switch (node.getKind()) { - case BLOCK: + case BLOCK -> { builder.space(); visitBlock((BlockTree) node, collapseEmptyOrNot, allowLeadingBlank, allowTrailingBlank); - break; - default: + } + default -> { builder.open(plusTwo); builder.breakOp(" "); scan(node, null); builder.close(); + } } } - private void visitStatements(List statements) { - boolean first = true; - PeekingIterator it = - Iterators.peekingIterator(statements.iterator()); + protected void visitStatements(List statements) { + boolean afterFirstToken = false; + PeekingIterator it = Iterators.peekingIterator(statements.iterator()); dropEmptyDeclarations(); while (it.hasNext()) { StatementTree tree = it.next(); builder.forcedBreak(); - if (!first) { + if (afterFirstToken) { builder.blankLineWanted(BlankLineWanted.PRESERVE); } markForPartialFormat(); - first = false; + afterFirstToken = true; List fragments = variableFragments(it, tree); if (!fragments.isEmpty()) { visitVariables( @@ -2036,12 +2411,21 @@ private void visitStatements(List statements) { } } + protected void typeDeclarationModifiers(ModifiersTree modifiers) { + List 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 declarationAnnotationBreak) { - builder.addAll(visitModifiers(modifiers, annotationDirection, declarationAnnotationBreak)); + List typeAnnotations = + visitModifiers(modifiers, annotationDirection, declarationAnnotationBreak); + visitAnnotations(typeAnnotations, BreakOrNot.NO, BreakOrNot.YES); } @Override @@ -2050,39 +2434,51 @@ public Void visitModifiers(ModifiersTree node, Void unused) { } /** Output combined modifiers and annotations and returns the trailing break. */ - private List visitModifiers( + @CheckReturnValue + protected ImmutableList visitModifiers( ModifiersTree modifiersTree, Direction annotationsDirection, Optional declarationAnnotationBreak) { return visitModifiers( + modifiersTree, modifiersTree.getAnnotations(), annotationsDirection, declarationAnnotationBreak); } - private List visitModifiers( + @CheckReturnValue + protected ImmutableList visitModifiers( + ModifiersTree modifiersTree, List annotationTrees, Direction annotationsDirection, Optional declarationAnnotationBreak) { - if (annotationTrees.isEmpty() && !nextIsModifier()) { - return EMPTY_LIST; + DeclarationModifiersAndTypeAnnotations splitModifiers = + splitModifiers(modifiersTree, annotationTrees); + return visitModifiers(splitModifiers, annotationsDirection, declarationAnnotationBreak); + } + + @CheckReturnValue + private ImmutableList visitModifiers( + DeclarationModifiersAndTypeAnnotations splitModifiers, + Direction annotationsDirection, + Optional declarationAnnotationBreak) { + if (splitModifiers.declarationModifiers().isEmpty()) { + return splitModifiers.typeAnnotations(); } - Deque annotations = new ArrayDeque<>(annotationTrees); + Deque declarationModifiers = + new ArrayDeque<>(splitModifiers.declarationModifiers()); builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; boolean lastWasAnnotation = false; - while (!annotations.isEmpty()) { - if (nextIsModifier()) { - break; - } - if (!first) { + while (!declarationModifiers.isEmpty() && !declarationModifiers.peekFirst().isModifier()) { + if (afterFirstToken) { builder.addAll( annotationsDirection.isVertical() ? forceBreakList(declarationAnnotationBreak) : breakList(declarationAnnotationBreak)); } - scan(annotations.removeFirst(), null); - first = false; + formatAnnotationOrModifier(declarationModifiers); + afterFirstToken = true; lastWasAnnotation = true; } builder.close(); @@ -2090,49 +2486,194 @@ private List visitModifiers( 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); } builder.open(ZERO); - first = true; - while (nextIsModifier() || !annotations.isEmpty()) { - if (!first) { - builder.addAll(breakFillList(Optional.absent())); - } - if (nextIsModifier()) { - token(builder.peekToken().get()); - } else { - scan(annotations.removeFirst(), null); - lastWasAnnotation = true; + afterFirstToken = false; + while (!declarationModifiers.isEmpty()) { + if (afterFirstToken) { + builder.addAll(breakFillList(Optional.empty())); } - first = false; + formatAnnotationOrModifier(declarationModifiers); + afterFirstToken = true; } builder.close(); - return breakFillList(Optional.absent()); - } - - boolean nextIsModifier() { - switch (builder.peekToken().get()) { - case "public": - case "protected": - case "private": - case "abstract": - case "static": - case "final": - case "transient": - case "volatile": - case "synchronized": - case "native": - case "strictfp": - case "default": - return true; - default: - return false; + builder.addAll(breakFillList(Optional.empty())); + return splitModifiers.typeAnnotations(); + } + + /** Represents an annotation or a modifier in a {@link ModifiersTree}. */ + @AutoOneOf(AnnotationOrModifier.Kind.class) + abstract static class AnnotationOrModifier implements Comparable { + 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() { + return switch (getKind()) { + case MODIFIER -> modifier().getPosition(); + case ANNOTATION -> getStartPosition(annotation()); + }; + } + + private static final Comparator 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. + * + *

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 declarationModifiers(); + + abstract ImmutableList typeAnnotations(); + + static DeclarationModifiersAndTypeAnnotations create( + ImmutableList declarationModifiers, + ImmutableList 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 annotations) { + if (annotations.isEmpty() && !isModifier(builder.peekToken().get())) { + return DeclarationModifiersAndTypeAnnotations.empty(); + } + RangeSet annotationRanges = TreeRangeSet.create(); + for (AnnotationTree annotationTree : annotations) { + annotationRanges.add( + Range.closedOpen( + getStartPosition(annotationTree), getEndPosition(annotationTree, getCurrentPath()))); + } + ImmutableList 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 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 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 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()); + } + } + case ANNOTATION -> scan(modifier.annotation(), null); + } + } + + 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) { + return switch (token) { + case "public", + "protected", + "private", + "abstract", + "static", + "final", + "transient", + "volatile", + "synchronized", + "native", + "strictfp", + "default", + "sealed", + "non", + "-" -> + true; + default -> false; + }; } @Override @@ -2173,16 +2714,18 @@ private void visitUnionType(VariableTree declaration) { builder.open(ZERO); sync(declaration); visitAndBreakModifiers( - declaration.getModifiers(), Direction.HORIZONTAL, Optional.absent()); + declaration.getModifiers(), + Direction.HORIZONTAL, + /* declarationAnnotationBreak= */ Optional.empty()); List union = type.getTypeAlternatives(); - boolean first = true; + boolean afterFirstToken = false; for (int i = 0; i < union.size() - 1; i++) { - if (!first) { + if (afterFirstToken) { builder.breakOp(" "); token("|"); builder.space(); } else { - first = false; + afterFirstToken = true; } scan(union.get(i), null); } @@ -2193,18 +2736,15 @@ private void visitUnionType(VariableTree declaration) { declareOne( DeclarationKind.NONE, Direction.HORIZONTAL, - Optional.absent(), + /* modifiers= */ Optional.empty(), last, - VarArgsOrNot.NO, // VarArgsOrNot.valueOf(declaration.isVarargs()), - ImmutableList.of(), // declaration.varargsAnnotations(), - declaration.getName(), - "", - // declaration.extraDimensions(), + /* name= */ declaration.getName(), + /* op= */ "", "=", - Optional.fromNullable(declaration.getInitializer()), - Optional.absent(), - Optional.absent(), - Optional.absent()); + Optional.ofNullable(declaration.getInitializer()), + /* trailing= */ Optional.empty(), + /* receiverExpression= */ Optional.empty(), + /* typeWithDims= */ Optional.empty()); builder.close(); } @@ -2228,44 +2768,42 @@ private static void walkInfix( } } - private void visitFormals( + protected void visitFormals( Optional receiver, List parameters) { if (!receiver.isPresent() && parameters.isEmpty()) { return; } builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; if (receiver.isPresent()) { - // TODO(jdd): Use builders. + // TODO(user): Use builders. declareOne( DeclarationKind.PARAMETER, Direction.HORIZONTAL, Optional.of(receiver.get().getModifiers()), receiver.get().getType(), - VarArgsOrNot.NO, - ImmutableList.of(), - receiver.get().getName(), + /* name= */ receiver.get().getName(), "", "", - Optional.absent(), - !parameters.isEmpty() ? Optional.of(",") : Optional.absent(), + /* initializer= */ Optional.empty(), + !parameters.isEmpty() ? Optional.of(",") : Optional.empty(), Optional.of(receiver.get().getNameExpression()), - Optional.absent()); - first = false; + /* typeWithDims= */ Optional.empty()); + afterFirstToken = true; } for (int i = 0; i < parameters.size(); i++) { VariableTree parameter = parameters.get(i); - if (!first) { + if (afterFirstToken) { builder.breakOp(" "); } visitToDeclare( DeclarationKind.PARAMETER, Direction.HORIZONTAL, parameter, - Optional.absent(), + /* initializer= */ Optional.empty(), "=", - i < parameters.size() - 1 ? Optional.of(",") : Optional.absent()); - first = false; + i < parameters.size() - 1 ? Optional.of(",") : /* a= */ Optional.empty()); + afterFirstToken = true; } builder.close(); } @@ -2274,14 +2812,14 @@ private void visitFormals( private void visitThrowsClause(List thrownExceptionTypes) { token("throws"); builder.breakToFill(" "); - boolean first = true; + boolean afterFirstToken = false; for (ExpressionTree thrownExceptionType : thrownExceptionTypes) { - if (!first) { + if (afterFirstToken) { token(","); - builder.breakToFill(" "); + builder.breakOp(" "); } scan(thrownExceptionType, null); - first = false; + afterFirstToken = true; } } @@ -2292,6 +2830,122 @@ public Void visitIdentifier(IdentifierTree node, Void unused) { return null; } + @Override + public Void visitModule(ModuleTree node, Void unused) { + for (AnnotationTree annotation : node.getAnnotations()) { + scan(annotation, null); + builder.forcedBreak(); + } + if (node.getModuleType() == ModuleTree.ModuleKind.OPEN) { + token("open"); + builder.space(); + } + token("module"); + builder.space(); + scan(node.getName(), null); + builder.space(); + if (node.getDirectives().isEmpty()) { + tokenBreakTrailingComment("{", plusTwo); + builder.blankLineWanted(BlankLineWanted.NO); + token("}", plusTwo); + } else { + builder.open(plusTwo); + token("{"); + builder.forcedBreak(); + Optional previousDirective = Optional.empty(); + for (DirectiveTree directiveTree : node.getDirectives()) { + markForPartialFormat(); + builder.blankLineWanted( + previousDirective.map(k -> !k.equals(directiveTree.getKind())).orElse(false) + ? BlankLineWanted.YES + : BlankLineWanted.NO); + builder.forcedBreak(); + scan(directiveTree, null); + previousDirective = Optional.of(directiveTree.getKind()); + } + builder.close(); + builder.forcedBreak(); + token("}"); + } + return null; + } + + private void visitDirective( + String name, + String separator, + ExpressionTree nameExpression, + @Nullable List items) { + token(name); + builder.space(); + scan(nameExpression, null); + if (items != null) { + builder.open(plusFour); + builder.space(); + token(separator); + builder.forcedBreak(); + boolean afterFirstToken = false; + for (ExpressionTree item : items) { + if (afterFirstToken) { + token(","); + builder.forcedBreak(); + } + scan(item, null); + afterFirstToken = true; + } + token(";"); + builder.close(); + } else { + token(";"); + } + } + + @Override + public Void visitExports(ExportsTree node, Void unused) { + visitDirective("exports", "to", node.getPackageName(), node.getModuleNames()); + return null; + } + + @Override + public Void visitOpens(OpensTree node, Void unused) { + visitDirective("opens", "to", node.getPackageName(), node.getModuleNames()); + return null; + } + + @Override + public Void visitProvides(ProvidesTree node, Void unused) { + visitDirective("provides", "with", node.getServiceName(), node.getImplementationNames()); + return null; + } + + @Override + public Void visitRequires(RequiresTree node, Void unused) { + token("requires"); + builder.space(); + while (true) { + if (builder.peekToken().equals(Optional.of("static"))) { + token("static"); + builder.space(); + } else if (builder.peekToken().equals(Optional.of("transitive"))) { + token("transitive"); + builder.space(); + } else { + break; + } + } + scan(node.getModuleName(), null); + token(";"); + return null; + } + + @Override + public Void visitUses(UsesTree node, Void unused) { + token("uses"); + builder.space(); + scan(node.getServiceName(), null); + token(";"); + return null; + } + /** Helper method for import declarations, names, and qualified names. */ private void visitName(Tree node) { Deque stack = new ArrayDeque<>(); @@ -2299,13 +2953,13 @@ private void visitName(Tree node) { stack.addFirst(((MemberSelectTree) node).getIdentifier()); } stack.addFirst(((IdentifierTree) node).getName()); - boolean first = true; + boolean afterFirstToken = false; for (Name name : stack) { - if (!first) { + if (afterFirstToken) { token("."); } token(name.toString()); - first = false; + afterFirstToken = true; } } @@ -2317,46 +2971,44 @@ private void visitToDeclare( String equals, Optional trailing) { sync(node); - boolean varargs = VarArgsOrNot.fromVariable(node).isYes(); - List varargsAnnotations = ImmutableList.of(); - Tree type = node.getType(); - if (varargs) { - if (type instanceof AnnotatedTypeTree) { - varargsAnnotations = ((AnnotatedTypeTree) type).getAnnotations(); - type = ((AnnotatedTypeTree) type).getUnderlyingType(); - } - type = ((ArrayTypeTree) type).getType(); + Optional typeWithDims; + Tree type; + if (node.getType() != null) { + TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getType(), SortedDims.YES); + typeWithDims = Optional.of(extractedDims); + type = extractedDims.node; + } else { + typeWithDims = Optional.empty(); + type = null; } declareOne( kind, annotationsDirection, Optional.of(node.getModifiers()), type, - VarArgsOrNot.valueOf(varargs), - varargsAnnotations, node.getName(), "", equals, initializer, trailing, - Optional.absent(), - Optional.absent()); + /* receiverExpression= */ Optional.empty(), + typeWithDims); } - /** Does not omit the leading '<', which should be associated with the type name. */ - private void typeParametersRest( + /** Does not omit the leading {@code "<"}, which should be associated with the type name. */ + protected void typeParametersRest( List typeParameters, Indent plusIndent) { builder.open(plusIndent); builder.breakOp(); builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; for (TypeParameterTree typeParameter : typeParameters) { - if (!first) { + if (afterFirstToken) { token(","); builder.breakOp(" "); } scan(typeParameter, null); - first = false; + afterFirstToken = true; } token(">"); builder.close(); @@ -2383,21 +3035,19 @@ void visitDot(ExpressionTree node0) { node = getArrayBase(node); } switch (node.getKind()) { - case MEMBER_SELECT: - node = ((MemberSelectTree) node).getExpression(); - break; - case METHOD_INVOCATION: - node = getMethodReceiver((MethodInvocationTree) node); - break; - case IDENTIFIER: + case MEMBER_SELECT -> node = ((MemberSelectTree) node).getExpression(); + case METHOD_INVOCATION -> node = getMethodReceiver((MethodInvocationTree) node); + case IDENTIFIER -> { node = null; break LOOP; - default: + } + default -> { // If the dot chain starts with a primary expression // (e.g. a class instance creation, or a conditional expression) // then remove it from the list and deal with it first. node = stack.removeFirst(); break LOOP; + } } } while (node != null); List items = new ArrayList<>(stack); @@ -2426,9 +3076,11 @@ void visitDot(ExpressionTree node0) { } } + Set prefixes = new LinkedHashSet<>(); + // Check if the dot chain has a prefix that looks like a type name, so we can // treat the type name-shaped part as a single syntactic unit. - int prefixIndex = TypeNameClassifier.typePrefixLength(simpleNames(stack)); + TypeNameClassifier.typePrefixLength(simpleNames(stack)).ifPresent(prefixes::add); int invocationCount = 0; int firstInvocationIndex = -1; @@ -2464,23 +3116,22 @@ void visitDot(ExpressionTree node0) { // myField // .foo(); // - if (invocationCount == 1) { - prefixIndex = firstInvocationIndex; + if (invocationCount == 1 && firstInvocationIndex > 0) { + prefixes.add(firstInvocationIndex); } - if (prefixIndex == -1 && items.get(0) instanceof IdentifierTree) { + if (prefixes.isEmpty() && items.get(0) instanceof IdentifierTree) { switch (((IdentifierTree) items.get(0)).getName().toString()) { - case "this": - case "super": - prefixIndex = 1; - break; - default: - break; + case "this", "super" -> prefixes.add(1); + default -> {} } } - if (prefixIndex > 0) { - visitDotWithPrefix(items, needDot, prefixIndex); + List streamPrefixes = handleStream(items); + streamPrefixes.forEach(x -> prefixes.add(x.intValue())); + if (!prefixes.isEmpty()) { + visitDotWithPrefix( + items, needDot, prefixes, streamPrefixes.isEmpty() ? INDEPENDENT : UNIFIED); } else { visitRegularDot(items, needDot); } @@ -2536,10 +3187,7 @@ private void visitRegularDot(List items, boolean needDot) { // .happens()) // .thenReturn(result); // - private boolean fillFirstArgument( - ExpressionTree e, - List items, - Indent indent) { + private boolean fillFirstArgument(ExpressionTree e, List items, Indent indent) { // is there a trailing dereference? if (items.size() < 2) { return false; @@ -2575,22 +3223,30 @@ private boolean fillFirstArgument( * * @param items in the chain * @param needDot whether a leading dot is needed - * @param prefixIndex the index of the last item in the prefix + * @param prefixes the terminal indices of 'prefixes' of the expression that should be treated as + * a syntactic unit */ - private void visitDotWithPrefix(List items, boolean needDot, int prefixIndex) { + private void visitDotWithPrefix( + List items, + boolean needDot, + Collection prefixes, + FillMode prefixFillMode) { // Are there method invocations or field accesses after the prefix? - boolean trailingDereferences = prefixIndex >= 0 && prefixIndex < items.size() - 1; + boolean trailingDereferences = !prefixes.isEmpty() && getLast(prefixes) < items.size() - 1; builder.open(plusFour); - builder.open(trailingDereferences ? ZERO : ZERO); + for (int times = 0; times < prefixes.size(); times++) { + builder.open(ZERO); + } + Deque unconsumedPrefixes = new ArrayDeque<>(ImmutableSortedSet.copyOf(prefixes)); BreakTag nameTag = genSym(); for (int i = 0; i < items.size(); i++) { ExpressionTree e = items.get(i); if (needDot) { FillMode fillMode; - if (prefixIndex >= 0 && i <= prefixIndex) { - fillMode = FillMode.INDEPENDENT; + if (!unconsumedPrefixes.isEmpty() && i <= unconsumedPrefixes.peekFirst()) { + fillMode = prefixFillMode; } else { fillMode = FillMode.UNIFIED; } @@ -2600,8 +3256,9 @@ private void visitDotWithPrefix(List items, boolean needDot, int } BreakTag tyargTag = genSym(); dotExpressionUpToArgs(e, Optional.of(tyargTag)); - if (prefixIndex >= 0 && i == prefixIndex) { + if (!unconsumedPrefixes.isEmpty() && i == unconsumedPrefixes.peekFirst()) { builder.close(); + unconsumedPrefixes.removeFirst(); } Indent tyargIndent = Indent.If.make(tyargTag, plusFour, ZERO); @@ -2615,24 +3272,23 @@ private void visitDotWithPrefix(List items, boolean needDot, int } /** Returns the simple names of expressions in a "." chain. */ - private List simpleNames(Deque stack) { + private static ImmutableList simpleNames(Deque stack) { ImmutableList.Builder simpleNames = ImmutableList.builder(); OUTER: for (ExpressionTree expression : stack) { boolean isArray = expression.getKind() == ARRAY_ACCESS; expression = getArrayBase(expression); switch (expression.getKind()) { - case MEMBER_SELECT: - simpleNames.add(((MemberSelectTree) expression).getIdentifier().toString()); - break; - case IDENTIFIER: - simpleNames.add(((IdentifierTree) expression).getName().toString()); - break; - case METHOD_INVOCATION: + case MEMBER_SELECT -> + simpleNames.add(((MemberSelectTree) expression).getIdentifier().toString()); + case IDENTIFIER -> simpleNames.add(((IdentifierTree) expression).getName().toString()); + case METHOD_INVOCATION -> { simpleNames.add(getMethodName((MethodInvocationTree) expression).toString()); break OUTER; - default: + } + default -> { break OUTER; + } } if (isArray) { break OUTER; @@ -2644,27 +3300,23 @@ private List simpleNames(Deque stack) { private void dotExpressionUpToArgs(ExpressionTree expression, Optional tyargTag) { expression = getArrayBase(expression); switch (expression.getKind()) { - case MEMBER_SELECT: + case MEMBER_SELECT -> { MemberSelectTree fieldAccess = (MemberSelectTree) expression; visit(fieldAccess.getIdentifier()); - break; - case METHOD_INVOCATION: + } + case METHOD_INVOCATION -> { MethodInvocationTree methodInvocation = (MethodInvocationTree) expression; if (!methodInvocation.getTypeArguments().isEmpty()) { builder.open(plusFour); addTypeArguments(methodInvocation.getTypeArguments(), ZERO); - // TODO(jdd): Should indent the name -4. + // TODO(user): Should indent the name -4. builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, tyargTag); builder.close(); } visit(getMethodName(methodInvocation)); - break; - case IDENTIFIER: - visit(((IdentifierTree) expression).getName()); - break; - default: - scan(expression, null); - break; + } + case IDENTIFIER -> visit(((IdentifierTree) expression).getName()); + default -> scan(expression, null); } } @@ -2672,14 +3324,14 @@ private void dotExpressionUpToArgs(ExpressionTree expression, Optional * 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; } @@ -2689,14 +3341,13 @@ private void dotExpressionArgsAndParen( Deque indices = getArrayIndices(expression); expression = getArrayBase(expression); switch (expression.getKind()) { - case METHOD_INVOCATION: + case METHOD_INVOCATION -> { builder.open(tyargIndent); MethodInvocationTree methodInvocation = (MethodInvocationTree) expression; addArguments(methodInvocation.getArguments(), indent); builder.close(); - break; - default: - break; + } + default -> {} } formatArrayIndices(indices); } @@ -2720,7 +3371,7 @@ private void formatArrayIndices(Deque indices) { * Returns all array indices for the given expression, e.g. given {@code foo[0][0]} returns the * expressions for {@code [0][0]}. */ - private Deque getArrayIndices(ExpressionTree expression) { + private static Deque getArrayIndices(ExpressionTree expression) { Deque indices = new ArrayDeque<>(); while (expression instanceof ArrayAccessTree) { ArrayAccessTree array = (ArrayAccessTree) expression; @@ -2737,14 +3388,14 @@ void addTypeArguments(List typeArguments, Indent plusIndent) { } token("<"); builder.open(plusIndent); - boolean first = true; + boolean afterFirstToken = false; for (Tree typeArgument : typeArguments) { - if (!first) { + if (afterFirstToken) { token(","); builder.breakToFill(" "); } scan(typeArgument, null); - first = false; + afterFirstToken = true; } builder.close(); token(">"); @@ -2765,11 +3416,11 @@ void addArguments(List arguments, Indent plusIndent) { if (arguments.size() % 2 == 0 && argumentsAreTabular(arguments) == 2) { builder.forcedBreak(); builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; for (int i = 0; i < arguments.size() - 1; i += 2) { ExpressionTree argument0 = arguments.get(i); ExpressionTree argument1 = arguments.get(i + 1); - if (!first) { + if (afterFirstToken) { token(","); builder.forcedBreak(); } @@ -2779,7 +3430,7 @@ void addArguments(List arguments, Indent plusIndent) { builder.breakOp(" "); scan(argument1, null); builder.close(); - first = false; + afterFirstToken = true; } builder.close(); } else if (isFormatMethod(arguments)) { @@ -2803,15 +3454,15 @@ void addArguments(List arguments, Indent plusIndent) { private void argList(List arguments) { builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; FillMode fillMode = hasOnlyShortItems(arguments) ? FillMode.INDEPENDENT : FillMode.UNIFIED; for (ExpressionTree argument : arguments) { - if (!first) { + if (afterFirstToken) { token(","); builder.breakOp(fillMode, " ", ZERO); } scan(argument, null); - first = false; + afterFirstToken = true; } builder.close(); } @@ -2854,14 +3505,9 @@ public void scan(JCTree tree) { return; } switch (tree.getKind()) { - case STRING_LITERAL: - break; - case PLUS: - super.scan(tree); - break; - default: - stringLiteral[0] = false; - break; + case STRING_LITERAL -> {} + case PLUS -> super.scan(tree); + default -> stringLiteral[0] = false; } if (tree.getKind() == STRING_LITERAL) { Object value = ((LiteralTree) tree).getValue(); @@ -2880,8 +3526,7 @@ private int argumentsAreTabular(List arguments) { return -1; } List> rows = new ArrayList<>(); - PeekingIterator it = - Iterators.peekingIterator(arguments.iterator()); + PeekingIterator it = Iterators.peekingIterator(arguments.iterator()); int start0 = actualColumn(it.peek()); { List row = new ArrayList<>(); @@ -2967,7 +3612,13 @@ private static boolean expressionsAreParallel( if (column >= row.size()) { continue; } - nodeTypes.add(row.get(column).getKind()); + // Treat UnaryTree expressions as their underlying type for the comparison (so, for example + // -ve and +ve numeric literals are considered the same). + if (row.get(column) instanceof UnaryTree) { + nodeTypes.add(((UnaryTree) row.get(column)).getExpression().getKind()); + } else { + nodeTypes.add(row.get(column).getKind()); + } } for (Multiset.Entry nodeType : nodeTypes.entrySet()) { if (nodeType.getCount() >= atLeastM) { @@ -2979,20 +3630,19 @@ private static boolean expressionsAreParallel( // General helper functions. - enum DeclarationKind { + /** Kind of declaration. */ + protected enum DeclarationKind { NONE, FIELD, PARAMETER } /** Declare one variable or variable-like thing. */ - int declareOne( + protected int declareOne( DeclarationKind kind, Direction annotationsDirection, Optional modifiers, Tree type, - VarArgsOrNot isVarargs, - List varargsAnnotations, Name name, String op, String equals, @@ -3013,29 +3663,37 @@ int declareOne( builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak)); } - Deque> dims = - new ArrayDeque<>( - typeWithDims.isPresent() - ? typeWithDims.get().dims - : Collections.>emptyList()); + Deque> dims = + 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)); - } - builder.open(type != null ? plusFour : ZERO); + List annotations = + visitModifiers( + declarationAndTypeModifiers, + annotationsDirection, + Optional.of(verticalAnnotationBreak)); + boolean isVar = + builder.peekToken().get().equals("var") + && (!name.contentEquals("var") || builder.peekToken(1).get().equals("var")); + boolean hasType = type != null || isVar; + builder.open(hasType ? plusFour : ZERO); { builder.open(ZERO); { builder.open(ZERO); { + visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES); if (typeWithDims.isPresent() && typeWithDims.get().node != null) { scan(typeWithDims.get().node, null); int totalDims = dims.size(); @@ -3043,17 +3701,15 @@ int declareOne( maybeAddDims(dims); builder.close(); baseDims = totalDims - dims.size(); + } else if (isVar) { + token("var"); } else { scan(type, null); } - if (isVarargs.isYes()) { - visitAnnotations(varargsAnnotations, BreakOrNot.YES, BreakOrNot.YES); - builder.op("..."); - } } builder.close(); - if (type != null) { + if (hasType) { builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO, Optional.of(typeBreak)); } @@ -3063,7 +3719,7 @@ int declareOne( if (receiverExpression.isPresent()) { scan(receiverExpression.get(), null); } else { - visit(name); + variableName(name); } builder.op(op); } @@ -3106,8 +3762,16 @@ int declareOne( return baseDims; } - private void maybeAddDims(Deque> annotations) { - maybeAddDims(new ArrayDeque(), annotations); + protected void variableName(Name name) { + if (name.isEmpty()) { + token("_"); + } else { + visit(name); + } + } + + private void maybeAddDims(Deque> annotations) { + maybeAddDims(new ArrayDeque<>(), annotations); } /** @@ -3123,23 +3787,23 @@ private void maybeAddDims(Deque> annotations) { * [[@A, @B], [@C]]} for {@code int @A [] @B @C []} */ private void maybeAddDims( - Deque dimExpressions, Deque> annotations) { + Deque dimExpressions, Deque> annotations) { boolean lastWasAnnotation = false; while (builder.peekToken().isPresent()) { switch (builder.peekToken().get()) { - case "@": + case "@" -> { if (annotations.isEmpty()) { return; } - List dimAnnotations = annotations.removeFirst(); + List dimAnnotations = annotations.removeFirst(); if (dimAnnotations.isEmpty()) { continue; } builder.breakToFill(" "); visitAnnotations(dimAnnotations, BreakOrNot.NO, BreakOrNot.NO); lastWasAnnotation = true; - break; - case "[": + } + case "[" -> { if (lastWasAnnotation) { builder.breakToFill(" "); } else { @@ -3151,9 +3815,22 @@ private void maybeAddDims( } token("]"); lastWasAnnotation = false; - break; - default: + } + case "." -> { + if (!builder.peekToken().get().equals(".") || !builder.peekToken(1).get().equals(".")) { + return; + } + if (lastWasAnnotation) { + builder.breakToFill(" "); + } else { + builder.breakToFill(); + } + builder.op("..."); + lastWasAnnotation = false; + } + default -> { return; + } } } } @@ -3164,26 +3841,28 @@ private void declareMany(List fragments, Direction annotationDirec ModifiersTree modifiers = fragments.get(0).getModifiers(); Tree type = fragments.get(0).getType(); - visitAndBreakModifiers(modifiers, annotationDirection, Optional.absent()); + visitAndBreakModifiers( + modifiers, annotationDirection, /* declarationAnnotationBreak= */ Optional.empty()); builder.open(plusFour); builder.open(ZERO); TypeWithDims extractedDims = DimensionHelpers.extractDims(type, SortedDims.YES); - Deque> dims = new ArrayDeque<>(extractedDims.dims); + Deque> dims = new ArrayDeque<>(extractedDims.dims); scan(extractedDims.node, null); int baseDims = dims.size(); maybeAddDims(dims); baseDims = baseDims - dims.size(); - boolean first = true; + boolean afterFirstToken = false; for (VariableTree fragment : fragments) { - if (!first) { + if (afterFirstToken) { token(","); } - TypeWithDims fragmentDims = variableFragmentDims(first, baseDims, fragment.getType()); + TypeWithDims fragmentDims = + variableFragmentDims(afterFirstToken, baseDims, fragment.getType()); dims = new ArrayDeque<>(fragmentDims.dims); builder.breakOp(" "); builder.open(ZERO); maybeAddDims(dims); - visit(fragment.getName()); + variableName(fragment.getName()); maybeAddDims(dims); ExpressionTree initializer = fragment.getInitializer(); if (initializer != null) { @@ -3195,10 +3874,10 @@ private void declareMany(List fragments, Direction annotationDirec builder.close(); } builder.close(); - if (first) { + if (!afterFirstToken) { builder.close(); } - first = false; + afterFirstToken = true; } builder.close(); token(";"); @@ -3206,7 +3885,7 @@ private void declareMany(List fragments, Direction annotationDirec } /** Add a list of declarations. */ - void addBodyDeclarations( + protected void addBodyDeclarations( List bodyDeclarations, BracesOrNot braces, FirstDeclarationsOrNot first0) { if (bodyDeclarations.isEmpty()) { if (braces.isYes()) { @@ -3214,6 +3893,12 @@ void addBodyDeclarations( tokenBreakTrailingComment("{", plusTwo); builder.blankLineWanted(BlankLineWanted.NO); builder.open(ZERO); + if (builder.peekToken().equals(Optional.of(";"))) { + builder.open(plusTwo); + dropEmptyDeclarations(); + builder.close(); + builder.forcedBreak(); + } token("}", plusTwo); builder.close(); } @@ -3226,7 +3911,7 @@ void addBodyDeclarations( builder.open(plusTwo); boolean first = first0.isYes(); boolean lastOneGotBlankLineBefore = false; - PeekingIterator it = Iterators.peekingIterator(bodyDeclarations.iterator()); + PeekingIterator it = Iterators.peekingIterator(bodyDeclarations.iterator()); while (it.hasNext()) { Tree bodyDeclaration = it.next(); dropEmptyDeclarations(); @@ -3264,6 +3949,26 @@ void addBodyDeclarations( } } + private void classDeclarationTypeList(String token, List types) { + if (types.isEmpty()) { + return; + } + builder.breakToFill(" "); + builder.open(types.size() > 1 ? plusFour : ZERO); + token(token); + builder.space(); + boolean afterFirstToken = false; + for (Tree type : types) { + if (afterFirstToken) { + token(","); + builder.breakOp(" "); + } + scan(type, null); + afterFirstToken = true; + } + 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 @@ -3271,7 +3976,8 @@ void addBodyDeclarations( * *

e.g. {@code int x, y;} is parsed as {@code int x; int y;}. */ - private List variableFragments(PeekingIterator it, Tree first) { + private static List variableFragments( + PeekingIterator it, Tree first) { List fragments = new ArrayList<>(); if (first.getKind() == VARIABLE) { int start = getStartPosition(first); @@ -3300,7 +4006,7 @@ private boolean hasJavaDoc(Tree bodyDeclaration) { } private static Optional getNextToken(Input input, int position) { - return Optional.fromNullable(input.getPositionTokenMap().get(position)); + return Optional.ofNullable(input.getPositionTokenMap().get(position)); } /** Does this list of trees end with the specified token? */ @@ -3316,28 +4022,29 @@ private boolean hasTrailingToken(Input input, List nodes, String /** * Can a local with a set of modifiers be declared with horizontal annotations? This is currently - * true if there is at most one marker annotation, and no others. + * true if there is at most one parameterless annotation, and no others. * * @param modifiers the list of {@link ModifiersTree}s * @return whether the local can be declared with horizontal annotations */ - private Direction canLocalHaveHorizontalAnnotations(ModifiersTree modifiers) { - int markerAnnotations = 0; + private static Direction canLocalHaveHorizontalAnnotations(ModifiersTree modifiers) { + int parameterlessAnnotations = 0; for (AnnotationTree annotation : modifiers.getAnnotations()) { if (annotation.getArguments().isEmpty()) { - markerAnnotations++; + parameterlessAnnotations++; } } - return markerAnnotations <= 1 && markerAnnotations == modifiers.getAnnotations().size() + return parameterlessAnnotations <= 1 + && parameterlessAnnotations == modifiers.getAnnotations().size() ? Direction.HORIZONTAL : Direction.VERTICAL; } /** * Should a field with a set of modifiers be declared with horizontal annotations? This is - * currently true if all annotations are marker annotations. + * 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; @@ -3351,8 +4058,12 @@ private Direction fieldAnnotationDirection(ModifiersTree modifiers) { * * @param token the {@link String} to wrap in a {@link Doc.Token} */ - final void token(String token) { - builder.token(token, Doc.Token.RealOrImaginary.REAL, ZERO, Optional.absent()); + protected final void token(String token) { + builder.token( + token, + Doc.Token.RealOrImaginary.REAL, + ZERO, + /* breakAndIndentTrailingComment= */ Optional.empty()); } /** @@ -3361,21 +4072,21 @@ final void token(String token) { * @param token the {@link String} to wrap in a {@link Doc.Token} * @param plusIndentCommentsBefore extra indent for comments before this token */ - final void token(String token, Indent plusIndentCommentsBefore) { + protected final void token(String token, Indent plusIndentCommentsBefore) { builder.token( - token, Doc.Token.RealOrImaginary.REAL, plusIndentCommentsBefore, Optional.absent()); + token, + Doc.Token.RealOrImaginary.REAL, + plusIndentCommentsBefore, + /* breakAndIndentTrailingComment= */ Optional.empty()); } /** Emit a {@link Doc.Token}, and breaks and indents trailing javadoc or block comments. */ final void tokenBreakTrailingComment(String token, Indent breakAndIndentTrailingComment) { builder.token( - token, - Doc.Token.RealOrImaginary.REAL, - ZERO, - Optional.of(breakAndIndentTrailingComment)); + token, Doc.Token.RealOrImaginary.REAL, ZERO, Optional.of(breakAndIndentTrailingComment)); } - private void markForPartialFormat() { + protected void markForPartialFormat() { if (!inExpression()) { builder.markForPartialFormat(); } @@ -3387,7 +4098,7 @@ private void markForPartialFormat() { * * @param node the ASTNode holding the input position */ - final void sync(Tree node) { + protected final void sync(Tree node) { builder.sync(((JCTree) node).getStartPosition()); } @@ -3399,4 +4110,82 @@ final BreakTag genSym() { public final String toString() { return MoreObjects.toStringHelper(this).add("builder", builder).toString(); } + + @Override + public Void visitBindingPattern(BindingPatternTree node, Void unused) { + sync(node); + VariableTree variableTree = node.getVariable(); + declareOne( + DeclarationKind.PARAMETER, + Direction.HORIZONTAL, + Optional.of(variableTree.getModifiers()), + variableTree.getType(), + variableTree.getName(), + /* op= */ "", + /* equals= */ "", + /* initializer= */ Optional.empty(), + /* trailing= */ Optional.empty(), + /* receiverExpression= */ Optional.empty(), + /* typeWithDims= */ Optional.empty()); + return null; + } + + @Override + public Void visitYield(YieldTree node, Void aVoid) { + sync(node); + token("yield"); + builder.space(); + scan(node.getValue(), null); + token(";"); + return null; + } + + @Override + public Void visitSwitchExpression(SwitchExpressionTree node, Void aVoid) { + sync(node); + visitSwitch(node.getExpression(), node.getCases()); + return null; + } + + @Override + public Void visitDefaultCaseLabel(DefaultCaseLabelTree node, Void unused) { + token("default"); + return null; + } + + @Override + public Void visitPatternCaseLabel(PatternCaseLabelTree node, Void unused) { + scan(node.getPattern(), null); + return null; + } + + @Override + public Void visitConstantCaseLabel(ConstantCaseLabelTree node, Void aVoid) { + scan(node.getConstantExpression(), null); + return null; + } + + @Override + public Void visitDeconstructionPattern(DeconstructionPatternTree node, Void unused) { + scan(node.getDeconstructor(), null); + builder.open(plusFour); + token("("); + builder.breakOp(); + boolean afterFirstToken = false; + for (PatternTree pattern : node.getNestedPatterns()) { + if (afterFirstToken) { + token(","); + builder.breakOp(" "); + } + afterFirstToken = true; + scan(pattern, null); + } + builder.close(); + token(")"); + return null; + } + + private void visitJcAnyPattern(JCTree.JCAnyPattern unused) { + token("_"); + } } 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 b5e3dacec..ea3731131 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java @@ -14,10 +14,12 @@ package com.google.googlejavaformat.java; -import static com.google.common.base.MoreObjects.firstNonNull; +import static java.lang.Math.min; +import static java.util.Comparator.comparing; import com.google.common.base.CharMatcher; import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.ImmutableList; import com.google.common.collect.Range; @@ -30,8 +32,6 @@ import com.google.googlejavaformat.OpsBuilder.BlankLineWanted; import com.google.googlejavaformat.Output; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,7 +48,7 @@ */ public final class JavaOutput extends Output { private final String lineSeparator; - private final JavaInput javaInput; // Used to follow along while emitting the output. + private final Input javaInput; // Used to follow along while emitting the output. private final CommentsHelper commentsHelper; // Used to re-flow comments. private final Map blankLines = new HashMap<>(); // Info on blank lines. private final RangeSet partialFormatRanges = TreeRangeSet.create(); @@ -57,17 +57,17 @@ public final class JavaOutput extends Output { private final int kN; // The number of tokens or comments in the input, excluding the EOF. private int iLine = 0; // Closest corresponding line number on input. private int lastK = -1; // Last {@link Tok} index output. - private int spacesPending = 0; private int newlinesPending = 0; private StringBuilder lineBuilder = new StringBuilder(); + private StringBuilder spacesPending = new StringBuilder(); /** * {@code JavaOutput} constructor. * - * @param javaInput the {@link JavaInput}, used to match up blank lines in the output + * @param javaInput the {@link Input}, used to match up blank lines in the output * @param commentsHelper the {@link CommentsHelper}, used to rewrite comments */ - public JavaOutput(String lineSeparator, JavaInput javaInput, CommentsHelper commentsHelper) { + public JavaOutput(String lineSeparator, Input javaInput, CommentsHelper commentsHelper) { this.lineSeparator = lineSeparator; this.javaInput = javaInput; this.commentsHelper = commentsHelper; @@ -90,7 +90,7 @@ public void markForPartialFormat(Token start, Token end) { partialFormatRanges.add(Range.closed(lo, hi)); } - // TODO(jdd): Add invariant. + // TODO(user): Add invariant. @Override public void append(String text, Range range) { if (!range.isEmpty()) { @@ -98,8 +98,8 @@ public void append(String text, Range range) { // Skip over input line we've passed. int iN = javaInput.getLineCount(); while (iLine < iN - && (javaInput.getRange1s(iLine).isEmpty() - || javaInput.getRange1s(iLine).upperEndpoint() <= range.lowerEndpoint())) { + && (javaInput.getRanges(iLine).isEmpty() + || javaInput.getRanges(iLine).upperEndpoint() <= range.lowerEndpoint())) { if (javaInput.getRanges(iLine).isEmpty()) { // Skipped over a blank line. sawNewlines = true; @@ -110,8 +110,8 @@ public void append(String text, Range range) { * Output blank line if we've called {@link OpsBuilder#blankLine}{@code (true)} here, or if * there's a blank line here and it's a comment. */ - BlankLineWanted wanted = firstNonNull(blankLines.get(lastK), BlankLineWanted.NO); - if (isComment(text) ? sawNewlines : wanted.wanted().or(sawNewlines)) { + BlankLineWanted wanted = blankLines.getOrDefault(lastK, BlankLineWanted.NO); + if ((sawNewlines && isComment(text)) || wanted.wanted().orElse(sawNewlines)) { ++newlinesPending; } } @@ -123,48 +123,44 @@ public void append(String text, Range range) { if (newlinesPending == 0) { ++newlinesPending; } - spacesPending = 0; + spacesPending = new StringBuilder(); } else { - boolean range0sSet = false; boolean rangesSet = false; int textN = text.length(); for (int i = 0; i < textN; i++) { char c = text.charAt(i); switch (c) { case ' ': - ++spacesPending; + spacesPending.append(' '); + break; + case '\t': + spacesPending.append('\t'); break; case '\r': if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { i++; } - // falls through + // falls through case '\n': - spacesPending = 0; + spacesPending = new StringBuilder(); ++newlinesPending; break; default: while (newlinesPending > 0) { - mutableLines.add(lineBuilder.toString()); + // drop leading blank lines + if (!mutableLines.isEmpty() || lineBuilder.length() > 0) { + mutableLines.add(lineBuilder.toString()); + } lineBuilder = new StringBuilder(); rangesSet = false; --newlinesPending; } - while (spacesPending > 0) { - lineBuilder.append(' '); - --spacesPending; + if (spacesPending.length() > 0) { + lineBuilder.append(spacesPending); + spacesPending = new StringBuilder(); } lineBuilder.append(c); if (!range.isEmpty()) { - if (!range0sSet) { - if (!range.isEmpty()) { - while (range0s.size() <= mutableLines.size()) { - range0s.add(Formatter.EMPTY_RANGE); - } - range0s.set(mutableLines.size(), union(range0s.get(mutableLines.size()), range)); - range0sSet = true; - } - } if (!rangesSet) { while (ranges.size() <= mutableLines.size()) { ranges.add(Formatter.EMPTY_RANGE); @@ -175,13 +171,6 @@ public void append(String text, Range range) { } } } - // TODO(jdd): Move others down here. Use common method for these. - if (!range.isEmpty()) { - while (range1s.size() <= mutableLines.size()) { - range1s.add(Formatter.EMPTY_RANGE); - } - range1s.set(mutableLines.size(), union(range1s.get(mutableLines.size()), range)); - } } if (!range.isEmpty()) { lastK = range.upperEndpoint(); @@ -190,29 +179,21 @@ public void append(String text, Range range) { @Override public void indent(int indent) { - spacesPending = indent; + spacesPending.append(Strings.repeat(" ", indent)); } /** Flush any incomplete last line, then add the EOF token into our data structures. */ - void flush() { + public void flush() { String lastLine = lineBuilder.toString(); - if (!lastLine.isEmpty()) { + if (!CharMatcher.whitespace().matchesAllOf(lastLine)) { mutableLines.add(lastLine); } int jN = mutableLines.size(); Range eofRange = Range.closedOpen(kN, kN + 1); - while (range0s.size() < jN) { - range0s.add(Formatter.EMPTY_RANGE); - } - range0s.add(eofRange); while (ranges.size() < jN) { ranges.add(Formatter.EMPTY_RANGE); } ranges.add(eofRange); - while (range1s.size() < jN) { - range1s.add(Formatter.EMPTY_RANGE); - } - range1s.add(eofRange); setLines(ImmutableList.copyOf(mutableLines)); } @@ -230,7 +211,7 @@ public CommentsHelper getCommentsHelper() { */ public ImmutableList getFormatReplacements(RangeSet iRangeSet0) { ImmutableList.Builder result = ImmutableList.builder(); - Map> kToJ = JavaOutput.makeKToIJ(this, kN); + Map> kToJ = JavaOutput.makeKToIJ(this); // Expand the token ranges to align with re-formattable boundaries. RangeSet breakableRanges = TreeRangeSet.create(); @@ -253,92 +234,84 @@ public ImmutableList getFormatReplacements(RangeSet iRange // Add all output lines in the given token range to the replacement. StringBuilder replacement = new StringBuilder(); - boolean needsBreakBefore = false; int replaceFrom = startTok.getPosition(); + // Replace leading whitespace in the input with the whitespace from the formatted file while (replaceFrom > 0) { char previous = javaInput.getText().charAt(replaceFrom - 1); - if (previous == '\n' || previous == '\r') { + if (!CharMatcher.whitespace().matches(previous)) { break; } - if (CharMatcher.whitespace().matches(previous)) { - replaceFrom--; - continue; - } - needsBreakBefore = true; - break; + replaceFrom--; } - if (needsBreakBefore) { - replacement.append(lineSeparator); + int i = kToJ.get(startTok.getIndex()).lowerEndpoint(); + // Include leading blank lines from the formatted output, unless the formatted range + // starts at the beginning of the file. + while (i > 0 && getLine(i - 1).isEmpty()) { + i--; } - - boolean first = true; - int i; - for (i = kToJ.get(startTok.getIndex()).lowerEndpoint(); - i < kToJ.get(endTok.getIndex()).upperEndpoint(); - i++) { + // Write out the formatted range. + for (; i < kToJ.get(endTok.getIndex()).upperEndpoint(); i++) { // It's possible to run out of output lines (e.g. if the input ended with // multiple trailing newlines). if (i < getLineCount()) { - if (first) { - first = false; - } else { + if (i > 0) { replacement.append(lineSeparator); } replacement.append(getLine(i)); } } - replacement.append(lineSeparator); - String trailingLine = i < getLineCount() ? getLine(i) : null; - - 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) { replaceTo = javaInput.getText().length(); } - - // Expand the partial formatting range to include non-breaking trailing - // whitespace. If the range ultimately ends in a newline, then preserve - // whatever original text was on the next line (i.e. don't re-indent - // the next line after the reformatted range). However, if the partial - // formatting range doesn't end in a newline, then break and re-indent. - boolean reIndent = true; - OUTER: + // Replace trailing whitespace in the input with the whitespace from the formatted file. + // If the trailing whitespace in the input includes one or more line breaks, preserve the + // whitespace after the last newline to avoid re-indenting the line following the formatted + // line. + int newline = -1; while (replaceTo < javaInput.getText().length()) { - char endChar = javaInput.getText().charAt(replaceTo); - switch (endChar) { - case '\r': - if (replaceTo + 1 < javaInput.getText().length() - && javaInput.getText().charAt(replaceTo + 1) == '\n') { - replaceTo++; - } - // falls through - case '\n': - replaceTo++; - reIndent = false; - break OUTER; - default: - break; + char next = javaInput.getText().charAt(replaceTo); + if (!CharMatcher.whitespace().matches(next)) { + break; } - if (CharMatcher.whitespace().matches(endChar)) { + int newlineLength = Newlines.hasNewlineAt(javaInput.getText(), replaceTo); + if (newlineLength != -1) { + newline = replaceTo; + // Skip over the entire newline; don't count the second character of \r\n as a newline. + replaceTo += newlineLength; + } else { replaceTo++; - continue; } - break; } - if (reIndent && trailingLine != null) { - int idx = CharMatcher.whitespace().negate().indexIn(trailingLine); - if (idx > 0) { - replacement.append(trailingLine, 0, idx); + if (newline != -1) { + replaceTo = newline; + } + + if (newline == -1) { + // There wasn't an existing trailing newline; add one. + replacement.append(lineSeparator); + } + for (; i < getLineCount(); i++) { + String after = getLine(i); + int idx = CharMatcher.whitespace().negate().indexIn(after); + if (idx == -1) { + // Write out trailing empty lines from the formatted output. + replacement.append(lineSeparator); + } else { + if (newline == -1) { + // If there wasn't a trailing newline in the input, indent the next line. + replacement.append(after, 0, idx); + } + break; } } result.add(Replacement.create(replaceFrom, replaceTo, replacement.toString())); } - return result.build(); } @@ -364,15 +337,7 @@ private Range expandToBreakableRegions(Range iRange) { public static String applyReplacements(String input, List replacements) { replacements = new ArrayList<>(replacements); - Collections.sort( - replacements, - new Comparator() { - @Override - public int compare(Replacement o1, Replacement o2) { - return Integer.compare( - o2.getReplaceRange().lowerEndpoint(), o1.getReplaceRange().lowerEndpoint()); - } - }); + replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()).reversed()); StringBuilder writer = new StringBuilder(input); for (Replacement replacement : replacements) { writer.replace( @@ -387,7 +352,7 @@ public int compare(Replacement o1, Replacement o2) { 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; } @@ -426,7 +391,7 @@ public String toString() { return MoreObjects.toStringHelper(this) .add("iLine", iLine) .add("lastK", lastK) - .add("spacesPending", spacesPending) + .add("spacesPending", spacesPending.toString().replace("\t", "\\t")) .add("newlinesPending", newlinesPending) .add("blankLines", blankLines) .add("super", super.toString()) diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java index a683e7af2..793c6220c 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java @@ -15,22 +15,23 @@ package com.google.googlejavaformat.java; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; +import com.sun.tools.javac.parser.JavaTokenizer; +import com.sun.tools.javac.parser.Scanner; +import com.sun.tools.javac.parser.ScannerFactory; +import com.sun.tools.javac.parser.Tokens.Comment; +import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle; +import com.sun.tools.javac.parser.Tokens.Token; +import com.sun.tools.javac.parser.Tokens.TokenKind; +import com.sun.tools.javac.util.Context; +import java.util.HashMap; +import java.util.Map; import java.util.Set; -import org.openjdk.tools.javac.parser.JavaTokenizer; -import org.openjdk.tools.javac.parser.Scanner; -import org.openjdk.tools.javac.parser.ScannerFactory; -import org.openjdk.tools.javac.parser.Tokens.Comment; -import org.openjdk.tools.javac.parser.Tokens.Comment.CommentStyle; -import org.openjdk.tools.javac.parser.Tokens.Token; -import org.openjdk.tools.javac.parser.Tokens.TokenKind; -import org.openjdk.tools.javac.parser.UnicodeReader; -import org.openjdk.tools.javac.util.Context; /** A wrapper around javac's lexer. */ -public class JavacTokens { +final class JavacTokens { /** The lexer eats terminal comments, so feed it one we don't care about. */ // TODO(b/33103797): fix javac and remove the work-around @@ -79,15 +80,16 @@ public static ImmutableList getTokens( } ScannerFactory fac = ScannerFactory.instance(context); char[] buffer = (source + EOF_COMMENT).toCharArray(); - Scanner scanner = - new AccessibleScanner(fac, new CommentSavingTokenizer(fac, buffer, buffer.length)); + CommentSavingTokenizer tokenizer = new CommentSavingTokenizer(fac, buffer, buffer.length); + Scanner scanner = new AccessibleScanner(fac, tokenizer); ImmutableList.Builder tokens = ImmutableList.builder(); + int end = source.length(); int last = 0; do { scanner.nextToken(); Token t = scanner.token(); if (t.comments != null) { - for (Comment c : Lists.reverse(t.comments)) { + for (CommentWithTextAndPosition c : getComments(t, tokenizer.comments())) { if (last < c.getSourcePos(0)) { tokens.add(new RawTok(null, null, last, c.getSourcePos(0))); } @@ -97,6 +99,9 @@ public static ImmutableList getTokens( } } if (stopTokens.contains(t.kind)) { + if (t.kind != TokenKind.EOF) { + end = t.pos; + } break; } if (last < t.pos) { @@ -110,42 +115,74 @@ public static ImmutableList getTokens( t.endPos)); last = t.endPos; } while (scanner.token().kind != TokenKind.EOF); - if (last < source.length()) { - tokens.add(new RawTok(null, null, last, source.length())); + if (last < end) { + tokens.add(new RawTok(null, null, last, end)); } return tokens.build(); } + private static ImmutableList getComments( + Token token, Map comments) { + if (token.comments == null) { + return ImmutableList.of(); + } + // javac stores the comments in reverse declaration order + return token.comments.stream().map(comments::get).collect(toImmutableList()).reverse(); + } + /** A {@link JavaTokenizer} that saves comments. */ static class CommentSavingTokenizer extends JavaTokenizer { + + private final Map comments = new HashMap<>(); + CommentSavingTokenizer(ScannerFactory fac, char[] buffer, int length) { super(fac, buffer, length); } + Map comments() { + return comments; + } + @Override protected Comment processComment(int pos, int endPos, CommentStyle style) { - char[] buf = reader.getRawCharacters(pos, endPos); - return new CommentWithTextAndPosition( - pos, endPos, new AccessibleReader(fac, buf, buf.length), style); + char[] buf = getRawCharactersReflectively(pos, endPos); + Comment comment = super.processComment(pos, endPos, style); + CommentWithTextAndPosition commentWithTextAndPosition = + new CommentWithTextAndPosition(pos, endPos, new String(buf)); + comments.put(comment, commentWithTextAndPosition); + return comment; + } + + 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. */ - static class CommentWithTextAndPosition implements Comment { + static class CommentWithTextAndPosition { private final int pos; private final int endPos; - private final AccessibleReader reader; - private final CommentStyle style; + private final String text; - private String text = null; - - public CommentWithTextAndPosition( - int pos, int endPos, AccessibleReader reader, CommentStyle style) { + public CommentWithTextAndPosition(int pos, int endPos, String text) { this.pos = pos; this.endPos = endPos; - this.reader = reader; - this.style = style; + this.text = text; } /** @@ -154,7 +191,6 @@ public CommentWithTextAndPosition( *

The handling of javadoc comments in javac has more logic to skip over leading whitespace * and '*' characters when indexing into doc comments, but we don't need any of that. */ - @Override public int getSourcePos(int index) { checkArgument( 0 <= index && index < (endPos - pos), @@ -164,47 +200,22 @@ public int getSourcePos(int index) { return pos + index; } - @Override - public CommentStyle getStyle() { - return style; - } - - @Override public String getText() { - String text = this.text; - if (text == null) { - this.text = text = new String(reader.getRawCharacters()); - } return text; } - /** - * We don't care about {@code @deprecated} javadoc tags (see the DepAnn check). - * - * @return false - */ - @Override - public boolean isDeprecated() { - return false; - } - @Override public String toString() { return String.format("Comment: '%s'", getText()); } } - // Scanner(ScannerFactory, JavaTokenizer) is package-private + // Scanner(ScannerFactory, JavaTokenizer) is protected static class AccessibleScanner extends Scanner { protected AccessibleScanner(ScannerFactory fac, JavaTokenizer tokenizer) { super(fac, tokenizer); } } - // UnicodeReader(ScannerFactory, char[], int) is package-private - static class AccessibleReader extends UnicodeReader { - protected AccessibleReader(ScannerFactory fac, char[] buffer, int length) { - super(fac, buffer, length); - } - } + private JavacTokens() {} } 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 7164d96bb..f1affa74d 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Main.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Main.java @@ -14,35 +14,40 @@ package com.google.googlejavaformat.java; +import static java.lang.Math.min; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Comparator.comparing; import com.google.common.io.ByteStreams; -import com.google.googlejavaformat.FormatterDiagnostic; +import com.google.common.util.concurrent.MoreExecutors; import com.google.googlejavaformat.java.JavaFormatterOptions.Style; import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; +import java.io.PrintStream; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.Collections; +import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; /** The main class for the Java formatter CLI. */ public final class Main { private static final int MAX_THREADS = 20; private static final String STDIN_FILENAME = ""; - static final String[] VERSION = { - "google-java-format: Version " + GoogleJavaFormatVersion.VERSION - }; + static String versionString() { + return "google-java-format: Version " + GoogleJavaFormatVersion.version(); + } private final PrintWriter outWriter; private final PrintWriter errWriter; @@ -61,21 +66,37 @@ public Main(PrintWriter outWriter, PrintWriter errWriter, InputStream inStream) * * @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)); + public static void main(String... args) { + int result = main(System.in, System.out, System.err, args); + System.exit(result); + } + + /** + * Package-private main entry point used by the {@link javax.tools.Tool Tool} implementation in + * the same package as this Main class. + */ + static int main(InputStream in, PrintStream out, PrintStream err, String... args) { + PrintWriter outWriter = new PrintWriter(new OutputStreamWriter(out, UTF_8)); + PrintWriter errWriter = new PrintWriter(new OutputStreamWriter(err, UTF_8)); + return main(in, outWriter, errWriter, args); + } + + /** + * Package-private main entry point used by the {@link java.util.spi.ToolProvider ToolProvider} + * implementation in the same package as this Main class. + */ + static int main(InputStream in, PrintWriter out, PrintWriter err, String... args) { try { - Main formatter = new Main(out, err, System.in); - result = formatter.format(args); + Main formatter = new Main(out, err, in); + return formatter.format(args); } catch (UsageException e) { err.print(e.getMessage()); - result = 0; + // We return exit code 2 to differentiate usage issues from code formatting issues. + return 2; } finally { err.flush(); out.flush(); } - System.exit(result); } /** @@ -88,9 +109,7 @@ public static void main(String[] args) { public int format(String... args) throws UsageException { CommandLineOptions parameters = processArgs(args); if (parameters.version()) { - for (String line : VERSION) { - errWriter.println(line); - } + errWriter.println(versionString()); return 0; } if (parameters.help()) { @@ -98,7 +117,10 @@ public int format(String... args) throws UsageException { } JavaFormatterOptions options = - JavaFormatterOptions.builder().style(parameters.aosp() ? Style.AOSP : Style.GOOGLE).build(); + JavaFormatterOptions.builder() + .style(parameters.aosp() ? Style.AOSP : Style.GOOGLE) + .formatJavadoc(parameters.formatJavadoc()) + .build(); if (parameters.stdin()) { return formatStdin(parameters, options); @@ -108,64 +130,82 @@ public int format(String... args) throws UsageException { } 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 inputs = new LinkedHashMap<>(); - Map> results = new LinkedHashMap<>(); + ExecutorCompletionService cs = + new ExecutorCompletionService<>(executorService); + boolean allOk = true; + + int files = 0; for (String fileName : parameters.files()) { if (!fileName.endsWith(".java")) { errWriter.println("Skipping non-Java file: " + fileName); continue; } Path path = Paths.get(fileName); - String input; try { - input = new String(Files.readAllBytes(path), UTF_8); + String input = new String(Files.readAllBytes(path), UTF_8); + cs.submit(new FormatFileCallable(parameters, path, input, options)); + files++; } catch (IOException e) { errWriter.println(fileName + ": could not read file: " + e.getMessage()); - return 1; + allOk = false; } - inputs.put(path, input); - results.put(path, executorService.submit(new FormatFileCallable(parameters, input, options))); } - boolean allOk = true; - for (Map.Entry> result : results.entrySet()) { - String formatted; + List results = new ArrayList<>(); + while (files > 0) { try { - formatted = result.getValue().get(); + files--; + results.add(cs.take().get()); } catch (InterruptedException e) { errWriter.println(e.getMessage()); allOk = false; continue; } catch (ExecutionException e) { - if (e.getCause() instanceof FormatterException) { - for (FormatterDiagnostic diagnostic : ((FormatterException) e.getCause()).diagnostics()) { - errWriter.println(result.getKey() + ":" + diagnostic.toString()); - } - } else { - errWriter.println(result.getKey() + ": error: " + e.getCause().getMessage()); - e.getCause().printStackTrace(errWriter); - } + errWriter.println("error: " + e.getCause().getMessage()); + e.getCause().printStackTrace(errWriter); + allOk = false; + continue; + } + } + Collections.sort(results, comparing(FormatFileCallable.Result::path)); + for (FormatFileCallable.Result result : results) { + Path path = result.path(); + if (result.exception() != null) { + errWriter.print(result.exception().formatDiagnostics(path.toString(), result.input())); allOk = false; continue; } + String formatted = result.output(); + boolean changed = result.changed(); + if (changed && parameters.setExitIfChanged()) { + allOk = false; + } if (parameters.inPlace()) { - if (formatted.equals(inputs.get(result.getKey()))) { + if (!changed) { continue; // preserve original file } try { - Files.write(result.getKey(), formatted.getBytes(UTF_8)); + Files.write(path, formatted.getBytes(UTF_8)); } catch (IOException e) { - errWriter.println(result.getKey() + ": could not write file: " + e.getMessage()); + errWriter.println(path + ": could not write file: " + e.getMessage()); allOk = false; continue; } + } else if (parameters.dryRun()) { + if (changed) { + outWriter.println(path); + } } else { outWriter.write(formatted); } } + if (!MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(5))) { + errWriter.println("Failed to shut down ExecutorService"); + allOk = false; + } return allOk ? 0 : 1; } @@ -176,17 +216,28 @@ private int formatStdin(CommandLineOptions parameters, JavaFormatterOptions opti } catch (IOException e) { throw new IOError(e); } - try { - String output = new FormatFileCallable(parameters, input, options).call(); - outWriter.write(output); - return 0; - } catch (FormatterException e) { - for (FormatterDiagnostic diagnostic : e.diagnostics()) { - errWriter.println(STDIN_FILENAME + ":" + diagnostic.toString()); + String stdinFilename = parameters.assumeFilename().orElse(STDIN_FILENAME); + boolean ok = true; + FormatFileCallable.Result result = + new FormatFileCallable(parameters, null, input, options).call(); + if (result.exception() != null) { + errWriter.print(result.exception().formatDiagnostics(stdinFilename, input)); + ok = false; + } else { + String output = result.output(); + boolean changed = result.changed(); + if (changed && parameters.setExitIfChanged()) { + ok = false; + } + if (parameters.dryRun()) { + if (changed) { + outWriter.println(stdinFilename); + } + } else { + outWriter.write(output); } - return 1; - // TODO(cpovirk): Catch other types of exception (as we do in the formatFiles case). } + return ok ? 0 : 1; } /** Parses and validates command-line flags. */ @@ -212,12 +263,21 @@ public static CommandLineOptions processArgs(String... args) throws UsageExcepti throw new UsageException("partial formatting is only support for a single file"); } if (parameters.offsets().size() != parameters.lengths().size()) { - throw new UsageException( - String.format("-offsets and -lengths flags must be provided in matching pairs")); + throw new UsageException("-offsets and -lengths flags must be provided in matching pairs"); } if (filesToFormat <= 0 && !parameters.version() && !parameters.help()) { throw new UsageException("no files were provided"); } + if (parameters.stdin() && !parameters.files().isEmpty()) { + throw new UsageException("cannot format from standard input and files simultaneously"); + } + if (parameters.assumeFilename().isPresent() && !parameters.stdin()) { + throw new UsageException( + "--assume-filename is only supported when formatting standard input"); + } + if (parameters.dryRun() && parameters.inPlace()) { + throw new UsageException("cannot use --dry-run and --in-place at the same time"); + } return parameters; } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java index 4e0f37d51..33ad13178 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java @@ -16,6 +16,9 @@ package com.google.googlejavaformat.java; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getLast; + import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; import com.google.common.collect.Range; @@ -23,61 +26,88 @@ import com.google.common.collect.TreeRangeMap; import com.google.googlejavaformat.Input.Tok; import com.google.googlejavaformat.Input.Token; +import com.sun.tools.javac.parser.Tokens.TokenKind; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.lang.model.element.Modifier; -import org.openjdk.tools.javac.parser.Tokens.TokenKind; +import org.jspecify.annotations.Nullable; /** Fixes sequences of modifiers to be in JLS order. */ final class ModifierOrderer { + /** Reorders all modifiers in the given text to be in JLS order. */ + static JavaInput reorderModifiers(String text) throws FormatterException { + return reorderModifiers( + new JavaInput(text), ImmutableList.of(Range.closedOpen(0, text.length()))); + } + /** - * Returns the {@link javax.lang.model.element.Modifier} for the given token kind, or {@code - * null}. + * A class that contains the tokens corresponding to a modifier. This is usually a single token + * (e.g. for {@code public}), but may be multiple tokens for modifiers containing {@code -} (e.g. + * {@code non-sealed}). */ - private static Modifier getModifier(TokenKind kind) { - if (kind == null) { - return null; + static class ModifierTokens implements Comparable { + private final ImmutableList tokens; + private final Modifier modifier; + + static ModifierTokens create(ImmutableList tokens) { + return new ModifierTokens(tokens, asModifier(tokens)); } - 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; + + static ModifierTokens empty() { + return new ModifierTokens(ImmutableList.of(), null); } - } - /** Reorders all modifiers in the given text to be in JLS order. */ - static JavaInput reorderModifiers(String text) throws FormatterException { - return reorderModifiers( - new JavaInput(text), Collections.singleton(Range.closedOpen(0, text.length()))); + ModifierTokens(ImmutableList tokens, Modifier modifier) { + this.tokens = tokens; + this.modifier = modifier; + } + + boolean isEmpty() { + return tokens.isEmpty() || modifier == null; + } + + Modifier modifier() { + return modifier; + } + + ImmutableList tokens() { + return tokens; + } + + private Token first() { + return tokens.get(0); + } + + private Token last() { + return getLast(tokens); + } + + int startPosition() { + return first().getTok().getPosition(); + } + + int endPosition() { + return last().getTok().getPosition() + last().getTok().getText().length(); + } + + ImmutableList getToksBefore() { + return first().getToksBefore(); + } + + ImmutableList getToksAfter() { + return last().getToksAfter(); + } + + @Override + public int compareTo(ModifierTokens o) { + checkState(!isEmpty()); // empty ModifierTokens are filtered out prior to sorting + return modifier.compareTo(o.modifier); + } } /** @@ -95,43 +125,37 @@ static JavaInput reorderModifiers(JavaInput javaInput, Collection Iterator it = javaInput.getTokens().iterator(); TreeRangeMap replacements = TreeRangeMap.create(); while (it.hasNext()) { - Token token = it.next(); - if (!tokenRanges.contains(token.getTok().getIndex())) { - continue; - } - Modifier mod = asModifier(token); - if (mod == null) { + ModifierTokens tokens = getModifierTokens(it); + if (tokens.isEmpty() + || !tokens.tokens().stream() + .allMatch(token -> tokenRanges.contains(token.getTok().getIndex()))) { continue; } - List modifierTokens = new ArrayList<>(); - List mods = new ArrayList<>(); + List modifierTokens = new ArrayList<>(); - int begin = token.getTok().getPosition(); - mods.add(mod); - modifierTokens.add(token); + int begin = tokens.startPosition(); + modifierTokens.add(tokens); int end = -1; while (it.hasNext()) { - token = it.next(); - mod = asModifier(token); - if (mod == null) { + tokens = getModifierTokens(it); + if (tokens.isEmpty()) { break; } - mods.add(mod); - modifierTokens.add(token); - end = token.getTok().getPosition() + token.getTok().length(); + modifierTokens.add(tokens); + end = tokens.endPosition(); } - if (!Ordering.natural().isOrdered(mods)) { - Collections.sort(mods); + if (!Ordering.natural().isOrdered(modifierTokens)) { + List sorted = Ordering.natural().sortedCopy(modifierTokens); StringBuilder replacement = new StringBuilder(); - for (int i = 0; i < mods.size(); i++) { + for (int i = 0; i < sorted.size(); i++) { if (i > 0) { addTrivia(replacement, modifierTokens.get(i).getToksBefore()); } - replacement.append(mods.get(i).toString()); - if (i < (modifierTokens.size() - 1)) { + replacement.append(sorted.get(i).modifier()); + if (i < (sorted.size() - 1)) { addTrivia(replacement, modifierTokens.get(i).getToksAfter()); } } @@ -147,12 +171,65 @@ private static void addTrivia(StringBuilder replacement, ImmutableList it) { + Token token = it.next(); + ImmutableList.Builder result = ImmutableList.builder(); + result.add(token); + if (!token.getTok().getText().equals("non")) { + return ModifierTokens.create(result.build()); + } + if (!it.hasNext()) { + return ModifierTokens.empty(); + } + Token dash = it.next(); + result.add(dash); + if (!dash.getTok().getText().equals("-") || !it.hasNext()) { + return ModifierTokens.empty(); + } + result.add(it.next()); + return ModifierTokens.create(result.build()); + } + + private static @Nullable Modifier asModifier(ImmutableList tokens) { + if (tokens.size() == 1) { + return asModifier(tokens.get(0)); + } + Modifier modifier = asModifier(getLast(tokens)); + if (modifier == null) { + return null; + } + return Modifier.valueOf("NON_" + modifier.name()); + } + /** * Returns the given token as a {@link javax.lang.model.element.Modifier}, or {@code null} if it * is not a modifier. */ - private static Modifier asModifier(Token token) { - return getModifier(((JavaInput.Tok) token.getTok()).kind()); + private static @Nullable Modifier asModifier(Token token) { + TokenKind kind = ((JavaInput.Tok) token.getTok()).kind(); + if (kind == null) { + return null; + } + return switch (kind) { + case PUBLIC -> Modifier.PUBLIC; + case PROTECTED -> Modifier.PROTECTED; + case PRIVATE -> Modifier.PRIVATE; + case ABSTRACT -> Modifier.ABSTRACT; + case STATIC -> Modifier.STATIC; + case DEFAULT -> Modifier.DEFAULT; + + case FINAL -> Modifier.FINAL; + case TRANSIENT -> Modifier.TRANSIENT; + case VOLATILE -> Modifier.VOLATILE; + case SYNCHRONIZED -> Modifier.SYNCHRONIZED; + case NATIVE -> Modifier.NATIVE; + case STRICTFP -> Modifier.STRICTFP; + default -> + switch (token.getTok().getText()) { + case "sealed" -> Modifier.SEALED; + default -> 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 590491704..18745a809 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java +++ b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java @@ -16,55 +16,46 @@ package com.google.googlejavaformat.java; -import static java.nio.charset.StandardCharsets.UTF_8; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.googlejavaformat.java.Trees.getEndPosition; +import static java.lang.Math.max; import com.google.common.base.CharMatcher; import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.collect.Range; import com.google.common.collect.RangeMap; import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeMap; import com.google.common.collect.TreeRangeSet; -import com.google.googlejavaformat.FormattingError; import com.google.googlejavaformat.Newlines; -import java.io.IOError; -import java.io.IOException; -import java.net.URI; +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; +import com.sun.source.util.DocTreePath; +import com.sun.source.util.DocTreePathScanner; +import com.sun.source.util.TreePathScanner; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.api.JavacTrees; +import com.sun.tools.javac.tree.DCTree; +import com.sun.tools.javac.tree.DCTree.DCReference; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; +import com.sun.tools.javac.tree.JCTree.JCFieldAccess; +import com.sun.tools.javac.tree.JCTree.JCImport; +import com.sun.tools.javac.util.Context; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import javax.tools.Diagnostic; -import javax.tools.DiagnosticCollector; -import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; -import javax.tools.SimpleJavaFileObject; -import javax.tools.StandardLocation; -import org.openjdk.source.doctree.DocCommentTree; -import org.openjdk.source.doctree.ReferenceTree; -import org.openjdk.source.tree.IdentifierTree; -import org.openjdk.source.tree.ImportTree; -import org.openjdk.source.tree.Tree; -import org.openjdk.source.util.DocTreePath; -import org.openjdk.source.util.DocTreePathScanner; -import org.openjdk.source.util.TreePathScanner; -import org.openjdk.source.util.TreeScanner; -import org.openjdk.tools.javac.api.JavacTrees; -import org.openjdk.tools.javac.file.JavacFileManager; -import org.openjdk.tools.javac.parser.JavacParser; -import org.openjdk.tools.javac.parser.ParserFactory; -import org.openjdk.tools.javac.tree.DCTree; -import org.openjdk.tools.javac.tree.DCTree.DCReference; -import org.openjdk.tools.javac.tree.JCTree; -import org.openjdk.tools.javac.tree.JCTree.JCCompilationUnit; -import org.openjdk.tools.javac.tree.JCTree.JCFieldAccess; -import org.openjdk.tools.javac.tree.JCTree.JCIdent; -import org.openjdk.tools.javac.tree.JCTree.JCImport; -import org.openjdk.tools.javac.util.Context; -import org.openjdk.tools.javac.util.Log; -import org.openjdk.tools.javac.util.Options; +import org.jspecify.annotations.Nullable; /** * Removes unused imports from a source file. Imports that are only used in javadoc are also @@ -72,14 +63,6 @@ */ public class RemoveUnusedImports { - /** Configuration for javadoc-only imports. */ - public enum JavadocOnlyImports { - /** Remove imports that are only used in javadoc, and fully qualify any {@code @link} tags. */ - REMOVE, - /** Keep imports that are only used in javadoc. */ - KEEP - } - // Visits an AST, recording all simple names that could refer to imported // types and also any javadoc references that could refer to imported // types (`@link`, `@see`, `@throws`, etc.) @@ -123,6 +106,31 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) { 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) 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) { @@ -146,7 +154,7 @@ private void scanJavadoc() { // scan javadoc comments, checking for references to imported types class DocTreeScanner extends DocTreePathScanner { @Override - public Void visitIdentifier(org.openjdk.source.doctree.IdentifierTree node, Void aVoid) { + public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVoid) { return null; } @@ -154,7 +162,9 @@ public Void visitIdentifier(org.openjdk.source.doctree.IdentifierTree node, Void 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) { @@ -192,8 +202,7 @@ public Void visitIdentifier(IdentifierTree node, Void aVoid) { } } - public static String removeUnusedImports( - final String contents, JavadocOnlyImports javadocOnlyImports) { + public static String removeUnusedImports(final String contents) throws FormatterException { Context context = new Context(); JCCompilationUnit unit = parse(context, contents); if (unit == null) { @@ -203,42 +212,17 @@ public static String removeUnusedImports( UnusedImportScanner scanner = new UnusedImportScanner(JavacTrees.instance(context)); scanner.scan(unit, null); return applyReplacements( - contents, - buildReplacements( - contents, unit, scanner.usedNames, scanner.usedInJavadoc, javadocOnlyImports)); + contents, buildReplacements(contents, unit, scanner.usedNames, scanner.usedInJavadoc)); } - private static JCCompilationUnit parse(Context context, String javaInput) { - DiagnosticCollector diagnostics = new DiagnosticCollector<>(); - context.put(DiagnosticListener.class, diagnostics); - Options.instance(context).put("allowStringFolding", "false"); - JCCompilationUnit unit; - JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8); - try { - fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of()); - } catch (IOException e) { - // impossible - throw new IOError(e); - } - SimpleJavaFileObject source = - new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) { - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - return javaInput; - } - }; - Log.instance(context).useSource(source); - ParserFactory parserFactory = ParserFactory.instance(context); - JavacParser parser = - parserFactory.newParser( - javaInput, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true); - unit = parser.parseCompilationUnit(); - unit.sourcefile = source; - Iterable> errorDiagnostics = - Iterables.filter(diagnostics.getDiagnostics(), Formatter.ERROR_DIAGNOSTIC); - if (!Iterables.isEmpty(errorDiagnostics)) { + private static JCCompilationUnit parse(Context context, String javaInput) + throws FormatterException { + List> errorDiagnostics = new ArrayList<>(); + JCTree.JCCompilationUnit unit = + Trees.parse(context, errorDiagnostics, /* allowStringFolding= */ false, javaInput); + if (!errorDiagnostics.isEmpty()) { // error handling is done during formatting - throw FormattingError.fromJavacDiagnostics(errorDiagnostics); + throw FormatterException.fromJavacDiagnostics(errorDiagnostics); } return unit; } @@ -248,77 +232,101 @@ private static RangeMap buildReplacements( String contents, JCCompilationUnit unit, Set usedNames, - Multimap> usedInJavadoc, - JavadocOnlyImports javadocOnlyImports) { + Multimap> usedInJavadoc) { RangeMap replacements = TreeRangeMap.create(); - for (JCImport importTree : unit.getImports()) { + for (JCTree importTree : unit.getImports()) { + if (isModuleImport(importTree)) { + continue; + } String simpleName = getSimpleName(importTree); - if (!isUnused(unit, usedNames, usedInJavadoc, javadocOnlyImports, importTree, simpleName)) { + if (!isUnused(unit, usedNames, usedInJavadoc, importTree, simpleName)) { continue; } // delete the import - int endPosition = importTree.getEndPosition(unit.endPositions); - endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition); + int endPosition = getEndPosition(importTree, unit); + 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()).equals(sep)) { + && contents.subSequence(endPosition, endPosition + sep.length()).toString().equals(sep)) { endPosition += sep.length(); } replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), ""); - // fully qualify any javadoc references with the same simple name as a deleted - // non-static import - if (!importTree.isStatic()) { - for (Range docRange : usedInJavadoc.get(simpleName)) { - if (docRange == null) { - continue; - } - String replaceWith = importTree.getQualifiedIdentifier().toString(); - replacements.put(docRange, replaceWith); - } - } } return replacements; } - private static String getSimpleName(JCImport importTree) { - return importTree.getQualifiedIdentifier() instanceof JCIdent - ? ((JCIdent) importTree.getQualifiedIdentifier()).getName().toString() - : ((JCFieldAccess) importTree.getQualifiedIdentifier()).getIdentifier().toString(); + private static String getSimpleName(JCTree importTree) { + return getQualifiedIdentifier(importTree).getIdentifier().toString(); } private static boolean isUnused( JCCompilationUnit unit, Set usedNames, Multimap> usedInJavadoc, - JavadocOnlyImports javadocOnlyImports, - JCImport importTree, + JCTree importTree, String simpleName) { - String qualifier = - importTree.getQualifiedIdentifier() instanceof JCFieldAccess - ? ((JCFieldAccess) importTree.getQualifiedIdentifier()).getExpression().toString() - : null; + JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier(importTree); + String qualifier = qualifiedIdentifier.getExpression().toString(); if (qualifier.equals("java.lang")) { return true; } if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) { return true; } - if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess - && ((JCFieldAccess) importTree.getQualifiedIdentifier()) - .getIdentifier() - .contentEquals("*")) { + if (qualifiedIdentifier.getIdentifier().contentEquals("*")) { return false; } if (usedNames.contains(simpleName)) { return false; } - if (usedInJavadoc.containsKey(simpleName) && javadocOnlyImports == JavadocOnlyImports.KEEP) { + if (usedInJavadoc.containsKey(simpleName)) { return false; } return true; } + private static final Method GET_QUALIFIED_IDENTIFIER_METHOD = getQualifiedIdentifierMethod(); + + private static @Nullable Method getQualifiedIdentifierMethod() { + try { + return JCImport.class.getMethod("getQualifiedIdentifier"); + } catch (NoSuchMethodException e) { + return null; + } + } + + private static JCFieldAccess getQualifiedIdentifier(JCTree importTree) { + checkArgument(!isModuleImport(importTree)); + // Use reflection because the return type is JCTree in some versions and JCFieldAccess in others + try { + return (JCFieldAccess) GET_QUALIFIED_IDENTIFIER_METHOD.invoke(importTree); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static final @Nullable Method IS_MODULE_METHOD = getIsModuleMethod(); + + private static @Nullable Method getIsModuleMethod() { + try { + return ImportTree.class.getMethod("isModule"); + } catch (NoSuchMethodException ignored) { + return null; + } + } + + private static boolean isModuleImport(JCTree importTree) { + if (IS_MODULE_METHOD == null) { + return false; + } + try { + return (boolean) IS_MODULE_METHOD.invoke(importTree); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + /** Applies the replacements to the given source, and re-format any edited javadoc. */ private static String applyReplacements(String source, RangeMap replacements) { // save non-empty fixed ranges for reformatting after fixes are applied @@ -341,18 +349,6 @@ private static String applyReplacements(String source, RangeMap } offset += replaceWith.length() - (range.upperEndpoint() - range.lowerEndpoint()); } - String result = sb.toString(); - - // If there were any non-empty replaced ranges (e.g. javadoc), reformat the fixed regions. - // We could avoid formatting twice in --fix-imports=also mode, but that is not the default - // and removing imports won't usually affect javadoc. - if (!fixedRanges.isEmpty()) { - try { - result = new Formatter().formatSource(result, fixedRanges.asRanges()); - } catch (FormatterException e) { - // javadoc reformatting is best-effort - } - } - return result; + return sb.toString(); } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/Replacement.java b/core/src/main/java/com/google/googlejavaformat/java/Replacement.java index dc44d710f..5df0991f2 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Replacement.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Replacement.java @@ -14,6 +14,9 @@ package com.google.googlejavaformat.java; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.common.collect.Range; import java.util.Objects; @@ -23,28 +26,20 @@ *

google-java-format doesn't depend on AutoValue, to allow AutoValue to depend on * google-java-format. */ -public class Replacement { +public final class Replacement { public static Replacement create(int startPosition, int endPosition, String replaceWith) { + checkArgument(startPosition >= 0, "startPosition must be non-negative"); + checkArgument(startPosition <= endPosition, "startPosition cannot be after endPosition"); return new Replacement(Range.closedOpen(startPosition, endPosition), replaceWith); } - public static Replacement create(Range range, String replaceWith) { - return new Replacement(range, replaceWith); - } - private final Range replaceRange; private final String replacementString; - Replacement(Range replaceRange, String replacementString) { - if (replaceRange == null) { - throw new NullPointerException("Null replaceRange"); - } - this.replaceRange = replaceRange; - if (replacementString == null) { - throw new NullPointerException("Null replacementString"); - } - this.replacementString = replacementString; + private Replacement(Range replaceRange, String replacementString) { + this.replaceRange = checkNotNull(replaceRange, "Null replaceRange"); + this.replacementString = checkNotNull(replacementString, "Null replacementString"); } /** The range of characters in the original source to replace. */ diff --git a/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java index 3827ef958..60fd77ea8 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java @@ -14,9 +14,12 @@ package com.google.googlejavaformat.java; +import static com.google.common.collect.ImmutableList.toImmutableList; + import com.google.common.base.CharMatcher; import com.google.common.base.Preconditions; import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeSet; @@ -57,9 +60,17 @@ public void closeBraces(int initialIndent) { } private static final int INDENTATION_SIZE = 2; - private final Formatter formatter = new Formatter(); + private final Formatter formatter; private static final CharMatcher NOT_WHITESPACE = CharMatcher.whitespace().negate(); + public SnippetFormatter() { + this(JavaFormatterOptions.defaultOptions()); + } + + public SnippetFormatter(JavaFormatterOptions formatterOptions) { + formatter = new Formatter(formatterOptions); + } + public String createIndentationString(int indentationLevel) { Preconditions.checkArgument( indentationLevel >= 0, @@ -87,7 +98,7 @@ private static List> offsetRanges(List> ranges, in } /** Runs the Google Java formatter on the given source, with only the given ranges specified. */ - public List format( + public ImmutableList format( SnippetKind kind, String source, List> ranges, @@ -114,14 +125,9 @@ public List format( wrapper.offset, replacement.length() - (wrapper.contents.length() - wrapper.offset - source.length())); - List replacements = toReplacements(source, replacement); - List filtered = new ArrayList<>(); - for (Replacement r : replacements) { - if (rangeSet.encloses(r.getReplaceRange())) { - filtered.add(r); - } - } - return filtered; + return toReplacements(source, replacement).stream() + .filter(r -> rangeSet.encloses(r.getReplaceRange())) + .collect(toImmutableList()); } /** @@ -142,7 +148,7 @@ private static List toReplacements(String source, String replacemen int i = NOT_WHITESPACE.indexIn(source); int j = NOT_WHITESPACE.indexIn(replacement); if (i != 0 || j != 0) { - replacements.add(Replacement.create(Range.closedOpen(0, i), replacement.substring(0, j))); + replacements.add(Replacement.create(0, i, replacement.substring(0, j))); } while (i != -1 && j != -1) { int i2 = NOT_WHITESPACE.indexIn(source, i + 1); @@ -152,8 +158,7 @@ private static List toReplacements(String source, String replacemen } if ((i2 - i) != (j2 - j) || !source.substring(i + 1, i2).equals(replacement.substring(j + 1, j2))) { - replacements.add( - Replacement.create(Range.closedOpen(i + 1, i2), replacement.substring(j + 1, j2))); + replacements.add(Replacement.create(i + 1, i2, replacement.substring(j + 1, j2))); } i = i2; j = j2; @@ -166,53 +171,38 @@ private SnippetWrapper snippetWrapper(SnippetKind kind, String source, int initi * Synthesize a dummy class around the code snippet provided by Eclipse. The dummy class is * correctly formatted -- the blocks use correct indentation, etc. */ - switch (kind) { - case COMPILATION_UNIT: - { - SnippetWrapper wrapper = new SnippetWrapper(); - for (int i = 1; i <= initialIndent; i++) { - wrapper.append("class Dummy {\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; - } - case CLASS_BODY_DECLARATIONS: - { - SnippetWrapper wrapper = new SnippetWrapper(); - for (int i = 1; i <= initialIndent; i++) { - wrapper.append("class Dummy {\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; + return switch (kind) { + case COMPILATION_UNIT, CLASS_BODY_DECLARATIONS -> { + SnippetWrapper wrapper = new SnippetWrapper(); + for (int i = 1; i <= initialIndent; i++) { + wrapper.append("class Dummy {\n").append(createIndentationString(i)); } - case STATEMENTS: - { - SnippetWrapper wrapper = new SnippetWrapper(); - wrapper.append("class Dummy {\n").append(createIndentationString(1)); - for (int i = 2; i <= initialIndent; i++) { - wrapper.append("{\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; + wrapper.appendSource(source); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + case STATEMENTS -> { + SnippetWrapper wrapper = new SnippetWrapper(); + wrapper.append("class Dummy {\n").append(createIndentationString(1)); + for (int i = 2; i <= initialIndent; i++) { + wrapper.append("{\n").append(createIndentationString(i)); } - case EXPRESSION: - { - SnippetWrapper wrapper = new SnippetWrapper(); - wrapper.append("class Dummy {\n").append(createIndentationString(1)); - for (int i = 2; i <= initialIndent; i++) { - wrapper.append("{\n").append(createIndentationString(i)); - } - wrapper.append("Object o = "); - wrapper.appendSource(source); - wrapper.append(";"); - wrapper.closeBraces(initialIndent); - return wrapper; + wrapper.appendSource(source); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + case EXPRESSION -> { + SnippetWrapper wrapper = new SnippetWrapper(); + wrapper.append("class Dummy {\n").append(createIndentationString(1)); + for (int i = 2; i <= initialIndent; i++) { + wrapper.append("{\n").append(createIndentationString(i)); } - default: - throw new IllegalArgumentException("Unknown snippet kind: " + kind); - } + wrapper.append("Object o = "); + wrapper.appendSource(source); + wrapper.append(";"); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + }; } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java new file mode 100644 index 000000000..22110ad8a --- /dev/null +++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java @@ -0,0 +1,498 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Iterables.getLast; +import static com.google.googlejavaformat.java.Trees.getEndPosition; +import static com.google.googlejavaformat.java.Trees.getStartPosition; +import static java.lang.Math.min; +import static java.util.stream.Collectors.joining; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; +import com.google.common.collect.TreeRangeMap; +import com.google.googlejavaformat.Newlines; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Position; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; + +/** Wraps string literals that exceed the column limit. */ +public final class StringWrapper { + + public static final String TEXT_BLOCK_DELIMITER = "\"\"\""; + + /** Reflows long string literals in the given Java source code. */ + public static String wrap(String input, Formatter formatter) throws FormatterException { + return StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, input, formatter); + } + + /** + * Reflows string literals in the given Java source code that extend past the given column limit. + */ + static String wrap(final int columnLimit, String input, Formatter formatter) + throws FormatterException { + if (!needWrapping(columnLimit, input)) { + // fast path + return input; + } + + TreeRangeMap replacements = getReflowReplacements(columnLimit, input); + String firstPass = formatter.formatSource(input, replacements.asMapOfRanges().keySet()); + + if (!firstPass.equals(input)) { + // If formatting the replacement ranges resulted in a change, recalculate the replacements on + // the updated input. + input = firstPass; + replacements = getReflowReplacements(columnLimit, input); + } + + String result = applyReplacements(input, replacements); + + { + // We really don't want bugs in this pass to change the behaviour of programs we're + // formatting, so check that the pretty-printed AST is the same before and after reformatting. + String expected = parse(input, /* allowStringFolding= */ true).toString(); + String actual = parse(result, /* allowStringFolding= */ true).toString(); + if (!expected.equals(actual)) { + throw new FormatterException( + String.format( + "Something has gone terribly wrong. We planned to make the below formatting change," + + " but have aborted because it would unexpectedly change the AST.\n" + + "Please file a bug: " + + "https://github.com/google/google-java-format/issues/new" + + "\n\n=== Actual: ===\n%s\n=== Expected: ===\n%s\n", + actual, expected)); + } + } + + return result; + } + + private static TreeRangeMap getReflowReplacements( + int columnLimit, final String input) throws FormatterException { + return new Reflower(columnLimit, input).getReflowReplacements(); + } + + private static class Reflower { + + private final String input; + private final int columnLimit; + private final String separator; + private final JCTree.JCCompilationUnit unit; + private final Position.LineMap lineMap; + + Reflower(int columnLimit, String input) throws FormatterException { + this.columnLimit = columnLimit; + this.input = input; + this.separator = Newlines.guessLineSeparator(input); + this.unit = parse(input, /* allowStringFolding= */ false); + this.lineMap = unit.getLineMap(); + } + + TreeRangeMap getReflowReplacements() { + // Paths to string literals that extend past the column limit. + List longStringLiterals = new ArrayList<>(); + // Paths to text blocks to be re-indented. + List textBlocks = new ArrayList<>(); + new LongStringsAndTextBlockScanner(longStringLiterals, textBlocks) + .scan(new TreePath(unit), null); + TreeRangeMap replacements = TreeRangeMap.create(); + indentTextBlocks(replacements, textBlocks); + wrapLongStrings(replacements, longStringLiterals); + return replacements; + } + + private class LongStringsAndTextBlockScanner extends TreePathScanner { + + private final List longStringLiterals; + private final List textBlocks; + + LongStringsAndTextBlockScanner(List longStringLiterals, List textBlocks) { + this.longStringLiterals = longStringLiterals; + this.textBlocks = textBlocks; + } + + @Override + public Void visitLiteral(LiteralTree literalTree, Void aVoid) { + if (literalTree.getKind() != Kind.STRING_LITERAL) { + return null; + } + int pos = getStartPosition(literalTree); + if (input.substring(pos, min(input.length(), pos + 3)).equals(TEXT_BLOCK_DELIMITER)) { + textBlocks.add(literalTree); + return null; + } + Tree parent = getCurrentPath().getParentPath().getLeaf(); + if (parent instanceof MemberSelectTree + && ((MemberSelectTree) parent).getExpression().equals(literalTree)) { + return null; + } + int endPosition = getEndPosition(literalTree, unit); + int lineEnd = endPosition; + while (Newlines.hasNewlineAt(input, lineEnd) == -1) { + lineEnd++; + } + if (lineMap.getColumnNumber(lineEnd) - 1 <= columnLimit) { + return null; + } + longStringLiterals.add(getCurrentPath()); + return null; + } + } + + private void indentTextBlocks( + TreeRangeMap replacements, List textBlocks) { + for (Tree tree : textBlocks) { + int startPosition = lineMap.getStartPosition(lineMap.getLineNumber(getStartPosition(tree))); + int endPosition = getEndPosition(tree, unit); + String text = input.substring(startPosition, endPosition); + int leadingWhitespace = CharMatcher.whitespace().negate().indexIn(text); + + // Find the source code of the text block with incidental whitespace removed. + // The first line of the text block is always """, and it does not affect incidental + // whitespace. + ImmutableList initialLines = text.lines().collect(toImmutableList()); + String stripped = initialLines.stream().skip(1).collect(joining(separator)).stripIndent(); + ImmutableList lines = stripped.lines().collect(toImmutableList()); + boolean deindent = + getLast(initialLines).stripTrailing().length() + == getLast(lines).stripTrailing().length(); + + String prefix = deindent ? "" : " ".repeat(leadingWhitespace); + + StringBuilder output = new StringBuilder(prefix).append(initialLines.get(0).stripLeading()); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + String trimmed = line.stripTrailing(); + output.append(separator); + if (!trimmed.isEmpty()) { + // Don't add incidental leading whitespace to empty lines + output.append(prefix); + } + if (i == lines.size() - 1) { + String withoutDelimiter = + trimmed + .substring(0, trimmed.length() - TEXT_BLOCK_DELIMITER.length()) + .stripTrailing(); + if (!withoutDelimiter.stripLeading().isEmpty()) { + output.append(withoutDelimiter).append('\\').append(separator).append(prefix); + } + // If the trailing line is just """, indenting it more than the prefix of incidental + // whitespace has no effect, and results in a javac text-blocks warning that 'trailing + // white space will be removed'. + output.append(TEXT_BLOCK_DELIMITER); + } else { + output.append(line); + } + } + replacements.put(Range.closedOpen(startPosition, endPosition), output.toString()); + } + } + + private void wrapLongStrings( + TreeRangeMap replacements, List longStringLiterals) { + for (TreePath path : longStringLiterals) { + // Find the outermost contiguous enclosing concatenation expression + TreePath enclosing = path; + while (enclosing.getParentPath().getLeaf().getKind() == Kind.PLUS) { + enclosing = enclosing.getParentPath(); + } + // Is the literal being wrapped the first in a chain of concatenation expressions? + // i.e. `ONE + TWO + THREE` + // We need this information to handle continuation indents. + AtomicBoolean first = new AtomicBoolean(false); + // Finds the set of string literals in the concat expression that includes the one that + // needs + // to be wrapped. + List flat = flatten(input, unit, path, enclosing, first); + // Zero-indexed start column + int startColumn = lineMap.getColumnNumber(getStartPosition(flat.get(0))) - 1; + + // Handling leaving trailing non-string tokens at the end of the literal, + // e.g. the trailing `);` in `foo("...");`. + int end = getEndPosition(getLast(flat), unit); + int lineEnd = end; + while (Newlines.hasNewlineAt(input, lineEnd) == -1) { + lineEnd++; + } + int trailing = lineEnd - end; + + // Get the original source text of the string literals, excluding `"` and `+`. + ImmutableList components = stringComponents(input, unit, flat); + replacements.put( + Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(getLast(flat), unit)), + reflow(separator, columnLimit, startColumn, trailing, components, first.get())); + } + } + } + + /** + * Returns the source text of the given string literal trees, excluding the leading and trailing + * double-quotes and the `+` operator. + */ + private static ImmutableList stringComponents( + String input, JCTree.JCCompilationUnit unit, List flat) { + ImmutableList.Builder result = ImmutableList.builder(); + StringBuilder piece = new StringBuilder(); + for (Tree tree : flat) { + // adjust for leading and trailing double quotes + String text = input.substring(getStartPosition(tree) + 1, getEndPosition(tree, unit) - 1); + int start = 0; + for (int idx = 0; idx < text.length(); idx++) { + if (CharMatcher.whitespace().matches(text.charAt(idx))) { + // continue below + } else if (hasEscapedWhitespaceAt(text, idx) != -1) { + // continue below + } else if (hasEscapedNewlineAt(text, idx) != -1) { + int length; + while ((length = hasEscapedNewlineAt(text, idx)) != -1) { + idx += length; + } + } else { + continue; + } + piece.append(text, start, idx); + result.add(piece.toString()); + piece = new StringBuilder(); + start = idx; + } + if (piece.length() > 0) { + result.add(piece.toString()); + piece = new StringBuilder(); + } + if (start < text.length()) { + piece.append(text, start, text.length()); + } + } + if (piece.length() > 0) { + result.add(piece.toString()); + } + return result.build(); + } + + static int hasEscapedWhitespaceAt(String input, int idx) { + if (input.startsWith("\\t", idx)) { + return 2; + } + return -1; + } + + static int hasEscapedNewlineAt(String input, int idx) { + int offset = 0; + if (input.startsWith("\\r", idx)) { + offset += 2; + } + if (input.startsWith("\\n", idx)) { + offset += 2; + } + return offset > 0 ? offset : -1; + } + + /** + * Reflows the given source text, trying to split on word boundaries. + * + * @param separator the line separator + * @param columnLimit the number of columns to wrap at + * @param startColumn the column position of the beginning of the original text + * @param trailing extra space to leave after the last line, 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, + int columnLimit, + int startColumn, + int trailing, + ImmutableList components, + boolean first0) { + // We have space between the start column and the limit to output the first line. + // Reserve two spaces for the start and end quotes. + int width = columnLimit - startColumn - 2; + Deque input = new ArrayDeque<>(components); + List lines = new ArrayList<>(); + boolean first = first0; + while (!input.isEmpty()) { + int length = 0; + List line = new ArrayList<>(); + // If we know this is going to be the last line, then remove a bit of width to account for the + // trailing characters. + if (totalLengthLessThanOrEqual(input, 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)) { + String text = input.removeFirst(); + line.add(text); + length += text.length(); + if (text.endsWith("\\n") || text.endsWith("\\r")) { + break; + } + } + if (line.isEmpty()) { + line.add(input.removeFirst()); + } + // add the split line to the output, and process whatever's left + lines.add(String.join("", line)); + if (first) { + width -= 6; // subsequent lines have a four-space continuation indent and a `+ ` + first = false; + } + } + + return lines.stream() + .collect( + joining( + "\"" + separator + Strings.repeat(" ", startColumn + (first0 ? 4 : -2)) + "+ \"", + "\"", + "\"")); + } + + private static boolean totalLengthLessThanOrEqual(Iterable input, int length) { + int total = 0; + for (String s : input) { + total += s.length(); + if (total > length) { + return false; + } + } + return true; + } + + /** + * Flattens the given binary expression tree, and extracts the subset that contains the given path + * and any adjacent nodes that are also string literals. + */ + private static List flatten( + String input, + JCTree.JCCompilationUnit unit, + TreePath path, + TreePath parent, + AtomicBoolean firstInChain) { + List flat = new ArrayList<>(); + + // flatten the expression tree with a pre-order traversal + ArrayDeque todo = new ArrayDeque<>(); + todo.add(parent.getLeaf()); + while (!todo.isEmpty()) { + Tree first = todo.removeFirst(); + if (first.getKind() == Tree.Kind.PLUS) { + BinaryTree bt = (BinaryTree) first; + todo.addFirst(bt.getRightOperand()); + todo.addFirst(bt.getLeftOperand()); + } else { + flat.add(first); + } + } + + int idx = flat.indexOf(path.getLeaf()); + Verify.verify(idx != -1); + + // walk outwards from the leaf for adjacent string literals to also reflow + int startIdx = idx; + int endIdx = idx + 1; + while (startIdx > 0 + && flat.get(startIdx - 1).getKind() == Tree.Kind.STRING_LITERAL + && noComments(input, unit, flat.get(startIdx - 1), flat.get(startIdx))) { + startIdx--; + } + while (endIdx < flat.size() + && flat.get(endIdx).getKind() == Tree.Kind.STRING_LITERAL + && noComments(input, unit, flat.get(endIdx - 1), flat.get(endIdx))) { + endIdx++; + } + + firstInChain.set(startIdx == 0); + return ImmutableList.copyOf(flat.subList(startIdx, endIdx)); + } + + private static boolean noComments( + String input, JCTree.JCCompilationUnit unit, Tree one, Tree two) { + return STRING_CONCAT_DELIMITER.matchesAllOf( + input.subSequence(getEndPosition(one, unit), getStartPosition(two))); + } + + public static final CharMatcher STRING_CONCAT_DELIMITER = + CharMatcher.whitespace().or(CharMatcher.anyOf("\"+")); + + /** + * Returns true if any lines in the given Java source exceed the column limit, or contain a {@code + * """} that could indicate a text block. + */ + private static boolean needWrapping(int columnLimit, String input) { + // TODO(cushon): consider adding Newlines.lineIterable? + Iterator it = Newlines.lineIterator(input); + while (it.hasNext()) { + String line = it.next(); + if (line.length() > columnLimit || line.contains(TEXT_BLOCK_DELIMITER)) { + return true; + } + } + return false; + } + + /** Parses the given Java source. */ + private static JCTree.JCCompilationUnit parse(String source, boolean allowStringFolding) + throws FormatterException { + List> errorDiagnostics = new ArrayList<>(); + Context context = new Context(); + JCTree.JCCompilationUnit unit = + Trees.parse(context, errorDiagnostics, allowStringFolding, source); + if (!errorDiagnostics.isEmpty()) { + // error handling is done during formatting + throw FormatterException.fromJavacDiagnostics(errorDiagnostics); + } + return unit; + } + + /** Applies replacements to the given string. */ + private static String applyReplacements( + String javaInput, TreeRangeMap replacementMap) throws FormatterException { + // process in descending order so the replacement ranges aren't perturbed if any replacements + // differ in size from the input + Map, String> ranges = replacementMap.asDescendingMapOfRanges(); + if (ranges.isEmpty()) { + return javaInput; + } + StringBuilder sb = new StringBuilder(javaInput); + for (Map.Entry, String> entry : ranges.entrySet()) { + Range range = entry.getKey(); + sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue()); + } + return sb.toString(); + } + + private StringWrapper() {} +} diff --git a/core/src/main/java/com/google/googlejavaformat/java/Trees.java b/core/src/main/java/com/google/googlejavaformat/java/Trees.java index 79ae50e1f..6b053771b 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Trees.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Trees.java @@ -14,24 +14,44 @@ package com.google.googlejavaformat.java; +import static com.google.googlejavaformat.java.Trees.getEndPosition; +import static com.google.googlejavaformat.java.Trees.getStartPosition; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.TreePath; +import com.sun.tools.javac.file.JavacFileManager; +import com.sun.tools.javac.parser.JavacParser; +import com.sun.tools.javac.parser.ParserFactory; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; +import com.sun.tools.javac.tree.Pretty; +import com.sun.tools.javac.tree.TreeInfo; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Log; +import com.sun.tools.javac.util.Options; import java.io.IOError; import java.io.IOException; +import java.net.URI; +import java.util.List; import javax.lang.model.element.Name; -import org.openjdk.source.tree.ClassTree; -import org.openjdk.source.tree.CompoundAssignmentTree; -import org.openjdk.source.tree.ExpressionTree; -import org.openjdk.source.tree.IdentifierTree; -import org.openjdk.source.tree.MemberSelectTree; -import org.openjdk.source.tree.MethodInvocationTree; -import org.openjdk.source.tree.ParenthesizedTree; -import org.openjdk.source.tree.Tree; -import org.openjdk.source.util.TreePath; -import org.openjdk.tools.javac.tree.JCTree; -import org.openjdk.tools.javac.tree.Pretty; -import org.openjdk.tools.javac.tree.TreeInfo; +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardLocation; /** Utilities for working with {@link Tree}s. */ -public class Trees { +class Trees { /** Returns the length of the source for the node. */ static int getLength(Tree tree, TreePath path) { return getEndPosition(tree, path) - getStartPosition(tree); @@ -44,8 +64,12 @@ static int getStartPosition(Tree expression) { /** Returns the source end position of the node. */ static int getEndPosition(Tree expression, TreePath path) { - return ((JCTree) expression) - .getEndPosition(((JCTree.JCCompilationUnit) path.getCompilationUnit()).endPositions); + return getEndPosition(expression, path.getCompilationUnit()); + } + + /** Returns the source end position of the node. */ + public static int getEndPosition(Tree tree, CompilationUnitTree unit) { + return ((JCTree) tree).getEndPosition(((JCCompilationUnit) unit).endPositions); } /** Returns the source text for the node. */ @@ -99,13 +123,10 @@ static int precedence(ExpressionTree expression) { static ClassTree getEnclosingTypeDeclaration(TreePath path) { for (; path != null; path = path.getParentPath()) { switch (path.getLeaf().getKind()) { - case CLASS: - case ENUM: - case INTERFACE: - case ANNOTATED_TYPE: + case CLASS, ENUM, INTERFACE, ANNOTATED_TYPE -> { return (ClassTree) path.getLeaf(); - default: - break; + } + default -> {} } } throw new AssertionError(); @@ -115,4 +136,54 @@ static ClassTree getEnclosingTypeDeclaration(TreePath path) { static ExpressionTree skipParen(ExpressionTree node) { return ((ParenthesizedTree) node).getExpression(); } + + static JCCompilationUnit parse( + Context context, + List> errorDiagnostics, + boolean allowStringFolding, + String javaInput) { + DiagnosticListener diagnostics = + diagnostic -> { + if (errorDiagnostic(diagnostic)) { + errorDiagnostics.add(diagnostic); + } + }; + context.put(DiagnosticListener.class, diagnostics); + Options.instance(context).put("--enable-preview", "true"); + Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding)); + JavacFileManager fileManager = new JavacFileManager(context, /* register= */ true, UTF_8); + try { + fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of()); + } catch (IOException e) { + // impossible + throw new IOError(e); + } + SimpleJavaFileObject source = + new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) { + @Override + public String getCharContent(boolean ignoreEncodingErrors) { + return javaInput; + } + }; + Log.instance(context).useSource(source); + ParserFactory parserFactory = ParserFactory.instance(context); + JavacParser parser = + parserFactory.newParser( + javaInput, + /* keepDocComments= */ true, + /* keepEndPos= */ true, + /* keepLineMap= */ true); + JCCompilationUnit unit = parser.parseCompilationUnit(); + unit.sourcefile = source; + return unit; + } + + private static boolean errorDiagnostic(Diagnostic input) { + if (input.getKind() != Diagnostic.Kind.ERROR) { + return false; + } + // accept constructor-like method declarations that don't match the name of their + // enclosing class + return !input.getCode().equals("compiler.err.invalid.meth.decl.ret.type.req"); + } } 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 b12265905..83c09b3ee 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java +++ b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java @@ -16,6 +16,7 @@ import com.google.common.base.Verify; import java.util.List; +import java.util.Optional; /** Heuristics for classifying qualified names as types. */ public final class TypeNameClassifier { @@ -29,20 +30,17 @@ private enum TyParseState { START(false) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - // if we see an UpperCamel later, assume this was a class - // e.g. com.google.FOO.Bar - return TyParseState.AMBIGUOUS; - case LOWER_CAMEL: - return TyParseState.REJECT; - case LOWERCASE: - // could be a package - return TyParseState.START; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE -> + // if we see an UpperCamel later, assume this was a class + // e.g. com.google.FOO.Bar + TyParseState.AMBIGUOUS; + case LOWER_CAMEL -> TyParseState.REJECT; + case LOWERCASE -> + // could be a package + TyParseState.START; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }, @@ -50,15 +48,10 @@ public TyParseState next(JavaCaseFormat n) { TYPE(true) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - case LOWER_CAMEL: - case LOWERCASE: - return TyParseState.FIRST_STATIC_MEMBER; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE, LOWER_CAMEL, LOWERCASE -> TyParseState.FIRST_STATIC_MEMBER; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }, @@ -82,16 +75,11 @@ public TyParseState next(JavaCaseFormat n) { AMBIGUOUS(false) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - return AMBIGUOUS; - case LOWER_CAMEL: - case LOWERCASE: - return TyParseState.REJECT; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE -> AMBIGUOUS; + case LOWER_CAMEL, LOWERCASE -> TyParseState.REJECT; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }; @@ -121,16 +109,16 @@ public boolean isSingleUnit() { *

  • com.google.ClassName.InnerClass.staticMemberName * */ - static int typePrefixLength(List nameParts) { + static Optional typePrefixLength(List nameParts) { TyParseState state = TyParseState.START; - int typeLength = -1; + Optional typeLength = Optional.empty(); for (int i = 0; i < nameParts.size(); i++) { state = state.next(JavaCaseFormat.from(nameParts.get(i))); if (state == TyParseState.REJECT) { break; } if (state.isSingleUnit()) { - typeLength = i; + typeLength = Optional.of(i); } } return typeLength; @@ -163,7 +151,7 @@ static JavaCaseFormat from(String name) { 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 6a2999133..50d55d4d4 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java +++ b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java @@ -19,7 +19,7 @@ import com.google.common.base.Joiner; /** Checked exception class for formatter command-line usage errors. */ -public final class UsageException extends Exception { +final class UsageException extends Exception { private static final Joiner NEWLINE_JOINER = Joiner.on(System.lineSeparator()); @@ -36,24 +36,37 @@ public final class UsageException extends Exception { " Send formatted output back to files, not stdout.", " -", " Format stdin -> stdout", + " --assume-filename, -assume-filename", + " File name to use for diagnostics when formatting standard input (default is ).", " --aosp, -aosp, -a", - " Use AOSP style instead of Google Style (4-space indentation)", + " Use AOSP style instead of Google Style (4-space indentation).", " --fix-imports-only", " Fix import order and remove any unused imports, but do no other formatting.", " --skip-sorting-imports", " Do not fix the import order. Unused imports will still be removed.", " --skip-removing-unused-imports", " Do not remove unused imports. Imports will still be sorted.", - " --length, -length", - " Character length to format.", + " --skip-reflowing-long-strings", + " Do not reflow string literals that exceed the column limit.", + " --skip-javadoc-formatting", + " Do not reformat javadoc.", + " --dry-run, -n", + " Prints the paths of the files whose contents would change if the formatter were run" + + " normally.", + " --set-exit-if-changed", + " Return exit code 1 if there are any formatting changes.", " --lines, -lines, --line, -line", - " Line range(s) to format, like 5:10 (1-based; default is all).", + " Line range(s) to format, e.g. the first 5 lines are 1:5 (1-based; default is all).", " --offset, -offset", " Character offset to format (0-based; default is all).", + " --length, -length", + " Character length to format.", " --help, -help, -h", - " Print this usage statement", + " Print this usage statement.", " --version, -version, -v", " Print the version.", + " @", + " Read options and filenames from file.", "", }; @@ -80,11 +93,15 @@ private static String buildMessage(String message) { appendLines(builder, USAGE); appendLines(builder, ADDITIONAL_USAGE); appendLines(builder, new String[] {""}); - appendLines(builder, Main.VERSION); + appendLine(builder, Main.versionString()); appendLines(builder, DOCS_LINK); return builder.toString(); } + private static void appendLine(StringBuilder builder, String line) { + builder.append(line).append(System.lineSeparator()); + } + private static void appendLines(StringBuilder builder, String[] lines) { NEWLINE_JOINER.appendTo(builder, lines).append(System.lineSeparator()); } diff --git a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java index 7c7894754..e8da9fac9 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java +++ b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java @@ -18,13 +18,14 @@ import com.google.googlejavaformat.java.Formatter; import java.io.IOException; -import javax.annotation.Nullable; import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.tools.FileObject; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** * A decorating {@link Filer} implementation which formats Java source files with a {@link @@ -37,8 +38,28 @@ public final class FormattingFiler implements Filer { private final Formatter formatter = new Formatter(); private final Messager messager; - /** @param delegate filer to decorate */ - public FormattingFiler(Filer delegate) { + /** + * Create a new {@link FormattingFiler}. + * + * @param processingEnv the processing environment + */ + public static Filer create(ProcessingEnvironment processingEnv) { + Filer delegate = processingEnv.getFiler(); + if (processingEnv.getOptions().containsKey("experimental_turbine_hjar")) { + return delegate; + } + return new FormattingFiler(delegate, processingEnv.getMessager()); + } + + /** + * Create a new {@link FormattingFiler}. + * + * @param delegate filer to decorate + * @deprecated prefer {@link #create(ProcessingEnvironment)} + */ + @Deprecated + public + FormattingFiler(Filer delegate) { this(delegate, null); } @@ -48,8 +69,11 @@ public FormattingFiler(Filer delegate) { * * @param delegate filer to decorate * @param messager to log warnings to + * @deprecated prefer {@link #create(ProcessingEnvironment)} */ - public FormattingFiler(Filer delegate, @Nullable Messager messager) { + @Deprecated + public + FormattingFiler(Filer delegate, @Nullable Messager messager) { this.delegate = checkNotNull(delegate); this.messager = messager; } diff --git a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java index 65b420f16..b65258b1a 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java +++ b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java @@ -22,11 +22,11 @@ import com.google.googlejavaformat.java.FormatterException; import java.io.IOException; import java.io.Writer; -import javax.annotation.Nullable; import javax.annotation.processing.Messager; import javax.tools.Diagnostic; import javax.tools.ForwardingJavaFileObject; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** A {@link JavaFileObject} decorator which {@linkplain Formatter formats} source code. */ final class FormattingJavaFileObject extends ForwardingJavaFileObject { @@ -58,6 +58,11 @@ public void write(char[] chars, int start, int end) throws IOException { stringBuilder.append(chars, start, end - start); } + @Override + public void write(String string) throws IOException { + stringBuilder.append(string); + } + @Override public void flush() throws IOException {} 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 b8d36e6e6..bb9f70040 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 @@ -21,7 +21,6 @@ import static java.util.regex.Pattern.compile; import com.google.common.collect.ImmutableList; -import com.google.googlejavaformat.java.JavaFormatterOptions; import com.google.googlejavaformat.java.javadoc.JavadocLexer.LexException; import java.util.List; import java.util.regex.Matcher; @@ -36,101 +35,57 @@ * single blank line if it's empty. */ public final class JavadocFormatter { + + static final int MAX_LINE_LENGTH = 100; + /** * Formats the given Javadoc comment, which must start with ∕✱✱ and end with ✱∕. The output will * start and end with the same characters. */ - public static String formatJavadoc(String input, int blockIndent, JavaFormatterOptions options) { + public static String formatJavadoc(String input, int blockIndent) { ImmutableList tokens; try { tokens = lex(input); } catch (LexException e) { return input; } - String result = render(tokens, blockIndent, options); - return makeSingleLineIfPossible(blockIndent, result, options); + String result = render(tokens, blockIndent); + return makeSingleLineIfPossible(blockIndent, result); } - private static String render(List input, int blockIndent, JavaFormatterOptions options) { - JavadocWriter output = new JavadocWriter(blockIndent, options); + private static String render(List input, int blockIndent) { + JavadocWriter output = new JavadocWriter(blockIndent); for (Token token : input) { switch (token.getType()) { - case BEGIN_JAVADOC: - output.writeBeginJavadoc(); - break; - case END_JAVADOC: + case BEGIN_JAVADOC -> output.writeBeginJavadoc(); + case END_JAVADOC -> { output.writeEndJavadoc(); return output.toString(); - case FOOTER_JAVADOC_TAG_START: - output.writeFooterJavadocTagStart(token); - break; - case LIST_OPEN_TAG: - output.writeListOpen(token); - break; - case LIST_CLOSE_TAG: - output.writeListClose(token); - break; - case LIST_ITEM_OPEN_TAG: - output.writeListItemOpen(token); - break; - case HEADER_OPEN_TAG: - output.writeHeaderOpen(token); - break; - case HEADER_CLOSE_TAG: - output.writeHeaderClose(token); - break; - case PARAGRAPH_OPEN_TAG: - output.writeParagraphOpen(standardizePToken(token)); - break; - case BLOCKQUOTE_OPEN_TAG: - case BLOCKQUOTE_CLOSE_TAG: - output.writeBlockquoteOpenOrClose(token); - break; - case PRE_OPEN_TAG: - output.writePreOpen(token); - break; - case PRE_CLOSE_TAG: - output.writePreClose(token); - break; - case CODE_OPEN_TAG: - output.writeCodeOpen(token); - break; - case CODE_CLOSE_TAG: - output.writeCodeClose(token); - break; - case TABLE_OPEN_TAG: - output.writeTableOpen(token); - break; - case TABLE_CLOSE_TAG: - output.writeTableClose(token); - break; - case MOE_BEGIN_STRIP_COMMENT: - output.requestMoeBeginStripComment(token); - break; - case MOE_END_STRIP_COMMENT: - output.writeMoeEndStripComment(token); - break; - case HTML_COMMENT: - output.writeHtmlComment(token); - break; - case BR_TAG: - output.writeBr(standardizeBrToken(token)); - break; - case WHITESPACE: - output.requestWhitespace(); - break; - case FORCED_NEWLINE: - output.writeLineBreakNoAutoIndent(); - break; - case LITERAL: - output.writeLiteral(token); - break; - case PARAGRAPH_CLOSE_TAG: - case LIST_ITEM_CLOSE_TAG: - case OPTIONAL_LINE_BREAK: - break; - default: - throw new AssertionError(token.getType()); + } + case FOOTER_JAVADOC_TAG_START -> output.writeFooterJavadocTagStart(token); + case SNIPPET_BEGIN -> output.writeSnippetBegin(token); + case SNIPPET_END -> output.writeSnippetEnd(token); + case LIST_OPEN_TAG -> output.writeListOpen(token); + case LIST_CLOSE_TAG -> output.writeListClose(token); + case LIST_ITEM_OPEN_TAG -> output.writeListItemOpen(token); + case HEADER_OPEN_TAG -> output.writeHeaderOpen(token); + case HEADER_CLOSE_TAG -> output.writeHeaderClose(token); + case PARAGRAPH_OPEN_TAG -> output.writeParagraphOpen(standardizePToken(token)); + case BLOCKQUOTE_OPEN_TAG, BLOCKQUOTE_CLOSE_TAG -> output.writeBlockquoteOpenOrClose(token); + case PRE_OPEN_TAG -> output.writePreOpen(token); + case PRE_CLOSE_TAG -> output.writePreClose(token); + case CODE_OPEN_TAG -> output.writeCodeOpen(token); + case CODE_CLOSE_TAG -> output.writeCodeClose(token); + case TABLE_OPEN_TAG -> output.writeTableOpen(token); + case TABLE_CLOSE_TAG -> output.writeTableClose(token); + case MOE_BEGIN_STRIP_COMMENT -> output.requestMoeBeginStripComment(token); + case MOE_END_STRIP_COMMENT -> output.writeMoeEndStripComment(token); + case HTML_COMMENT -> output.writeHtmlComment(token); + case BR_TAG -> output.writeBr(standardizeBrToken(token)); + case WHITESPACE -> output.requestWhitespace(); + case FORCED_NEWLINE -> output.writeLineBreakNoAutoIndent(); + case LITERAL -> output.writeLiteral(token); + case PARAGRAPH_CLOSE_TAG, LIST_ITEM_CLOSE_TAG, OPTIONAL_LINE_BREAK -> {} } } throw new AssertionError(); @@ -163,17 +118,31 @@ private static Token standardize(Token token, Token standardToken) { * Returns the given string or a one-line version of it (e.g., "∕✱✱ Tests for foos. ✱∕") if it * fits on one line. */ - private static String makeSingleLineIfPossible( - int blockIndent, String input, JavaFormatterOptions options) { - int oneLinerContentLength = options.maxLineLength() - "/** */".length() - blockIndent; + private static String makeSingleLineIfPossible(int blockIndent, String input) { 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/JavadocLexer.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java index b605af85f..d40f34c6b 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java @@ -42,6 +42,8 @@ import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_CLOSE_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_OPEN_TAG; +import static com.google.googlejavaformat.java.javadoc.Token.Type.SNIPPET_BEGIN; +import static com.google.googlejavaformat.java.javadoc.Token.Type.SNIPPET_END; import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_CLOSE_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_OPEN_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.WHITESPACE; @@ -97,6 +99,7 @@ private static String stripJavadocBeginAndEnd(String input) { private final NestingCounter preDepth = new NestingCounter(); private final NestingCounter codeDepth = new NestingCounter(); private final NestingCounter tableDepth = new NestingCounter(); + private boolean outerInlineTagIsSnippet; private boolean somethingSinceNewline; private JavadocLexer(CharStream input) { @@ -158,13 +161,26 @@ private Type consumeToken() throws LexException { } somethingSinceNewline = true; - if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) { + if (input.tryConsumeRegex(SNIPPET_TAG_OPEN_PATTERN)) { + if (braceDepth.value() == 0) { + braceDepth.increment(); + outerInlineTagIsSnippet = true; + return SNIPPET_BEGIN; + } + braceDepth.increment(); + return LITERAL; + } else if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) { braceDepth.increment(); return LITERAL; } else if (input.tryConsume("{")) { braceDepth.incrementIfPositive(); return LITERAL; } else if (input.tryConsume("}")) { + if (outerInlineTagIsSnippet && braceDepth.value() == 1) { + braceDepth.decrementIfPositive(); + outerInlineTagIsSnippet = false; + return SNIPPET_END; + } braceDepth.decrementIfPositive(); return LITERAL; } @@ -239,7 +255,10 @@ private Type consumeToken() throws LexException { } private boolean preserveExistingFormatting() { - return preDepth.isPositive() || tableDepth.isPositive() || codeDepth.isPositive(); + return preDepth.isPositive() + || tableDepth.isPositive() + || codeDepth.isPositive() + || outerInlineTagIsSnippet; } private void checkMatchingTags() throws LexException { @@ -400,6 +419,7 @@ private static ImmutableList optionalizeSpacesAfterLinks(List inpu *

    Also trim leading and trailing blank lines, and move the trailing `}` to its own line. */ private static ImmutableList deindentPreCodeBlocks(List input) { + // TODO: b/323389829 - De-indent {@snippet ...} blocks, too. ImmutableList.Builder output = ImmutableList.builder(); for (PeekingIterator tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) { if (tokens.peek().getType() != PRE_OPEN_TAG) { @@ -507,9 +527,9 @@ private static boolean hasMultipleNewlines(String s) { // Match "@param " specially in case the is a

    or other HTML tag we treat specially. private static final Pattern FOOTER_TAG_PATTERN = compile("^@(param\\s+<\\w+>|[a-z]\\w*)"); private static final Pattern MOE_BEGIN_STRIP_COMMENT_PATTERN = - compile("^"); + compile("^"); private static final Pattern MOE_END_STRIP_COMMENT_PATTERN = - compile("^"); + compile("^"); private static final Pattern HTML_COMMENT_PATTERN = fullCommentPattern(); private static final Pattern PRE_OPEN_PATTERN = openTagPattern("pre"); private static final Pattern PRE_CLOSE_PATTERN = closeTagPattern("pre"); @@ -528,6 +548,7 @@ private static boolean hasMultipleNewlines(String s) { private static final Pattern BLOCKQUOTE_OPEN_PATTERN = openTagPattern("blockquote"); private static final Pattern BLOCKQUOTE_CLOSE_PATTERN = closeTagPattern("blockquote"); private static final Pattern BR_PATTERN = openTagPattern("br"); + private static final Pattern SNIPPET_TAG_OPEN_PATTERN = compile("^[{]@snippet\\b"); private static final Pattern INLINE_TAG_OPEN_PATTERN = compile("^[{]@\\w*"); /* * We exclude < so that we don't swallow following HTML tags. This lets us fix up "foo

    " (~400 @@ -539,7 +560,7 @@ private static boolean hasMultipleNewlines(String s) { * with matching only one character here. That would eliminate the need for the regex entirely. * That might be faster or slower than what we do now. */ - private static final Pattern LITERAL_PATTERN = compile("^.[^ \t\n@<{}*]*"); + private static final Pattern LITERAL_PATTERN = compile("^.[^ \t\n@<{}*]*", DOTALL); private static Pattern fullCommentPattern() { return compile("^", DOTALL); 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 cccec4f0a..5e6af1795 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,10 +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.JavaFormatterOptions; import com.google.googlejavaformat.java.javadoc.Token.Type; /** @@ -41,8 +39,8 @@ */ final class JavadocWriter { private final int blockIndent; - private final JavaFormatterOptions options; private final StringBuilder output = new StringBuilder(); + /** * Whether we are inside an {@code

  • } element, excluding the case in which the {@code
  • } * contains a {@code