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/.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 4914ad11e..000000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -language: java - -notifications: - email: - recipients: - - google-java-format-dev+ci@google.com - on_success: change - on_failure: always - -jdk: - - oraclejdk8 - - oraclejdk9 - - openjdk10 - - openjdk11 - - openjdk-ea - -matrix: - allow_failures: - - jdk: openjdk-ea - -# see https://github.com/travis-ci/travis-ci/issues/8408 -before_install: -- unset _JAVA_OPTIONS - -install: echo "The default Travis install script is being skipped!" - -# use travis-ci docker based infrastructure -sudo: false - -cache: - directories: - - $HOME/.m2 - -script: -- mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -- mvn test -B - -env: - global: - - secure: KkUX74NDDk95WR60zwN6x6pz49KAfR0zUu1thxl8Kke0+WVoIv1EBo7/e4ZXTdBKxlzQCX9Aa0OlIyUlhGJeuNIGtX16lcNyZNmKSacfrT68MpZqi+nAiYp8tnOyW/zuI+shSKHkGQOFq6c9KTtR9vG8kjr1Q9dNl/H5QjGaG1ZMiU/mGH9ompf+0nQTMDLKaEWV+SpKGjK5U1Zs2p08I9KKePbZoi9L2oAw5cH9wW8Q3pQJds6Rkwy9aecxRd4xmTza7Lb04dmByjqY8gsIjrTN0onOndBmLKTHiH5NVLKf0ilEVGiMQ1x4eCQolcRpGzxdTTKI0ahiWS59UABVoy1sXYqkIbZjpmMuGhHvbRir7YEXaG8LRUAxdWd9drJfvKQeBphQlIJKwajHSiMAdc9zisQg1UW75HSGKoPDHpzq+P7YBil2PUjk+5yUy5OytX6IebFT4KdeCO2ayu338yqb2t8q98elMoD5jwFVD0tpkLQ6xsYodClSGfMCVfP2zTkB7c4sHZV7tJS68CiNt7sCwz9CTNApFiSWMBxLKkKQ7VSBTy9bAn+phvW0u/maGsrRnehmsV3PVPtEsMlrqeMGwaPqIwx1l6otVQCnGRt3e8z3HoxY6AaBPaX0Z8lH2y+BxYhWTYzGhRxyyV666u/9yekTXmH53c7at7mau6Q= - - secure: VWnZcPA4esdaMJgh0Mui7K5O++AGZY3AYswufd0UUbAmnK60O6cDBOSelnr7hImDgZ09L2RWMXIVCt4b+UFXoIhqrvZKVitUXPldS6uNJeGT9p6quFf36o8Wf0ppKWnPd66AY6ECnE75Ujn1Maw899kb3zY2SvIvzA7HlXqtmowHCVGoJ4ou6LQxJpVEJ4hjvS2gQMF9W31uOzRzMI1JhdZioYmqe6eq9sGmRZZiYON7jBqX8f4XW0tTZoK+dVRNwYQcwyqcvQpxeI15VWDq5cqjBw3ps5XSEYTNIFUXREnEEi+vLdCuw/YRZp1ij7LiQKp6bcb2KROXaWii4VpUNWxIAflm4Nvn/8pa/3CUwqIbxTSAL+Qkb2iEzuYuNPGLr72mQgGEnlSpaqzUx0miwjJ41x3Q8mf72ihIME7YQGMDJL7TA7/GjXFeSxroPk65tbssAGmbjwGGJX67NHUzeQPW2QPA2cohCHyopKB9GqhKgKwKjenkCUaBGCaZReZz9XkSkHTXlxxSakMTmgJnA9To9d2lPOy0nppUvrd/0uAbPuxxCZqXElRvOvHKzpV1ZpKpqSxvjh63mCQRTi2rFiPn8uFaajai9mHaPoGmNwQwIUbAviNqifuIEPpc6cOuyV0MWJwdFLo1SKamJya/MwQz+IwXuY2TX7Fmv9HovdM= - -after_success: -- util/publish-snapshot-on-commit.sh diff --git a/README.md b/README.md index 72d881944..a5c400122 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,34 @@ ## 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.7-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 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). @@ -35,50 +50,91 @@ is available from the plugin repository. To install it, go to your IDE's settings and select the `Plugins` category. Click the `Marketplace` tab, search for the `google-java-format` plugin, and click the `Install` button. -The plugin will be disabled by default. To enable it in the current project, go -to `File→Settings...→google-java-format Settings` (or `IntelliJ -IDEA→Preferences...→Other Settings→google-java-format Settings` on macOS) and -check the `Enable google-java-format` checkbox. (A notification will be -presented when you first open a project offering to do this for you.) +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. -To enable it by default in new projects, use `File→Other Settings→Default -Settings...`. +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". -When enabled, it will replace the normal `Reformat Code` action, which can be -triggered from the `Code` menu or with the Ctrl-Alt-L (by default) keyboard -shortcut. +When enabled, it will replace the normal `Reformat Code` and `Optimize Imports` +actions. -The import ordering is not handled by this plugin, unfortunately. To fix the -import order, download the -[IntelliJ Java Google Style file](https://raw.githubusercontent.com/google/styleguide/gh-pages/intellij-java-google-style.xml) -and import it into File→Settings→Editor→Code Style. +#### 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.6/google-java-format-eclipse-plugin_1.6.0.jar) -can be downloaded from the releases page. Drop it into the Eclipse +The latest version of the `google-java-format` Eclipse plugin can be downloaded +from the [releases page](https://github.com/google/google-java-format/releases). +Drop it into the Eclipse [drop-ins folder](http://help.eclipse.org/neon/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Freference%2Fmisc%2Fp2_dropins_format.html) to activate the plugin. -The plugin adds a `google-java-format` formatter implementation that can be -configured in `Window > Preferences > Java > Code Style > Formatter > Formatter -Implementation`. +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". + +#### 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/master/plugin-gradle#applying-to-java-source-google-java-format): + * [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 - * [coveo/fmt-maven-plugin](https://github.com/coveo/fmt-maven-plugin) + * [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. -* [maltzj/google-style-precommit-hook](https://github.com/maltzj/google-style-precommit-hook): - A pre-commit (pre-commit.com) hook that will automatically run GJF whenever - you commit code to your repository +* 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 @@ -86,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.7 + ${google-java-format.version} ``` @@ -100,7 +169,7 @@ configuration. ```groovy dependencies { - compile 'com.google.googlejavaformat:google-java-format:1.7' + implementation 'com.google.googlejavaformat:google-java-format:$googleJavaFormatVersion' } ``` 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 e85174f22..3b106cf71 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -22,7 +22,7 @@ com.google.googlejavaformat google-java-format-parent - 1.8-SNAPSHOT + HEAD-SNAPSHOT google-java-format @@ -39,15 +39,11 @@ com.google.guava guava - - com.google.errorprone - javac-shaded - - 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 + @@ -70,10 +75,15 @@ com.google.truth truth + + com.google.truth.extensions + truth-java8-extension + test + com.google.testing.compile compile-testing - 0.15 + 0.19 test @@ -83,14 +93,22 @@ maven-javadoc-plugin + 17 UTF-8 UTF-8 UTF-8 - https://google.github.io/guava/releases/${guava.version}/api/docs/ - https://javadoc.io/page/com.google.code.findbugs/jsr305/${jsr305.version}/ - https://docs.oracle.com/javase/8/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 + @@ -105,7 +123,7 @@ org.apache.maven.plugins maven-shade-plugin - 2.4.3 + 3.2.4 shade-all-deps @@ -151,51 +169,6 @@ - - maven-resources-plugin - 3.0.1 - - - copy-resources - package - - copy-resources - - - ../eclipse_plugin/lib - - - target - ${project.artifactId}-${project.version}.jar - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 2.10 - - - copy-dependencies - package - - copy-dependencies - - - ../eclipse_plugin/lib - true - true - true - org.eclipse.jdt.core - compile - provided - - - - com.google.code.maven-replacer-plugin replacer @@ -222,7 +195,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.0.0 + 3.3.0 add-source @@ -238,6 +211,68 @@ + + 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 e663c9603..6414a3fb1 100644 --- a/core/src/main/java/com/google/googlejavaformat/Doc.java +++ b/core/src/main/java/com/google/googlejavaformat/Doc.java @@ -15,8 +15,12 @@ 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.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; @@ -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( @@ -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 @@ -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,7 +718,7 @@ 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()) { @@ -726,10 +728,10 @@ float computeWidth() { // 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 @@ -740,7 +742,7 @@ String computeFlat() { if (tok.isSlashSlashComment() && !tok.getOriginalText().startsWith("// ")) { return "// " + tok.getOriginalText().substring("//".length()); } - return tok.getOriginalText(); + 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/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/Newlines.java b/core/src/main/java/com/google/googlejavaformat/Newlines.java index dbb82d3c5..6a1241c36 100644 --- a/core/src/main/java/com/google/googlejavaformat/Newlines.java +++ b/core/src/main/java/com/google/googlejavaformat/Newlines.java @@ -73,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"; @@ -135,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 e8e100f3f..7f0fabb34 100644 --- a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java +++ b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java @@ -14,7 +14,12 @@ package com.google.googlejavaformat; +import static java.lang.Math.max; +import static java.lang.Math.min; + import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -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; @@ -154,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) { @@ -278,6 +283,28 @@ public final Optional peekToken(int skip) { : 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(); + 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(); + } + /** * Emit an optional token iff it exists on the input. This is used to emit tokens whose existence * has been lost in the AST. @@ -289,7 +316,7 @@ public final void guessToken(String token) { token, Doc.Token.RealOrImaginary.IMAGINARY, ZERO, - /* breakAndIndentTrailingComment= */ Optional.empty()); + /* breakAndIndentTrailingComment= */ Optional.empty()); } public final void token( @@ -332,7 +359,7 @@ public final void op(String op) { op.substring(i, i + 1), Doc.Token.RealOrImaginary.REAL, ZERO, - /* breakAndIndentTrailingComment= */ Optional.empty()); + /* breakAndIndentTrailingComment= */ Optional.empty()); } } @@ -400,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, /* optionalTag= */ Optional.empty()); + breakOp(fillMode, flat, plusIndent, /* optionalTag= */ Optional.empty()); } /** 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 80f1579bb..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,150 +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 sortImports; - private final boolean removeUnusedImports; - private final boolean dryRun; - private final boolean setExitIfChanged; - private final Optional assumeFilename; - private final boolean reflowLongStrings; - - 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) { - this.files = files; - this.inPlace = inPlace; - this.lines = lines; - this.offsets = offsets; - this.lengths = lengths; - this.aosp = aosp; - this.version = version; - this.help = help; - this.stdin = stdin; - this.fixImportsOnly = fixImportsOnly; - this.sortImports = sortImports; - this.removeUnusedImports = removeUnusedImports; - this.dryRun = dryRun; - this.setExitIfChanged = setExitIfChanged; - this.assumeFilename = assumeFilename; - this.reflowLongStrings = reflowLongStrings; - } - - /** 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; - } - - /** Sort imports. */ - boolean sortImports() { - return sortImports; - } - - /** Remove unused imports. */ - boolean removeUnusedImports() { - return removeUnusedImports; - } - - /** - * Print the paths of the files whose contents would change if the formatter were run normally. - */ - boolean dryRun() { - return dryRun; - } - - /** Return exit code 1 if there are any formatting changes. */ - boolean setExitIfChanged() { - return setExitIfChanged; - } - - /** Return the name to use for diagnostics when formatting standard input. */ - Optional assumeFilename() { - return assumeFilename; - } - - boolean reflowLongStrings() { - return reflowLongStrings; - } +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() { @@ -165,124 +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 { + @AutoBuilder + interface 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 sortImports = true; - private boolean removeUnusedImports = true; - private boolean dryRun = false; - private boolean setExitIfChanged = false; - private Optional assumeFilename = Optional.empty(); - private boolean reflowLongStrings = true; + ImmutableList.Builder filesBuilder(); - ImmutableList.Builder filesBuilder() { - return files; - } + Builder inPlace(boolean inPlace); - Builder inPlace(boolean inPlace) { - this.inPlace = inPlace; - return this; - } + Builder lines(ImmutableRangeSet lines); - ImmutableRangeSet.Builder linesBuilder() { - return lines; - } + ImmutableList.Builder offsetsBuilder(); - Builder addOffset(Integer offset) { - offsets.add(offset); + @CanIgnoreReturnValue + default Builder addOffset(Integer offset) { + offsetsBuilder().add(offset); return this; } - Builder addLength(Integer length) { - lengths.add(length); - return this; - } + ImmutableList.Builder lengthsBuilder(); - Builder aosp(boolean aosp) { - this.aosp = aosp; + @CanIgnoreReturnValue + default Builder addLength(Integer length) { + lengthsBuilder().add(length); return this; } - Builder version(boolean version) { - this.version = version; - return this; - } + Builder aosp(boolean aosp); - Builder help(boolean help) { - this.help = help; - return this; - } + Builder version(boolean version); - Builder stdin(boolean stdin) { - this.stdin = stdin; - return this; - } + Builder help(boolean help); - Builder fixImportsOnly(boolean fixImportsOnly) { - this.fixImportsOnly = fixImportsOnly; - return this; - } + Builder stdin(boolean stdin); - Builder sortImports(boolean sortImports) { - this.sortImports = sortImports; - return this; - } + Builder fixImportsOnly(boolean fixImportsOnly); - Builder removeUnusedImports(boolean removeUnusedImports) { - this.removeUnusedImports = removeUnusedImports; - return this; - } + Builder sortImports(boolean sortImports); - Builder dryRun(boolean dryRun) { - this.dryRun = dryRun; - return this; - } + Builder removeUnusedImports(boolean removeUnusedImports); - Builder setExitIfChanged(boolean setExitIfChanged) { - this.setExitIfChanged = setExitIfChanged; - return this; - } + Builder dryRun(boolean dryRun); - Builder assumeFilename(String assumeFilename) { - this.assumeFilename = Optional.of(assumeFilename); - return this; - } + Builder setExitIfChanged(boolean setExitIfChanged); - Builder reflowLongStrings(boolean reflowLongStrings) { - this.reflowLongStrings = reflowLongStrings; - return this; - } + Builder assumeFilename(String assumeFilename); - CommandLineOptions build() { - return new CommandLineOptions( - files.build(), - inPlace, - lines.build(), - offsets.build(), - lengths.build(), - aosp, - version, - help, - stdin, - fixImportsOnly, - sortImports, - removeUnusedImports, - dryRun, - setExitIfChanged, - assumeFilename, - reflowLongStrings); - } + 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 5abc54a6e..f5ce703e8 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java +++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java @@ -20,6 +20,8 @@ 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; @@ -43,6 +45,9 @@ static CommandLineOptions parse(Iterable options) { 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("-")) { @@ -54,78 +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 "--skip-sorting-imports": - optionsBuilder.sortImports(false); - break; - case "--skip-removing-unused-imports": - optionsBuilder.removeUnusedImports(false); - break; - case "--skip-reflowing-long-strings": - optionsBuilder.reflowLongStrings(false); - break; - case "-": - optionsBuilder.stdin(true); - break; - case "-n": - case "--dry-run": - optionsBuilder.dryRun(true); - break; - case "--set-exit-if-changed": - optionsBuilder.setExitIfChanged(true); - break; - case "-assume-filename": - case "--assume-filename": - optionsBuilder.assumeFilename(getValue(flag, it, value)); - break; - default: - throw new IllegalArgumentException("unexpected flag: " + flag); + 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(); } @@ -154,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)); } @@ -166,17 +128,18 @@ 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); + }; } /** 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 a18db691e..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. @@ -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; - } + 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 89cb339a1..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,40 +14,73 @@ 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 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. */ -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); - if (parameters.reflowLongStrings()) { - formatted = StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, 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); } - return formatted; } private String fixImports(String input) throws FormatterException { 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 a6f931109..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,10 +14,8 @@ package com.google.googlejavaformat.java; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; @@ -31,26 +29,14 @@ import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.Op; import com.google.googlejavaformat.OpsBuilder; -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.List; -import org.openjdk.javax.tools.Diagnostic; -import org.openjdk.javax.tools.DiagnosticCollector; -import org.openjdk.javax.tools.DiagnosticListener; -import org.openjdk.javax.tools.JavaFileObject; -import org.openjdk.javax.tools.SimpleJavaFileObject; -import org.openjdk.javax.tools.StandardLocation; -import org.openjdk.tools.javac.file.JavacFileManager; -import org.openjdk.tools.javac.main.Option; -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; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; /** * This is google-java-format, a new Java formatter that follows the Google Java Style Guide quite @@ -113,48 +99,19 @@ public Formatter(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"); - // TODO(cushon): this should default to the latest supported source level, remove this after - // backing out - // https://github.com/google/error-prone-javac/commit/c97f34ddd2308302587ce2de6d0c984836ea5b9f - Options.instance(context).put(Option.SOURCE, "9"); - 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(), Formatter::errorDiagnostic); - if (!Iterables.isEmpty(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(); @@ -167,15 +124,9 @@ static boolean errorDiagnostic(Diagnostic input) { if (input.getKind() != Diagnostic.Kind.ERROR) { return false; } - switch (input.getCode()) { - case "compiler.err.invalid.meth.decl.ret.type.req": - // accept constructor-like method declarations that don't match the name of their - // enclosing class - return false; - default: - break; - } - return true; + // 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"); } /** @@ -219,7 +170,7 @@ 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); + formatted = StringWrapper.wrap(formatted, this); return formatted; } @@ -252,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 972b8ce06..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,19 +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.common.collect.Iterables; import com.google.googlejavaformat.FormatterDiagnostic; import java.util.List; -import org.openjdk.javax.tools.Diagnostic; -import org.openjdk.javax.tools.JavaFileObject; +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)); @@ -46,12 +49,33 @@ public List diagnostics() { } public static FormatterException fromJavacDiagnostics( - Iterable> diagnostics) { - return new FormatterException(Iterables.transform(diagnostics, d -> toFormatterDiagnostic(d))); + 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/ImportOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java index a364c15e8..375a3289b 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java @@ -25,13 +25,13 @@ import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.java.JavaFormatterOptions.Style; import com.google.googlejavaformat.java.JavaInput.Tok; +import com.sun.tools.javac.parser.Tokens.TokenKind; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.BiFunction; import java.util.stream.Stream; -import org.openjdk.tools.javac.parser.Tokens.TokenKind; /** Orders imports in Java source code. */ public class ImportOrderer { @@ -122,18 +122,24 @@ private String reorderImports() throws FormatterException { /** * 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::isStatic, trueFirst()).thenComparing(Import::imported); + 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::isStatic, trueFirst()) + Comparator.comparing(Import::importType) .thenComparing(Import::isAndroid, trueFirst()) .thenComparing(Import::isThirdParty, trueFirst()) .thenComparing(Import::isJava, trueFirst()) @@ -144,7 +150,7 @@ private String reorderImports() throws FormatterException { * Import}s based on Google style. */ private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) { - return prev.isStatic() && !curr.isStatic(); + return !prev.importType().equals(curr.importType()); } /** @@ -152,7 +158,7 @@ private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) { * Import}s based on AOSP style. */ private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) { - if (prev.isStatic() && !curr.isStatic()) { + if (!prev.importType().equals(curr.importType())) { return true; } // insert blank line between "com.android" from "com.anythingelse" @@ -183,16 +189,22 @@ private ImportOrderer(String text, ImmutableList toks, Style style) { } } + enum ImportType { + STATIC, + MODULE, + NORMAL + } + /** An import statement. */ class Import { private final String imported; - private final boolean isStatic; 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; } /** The name being imported, for example {@code java.util.List}. */ @@ -200,9 +212,9 @@ String imported() { return imported; } - /** True if this is {@code import static}. */ - boolean isStatic() { - return isStatic; + /** Returns the {@link ImportType}. */ + ImportType importType() { + return importType; } /** The top-level package of the import. */ @@ -218,13 +230,10 @@ boolean isAndroid() { /** True if this is a Java import per AOSP style. */ boolean isJava() { - switch (topLevel()) { - case "java": - case "javax": - return true; - default: - return false; - } + return switch (topLevel()) { + case "java", "javax" -> true; + default -> false; + }; } /** @@ -248,8 +257,10 @@ public boolean isThirdParty() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("import "); - if (isStatic()) { - sb.append("static "); + switch (importType) { + case STATIC -> sb.append("static "); + case MODULE -> sb.append("module "); + case NORMAL -> {} } sb.append(imported()).append(';'); if (trailing().trim().isEmpty()) { @@ -304,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++; @@ -346,7 +362,11 @@ private ImportsAndIndex scanImports(int i) throws FormatterException { 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. 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 b29bc0115..9526b892c 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java @@ -30,9 +30,11 @@ public final class JavaCommentsHelper implements CommentsHelper { private final String lineSeparator; + private final JavaFormatterOptions options; public JavaCommentsHelper(String lineSeparator, JavaFormatterOptions options) { this.lineSeparator = lineSeparator; + this.options = options; } @Override @@ -41,21 +43,27 @@ public String rewrite(Tok tok, int maxWidth, int column0) { return tok.getOriginalText(); } String text = tok.getOriginalText(); - if (tok.isJavadocComment()) { + 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 @@ -177,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 950eca646..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,7 +28,8 @@ * preferences, and in fact it would work directly against our primary goals. */ @Immutable -public class JavaFormatterOptions { +@AutoValue +public abstract class JavaFormatterOptions { public enum Style { /** The default Google Java Style configuration. */ @@ -47,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(); + return style().indentationMultiplier(); } + public abstract boolean formatJavadoc(); + + public abstract boolean reorderModifiers(); + /** Returns the code style. */ - public Style style() { - return style; - } + public abstract Style style(); /** Returns the default formatting options. */ public static JavaFormatterOptions defaultOptions() { @@ -70,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 bd3286d2b..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,24 +33,30 @@ 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; import java.util.Iterator; import java.util.List; -import org.openjdk.javax.tools.Diagnostic; -import org.openjdk.javax.tools.DiagnosticCollector; -import org.openjdk.javax.tools.DiagnosticListener; -import org.openjdk.javax.tools.JavaFileObject; -import org.openjdk.javax.tools.JavaFileObject.Kind; -import org.openjdk.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 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.jspecify.annotations.Nullable; /** {@code JavaInput} extends {@link Input} to represent a Java input document. */ public final class JavaInput extends Input { @@ -154,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 @@ -308,7 +316,7 @@ private static ImmutableMap makePositionToColumnMap(List for (Tok tok : toks) { builder.put(tok.getPosition(), tok.getColumn()); } - return builder.build(); + return builder.buildOrThrow(); } /** @@ -345,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); @@ -356,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; @@ -465,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) { @@ -510,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))) { @@ -553,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; } @@ -590,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]; } @@ -658,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 bd462de39..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,39 +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 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 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 com.google.auto.value.AutoOneOf; +import com.google.auto.value.AutoValue; import com.google.common.base.MoreObjects; import com.google.common.base.Predicate; import com.google.common.base.Throwables; import com.google.common.base.Verify; import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Multiset; import com.google.common.collect.PeekingIterator; +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; import com.google.common.collect.Streams; +import com.google.common.collect.TreeRangeSet; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.googlejavaformat.CloseOp; import com.google.googlejavaformat.Doc; import com.google.googlejavaformat.Doc.FillMode; 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; @@ -69,11 +80,90 @@ 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.Collection; -import java.util.Collections; +import java.util.Comparator; import java.util.Deque; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -81,83 +171,17 @@ import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.openjdk.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.DirectiveTree; -import org.openjdk.source.tree.DoWhileLoopTree; -import org.openjdk.source.tree.EmptyStatementTree; -import org.openjdk.source.tree.EnhancedForLoopTree; -import org.openjdk.source.tree.ExportsTree; -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.ModuleTree; -import org.openjdk.source.tree.NewArrayTree; -import org.openjdk.source.tree.NewClassTree; -import org.openjdk.source.tree.OpensTree; -import org.openjdk.source.tree.ParameterizedTypeTree; -import org.openjdk.source.tree.ParenthesizedTree; -import org.openjdk.source.tree.PrimitiveTypeTree; -import org.openjdk.source.tree.ProvidesTree; -import org.openjdk.source.tree.RequiresTree; -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.UsesTree; -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 javax.lang.model.element.Name; +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; @@ -167,7 +191,7 @@ boolean isVertical() { } /** Whether to break or not. */ - enum BreakOrNot { + protected enum BreakOrNot { YES, NO; @@ -177,7 +201,7 @@ boolean isYes() { } /** Whether to collapse empty blocks. */ - enum CollapseEmptyOrNot { + protected enum CollapseEmptyOrNot { YES, NO; @@ -191,7 +215,7 @@ boolean isYes() { } /** Whether to allow leading blank lines in blocks. */ - enum AllowLeadingBlankLine { + protected enum AllowLeadingBlankLine { YES, NO; @@ -201,7 +225,7 @@ static AllowLeadingBlankLine valueOf(boolean b) { } /** Whether to allow trailing blank lines in blocks. */ - enum AllowTrailingBlankLine { + protected enum AllowTrailingBlankLine { YES, NO; @@ -211,7 +235,7 @@ static AllowTrailingBlankLine valueOf(boolean b) { } /** Whether to include braces. */ - enum BracesOrNot { + protected enum BracesOrNot { YES, NO; @@ -259,7 +283,7 @@ boolean isYes() { } /** Whether these declarations are the first in the block. */ - enum FirstDeclarationsOrNot { + protected enum FirstDeclarationsOrNot { YES, NO; @@ -268,14 +292,34 @@ boolean isYes() { } } - private final OpsBuilder builder; + // TODO(cushon): generalize this + private static final ImmutableMultimap TYPE_ANNOTATIONS = typeAnnotations(); - 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 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 final Set typeAnnotationSimpleNames = new HashSet<>(); private static final ImmutableList breakList(Optional breakTag) { return ImmutableList.of(Doc.Break.make(Doc.FillMode.UNIFIED, " ", ZERO, breakTag)); @@ -292,8 +336,6 @@ private static final ImmutableList forceBreakList(Optional breakTa 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. @@ -323,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 { @@ -340,16 +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()) { @@ -358,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()) { @@ -367,22 +415,35 @@ 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(); @@ -392,32 +453,35 @@ private void dropEmptyDeclarations() { } } + // 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, - /* declarationAnnotationBreak= */ Optional.empty()); + typeDeclarationModifiers(node.getModifiers()); builder.open(ZERO); token("@"); token("interface"); @@ -454,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); @@ -486,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); @@ -503,7 +567,7 @@ public boolean visitArrayInitializer(List expressions) } builder.guessToken(","); builder.close(); - first = false; + afterFirstToken = true; } builder.breakOp(minusTwo); builder.close(); @@ -534,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) { @@ -578,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(); @@ -676,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.empty())); + node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.empty()); + visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES); } scan(node.getIdentifier(), null); addArguments(node.getArguments(), plusFour); @@ -799,10 +864,7 @@ private void visitEnumConstantDeclaration(VariableTree enumConstant) { public boolean visitEnumDeclaration(ClassTree node) { sync(node); builder.open(ZERO); - visitAndBreakModifiers( - node.getModifiers(), - Direction.VERTICAL, - /* declarationAnnotationBreak= */ Optional.empty()); + typeDeclarationModifiers(node.getModifiers()); builder.open(plusFour); token("enum"); builder.breakOp(" "); @@ -816,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(); @@ -867,16 +929,16 @@ public boolean visitEnumDeclaration(ClassTree node) { 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().orElse("").equals(",")) { token(","); @@ -902,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; @@ -959,18 +1077,19 @@ void visitVariables( Optional.ofNullable(fragment.getInitializer()), Optional.of(";"), /* receiverExpression= */ Optional.empty(), - Optional.ofNullable(variableFragmentDims(true, 0, fragment.getType()))); + 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); @@ -997,15 +1116,15 @@ public Void visitForLoop(ForLoopTree node, Void unused) { 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(); @@ -1062,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 { @@ -1090,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) { @@ -1111,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(); @@ -1125,6 +1249,42 @@ 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); @@ -1157,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; @@ -1167,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; @@ -1202,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(")"); @@ -1251,14 +1418,14 @@ public Void visitAnnotation(AnnotationTree node, Void unused) { 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(), JavaInputAstVisitor::isArrayValue); for (ExpressionTree argument : node.getArguments()) { - if (!first) { + if (afterFirstToken) { token(","); if (hasArrayInitializer) { builder.forcedBreak(); @@ -1271,7 +1438,7 @@ public Void visitAnnotation(AnnotationTree node, Void unused) { } else { scan(argument, null); } - first = false; + afterFirstToken = true; } token(")"); builder.close(); @@ -1331,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++) { @@ -1347,17 +1524,31 @@ public Void visitMethod(MethodTree node, Void unused) { } } } - builder.addAll( + List typeAnnotations = visitModifiers( - annotations, Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.empty())); + 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); @@ -1365,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); @@ -1406,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(); } @@ -1416,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.ofNullable(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); } @@ -1585,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 signed 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(); } @@ -1597,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()) { @@ -1627,14 +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(","); builder.breakOp(" "); } scan(typeArgument, null); - first = false; + afterFirstToken = true; } builder.close(); builder.close(); @@ -1676,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; } @@ -1695,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; } @@ -1777,46 +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(); + } + + 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.open(plusTwo); - visitStatements(node.getStatements()); - builder.close(); return null; } @Override public Void visitSwitch(SwitchTree node, Void unused) { sync(node); + visitSwitch(node.getExpression(), node.getCases()); + return null; + } + + protected void visitSwitch(ExpressionTree expression, List 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 @@ -1854,9 +2121,9 @@ 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(); } if (resource instanceof VariableTree) { @@ -1881,7 +2148,7 @@ public Void visitTry(TryTree node, Void unused) { token(";"); builder.space(); } - first = false; + afterFirstToken = true; } if (builder.peekToken().equals(Optional.of(";"))) { token(";"); @@ -1920,14 +2187,11 @@ public Void visitTry(TryTree node, Void unused) { public void visitClassDeclaration(ClassTree node) { sync(node); - List breaks = - visitModifiers( - node.getModifiers(), - Direction.VERTICAL, - /* declarationAnnotationBreak= */ Optional.empty()); + 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()); @@ -1939,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(" "); @@ -1947,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) { @@ -1985,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(); @@ -2043,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(" "); @@ -2063,8 +2315,16 @@ void visitAnnotations( } } + void verticalAnnotations(List annotations) { + for (AnnotationTree annotation : annotations) { + builder.forcedBreak(); + scan(annotation, null); + builder.forcedBreak(); + } + } + /** Helper method for blocks. */ - private void visitBlock( + protected void visitBlock( BlockTree node, CollapseEmptyOrNot collapseEmptyOrNot, AllowLeadingBlankLine allowLeadingBlankLine, @@ -2114,30 +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; + 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( @@ -2150,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 @@ -2164,37 +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.getAnnotations(), annotationsDirection, declarationAnnotationBreak); + 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(); @@ -2202,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) { + afterFirstToken = false; + while (!declarationModifiers.isEmpty()) { + if (afterFirstToken) { builder.addAll(breakFillList(Optional.empty())); } - if (nextIsModifier()) { - token(builder.peekToken().get()); - } else { - scan(annotations.removeFirst(), null); - lastWasAnnotation = true; - } - first = false; + formatAnnotationOrModifier(declarationModifiers); + afterFirstToken = true; } builder.close(); - return breakFillList(Optional.empty()); - } - - boolean nextIsModifier() { - switch (builder.peekToken().get()) { - case "public": - case "protected": - case "private": - case "abstract": - case "static": - case "final": - case "transient": - case "volatile": - case "synchronized": - case "native": - case "strictfp": - case "default": - return true; - default: - return false; + 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 @@ -2289,14 +2718,14 @@ private void visitUnionType(VariableTree declaration) { 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); } @@ -2339,15 +2768,15 @@ 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, @@ -2360,11 +2789,11 @@ private void visitFormals( !parameters.isEmpty() ? Optional.of(",") : Optional.empty(), Optional.of(receiver.get().getNameExpression()), /* typeWithDims= */ Optional.empty()); - first = false; + afterFirstToken = true; } for (int i = 0; i < parameters.size(); i++) { VariableTree parameter = parameters.get(i); - if (!first) { + if (afterFirstToken) { builder.breakOp(" "); } visitToDeclare( @@ -2374,7 +2803,7 @@ private void visitFormals( /* initializer= */ Optional.empty(), "=", i < parameters.size() - 1 ? Optional.of(",") : /* a= */ Optional.empty()); - first = false; + afterFirstToken = true; } builder.close(); } @@ -2383,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; } } @@ -2454,14 +2883,14 @@ private void visitDirective( builder.space(); token(separator); builder.forcedBreak(); - boolean first = true; + boolean afterFirstToken = false; for (ExpressionTree item : items) { - if (!first) { + if (afterFirstToken) { token(","); builder.forcedBreak(); } scan(item, null); - first = false; + afterFirstToken = true; } token(";"); builder.close(); @@ -2524,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; } } @@ -2542,34 +2971,44 @@ private void visitToDeclare( String equals, Optional trailing) { sync(node); + 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()), - node.getType(), + type, node.getName(), "", equals, initializer, trailing, /* receiverExpression= */ Optional.empty(), - /* typeWithDims= */ 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(); @@ -2596,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); @@ -2685,12 +3122,8 @@ void visitDot(ExpressionTree node0) { if (prefixes.isEmpty() && items.get(0) instanceof IdentifierTree) { switch (((IdentifierTree) items.get(0)).getName().toString()) { - case "this": - case "super": - prefixes.add(1); - break; - default: - break; + case "this", "super" -> prefixes.add(1); + default -> {} } } @@ -2839,24 +3272,23 @@ private void visitDotWithPrefix( } /** 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; @@ -2868,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); } } @@ -2896,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; } @@ -2913,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); } @@ -2944,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; @@ -2961,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(">"); @@ -2989,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(); } @@ -3003,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)) { @@ -3027,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(); } @@ -3078,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(); @@ -3190,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) { @@ -3202,14 +3630,15 @@ 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, @@ -3234,27 +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(); @@ -3262,13 +3701,15 @@ int declareOne( maybeAddDims(dims); builder.close(); baseDims = totalDims - dims.size(); + } else if (isVar) { + token("var"); } else { scan(type, null); } } builder.close(); - if (type != null) { + if (hasType) { builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO, Optional.of(typeBreak)); } @@ -3278,7 +3719,7 @@ int declareOne( if (receiverExpression.isPresent()) { scan(receiverExpression.get(), null); } else { - visit(name); + variableName(name); } builder.op(op); } @@ -3321,7 +3762,15 @@ int declareOne( return baseDims; } - private void maybeAddDims(Deque> annotations) { + protected void variableName(Name name) { + if (name.isEmpty()) { + token("_"); + } else { + visit(name); + } + } + + private void maybeAddDims(Deque> annotations) { maybeAddDims(new ArrayDeque<>(), annotations); } @@ -3338,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 { @@ -3366,8 +3815,8 @@ private void maybeAddDims( } token("]"); lastWasAnnotation = false; - break; - case ".": + } + case "." -> { if (!builder.peekToken().get().equals(".") || !builder.peekToken(1).get().equals(".")) { return; } @@ -3378,9 +3827,10 @@ private void maybeAddDims( } builder.op("..."); lastWasAnnotation = false; - break; - default: + } + default -> { return; + } } } } @@ -3396,22 +3846,23 @@ private void declareMany(List fragments, Direction annotationDirec 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) { @@ -3423,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(";"); @@ -3434,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()) { @@ -3442,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(); } @@ -3492,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 @@ -3499,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); @@ -3544,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; @@ -3579,7 +4058,7 @@ private Direction fieldAnnotationDirection(ModifiersTree modifiers) { * * @param token the {@link String} to wrap in a {@link Doc.Token} */ - final void token(String token) { + protected final void token(String token) { builder.token( token, Doc.Token.RealOrImaginary.REAL, @@ -3593,7 +4072,7 @@ 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, @@ -3607,7 +4086,7 @@ final void tokenBreakTrailingComment(String token, Indent breakAndIndentTrailing token, Doc.Token.RealOrImaginary.REAL, ZERO, Optional.of(breakAndIndentTrailingComment)); } - private void markForPartialFormat() { + protected void markForPartialFormat() { if (!inExpression()) { builder.markForPartialFormat(); } @@ -3619,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()); } @@ -3631,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 7b789977a..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 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; @@ -46,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(); @@ -55,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; @@ -88,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()) { @@ -109,7 +111,7 @@ public void append(String text, Range range) { * there's a blank line here and it's a comment. */ BlankLineWanted wanted = blankLines.getOrDefault(lastK, BlankLineWanted.NO); - if (isComment(text) ? sawNewlines : wanted.wanted().orElse(sawNewlines)) { + if ((sawNewlines && isComment(text)) || wanted.wanted().orElse(sawNewlines)) { ++newlinesPending; } } @@ -121,7 +123,7 @@ public void append(String text, Range range) { if (newlinesPending == 0) { ++newlinesPending; } - spacesPending = 0; + spacesPending = new StringBuilder(); } else { boolean rangesSet = false; int textN = text.length(); @@ -129,15 +131,18 @@ public void append(String text, Range range) { 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: @@ -150,9 +155,9 @@ public void append(String text, Range range) { 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()) { @@ -174,11 +179,11 @@ 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 (!CharMatcher.whitespace().matchesAllOf(lastLine)) { mutableLines.add(lastLine); @@ -257,8 +262,7 @@ public ImmutableList getFormatReplacements(RangeSet iRange } } - 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) { @@ -300,7 +304,7 @@ public ImmutableList getFormatReplacements(RangeSet iRange } else { if (newline == -1) { // If there wasn't a trailing newline in the input, indent the next line. - replacement.append(after.substring(0, idx)); + replacement.append(after, 0, idx); } break; } @@ -348,7 +352,7 @@ public static String applyReplacements(String input, List replaceme 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; } @@ -387,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 72f8bce3a..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. */ -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,8 +80,8 @@ 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; @@ -88,7 +89,7 @@ public static ImmutableList getTokens( 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))); } @@ -120,36 +121,68 @@ public static ImmutableList getTokens( 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; } /** @@ -158,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), @@ -168,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 2304e5e59..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,33 +14,38 @@ 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 versionString() { + static String versionString() { return "google-java-format: Version " + GoogleJavaFormatVersion.version(); } @@ -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); } /** @@ -96,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); @@ -106,53 +130,56 @@ 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); - inputs.put(path, input); - results.put( - path, executorService.submit(new FormatFileCallable(parameters, input, options))); + 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()); allOk = false; } } - for (Map.Entry> result : results.entrySet()) { - Path path = result.getKey(); - 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(path + ":" + diagnostic.toString()); - } - } else { - errWriter.println(path + ": error: " + e.getCause().getMessage()); - e.getCause().printStackTrace(errWriter); - } + errWriter.println("error: " + e.getCause().getMessage()); + e.getCause().printStackTrace(errWriter); allOk = false; continue; } - boolean changed = !formatted.equals(inputs.get(path)); + } + 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; } @@ -175,6 +202,10 @@ private int formatFiles(CommandLineOptions parameters, JavaFormatterOptions opti outWriter.write(formatted); } } + if (!MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(5))) { + errWriter.println("Failed to shut down ExecutorService"); + allOk = false; + } return allOk ? 0 : 1; } @@ -187,9 +218,14 @@ private int formatStdin(CommandLineOptions parameters, JavaFormatterOptions opti } String stdinFilename = parameters.assumeFilename().orElse(STDIN_FILENAME); boolean ok = true; - try { - String output = new FormatFileCallable(parameters, input, options).call(); - boolean changed = !input.equals(output); + 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; } @@ -200,12 +236,6 @@ private int formatStdin(CommandLineOptions parameters, JavaFormatterOptions opti } else { outWriter.write(output); } - } catch (FormatterException e) { - for (FormatterDiagnostic diagnostic : e.diagnostics()) { - errWriter.println(stdinFilename + ":" + diagnostic.toString()); - } - ok = false; - // TODO(cpovirk): Catch other types of exception (as we do in the formatFiles case). } return ok ? 0 : 1; } 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 239973226..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 org.openjdk.javax.lang.model.element.Modifier; -import org.openjdk.tools.javac.parser.Tokens.TokenKind; +import javax.lang.model.element.Modifier; +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), ImmutableList.of(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 fe2133a08..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,12 +16,12 @@ 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; @@ -29,42 +29,33 @@ import com.google.common.collect.TreeRangeMap; import com.google.common.collect.TreeRangeSet; 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 org.openjdk.javax.tools.Diagnostic; -import org.openjdk.javax.tools.DiagnosticCollector; -import org.openjdk.javax.tools.DiagnosticListener; -import org.openjdk.javax.tools.JavaFileObject; -import org.openjdk.javax.tools.SimpleJavaFileObject; -import org.openjdk.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.main.Option; -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 javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** * Removes unused imports from a source file. Imports that are only used in javadoc are also @@ -72,19 +63,6 @@ */ public class RemoveUnusedImports { - /** - * Configuration for javadoc-only imports. - * - * @deprecated This configuration is no longer supported and will be removed in the future. - */ - @Deprecated - 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.) @@ -128,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) { @@ -151,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; } @@ -159,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) { @@ -197,17 +202,8 @@ public Void visitIdentifier(IdentifierTree node, Void aVoid) { } } - /** @deprecated use {@link removeUnusedImports(String)} instead. */ - @Deprecated - public static String removeUnusedImports( - final String contents, JavadocOnlyImports javadocOnlyImports) throws FormatterException { - return removeUnusedImports(contents); - } - public static String removeUnusedImports(final String contents) throws FormatterException { Context context = new Context(); - // TODO(cushon): this should default to the latest supported source level, same as in Formatter - Options.instance(context).put(Option.SOURCE, "9"); JCCompilationUnit unit = parse(context, contents); if (unit == null) { // error handling is done during formatting @@ -221,34 +217,10 @@ public static String removeUnusedImports(final String contents) throws Formatter private static JCCompilationUnit parse(Context context, String javaInput) throws FormatterException { - 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::errorDiagnostic); - if (!Iterables.isEmpty(errorDiagnostics)) { + List> errorDiagnostics = new ArrayList<>(); + JCTree.JCCompilationUnit unit = + Trees.parse(context, errorDiagnostics, /* allowStringFolding= */ false, javaInput); + if (!errorDiagnostics.isEmpty()) { // error handling is done during formatting throw FormatterException.fromJavacDiagnostics(errorDiagnostics); } @@ -262,59 +234,46 @@ private static RangeMap buildReplacements( Set usedNames, 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, 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()).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, - JCImport importTree, + JCTree importTree, String simpleName) { - String qualifier = - ((JCFieldAccess) importTree.getQualifiedIdentifier()).getExpression().toString(); + 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; } @@ -327,6 +286,47 @@ private static boolean isUnused( 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 @@ -349,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 index c371a8abd..22110ad8a 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java @@ -14,21 +14,30 @@ package com.google.googlejavaformat.java; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Iterables.getLast; -import static java.nio.charset.StandardCharsets.UTF_8; +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.Iterables; import com.google.common.collect.Range; import com.google.common.collect.TreeRangeMap; import com.google.googlejavaformat.Newlines; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.URI; +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; @@ -36,63 +45,121 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; -import org.openjdk.javax.tools.Diagnostic; -import org.openjdk.javax.tools.DiagnosticCollector; -import org.openjdk.javax.tools.DiagnosticListener; -import org.openjdk.javax.tools.JavaFileObject; -import org.openjdk.javax.tools.SimpleJavaFileObject; -import org.openjdk.javax.tools.StandardLocation; -import org.openjdk.source.tree.BinaryTree; -import org.openjdk.source.tree.LiteralTree; -import org.openjdk.source.tree.MemberSelectTree; -import org.openjdk.source.tree.Tree; -import org.openjdk.source.tree.Tree.Kind; -import org.openjdk.source.util.TreePath; -import org.openjdk.source.util.TreePathScanner; -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; -import org.openjdk.tools.javac.util.Context; -import org.openjdk.tools.javac.util.Log; -import org.openjdk.tools.javac.util.Options; -import org.openjdk.tools.javac.util.Position; +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) throws FormatterException { - return StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, input); + 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, final String input) throws FormatterException { - if (!longLines(columnLimit, input)) { + static String wrap(final int columnLimit, String input, Formatter formatter) + throws FormatterException { + if (!needWrapping(columnLimit, input)) { // fast path return input; } - JCTree.JCCompilationUnit unit = parse(input, /* allowStringFolding= */ false); - String separator = Newlines.guessLineSeparator(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; + } - // Paths to string literals that extend past the column limit. - List toFix = new ArrayList<>(); - final Position.LineMap lineMap = unit.getLineMap(); - new TreePathScanner() { @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(unit, literalTree); + int endPosition = getEndPosition(literalTree, unit); int lineEnd = endPosition; while (Newlines.hasNewlineAt(input, lineEnd) == -1) { lineEnd++; @@ -100,61 +167,95 @@ public Void visitLiteral(LiteralTree literalTree, Void aVoid) { if (lineMap.getColumnNumber(lineEnd) - 1 <= columnLimit) { return null; } - toFix.add(getCurrentPath()); + longStringLiterals.add(getCurrentPath()); return null; } - }.scan(new TreePath(unit), null); - - TreeRangeMap replacements = TreeRangeMap.create(); - for (TreePath path : toFix) { - // Find the outermost contiguous enclosing concatenation expression - TreePath enclosing = path; - while (enclosing.getParentPath().getLeaf().getKind() == Tree.Kind.PLUS) { - enclosing = enclosing.getParentPath(); - } - // Is the literal being wrapped the first in a chain of concatenation expressions? - // i.e. `ONE + TWO + THREE` - // We need this information to handle continuation indents. - AtomicBoolean first = new AtomicBoolean(false); - // Finds the set of string literals in the concat expression that includes the one that needs - // to be wrapped. - List flat = flatten(input, unit, path, enclosing, first); - // Zero-indexed start column - int startColumn = lineMap.getColumnNumber(getStartPosition(flat.get(0))) - 1; - - // Handling leaving trailing non-string tokens at the end of the literal, - // e.g. the trailing `);` in `foo("...");`. - int end = getEndPosition(unit, getLast(flat)); - int lineEnd = end; - while (Newlines.hasNewlineAt(input, lineEnd) == -1) { - lineEnd++; - } - int trailing = lineEnd - end; - - // Get the original source text of the string literals, excluding `"` and `+`. - ImmutableList components = stringComponents(input, unit, flat); - replacements.put( - Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(unit, getLast(flat))), - reflow(separator, columnLimit, startColumn, trailing, components, first.get())); } - String result = applyReplacements(input, replacements); - { - // We really don't want bugs in this pass to change the behaviour of programs we're - // formatting, so check that the pretty-printed AST is the same before and after reformatting. - String expected = parse(input, /* allowStringFolding= */ true).toString(); - String actual = parse(result, /* allowStringFolding= */ true).toString(); - if (!expected.equals(actual)) { - throw new FormatterException( - String.format( - "Something has gone terribly wrong. Please file a bug: " - + "https://github.com/google/google-java-format/issues/new" - + "\n\n=== Actual: ===\n%s\n=== Expected: ===\n%s\n", - actual, expected)); + 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()); } } - return result; + 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())); + } + } } /** @@ -167,7 +268,7 @@ private static ImmutableList stringComponents( StringBuilder piece = new StringBuilder(); for (Tree tree : flat) { // adjust for leading and trailing double quotes - String text = input.substring(getStartPosition(tree) + 1, getEndPosition(unit, tree) - 1); + 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))) { @@ -202,19 +303,21 @@ private static ImmutableList stringComponents( } static int hasEscapedWhitespaceAt(String input, int idx) { - return Stream.of("\\t") - .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1) - .filter(x -> x != -1) - .findFirst() - .orElse(-1); + if (input.startsWith("\\t", idx)) { + return 2; + } + return -1; } static int hasEscapedNewlineAt(String input, int idx) { - return Stream.of("\\r\\n", "\\r", "\\n") - .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1) - .filter(x -> x != -1) - .findFirst() - .orElse(-1); + int offset = 0; + if (input.startsWith("\\r", idx)) { + offset += 2; + } + if (input.startsWith("\\n", idx)) { + offset += 2; + } + return offset > 0 ? offset : -1; } /** @@ -223,9 +326,10 @@ static int hasEscapedNewlineAt(String input, int idx) { * @param separator the line separator * @param columnLimit the number of columns to wrap at * @param startColumn the column position of the beginning of the original text - * @param trailing extra space to leave after the last line - * @param components the text to reflow - * @param first0 true if the text includes the beginning of its enclosing concat chain, i.e. a + * @param trailing extra space to leave after the last line, to accommodate a ; or ) + * @param components the text to reflow. This is a list of “words” of a single literal. Its first + * and last quotes have been stripped + * @param first0 true if the text includes the beginning of its enclosing concat chain */ private static String reflow( String separator, @@ -235,7 +339,7 @@ private static String reflow( ImmutableList components, boolean first0) { // We have space between the start column and the limit to output the first line. - // Reserve two spaces for the quotes. + // Reserve two spaces for the start and end quotes. int width = columnLimit - startColumn - 2; Deque input = new ArrayDeque<>(components); List lines = new ArrayList<>(); @@ -243,10 +347,13 @@ private static String reflow( while (!input.isEmpty()) { int length = 0; List line = new ArrayList<>(); - if (input.stream().mapToInt(x -> x.length()).sum() <= width) { + // If we know this is going to be the last line, then remove a bit of width to account for the + // trailing characters. + if (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)) { + while (!input.isEmpty() && (length <= 4 || (length + input.peekFirst().length()) <= width)) { String text = input.removeFirst(); line.add(text); length += text.length(); @@ -273,6 +380,17 @@ private static String reflow( "\"")); } + 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. @@ -323,27 +441,22 @@ && noComments(input, unit, flat.get(endIdx - 1), flat.get(endIdx))) { private static boolean noComments( String input, JCTree.JCCompilationUnit unit, Tree one, Tree two) { return STRING_CONCAT_DELIMITER.matchesAllOf( - input.subSequence(getEndPosition(unit, one), getStartPosition(two))); + input.subSequence(getEndPosition(one, unit), getStartPosition(two))); } public static final CharMatcher STRING_CONCAT_DELIMITER = CharMatcher.whitespace().or(CharMatcher.anyOf("\"+")); - private static int getEndPosition(JCTree.JCCompilationUnit unit, Tree tree) { - return ((JCTree) tree).getEndPosition(unit.endPositions); - } - - private static int getStartPosition(Tree tree) { - return ((JCTree) tree).getStartPosition(); - } - - /** Returns true if any lines in the given Java source exceed the column limit. */ - private static boolean longLines(int columnLimit, String input) { + /** + * 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) { + if (line.length() > columnLimit || line.contains(TEXT_BLOCK_DELIMITER)) { return true; } } @@ -353,34 +466,11 @@ private static boolean longLines(int columnLimit, String input) { /** Parses the given Java source. */ private static JCTree.JCCompilationUnit parse(String source, boolean allowStringFolding) throws FormatterException { - DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + List> errorDiagnostics = new ArrayList<>(); Context context = new Context(); - context.put(DiagnosticListener.class, diagnostics); - Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding)); - JCTree.JCCompilationUnit unit; - JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8); - try { - fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - SimpleJavaFileObject sjfo = - new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) { - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) { - return source; - } - }; - Log.instance(context).useSource(sjfo); - ParserFactory parserFactory = ParserFactory.instance(context); - JavacParser parser = - parserFactory.newParser( - source, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true); - unit = parser.parseCompilationUnit(); - unit.sourcefile = sjfo; - Iterable> errorDiagnostics = - Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic); - if (!Iterables.isEmpty(errorDiagnostics)) { + JCTree.JCCompilationUnit unit = + Trees.parse(context, errorDiagnostics, allowStringFolding, source); + if (!errorDiagnostics.isEmpty()) { // error handling is done during formatting throw FormatterException.fromJavacDiagnostics(errorDiagnostics); } 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 69b954c4f..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,21 +14,41 @@ 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 org.openjdk.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 java.net.URI; +import java.util.List; +import javax.lang.model.element.Name; +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. */ class Trees { @@ -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 4e871a67f..83c09b3ee 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java +++ b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java @@ -30,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; + }; } }, @@ -51,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; + }; } }, @@ -83,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; + }; } }; @@ -164,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 52dadb6ec..50d55d4d4 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java +++ b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java @@ -46,15 +46,17 @@ final class UsageException extends Exception { " Do not fix the import order. Unused imports will still be removed.", " --skip-removing-unused-imports", " Do not remove unused imports. Imports will still be sorted.", - " . --skip-reflowing-long-strings", + " --skip-reflowing-long-strings", " Do not reflow string literals that exceed the column limit.", + " --skip-javadoc-formatting", + " 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", 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 83728a0b4..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 { 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 5addc6770..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 @@ -57,82 +57,35 @@ 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(); @@ -166,15 +119,30 @@ private static Token standardize(Token token, Token standardToken) { * fits on one line. */ private static String makeSingleLineIfPossible(int blockIndent, String input) { - int oneLinerContentLength = MAX_LINE_LENGTH - "/** */".length() - blockIndent; Matcher matcher = ONE_CONTENT_LINE_PATTERN.matcher(input); - if (matcher.matches() && matcher.group(1).isEmpty()) { - return "/** */"; - } else if (matcher.matches() && matcher.group(1).length() <= oneLinerContentLength) { - return "/** " + matcher.group(1) + " */"; + if (matcher.matches()) { + String line = matcher.group(1); + if (line.isEmpty()) { + return "/** */"; + } else if (oneLineJavadoc(line, blockIndent)) { + return "/** " + line + " */"; + } } return input; } + private static boolean oneLineJavadoc(String line, int blockIndent) { + int oneLinerContentLength = MAX_LINE_LENGTH - "/** */".length() - blockIndent; + if (line.length() > oneLinerContentLength) { + return false; + } + // If the javadoc contains only a tag, use multiple lines to encourage writing a summary + // fragment, unless it's /* @hide */. + if (line.startsWith("@") && !line.equals("@hide")) { + return false; + } + return true; + } + private JavadocFormatter() {} } diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocLexer.java index 108d4a7bf..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 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 c2431c456..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,9 +27,7 @@ import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG; import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Ordering; import com.google.googlejavaformat.java.javadoc.Token.Type; /** @@ -41,6 +40,7 @@ final class JavadocWriter { private final int blockIndent; private final StringBuilder output = new StringBuilder(); + /** * Whether we are inside an {@code

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