diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..1a6f7b71d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" + groups: + dependencies: + applies-to: version-updates + patterns: + - "*" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + applies-to: version-updates + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..c1afc2084 --- /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.13.1 + with: + access_token: ${{ github.token }} + - name: "Check out repository" + uses: actions/checkout@v7 + - 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@v5 + 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.13.1 + with: + access_token: ${{ github.token }} + - name: "Check out repository" + uses: actions/checkout@v7 + - 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@v7 + - name: "Set up JDK 21" + uses: actions/setup-java@v5 + 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..74a607e1a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,124 @@ +# 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@v7 + + - name: Set up JDK + uses: actions/setup-java@v5 + 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 + env: + GITHUB_EVENT_INPUTS_VERSION: ${{ github.event.inputs.version }} + + - 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}" + env: + GITHUB_EVENT_INPUTS_VERSION: ${{ github.event.inputs.version }} + + - name: Add Artifacts to Release Entry + uses: softprops/action-gh-release@v3.0.1 + 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@v7 + - name: "Set up GraalVM" + uses: graalvm/setup-graalvm@v1 + with: + java-version: "25" + distribution: "graalvm-community" + github-token: ${{ secrets.GITHUB_TOKEN }} + native-image-job-reports: "true" + cache: "maven" + - name: Bump Version Number + run: mvn --no-transfer-progress versions:set versions:commit -DnewVersion="${{ github.event.inputs.version }}" + - name: "Native" + run: mvn -Pnative -DskipTests package -pl core -am + - name: "Move outputs" + run: cp core/target/google-java-format${{ env.EXTENSION }} google-java-format_${{ env.SUFFIX }}${{ env.EXTENSION }} + - name: "Upload native-image" + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release upload "v${{ github.event.inputs.version }}" "google-java-format_${{ env.SUFFIX }}${{ env.EXTENSION }}" diff --git a/.gitignore b/.gitignore index db4325627..235f69d3d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ target/ bin/ out/ +eclipse_plugin/lib/ diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 000000000..2e8f1d41f --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,13 @@ +--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED + +-Djdk.xml.maxGeneralEntitySizeLimit=0 +-Djdk.xml.totalEntitySizeLimit=0 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 05b3a2e05..000000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -language: java - -notifications: - email: - recipients: - - google-java-format-dev+ci@google.com - on_success: change - on_failure: always - -matrix: - allow_failures: - - jdk: oraclejdk9 - - env: JDK_RELEASE='10 Early-Access' - include: -# JDK 8 - - jdk: oraclejdk8 - env: JDK_RELEASE='8' -# JDK 9 - - jdk: oraclejdk9 - env: JDK_RELEASE='9' -# JDK 10 - - env: JDK_RELEASE='10 Early-Access' - before_install: unset _JAVA_OPTIONS && . ./scripts/install-jdk-10.sh - -# see https://github.com/travis-ci/travis-ci/issues/8408 -before_install: - - unset _JAVA_OPTIONS - -# use travis-ci docker based infrastructure -sudo: false - -cache: - directories: - - $HOME/.m2 - -install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V - -script: mvn test -B - -env: - global: - - secure: KkUX74NDDk95WR60zwN6x6pz49KAfR0zUu1thxl8Kke0+WVoIv1EBo7/e4ZXTdBKxlzQCX9Aa0OlIyUlhGJeuNIGtX16lcNyZNmKSacfrT68MpZqi+nAiYp8tnOyW/zuI+shSKHkGQOFq6c9KTtR9vG8kjr1Q9dNl/H5QjGaG1ZMiU/mGH9ompf+0nQTMDLKaEWV+SpKGjK5U1Zs2p08I9KKePbZoi9L2oAw5cH9wW8Q3pQJds6Rkwy9aecxRd4xmTza7Lb04dmByjqY8gsIjrTN0onOndBmLKTHiH5NVLKf0ilEVGiMQ1x4eCQolcRpGzxdTTKI0ahiWS59UABVoy1sXYqkIbZjpmMuGhHvbRir7YEXaG8LRUAxdWd9drJfvKQeBphQlIJKwajHSiMAdc9zisQg1UW75HSGKoPDHpzq+P7YBil2PUjk+5yUy5OytX6IebFT4KdeCO2ayu338yqb2t8q98elMoD5jwFVD0tpkLQ6xsYodClSGfMCVfP2zTkB7c4sHZV7tJS68CiNt7sCwz9CTNApFiSWMBxLKkKQ7VSBTy9bAn+phvW0u/maGsrRnehmsV3PVPtEsMlrqeMGwaPqIwx1l6otVQCnGRt3e8z3HoxY6AaBPaX0Z8lH2y+BxYhWTYzGhRxyyV666u/9yekTXmH53c7at7mau6Q= - - secure: VWnZcPA4esdaMJgh0Mui7K5O++AGZY3AYswufd0UUbAmnK60O6cDBOSelnr7hImDgZ09L2RWMXIVCt4b+UFXoIhqrvZKVitUXPldS6uNJeGT9p6quFf36o8Wf0ppKWnPd66AY6ECnE75Ujn1Maw899kb3zY2SvIvzA7HlXqtmowHCVGoJ4ou6LQxJpVEJ4hjvS2gQMF9W31uOzRzMI1JhdZioYmqe6eq9sGmRZZiYON7jBqX8f4XW0tTZoK+dVRNwYQcwyqcvQpxeI15VWDq5cqjBw3ps5XSEYTNIFUXREnEEi+vLdCuw/YRZp1ij7LiQKp6bcb2KROXaWii4VpUNWxIAflm4Nvn/8pa/3CUwqIbxTSAL+Qkb2iEzuYuNPGLr72mQgGEnlSpaqzUx0miwjJ41x3Q8mf72ihIME7YQGMDJL7TA7/GjXFeSxroPk65tbssAGmbjwGGJX67NHUzeQPW2QPA2cohCHyopKB9GqhKgKwKjenkCUaBGCaZReZz9XkSkHTXlxxSakMTmgJnA9To9d2lPOy0nppUvrd/0uAbPuxxCZqXElRvOvHKzpV1ZpKpqSxvjh63mCQRTi2rFiPn8uFaajai9mHaPoGmNwQwIUbAviNqifuIEPpc6cOuyV0MWJwdFLo1SKamJya/MwQz+IwXuY2TX7Fmv9HovdM= - -after_success: -- util/publish-snapshot-on-commit.sh diff --git a/README.md b/README.md index c14f5f063..b90dc4a91 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.5-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 21). 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). @@ -27,34 +42,99 @@ To reformat changed lines in a specific patch, use formatting. This is a deliberate design decision to unify our code formatting on a single format.* -### IntelliJ +### IntelliJ, Android Studio, and other JetBrains IDEs + +A +[google-java-format IntelliJ plugin](https://plugins.jetbrains.com/plugin/8527) +is available from the plugin repository. To install it, go to your IDE's +settings and select the `Plugins` category. Click the `Marketplace` tab, search +for the `google-java-format` plugin, and click the `Install` button. + +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, +[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". -A [google-java-format IntelliJ -plugin](https://plugins.jetbrains.com/plugin/8527) is available from the plugin -repository. +When enabled, it will replace the normal `Reformat Code` and `Optimize Imports` +actions. -The plugin will not be enabled by default. To enable it in the current project, -go to "File→Settings...→google-java-format Settings" and check the "Enable" -checkbox. +#### IntelliJ JRE Config -To enable it by default in new projects, use "File→Other Settings→Default -Settings...". +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: -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. +``` +--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +``` + +Once you've done that, restart the IDE. ### Eclipse -A [google-java-format Eclipse -plugin](https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-eclipse-plugin-1.3.0.jar) -can be downloaded from the releases page. 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) +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/main/plugin-gradle#google-java-format) + * [sherter/google-java-format-gradle-plugin](https://github.com/sherter/google-java-format-gradle-plugin) +* Apache Maven plugins + * [spotless](https://github.com/diffplug/spotless/tree/main/plugin-maven#google-java-format) + * [spotify/fmt-maven-plugin](https://github.com/spotify/fmt-maven-plugin) + * [talios/googleformatter-maven-plugin](https://github.com/talios/googleformatter-maven-plugin) + * [Cosium/maven-git-code-format](https://github.com/Cosium/maven-git-code-format): + A maven plugin that automatically deploys google-java-format as a + pre-commit git hook. +* SBT plugins + * [sbt/sbt-java-formatter](https://github.com/sbt/sbt-java-formatter) +* [Github Actions](https://github.com/features/actions) + * [googlejavaformat-action](https://github.com/axel-op/googlejavaformat-action): + Automatically format your Java files when you push on github ### as a library @@ -62,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.5 + ${google-java-format.version} ``` @@ -76,7 +169,7 @@ configuration. ```groovy dependencies { - compile 'com.google.googlejavaformat:google-java-format:1.5' + implementation 'com.google.googlejavaformat:google-java-format:$googleJavaFormatVersion' } ``` @@ -99,7 +192,9 @@ Your starting point should be the instance methods of ## Building from source - mvn install +``` +mvn install +``` ## Contributing diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index a4c7ee9cb..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -os: Visual Studio 2015 -install: - - ps: | - Add-Type -AssemblyName System.IO.Compression.FileSystem - if (!(Test-Path -Path "C:\maven" )) { - (new-object System.Net.WebClient).DownloadFile( - 'http://www.us.apache.org/dist/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip', - 'C:\maven-bin.zip' - ) - [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\maven-bin.zip", "C:\maven") - } - - cmd: SET PATH=C:\maven\apache-maven-3.3.9\bin;%JAVA_HOME%\bin;%PATH% - - cmd: SET MAVEN_OPTS=-XX:MaxPermSize=2g -Xmx4g - - cmd: SET JAVA_OPTS=-XX:MaxPermSize=2g -Xmx4g - -build_script: - - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V - -test_script: - - mvn test -B - -cache: - - C:\maven\ - - C:\Users\appveyor\.m2 - -notifications: - - provider: Email - to: - - google-java-format-dev+ci@google.com - on_build_success: false - on_build_failure: true - on_build_status_changed: true diff --git a/core/pom.xml b/core/pom.xml index 181035ece..273e314fb 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -22,7 +22,7 @@ com.google.googlejavaformat google-java-format-parent - 1.6-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,26 @@ error_prone_annotations true - + + com.google.auto.value + auto-value-annotations + true + + + com.google.auto.service + auto-service-annotations + true + + + org.commonmark + commonmark + 0.28.0 + + + org.commonmark + commonmark-ext-gfm-tables + 0.28.0 + @@ -70,10 +85,15 @@ com.google.truth truth + + com.google.truth.extensions + truth-java8-extension + test + com.google.testing.compile compile-testing - 0.15 + 0.19 test @@ -87,10 +107,17 @@ UTF-8 UTF-8 - http://google.github.io/guava/releases/${guava.version}/api/docs/ - http://jsr-305.googlecode.com/svn/trunk/javadoc/ - http://docs.oracle.com/javase/7/docs/api/ + https://guava.dev/releases/${guava.version}/api/docs/ + https://docs.oracle.com/en/java/javase/11/docs/api + + --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED,com.google.googlejavaformat + @@ -105,7 +132,7 @@ org.apache.maven.plugins maven-shade-plugin - 2.4.3 + 3.2.4 shade-all-deps @@ -151,51 +178,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 +204,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.0.0 + 3.3.0 add-source @@ -240,4 +222,58 @@ + + + + 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 0dc25737a..54c7da6c7 100644 --- a/core/src/main/java/com/google/googlejavaformat/Doc.java +++ b/core/src/main/java/com/google/googlejavaformat/Doc.java @@ -15,15 +15,19 @@ package com.google.googlejavaformat; import static com.google.common.collect.Iterables.getLast; +import static com.google.googlejavaformat.CommentsHelper.reformatParameterComment; +import static java.lang.Math.max; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.Iterators; import com.google.common.collect.Range; import com.google.googlejavaformat.Output.BreakTag; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * {@link com.google.googlejavaformat.java.JavaInputAstVisitor JavaInputAstVisitor} outputs a @@ -59,20 +63,18 @@ public enum FillMode { FORCED } - /** State for writing. */ - public static final class State { - final int lastIndent; - final int indent; - final int column; - final boolean mustBreak; - - State(int lastIndent, int indent, int column, boolean mustBreak) { - this.lastIndent = lastIndent; - this.indent = indent; - this.column = column; - this.mustBreak = mustBreak; - } + /** + * 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 record State(int lastIndent, int indent, int column, boolean mustBreak) { public State(int indent0, int column0) { this(indent0, indent0, column0, false); } @@ -84,58 +86,36 @@ State withColumn(int column) { State withMustBreak(boolean mustBreak) { return new State(lastIndent, indent, column, mustBreak); } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("lastIndent", lastIndent) - .add("indent", indent) - .add("column", column) - .add("mustBreak", mustBreak) - .toString(); - } } private static final Range 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 +124,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 +189,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 +229,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( @@ -273,8 +245,8 @@ private static void splitByBreaks(List docs, List> splits, List()); for (Doc doc : docs) { - if (doc instanceof Break) { - breaks.add((Break) doc); + if (doc instanceof Break b) { + breaks.add(b); splits.add(new ArrayList<>()); } else { getLast(splits).add(doc); @@ -288,7 +260,7 @@ private State computeBroken(CommentsHelper commentsHelper, int maxWidth, State s state = computeBreakAndSplit( - commentsHelper, maxWidth, state, /* optBreakDoc= */ Optional.absent(), splits.get(0)); + commentsHelper, maxWidth, state, /* optBreakDoc= */ Optional.empty(), splits.get(0)); // Handle following breaks and split. for (int i = 0; i < breaks.size(); i++) { @@ -306,8 +278,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 +331,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 +375,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 +446,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 +463,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 +503,8 @@ public void add(DocBuilder builder) { } @Override - float computeWidth() { - return 1.0F; + int computeWidth() { + return 1; } @Override @@ -576,7 +556,7 @@ private Break(FillMode fillMode, String flat, Indent plusIndent, Optional= 0); - checkArgument(column >= 0); +/** + * An error that prevented formatting from succeeding. + * + * @param line the line number on which the error occurred, or {@code -1} if the error does not have + * a line number. + * @param column the 1-indexed column number on which the error occurred, or {@code -1} if the error + * does not have a column. + * @param message a description of the problem that prevented formatting from succeeding. + */ +public record FormatterDiagnostic(int line, int column, String message) { + public FormatterDiagnostic { + checkArgument(line >= -1); + checkArgument(column >= -1); checkNotNull(message); - return new FormatterDiagnostic(lineNumber, column, message); - } - - private FormatterDiagnostic(int lineNumber, int column, String message) { - this.lineNumber = lineNumber; - this.column = column; - this.message = message; - } - - /** - * Returns the line number on which the error occurred, or {@code -1} if the error does not have a - * line number. - */ - public int line() { - return lineNumber; - } - - /** - * Returns the 0-indexed column number on which the error occurred, or {@code -1} if the error - * does not have a column. - */ - public int column() { - return column; } - /** Returns a description of the problem that prevented formatting from succeeding. */ - public String message() { - return message; + public FormatterDiagnostic(String message) { + this(-1, -1, message); } + @Override public String toString() { StringBuilder sb = new StringBuilder(); - if (lineNumber >= 0) { - sb.append(lineNumber).append(':'); + if (line >= 0) { + sb.append(line).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) { + if (line >= 0 || column >= 0) { sb.append(' '); } sb.append("error: ").append(message); diff --git a/core/src/main/java/com/google/googlejavaformat/Input.java b/core/src/main/java/com/google/googlejavaformat/Input.java index c2af02b7f..ad15e71f8 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(); @@ -127,7 +141,7 @@ public String toString() { * numbers. */ public FormatterDiagnostic createDiagnostic(int inputPosition, String message) { - return FormatterDiagnostic.create( + return new FormatterDiagnostic( getLineNumber(inputPosition), getColumnNumber(inputPosition), message); } } diff --git a/core/src/main/java/com/google/googlejavaformat/InputOutput.java b/core/src/main/java/com/google/googlejavaformat/InputOutput.java index b072cbe7c..f7aa892b4 100644 --- a/core/src/main/java/com/google/googlejavaformat/InputOutput.java +++ b/core/src/main/java/com/google/googlejavaformat/InputOutput.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Map; -/** This interface defines methods common to an {@link Input} or an {@link Output}. */ +/** This class defines methods common to an {@link Input} or an {@link Output}. */ public abstract class InputOutput { private ImmutableList lines = ImmutableList.of(); @@ -83,7 +83,7 @@ protected final void computeRanges(List toks) { * Given an {@code InputOutput}, compute the map from tok indices to line ranges. * * @param put the {@code InputOutput} - * @return the map from {@link com.google.googlejavaformat.java.JavaInput.Tok} indices to line + * @return the map from {@code com.google.googlejavaformat.java.JavaInput.Tok} indices to line * ranges in this {@code put} */ public static Map> makeKToIJ(InputOutput put) { @@ -114,11 +114,6 @@ public final Range getRanges(int lineI) { @Override public String toString() { - return "InputOutput{" - + "lines=" - + lines - + ", ranges=" - + ranges - + '}'; + return "InputOutput{" + "lines=" + lines + ", ranges=" + ranges + '}'; } } diff --git a/core/src/main/java/com/google/googlejavaformat/Newlines.java b/core/src/main/java/com/google/googlejavaformat/Newlines.java index 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 4375a90ce..dbd2eb3ce 100644 --- a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java +++ b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java @@ -14,8 +14,12 @@ package com.google.googlejavaformat; +import static java.lang.Math.max; +import static java.lang.Math.min; + import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -26,6 +30,7 @@ import com.google.googlejavaformat.Output.BreakTag; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * An {@code OpsBuilder} creates a list of {@link Op}s, which is turned into a {@link Doc} by {@link @@ -33,26 +38,26 @@ */ public final class OpsBuilder { - /** @return the actual size of the AST node at position, including comments. */ + /** Returns the actual size of the AST node at position, including comments. */ public int actualSize(int position, int length) { Token startToken = input.getPositionTokenMap().get(position); int start = startToken.getTok().getPosition(); for (Tok tok : startToken.getToksBefore()) { if (tok.isComment()) { - start = Math.min(start, tok.getPosition()); + start = min(start, tok.getPosition()); } } Token endToken = input.getPositionTokenMap().get(position + length - 1); int end = endToken.getTok().getPosition() + endToken.getTok().length(); for (Tok tok : endToken.getToksAfter()) { if (tok.isComment()) { - end = Math.max(end, tok.getPosition() + tok.length()); + end = max(end, tok.getPosition() + tok.length()); } } return end - start; } - /** @return the start column of the token at {@code position}, including leading comments. */ + /** Returns the start column of the token at {@code position}, including leading comments. */ public Integer actualStartColumn(int position) { Token startToken = input.getPositionTokenMap().get(position); int start = startToken.getTok().getPosition(); @@ -62,7 +67,7 @@ public Integer actualStartColumn(int position) { return start; } if (tok.isComment()) { - start = Math.min(start, tok.getPosition()); + start = min(start, tok.getPosition()); } } return start; @@ -82,7 +87,7 @@ public abstract static class BlankLineWanted { * declaration). Overrides conditional blank lines. */ public static final BlankLineWanted PRESERVE = - new SimpleBlankLine(/* wanted= */ Optional.absent()); + new SimpleBlankLine(/* wanted= */ Optional.empty()); /** Is the blank line wanted? */ public abstract Optional wanted(); @@ -128,16 +133,15 @@ public Optional wanted() { return Optional.of(true); } } - return Optional.absent(); + return Optional.empty(); } @Override public BlankLineWanted merge(BlankLineWanted other) { - if (!(other instanceof ConditionalBlankLine)) { + if (!(other instanceof ConditionalBlankLine conditionalBlankLine)) { return other; } - return new ConditionalBlankLine( - Iterables.concat(this.tags, ((ConditionalBlankLine) other).tags)); + return new ConditionalBlankLine(Iterables.concat(this.tags, conditionalBlankLine.tags)); } } } @@ -154,7 +158,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) { @@ -243,7 +247,7 @@ public final void drain() { token, Doc.Token.RealOrImaginary.IMAGINARY, ZERO, - /* breakAndIndentTrailingComment= */ Optional.absent())); + /* breakAndIndentTrailingComment= */ Optional.empty())); } } this.inputPosition = inputPosition; @@ -266,10 +270,38 @@ public final void close() { /** Return the text of the next {@link Input.Token}, or absent if there is none. */ public final Optional peekToken() { + return peekToken(0); + } + + /** Return the text of an upcoming {@link Input.Token}, or absent if there is none. */ + public final Optional peekToken(int skip) { ImmutableList tokens = input.getTokens(); - return tokenI < tokens.size() - ? Optional.of(tokens.get(tokenI).getTok().getOriginalText()) - : Optional.absent(); + int idx = tokenI + skip; + return idx < tokens.size() + ? Optional.of(tokens.get(idx).getTok().getOriginalText()) + : Optional.empty(); + } + + /** + * Returns the {@link Input.Tok}s starting at the current source position, which are satisfied by + * the given predicate. + */ + public ImmutableList peekTokens(int startPosition, Predicate predicate) { + ImmutableList tokens = input.getTokens(); + 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(); } /** @@ -283,7 +315,7 @@ public final void guessToken(String token) { token, Doc.Token.RealOrImaginary.IMAGINARY, ZERO, - /* breakAndIndentTrailingComment= */ Optional.absent()); + /* breakAndIndentTrailingComment= */ Optional.empty()); } public final void token( @@ -292,7 +324,7 @@ public final void token( Indent plusIndentCommentsBefore, Optional breakAndIndentTrailingComment) { ImmutableList tokens = input.getTokens(); - if (token.equals(peekToken().orNull())) { // Found the input token. Output it. + if (token.equals(peekToken().orElse(null))) { // Found the input token. Output it. add( Doc.Token.make( tokens.get(tokenI++), @@ -308,7 +340,8 @@ public final void token( throw new FormattingError( diagnostic( String.format( - "expected token: '%s'; generated %s instead", peekToken().orNull(), token))); + "expected token: '%s'; generated %s instead", + peekToken().orElse(null), token))); } } } @@ -325,7 +358,7 @@ public final void op(String op) { op.substring(i, i + 1), Doc.Token.RealOrImaginary.REAL, ZERO, - /* breakAndIndentTrailingComment= */ Optional.absent()); + /* breakAndIndentTrailingComment= */ Optional.empty()); } } @@ -393,7 +426,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.absent()); + breakOp(fillMode, flat, plusIndent, /* optionalTag= */ Optional.empty()); } /** @@ -461,13 +494,13 @@ public final ImmutableList build() { int opsN = ops.size(); for (int i = 0; i < opsN; i++) { Op op = ops.get(i); - if (op instanceof Doc.Token) { + if (op instanceof Doc.Token tokenOp) { /* * Token ops can have associated non-tokens, including comments, which we need to insert. * They can also cause line breaks, so we insert them before or after the current level, * when possible. */ - Doc.Token tokenOp = (Doc.Token) op; + Input.Token token = tokenOp.getToken(); int j = i; // Where to insert toksBefore before. while (0 < j && ops.get(j - 1) instanceof OpenOp) { @@ -530,7 +563,7 @@ public final ImmutableList build() { Doc.Break.make( Doc.FillMode.FORCED, "", - tokenOp.breakAndIndentTrailingComment().or(Const.ZERO))); + tokenOp.breakAndIndentTrailingComment().orElse(Const.ZERO))); } else { tokOps.put(k + 1, SPACE); } @@ -582,8 +615,8 @@ public final ImmutableList build() { Op op = ops.get(i); if (afterForcedBreak && (op instanceof Doc.Space - || (op instanceof Doc.Break - && ((Doc.Break) op).getPlusIndent() == 0 + || (op instanceof Doc.Break b + && b.getPlusIndent() == 0 && " ".equals(((Doc) op).getFlat())))) { continue; } @@ -602,7 +635,7 @@ public final ImmutableList build() { } private static boolean isForcedBreak(Op op) { - return op instanceof Doc.Break && ((Doc.Break) op).isForced(); + return op instanceof Doc.Break b && b.isForced(); } private static List makeComment(Input.Tok comment) { diff --git a/core/src/main/java/com/google/googlejavaformat/Output.java b/core/src/main/java/com/google/googlejavaformat/Output.java index cbf6a9f35..ea039fa83 100644 --- a/core/src/main/java/com/google/googlejavaformat/Output.java +++ b/core/src/main/java/com/google/googlejavaformat/Output.java @@ -15,16 +15,16 @@ package com.google.googlejavaformat; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; import com.google.common.collect.Range; import com.google.googlejavaformat.OpsBuilder.BlankLineWanted; +import java.util.Optional; /** An output from the formatter. */ public abstract class Output extends InputOutput { /** Unique identifier for a break. */ public static final class BreakTag { - Optional taken = Optional.absent(); + Optional taken = Optional.empty(); public void recordBroken(boolean broken) { // TODO(cushon): enforce invariants. @@ -36,7 +36,7 @@ public void recordBroken(boolean broken) { } public boolean wasBreakTaken() { - return taken.or(false); + return taken.orElse(false); } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java index dcb7dba52..698a3ea9f 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java +++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java @@ -14,143 +14,52 @@ 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. + * @param reorderModifiers Reorder modifiers into the JLS-recommended order. */ -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; - - 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) { - 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; - } - - /** 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; - } +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, + boolean reorderModifiers) { /** Returns true if partial formatting was selected. */ boolean isSelection() { @@ -158,117 +67,73 @@ boolean isSelection() { } static Builder builder() { - return new Builder(); + return new AutoBuilder_CommandLineOptions_Builder() + .sortImports(true) + .removeUnusedImports(true) + .reflowLongStrings(true) + .formatJavadoc(true) + .reorderModifiers(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(); + 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); - CommandLineOptions build() { - return new CommandLineOptions( - files.build(), - inPlace, - lines.build(), - offsets.build(), - lengths.build(), - aosp, - version, - help, - stdin, - fixImportsOnly, - sortImports, - removeUnusedImports, - dryRun, - setExitIfChanged, - assumeFilename); - } + Builder assumeFilename(String assumeFilename); + + Builder reflowLongStrings(boolean reflowLongStrings); + + Builder formatJavadoc(boolean formatJavadoc); + + Builder reorderModifiers(boolean reorderModifiers); + + 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 7b278072b..fddf9be8a 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,75 +59,36 @@ 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 "-": - 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 "--skip-reordering-modifiers" -> optionsBuilder.reorderModifiers(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(); } @@ -151,7 +117,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)); } @@ -163,17 +129,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..4d5b40966 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java +++ b/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java @@ -14,36 +14,35 @@ package com.google.googlejavaformat.java; +import static java.util.Objects.requireNonNull; + 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; +import org.jspecify.annotations.Nullable; /** * Utilities for working with array dimensions. * *

javac's parser does not preserve concrete syntax for mixed-notation arrays, so we have to - * re-lex the input to extra it. + * re-lex the input to extract it. * *

For example, {@code int [] a;} cannot be distinguished from {@code int [] a [];} in the AST. */ -class DimensionHelpers { +final class DimensionHelpers { /** The array dimension specifiers (including any type annotations) associated with a type. */ - static class TypeWithDims { - final Tree node; - final ImmutableList> dims; - - public TypeWithDims(Tree node, ImmutableList> dims) { - this.node = node; - this.dims = dims; + record TypeWithDims(@Nullable Tree node, ImmutableList> dims) { + TypeWithDims { + requireNonNull(dims, "dims"); } } @@ -106,19 +105,20 @@ 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; + }; } + + private DimensionHelpers() {} } 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 061ae4b75..ce63efe95 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java +++ b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java @@ -14,37 +14,73 @@ package com.google.googlejavaformat.java; +import static java.util.Objects.requireNonNull; + 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 { + + record Result( + @Nullable Path path, + String input, + @Nullable String output, + @Nullable FormatterException exception) { + Result { + requireNonNull(input, "input"); + } + + boolean changed() { + return !input().equals(output()); + } + + static Result create( + @Nullable Path path, + String input, + @Nullable String output, + @Nullable FormatterException exception) { + return new 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) { + FormatFileCallable( + CommandLineOptions parameters, Path path, String input, JavaFormatterOptions options) { + this.path = path; this.input = input; this.parameters = parameters; this.options = options; } @Override - public String call() throws FormatterException { - if (parameters.fixImportsOnly()) { - return fixImports(input); - } + public Result call() { + try { + if (parameters.fixImportsOnly()) { + return Result.create(path, input, fixImports(input), /* exception= */ null); + } - String formatted = - new Formatter(options).formatSource(input, characterRanges(input).asRanges()); - formatted = fixImports(formatted); - return formatted; + Formatter formatter = new Formatter(options); + String formatted = formatter.formatSource(input, characterRanges(input).asRanges()); + formatted = fixImports(formatted); + if (parameters.reflowLongStrings()) { + formatted = StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, formatted, formatter); + } + return Result.create(path, input, formatted, /* exception= */ null); + } catch (FormatterException e) { + return Result.create(path, input, /* output= */ null, e); + } } private String fixImports(String input) throws FormatterException { @@ -52,7 +88,7 @@ private String fixImports(String input) throws FormatterException { input = RemoveUnusedImports.removeUnusedImports(input); } if (parameters.sortImports()) { - input = ImportOrderer.reorderImports(input); + input = ImportOrderer.reorderImports(input, options.style()); } return input; } diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java index 0077bb79a..6f021b6a2 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,9 @@ 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.ImmutableSet; import com.google.common.collect.Iterators; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; @@ -25,32 +24,21 @@ import com.google.common.io.CharSink; import com.google.common.io.CharSource; import com.google.errorprone.annotations.Immutable; +import com.google.googlejavaformat.CommentsHelper; import com.google.googlejavaformat.Doc; import com.google.googlejavaformat.DocBuilder; import com.google.googlejavaformat.FormattingError; import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.Op; import com.google.googlejavaformat.OpsBuilder; -import 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 @@ -59,10 +47,10 @@ *

This formatter uses the javac parser to generate an AST. Because the AST loses information * about the non-tokens in the input (including newlines, comments, etc.), and even some tokens * (e.g., optional commas or semicolons), this formatter lexes the input again and follows along in - * the resulting list of tokens. Its lexer splits all multi-character operators (like ">>") into - * multiple single-character operators. Each non-token is assigned to a token---non-tokens following - * a token on the same line go with that token; those following go with the next token--- and there - * is a final EOF token to hold final comments. + * the resulting list of tokens. Its lexer splits all multi-character operators (like ">>") + * into multiple single-character operators. Each non-token is assigned to a token---non-tokens + * following a token on the same line go with that token; those following go with the next token--- + * and there is a final EOF token to hold final comments. * *

The formatter walks the AST to generate a Greg Nelson/Derek Oppen-style list of formatting * {@link Op}s [1--2] that then generates a structured {@link Doc}. Each AST node type has a visitor @@ -87,6 +75,8 @@ @Immutable public final class Formatter { + public static final int MAX_LINE_LENGTH = 100; + static final Range EMPTY_RANGE = Range.closedOpen(-1, -1); private final JavaFormatterOptions options; @@ -111,53 +101,30 @@ 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); + ImmutableSet.Builder markdownJavadocPositions = ImmutableSet.builder(); // Output the compilation unit. - new JavaInputAstVisitor(builder, options.indentationMultiplier()).scan(unit, null); + JavaInputAstVisitor visitor = + new JavaInputAstVisitor(builder, options.indentationMultiplier(), markdownJavadocPositions); + visitor.scan(unit, null); builder.sync(javaInput.getText().length()); builder.drain(); Doc doc = new DocBuilder().withOps(builder.build()).build(); - doc.computeBreaks( - javaOutput.getCommentsHelper(), options.maxLineLength(), new Doc.State(+0, 0)); + CommentsHelper commentsHelper = + new JavaCommentsHelper( + Newlines.guessLineSeparator(javaInput.getText()), + options, + markdownJavadocPositions.build()); + doc.computeBreaks(commentsHelper, MAX_LINE_LENGTH, new Doc.State(+0, 0)); doc.write(javaOutput); javaOutput.flush(); } @@ -166,15 +133,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"); } /** @@ -215,9 +176,11 @@ public String formatSource(String input) throws FormatterException { * Google Java Style Guide - 3.3.3 Import ordering and spacing */ public String formatSourceAndFixImports(String input) throws FormatterException { - input = ImportOrderer.reorderImports(input); + input = ImportOrderer.reorderImports(input, options.style()); input = RemoveUnusedImports.removeUnusedImports(input); - return formatSource(input); + String formatted = formatSource(input); + formatted = StringWrapper.wrap(formatted, this); + return formatted; } /** @@ -249,11 +212,16 @@ 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 = - new JavaOutput(lineSeparator, javaInput, new JavaCommentsHelper(lineSeparator, options)); + new JavaOutput( + lineSeparator, + javaInput, + new JavaCommentsHelper(lineSeparator, options, ImmutableSet.of())); try { format(javaInput, javaOutput, options); } catch (FormattingError e) { 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..2c4e956f1 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java +++ b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java @@ -14,22 +14,25 @@ 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)); + this(new FormatterDiagnostic(message)); } public FormatterException(FormatterDiagnostic diagnostic) { @@ -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( + return new FormatterDiagnostic( (int) input.getLineNumber(), (int) input.getColumnNumber(), input.getMessage(ENGLISH)); } + + public String formatDiagnostics(String path, String input) { + List lines = Splitter.on(NEWLINE_PATTERN).splitToList(input); + StringBuilder sb = new StringBuilder(); + for (FormatterDiagnostic diagnostic : diagnostics()) { + sb.append(path).append(":").append(diagnostic).append(System.lineSeparator()); + int line = diagnostic.line(); + int column = diagnostic.column(); + if (line != -1 && column != -1) { + sb.append(CharMatcher.breakingWhitespace().trimTrailingFrom(lines.get(line - 1))) + .append(System.lineSeparator()); + sb.append(" ".repeat(column - 1)).append('^').append(System.lineSeparator()); + } + } + return sb.toString(); + } + + private static final Pattern NEWLINE_PATTERN = Pattern.compile("\\R"); } diff --git a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatTool.java b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatTool.java new file mode 100644 index 000000000..3c315aaf1 --- /dev/null +++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatTool.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import static com.google.common.collect.Sets.toImmutableEnumSet; + +import com.google.auto.service.AutoService; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Set; +import javax.lang.model.SourceVersion; +import javax.tools.Tool; + +/** Provide a way to be invoked without necessarily starting a new VM. */ +@AutoService(Tool.class) +public class GoogleJavaFormatTool implements Tool { + @Override + public String name() { + return "google-java-format"; + } + + @Override + public Set getSourceVersions() { + return Arrays.stream(SourceVersion.values()).collect(toImmutableEnumSet()); + } + + @Override + public int run(InputStream in, OutputStream out, OutputStream err, String... args) { + PrintStream outStream = new PrintStream(out); + PrintStream errStream = new PrintStream(err); + try { + return Main.main(in, outStream, errStream, args); + } catch (RuntimeException e) { + errStream.print(e.getMessage()); + errStream.flush(); + return 1; // pass non-zero value back indicating an error has happened + } + } +} diff --git a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProvider.java b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProvider.java new file mode 100644 index 000000000..438eac596 --- /dev/null +++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import com.google.auto.service.AutoService; +import java.io.PrintWriter; +import java.util.spi.ToolProvider; + +/** Provide a way to be invoked without necessarily starting a new VM. */ +@AutoService(ToolProvider.class) +public class GoogleJavaFormatToolProvider implements ToolProvider { + @Override + public String name() { + return "google-java-format"; + } + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + try { + return Main.main(System.in, out, err, args); + } catch (RuntimeException e) { + err.print(e.getMessage()); + err.flush(); + return 1; // pass non-zero value back indicating an error has happened + } + } +} diff --git a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template index eed8e1b8c..88706fbf1 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template +++ b/core/src/main/java/com/google/googlejavaformat/java/GoogleJavaFormatVersion.java.template @@ -14,8 +14,6 @@ package com.google.googlejavaformat.java; -import java.util.Optional; - class GoogleJavaFormatVersion { static String version() { diff --git a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java index 54aa2d7d3..70611cd21 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java @@ -14,108 +14,61 @@ package com.google.googlejavaformat.java; import static com.google.common.collect.Iterables.getLast; +import static com.google.common.primitives.Booleans.trueFirst; import com.google.common.base.CharMatcher; -import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.googlejavaformat.Newlines; +import com.google.googlejavaformat.java.JavaFormatterOptions.Style; import com.google.googlejavaformat.java.JavaInput.Tok; +import com.sun.tools.javac.parser.Tokens.TokenKind; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; -import org.openjdk.tools.javac.parser.Tokens.TokenKind; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.stream.Stream; /** Orders imports in Java source code. */ public class ImportOrderer { + + private static final Splitter DOT_SPLITTER = Splitter.on('.'); + /** * Reorder the inputs in {@code text}, a complete Java program. On success, another complete Java * program is returned, which is the same as the original except the imports are in order. * * @throws FormatterException if the input could not be parsed. */ - public static String reorderImports(String text) throws FormatterException { + public static String reorderImports(String text, Style style) throws FormatterException { ImmutableList toks = JavaInput.buildToks(text, CLASS_START); - return new ImportOrderer(text, toks).reorderImports(); + return new ImportOrderer(text, toks, style).reorderImports(); } /** - * {@link TokenKind}s that indicate the start of a type definition. We use this to avoid scanning - * the whole file, since we know that imports must precede any type definition. - */ - private static final ImmutableSet CLASS_START = - ImmutableSet.of(TokenKind.CLASS, TokenKind.INTERFACE, TokenKind.ENUM); - - /** - * We use this set to find the first import, and again to check that there are no imports after - * the place we stopped gathering them. An annotation definition ({@code @interface}) is two - * tokens, the second which is {@code interface}, so we don't need a separate entry for that. + * Reorder the inputs in {@code text}, a complete Java program, in Google style. On success, + * another complete Java program is returned, which is the same as the original except the imports + * are in order. + * + * @deprecated Use {@link #reorderImports(String, Style)} instead + * @throws FormatterException if the input could not be parsed. */ - private static final ImmutableSet IMPORT_OR_CLASS_START = - ImmutableSet.of("import", "class", "interface", "enum"); - - private final String text; - private final ImmutableList toks; - private final String lineSeparator; - - private ImportOrderer(String text, ImmutableList toks) throws FormatterException { - this.text = text; - this.toks = toks; - this.lineSeparator = Newlines.guessLineSeparator(text); - } - - /** An import statement. */ - private class Import implements Comparable { - /** The name being imported, for example {@code java.util.List}. */ - final String imported; - - /** The characters after the final {@code ;}, up to and including the line terminator. */ - final String trailing; - - /** True if this is {@code import static}. */ - final boolean isStatic; - - Import(String imported, String trailing, boolean isStatic) { - this.imported = imported; - this.trailing = trailing.trim(); - this.isStatic = isStatic; - } - - // This is how the sorting happens, including sorting static imports before non-static ones. - @Override - public int compareTo(Import that) { - if (this.isStatic != that.isStatic) { - return this.isStatic ? -1 : +1; - } - return this.imported.compareTo(that.imported); - } - - // This is a complete line to be output for this import, including the line terminator. - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("import "); - if (isStatic) { - sb.append("static "); - } - sb.append(imported).append(';'); - if (!trailing.isEmpty()) { - sb.append(' ').append(trailing); - } - sb.append(lineSeparator); - return sb.toString(); - } + @Deprecated + public static String reorderImports(String text) throws FormatterException { + return reorderImports(text, Style.GOOGLE); } private String reorderImports() throws FormatterException { - int firstImportStart; Optional maybeFirstImport = findIdentifier(0, IMPORT_OR_CLASS_START); if (!maybeFirstImport.isPresent() || !tokenAt(maybeFirstImport.get()).equals("import")) { // No imports, so nothing to do. return text; } - firstImportStart = maybeFirstImport.get(); + int firstImportStart = maybeFirstImport.get(); int unindentedFirstImportStart = unindent(firstImportStart); ImportsAndIndex imports = scanImports(firstImportStart); @@ -150,6 +103,155 @@ private String reorderImports() throws FormatterException { return result.toString(); } + /** + * {@link TokenKind}s that indicate the start of a type definition. We use this to avoid scanning + * the whole file, since we know that imports must precede any type definition. + */ + private static final ImmutableSet CLASS_START = + ImmutableSet.of(TokenKind.CLASS, TokenKind.INTERFACE, TokenKind.ENUM); + + /** + * We use this set to find the first import, and again to check that there are no imports after + * the place we stopped gathering them. An annotation definition ({@code @interface}) is two + * tokens, the second which is {@code interface}, so we don't need a separate entry for that. + */ + private static final ImmutableSet IMPORT_OR_CLASS_START = + ImmutableSet.of("import", "class", "interface", "enum"); + + /** + * A {@link Comparator} that orders {@link Import}s by Google Style, defined at + * https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing. + * + *

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

Module imports are not mentioned by Android Style, so we make an arbitrary choice about + * where to include them if they are present. + */ + private static final Comparator AOSP_IMPORT_COMPARATOR = + Comparator.comparing(Import::importType) + .thenComparing(Import::isAndroid, trueFirst()) + .thenComparing(Import::isThirdParty, trueFirst()) + .thenComparing(Import::isJava, trueFirst()) + .thenComparing(Import::imported); + + /** + * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link + * Import}s based on Google style. + */ + private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) { + return !prev.importType().equals(curr.importType()); + } + + /** + * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link + * Import}s based on AOSP style. + */ + private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) { + if (!prev.importType().equals(curr.importType())) { + return true; + } + // insert blank line between "com.android" from "com.anythingelse" + if (prev.isAndroid() && !curr.isAndroid()) { + return true; + } + return !prev.topLevel().equals(curr.topLevel()); + } + + private final String text; + private final ImmutableList toks; + private final String lineSeparator; + private final Comparator importComparator; + private final BiFunction shouldInsertBlankLineFn; + + private ImportOrderer(String text, ImmutableList toks, Style style) { + this.text = text; + this.toks = toks; + this.lineSeparator = Newlines.guessLineSeparator(text); + if (style.equals(Style.GOOGLE)) { + this.importComparator = GOOGLE_IMPORT_COMPARATOR; + this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineGoogle; + } else if (style.equals(Style.AOSP)) { + this.importComparator = AOSP_IMPORT_COMPARATOR; + this.shouldInsertBlankLineFn = ImportOrderer::shouldInsertBlankLineAosp; + } else { + throw new IllegalArgumentException("Unsupported code style: " + style); + } + } + + private enum ImportType { + STATIC, + MODULE, + NORMAL + } + + /** + * An import statement. + * + * @param imported the name being imported, for example {@code java.util.List}. + * @param trailing the {@code //} comment lines after the final {@code ;}, up to and including the + * line terminator of the last one. Note: In case two imports were separated by a space (which + * is disallowed by the style guide), the trailing whitespace of the first import does not + * include a line terminator. + * @param importType the {@link ImportType} of the import. + * @param lineSeparator the line separator to use when formatting the import. + */ + private record Import( + String imported, String trailing, ImportType importType, String lineSeparator) { + /** The top-level package of the import. */ + String topLevel() { + return DOT_SPLITTER.split(imported).iterator().next(); + } + + /** True if this is an Android import per AOSP style. */ + boolean isAndroid() { + return Stream.of("android.", "androidx.", "dalvik.", "libcore.", "com.android.") + .anyMatch(imported::startsWith); + } + + /** True if this is a Java import per AOSP style. */ + boolean isJava() { + return switch (topLevel()) { + case "java", "javax" -> true; + default -> false; + }; + } + + /** True if this is a third-party import per AOSP style. */ + boolean isThirdParty() { + return !(isAndroid() || isJava()); + } + + // One or multiple lines, the import itself and following comments, including the line + // terminator. + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("import "); + switch (importType) { + case STATIC -> sb.append("static "); + case MODULE -> sb.append("module "); + case NORMAL -> {} + } + sb.append(imported()).append(';'); + if (trailing().trim().isEmpty()) { + sb.append(lineSeparator); + } else { + sb.append(trailing()); + } + return sb.toString(); + } + } + private String tokString(int start, int end) { StringBuilder sb = new StringBuilder(); for (int i = start; i < end; i++) { @@ -158,15 +260,7 @@ private String tokString(int start, int end) { return sb.toString(); } - private static class ImportsAndIndex { - final ImmutableSortedSet imports; - final int index; - - ImportsAndIndex(ImmutableSortedSet imports, int index) { - this.imports = imports; - this.index = index; - } - } + private record ImportsAndIndex(ImmutableSortedSet imports, int index) {} /** * Scans a sequence of import lines. The parsing uses this approximate grammar: @@ -175,7 +269,7 @@ private static class ImportsAndIndex { * -> ( | )* * -> "import" ("static" )? * ("." )* ("." "*")? ? ";" - * ? ? + * ? ? ( )* * } * * @param i the index to start parsing at. @@ -184,7 +278,7 @@ private static class ImportsAndIndex { */ private ImportsAndIndex scanImports(int i) throws FormatterException { int afterLastImport = i; - ImmutableSortedSet.Builder imports = ImmutableSortedSet.naturalOrder(); + ImmutableSortedSet.Builder imports = ImmutableSortedSet.orderedBy(importComparator); // JavaInput.buildToks appends a zero-width EOF token after all tokens. It won't match any // of our tests here and protects us from running off the end of the toks list. Since it is // zero-width it doesn't matter if we include it in our string concatenation at the end. @@ -193,8 +287,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++; @@ -221,15 +320,25 @@ private ImportsAndIndex scanImports(int i) throws FormatterException { trailing.append(tokenAt(i)); i++; } - if (isSlashSlashCommentToken(i)) { + if (isNewlineToken(i)) { trailing.append(tokenAt(i)); i++; } - if (isNewlineToken(i)) { + // Gather (if any) all single line comments and accompanied line terminators following this + // import + while (isSlashSlashCommentToken(i)) { trailing.append(tokenAt(i)); i++; + if (isNewlineToken(i)) { + trailing.append(tokenAt(i)); + i++; + } } - imports.add(new Import(importedName, trailing.toString(), isStatic)); + 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, lineSeparator)); // 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. @@ -245,20 +354,18 @@ private ImportsAndIndex scanImports(int i) throws FormatterException { private String reorderedImportsString(ImmutableSortedSet imports) { Preconditions.checkArgument(!imports.isEmpty(), "imports"); - Import firstImport = imports.iterator().next(); - - // Pretend that the first import was preceded by another import of the same kind - // (static or non-static), so we don't insert a newline there. - boolean lastWasStatic = firstImport.isStatic; + // Pretend that the first import was preceded by another import of the same kind, so we don't + // insert a newline there. + Import prevImport = imports.iterator().next(); StringBuilder sb = new StringBuilder(); - for (Import thisImport : imports) { - if (lastWasStatic && !thisImport.isStatic) { + for (Import currImport : imports) { + if (shouldInsertBlankLineFn.apply(prevImport, currImport)) { // Blank line between static and non-static imports. sb.append(lineSeparator); } - lastWasStatic = thisImport.isStatic; - sb.append(thisImport); + sb.append(currImport); + prevImport = currImport; } return sb.toString(); } @@ -310,7 +417,7 @@ private StringAndIndex scanImported(int start) throws FormatterException { /** * Returns the index of the first place where one of the given identifiers occurs, or {@code - * Optional.absent()} if there is none. + * Optional.empty()} if there is none. * * @param start the index to start looking at * @param identifiers the identifiers to look for @@ -324,7 +431,7 @@ private Optional findIdentifier(int start, ImmutableSet identif } } } - return Optional.absent(); + return Optional.empty(); } /** Returns the given token, or the preceding token if it is a whitespace token. */ diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java index 2b9696058..7c6e42958 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java @@ -16,6 +16,7 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; import com.google.googlejavaformat.CommentsHelper; import com.google.googlejavaformat.Input.Tok; import com.google.googlejavaformat.Newlines; @@ -27,14 +28,19 @@ import java.util.regex.Pattern; /** {@code JavaCommentsHelper} extends {@link CommentsHelper} to rewrite Java comments. */ -public final class JavaCommentsHelper implements CommentsHelper { +final class JavaCommentsHelper implements CommentsHelper { - private final JavaFormatterOptions options; private final String lineSeparator; + private final JavaFormatterOptions options; + private final ImmutableSet markdownJavadocPositions; - public JavaCommentsHelper(String lineSeparator, JavaFormatterOptions options) { + JavaCommentsHelper( + String lineSeparator, + JavaFormatterOptions options, + ImmutableSet markdownJavadocPositions) { this.lineSeparator = lineSeparator; this.options = options; + this.markdownJavadocPositions = markdownJavadocPositions; } @Override @@ -43,21 +49,33 @@ public String rewrite(Tok tok, int maxWidth, int column0) { return tok.getOriginalText(); } String text = tok.getOriginalText(); - if (tok.isJavadocComment()) { - text = JavadocFormatter.formatJavadoc(text, column0, options); + if (tok.isJavadocComment() && options.formatJavadoc()) { + if (text.startsWith("///")) { + if (markdownJavadocPositions.contains(tok.getPosition())) { + return JavadocFormatter.formatJavadoc(text, column0); + } + } else { + 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 indentLineComments(tok, 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 @@ -91,8 +109,8 @@ private String preserveIndentation(List lines, int column0) { } // Wraps and re-indents line comments. - private String indentLineComments(List lines, int column0) { - lines = wrapLineComments(lines, column0, options); + private String indentLineComments(Tok tok, List lines, int column0) { + lines = wrapLineComments(tok, lines, column0); StringBuilder builder = new StringBuilder(); builder.append(lines.get(0).trim()); String indentString = Strings.repeat(" ", column0); @@ -102,21 +120,33 @@ private String indentLineComments(List lines, int column0) { return builder.toString(); } + // Preserve special `//noinspection` and `//$NON-NLS-x$` comments used by IDEs, which cannot + // contain leading spaces. private static final Pattern LINE_COMMENT_MISSING_SPACE_PREFIX = - Pattern.compile("^(//+)(?!noinspection)[^\\s/]"); + Pattern.compile("^(//+)(?!noinspection|\\$NON-NLS-\\d+\\$)[^\\s/]"); - private List wrapLineComments( - List lines, int column0, JavaFormatterOptions options) { + private List wrapLineComments(Tok tok, List lines, int column0) { List result = new ArrayList<>(); for (String line : lines) { + if (markdownJavadocPositions.contains(tok.getPosition())) { + // Don't wrap markdown comments. Eventually we will format them properly, but for now at + // least don't mangle them by wrapping with `// ` on the continuation lines. + result.add(line); + continue; + } // Add missing leading spaces to line comments: `//foo` -> `// foo`. Matcher matcher = LINE_COMMENT_MISSING_SPACE_PREFIX.matcher(line); if (matcher.find()) { int length = matcher.group(1).length(); line = Strings.repeat("/", length) + " " + line.substring(length); } - while (line.length() + column0 > options.maxLineLength()) { - int idx = options.maxLineLength() - column0; + if (line.startsWith("// MOE:")) { + // don't wrap comments for https://github.com/google/MOE + result.add(line); + continue; + } + while (line.length() + column0 > Formatter.MAX_LINE_LENGTH) { + int idx = Formatter.MAX_LINE_LENGTH - column0; // only break on whitespace characters, and ignore the leading `// ` while (idx >= 2 && !CharMatcher.whitespace().matches(line.charAt(idx))) { idx--; @@ -173,4 +203,3 @@ private static boolean javadocShaped(List lines) { return true; } } - diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java index 28abbd05d..509e0d33d 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,9 @@ package com.google.googlejavaformat.java; +import static java.util.Objects.requireNonNull; + +import com.google.auto.value.AutoBuilder; import com.google.errorprone.annotations.Immutable; /** @@ -25,14 +28,16 @@ *

The goal of google-java-format is to provide consistent formatting, and to free developers * from arguments over style choices. It is an explicit non-goal to support developers' individual * preferences, and in fact it would work directly against our primary goals. + * + * @param style Returns the code style. */ @Immutable -public class JavaFormatterOptions { - - static final int DEFAULT_MAX_LINE_LENGTH = 100; +public record JavaFormatterOptions(boolean formatJavadoc, boolean reorderModifiers, Style style) { + public JavaFormatterOptions { + requireNonNull(style, "style"); + } public enum Style { - /** The default Google Java Style configuration. */ GOOGLE(1), @@ -50,20 +55,9 @@ int indentationMultiplier() { } } - private final Style style; - - private JavaFormatterOptions(Style style) { - this.style = style; - } - - /** Returns the maximum formatted width */ - public int maxLineLength() { - return DEFAULT_MAX_LINE_LENGTH; - } - - /** Returns the multiplier for the unit of indent */ + /** Returns the multiplier for the unit of indent. */ public int indentationMultiplier() { - return style.indentationMultiplier(); + return style().indentationMultiplier(); } /** Returns the default formatting options. */ @@ -73,22 +67,22 @@ public static JavaFormatterOptions defaultOptions() { /** Returns a builder for {@link JavaFormatterOptions}. */ public static Builder builder() { - return new Builder(); + return new AutoBuilder_JavaFormatterOptions_Builder() + .style(Style.GOOGLE) + .formatJavadoc(true) + .reorderModifiers(true); } /** A builder for {@link JavaFormatterOptions}. */ - public static class Builder { - private Style style = Style.GOOGLE; + @AutoBuilder + 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..879a83c50 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java @@ -33,27 +33,33 @@ 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 { +final class JavaInput extends Input { /** * A {@code JavaInput} is a sequence of {@link Tok}s that cover the Java input. A {@link Tok} is * either a token (if {@code isToken()}), or a non-token, which is a comment (if {@code @@ -154,7 +160,13 @@ public boolean isSlashStarComment() { @Override public boolean isJavadocComment() { - return text.startsWith("/**") && text.length() > 4; + // comments like `/***` or `////` are also javadoc, but their formatting probably won't be + // improved by the javadoc formatter + return ((text.startsWith("/**") && !text.startsWith("/***")) + || (Runtime.version().feature() >= 23 + && text.startsWith("///") + && !text.startsWith("////"))) + && text.length() > 4; } @Override @@ -267,7 +279,7 @@ public String toString() { * @param text the input text * @throws FormatterException if the input cannot be parsed */ - public JavaInput(String text) throws FormatterException { + JavaInput(String text) throws FormatterException { this.text = checkNotNull(text); setLines(ImmutableList.copyOf(Newlines.lineIterator(text))); ImmutableList toks = buildToks(text); @@ -308,7 +320,7 @@ private static ImmutableMap makePositionToColumnMap(List for (Tok tok : toks) { builder.put(tok.getPosition(), tok.getColumn()); } - return builder.build(); + return builder.buildOrThrow(); } /** @@ -345,7 +357,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 +370,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 +487,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 +565,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 +606,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()) { + private 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 +640,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]; } @@ -651,19 +703,16 @@ public int getColumnNumber(int inputPosition) { // TODO(cushon): refactor JavaInput so the CompilationUnit can be passed into // the constructor. - public void setCompilationUnit(JCCompilationUnit unit) { + void setCompilationUnit(JCCompilationUnit unit) { this.unit = unit; } - public RangeSet characterRangesToTokenRanges(Collection> characterRanges) + 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 144516ecd..05abb6ad1 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,35 +30,51 @@ import static com.google.googlejavaformat.java.Trees.operatorName; import static com.google.googlejavaformat.java.Trees.precedence; import static com.google.googlejavaformat.java.Trees.skipParen; -import static org.openjdk.source.tree.Tree.Kind.ANNOTATION; -import static org.openjdk.source.tree.Tree.Kind.ARRAY_ACCESS; -import static org.openjdk.source.tree.Tree.Kind.ASSIGNMENT; -import static org.openjdk.source.tree.Tree.Kind.BLOCK; -import static org.openjdk.source.tree.Tree.Kind.EXTENDS_WILDCARD; -import static org.openjdk.source.tree.Tree.Kind.IF; -import static org.openjdk.source.tree.Tree.Kind.METHOD_INVOCATION; -import static org.openjdk.source.tree.Tree.Kind.NEW_ARRAY; -import static org.openjdk.source.tree.Tree.Kind.NEW_CLASS; -import static org.openjdk.source.tree.Tree.Kind.STRING_LITERAL; -import static org.openjdk.source.tree.Tree.Kind.UNION_TYPE; -import static org.openjdk.source.tree.Tree.Kind.VARIABLE; - +import static com.sun.source.tree.Tree.Kind.ANNOTATION; +import static com.sun.source.tree.Tree.Kind.ARRAY_ACCESS; +import static com.sun.source.tree.Tree.Kind.ASSIGNMENT; +import static com.sun.source.tree.Tree.Kind.BLOCK; +import static com.sun.source.tree.Tree.Kind.EXTENDS_WILDCARD; +import static com.sun.source.tree.Tree.Kind.IF; +import static com.sun.source.tree.Tree.Kind.METHOD_INVOCATION; +import static com.sun.source.tree.Tree.Kind.NEW_ARRAY; +import static com.sun.source.tree.Tree.Kind.NEW_CLASS; +import static com.sun.source.tree.Tree.Kind.STRING_LITERAL; +import static com.sun.source.tree.Tree.Kind.UNION_TYPE; +import static com.sun.source.tree.Tree.Kind.VARIABLE; +import static com.sun.tools.javac.code.Flags.COMPACT_RECORD_CONSTRUCTOR; +import static com.sun.tools.javac.code.Flags.RECORD; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +import com.google.auto.value.AutoOneOf; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; +import com.google.common.base.Predicate; import com.google.common.base.Throwables; import com.google.common.base.Verify; import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Multiset; import com.google.common.collect.PeekingIterator; +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.Streams; +import com.google.common.collect.TreeRangeSet; +import com.google.errorprone.annotations.CheckReturnValue; import com.google.googlejavaformat.CloseOp; import com.google.googlejavaformat.Doc; import com.google.googlejavaformat.Doc.FillMode; import com.google.googlejavaformat.FormattingError; import com.google.googlejavaformat.Indent; import com.google.googlejavaformat.Input; +import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.Op; import com.google.googlejavaformat.OpenOp; import com.google.googlejavaformat.OpsBuilder; @@ -65,90 +82,109 @@ 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.Collections; +import java.util.Collection; +import java.util.Comparator; import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; import java.util.regex.Pattern; -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 java.util.stream.Stream; +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 { +class JavaInputAstVisitor extends TreePathScanner { /** Direction for Annotations (usually VERTICAL). */ - enum Direction { + private enum Direction { VERTICAL, HORIZONTAL; @@ -158,7 +194,7 @@ boolean isVertical() { } /** Whether to break or not. */ - enum BreakOrNot { + private enum BreakOrNot { YES, NO; @@ -168,7 +204,7 @@ boolean isYes() { } /** Whether to collapse empty blocks. */ - enum CollapseEmptyOrNot { + private enum CollapseEmptyOrNot { YES, NO; @@ -182,17 +218,13 @@ boolean isYes() { } /** Whether to allow leading blank lines in blocks. */ - enum AllowLeadingBlankLine { + private enum AllowLeadingBlankLine { YES, NO; - - static AllowLeadingBlankLine valueOf(boolean b) { - return b ? YES : NO; - } } /** Whether to allow trailing blank lines in blocks. */ - enum AllowTrailingBlankLine { + private enum AllowTrailingBlankLine { YES, NO; @@ -202,17 +234,7 @@ static AllowTrailingBlankLine valueOf(boolean b) { } /** Whether to include braces. */ - enum BracesOrNot { - YES, - NO; - - boolean isYes() { - return this == YES; - } - } - - /** Whether or not to include dimensions. */ - enum DimensionsOrNot { + private enum BracesOrNot { YES, NO; @@ -221,26 +243,8 @@ boolean isYes() { } } - /** Whether or not the declaration is Varargs. */ - enum VarArgsOrNot { - YES, - NO; - - static VarArgsOrNot valueOf(boolean b) { - return b ? YES : NO; - } - - boolean isYes() { - return this == YES; - } - - static VarArgsOrNot fromVariable(VariableTree node) { - return valueOf((((JCTree.JCVariableDecl) node).mods.flags & Flags.VARARGS) == Flags.VARARGS); - } - } - - /** Whether the formal parameter declaration is a receiver. */ - enum ReceiverParameter { + /** Whether these declarations are the first in the block. */ + private enum FirstDeclarationsOrNot { YES, NO; @@ -249,17 +253,25 @@ boolean isYes() { } } - /** Whether these declarations are the first in the block. */ - enum FirstDeclarationsOrNot { - YES, - NO; + // TODO(cushon): generalize this + private static final ImmutableMultimap TYPE_ANNOTATIONS = typeAnnotations(); - boolean isYes() { - return this == YES; + private static ImmutableSetMultimap typeAnnotations() { + ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder(); + for (String annotation : + ImmutableList.of( + "org.jspecify.annotations.NonNull", + "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(); } private final OpsBuilder builder; + private final ImmutableSet.Builder markdownJavadocPositions; private static final Indent.Const ZERO = Indent.Const.ZERO; private final int indentMultiplier; @@ -268,6 +280,8 @@ boolean isYes() { private final Indent.Const plusTwo; private final Indent.Const plusFour; + private final Set typeAnnotationSimpleNames = new HashSet<>(); + private static final ImmutableList breakList(Optional breakTag) { return ImmutableList.of(Doc.Break.make(Doc.FillMode.UNIFIED, " ", ZERO, breakTag)); } @@ -283,8 +297,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. @@ -296,9 +308,13 @@ private static final ImmutableList forceBreakList(Optional breakTa * * @param builder the {@link OpsBuilder} */ - public JavaInputAstVisitor(OpsBuilder builder, int indentMultiplier) { + JavaInputAstVisitor( + OpsBuilder builder, + int indentMultiplier, + ImmutableSet.Builder markdownJavadocPositions) { this.builder = builder; this.indentMultiplier = indentMultiplier; + this.markdownJavadocPositions = markdownJavadocPositions; minusTwo = Indent.Const.make(-2, indentMultiplier); minusFour = Indent.Const.make(-4, indentMultiplier); plusTwo = Indent.Const.make(+2, indentMultiplier); @@ -314,6 +330,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 jcAnyPattern) { + visitJcAnyPattern(jcAnyPattern); + return null; + } inExpression.addLast(tree instanceof ExpressionTree || inExpression.peekLast()); int previous = builder.depth(); try { @@ -331,15 +353,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()) { @@ -348,65 +371,88 @@ public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { scan(importDeclaration, null); builder.forcedBreak(); } - first = false; + afterFirstToken = true; } dropEmptyDeclarations(); for (Tree type : node.getTypeDecls()) { - if (type.getKind() == Tree.Kind.IMPORT) { - // javac treats extra semicolons in the import list as type declarations - // TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed - continue; - } - if (!first) { + 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; } - /** Skips over extra semi-colons at the top-level, or in a class member declaration lists. */ + private 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 semicolons at the top-level, or in a class member declaration lists. */ private void dropEmptyDeclarations() { if (builder.peekToken().equals(Optional.of(";"))) { while (builder.peekToken().equals(Optional.of(";"))) { + builder.forcedBreak(); markForPartialFormat(); token(";"); } } } + private void recordMarkdownJavadocPosition(Tree tree) { + OptionalInt javadocPosition = javadocPosition(tree); + if (javadocPosition.isPresent()) { + int pos = javadocPosition.getAsInt(); + if (builder.getInput().getText().startsWith("///", pos)) { + markdownJavadocPositions.add(pos); + } + } + } + + // 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; + } + recordMarkdownJavadocPosition(tree); 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; } - public void visitAnnotationType(ClassTree node) { + private void visitImplicitClass(ClassTree node) { + builder.open(minusTwo); + addBodyDeclarations(node.getMembers(), BracesOrNot.NO, FirstDeclarationsOrNot.YES); + builder.close(); + } + + private void visitAnnotationType(ClassTree node) { sync(node); builder.open(ZERO); - visitAndBreakModifiers( - node.getModifiers(), - Direction.VERTICAL, - /* declarationAnnotationBreak= */ Optional.absent()); + typeDeclarationModifiers(node.getModifiers()); builder.open(ZERO); token("@"); token("interface"); @@ -439,14 +485,14 @@ public Void visitNewArray(NewArrayTree node, Void unused) { builder.space(); TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getType(), SortedDims.YES); - Tree base = extractedDims.node; + Tree base = extractedDims.node(); 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(extractedDims.dims); + annotations.addAll(node.getDimAnnotations()); + annotations.addAll(extractedDims.dims()); scan(base, null); builder.open(ZERO); @@ -463,7 +509,7 @@ public Void visitNewArray(NewArrayTree node, Void unused) { return null; } - public boolean visitArrayInitializer(List expressions) { + private boolean visitArrayInitializer(List expressions) { int cols; if (expressions.isEmpty()) { tokenBreakTrailingComment("{", plusTwo); @@ -475,9 +521,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); @@ -492,7 +538,7 @@ public boolean visitArrayInitializer(List expressions) } builder.guessToken(","); builder.close(); - first = false; + afterFirstToken = true; } builder.breakOp(minusTwo); builder.close(); @@ -523,15 +569,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) { @@ -566,8 +612,8 @@ public Void visitArrayType(ArrayTypeTree node, Void unused) { 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); + scan(extractedDims.node(), null); + Deque> dims = new ArrayDeque<>(extractedDims.dims()); maybeAddDims(dims); Verify.verify(dims.isEmpty()); builder.close(); @@ -665,9 +711,10 @@ public Void visitNewClass(NewClassTree node, Void unused) { builder.space(); addTypeArguments(node.getTypeArguments(), plusFour); if (node.getClassBody() != null) { - builder.addAll( + List annotations = visitModifiers( - node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.absent())); + node.getClassBody().getModifiers(), Direction.HORIZONTAL, Optional.empty()); + visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES); } scan(node.getIdentifier(), null); addArguments(node.getArguments(), plusFour); @@ -754,7 +801,7 @@ public Void visitEnhancedForLoop(EnhancedForLoopTree node, Void unused) { node.getVariable(), Optional.of(node.getExpression()), ":", - /* trailing= */ Optional.absent()); + /* trailing= */ Optional.empty()); builder.close(); token(")"); builder.close(); @@ -785,13 +832,10 @@ private void visitEnumConstantDeclaration(VariableTree enumConstant) { } } - public boolean visitEnumDeclaration(ClassTree node) { + private boolean visitEnumDeclaration(ClassTree node) { sync(node); builder.open(ZERO); - visitAndBreakModifiers( - node.getModifiers(), - Direction.VERTICAL, - /* declarationAnnotationBreak= */ Optional.absent()); + typeDeclarationModifiers(node.getModifiers()); builder.open(plusFour); token("enum"); builder.breakOp(" "); @@ -805,14 +849,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(); @@ -823,8 +867,8 @@ public boolean visitEnumDeclaration(ClassTree node) { ArrayList enumConstants = new ArrayList<>(); ArrayList members = new ArrayList<>(); for (Tree member : node.getMembers()) { - if (member instanceof JCTree.JCVariableDecl) { - JCTree.JCVariableDecl variableDecl = (JCTree.JCVariableDecl) member; + if (member instanceof JCTree.JCVariableDecl variableDecl) { + if ((variableDecl.mods.flags & Flags.ENUM) == Flags.ENUM) { enumConstants.add(variableDecl); continue; @@ -833,27 +877,41 @@ public boolean visitEnumDeclaration(ClassTree node) { members.add(member); } if (enumConstants.isEmpty() && members.isEmpty()) { - builder.open(ZERO); - builder.blankLineWanted(BlankLineWanted.NO); - token("}"); - builder.close(); + if (builder.peekToken().equals(Optional.of(";"))) { + builder.open(plusTwo); + builder.forcedBreak(); + token(";"); + builder.forcedBreak(); + dropEmptyDeclarations(); + builder.close(); + builder.open(ZERO); + builder.forcedBreak(); + builder.blankLineWanted(BlankLineWanted.NO); + token("}", plusTwo); + builder.close(); + } else { + builder.open(ZERO); + builder.blankLineWanted(BlankLineWanted.NO); + token("}"); + builder.close(); + } } else { builder.open(plusTwo); builder.blankLineWanted(BlankLineWanted.NO); builder.forcedBreak(); builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; for (VariableTree enumConstant : enumConstants) { - if (!first) { + if (afterFirstToken) { token(","); builder.forcedBreak(); builder.blankLineWanted(BlankLineWanted.PRESERVE); } markForPartialFormat(); visitEnumConstantDeclaration(enumConstant); - first = false; + afterFirstToken = true; } - if (builder.peekToken().or("").equals(",")) { + if (builder.peekToken().orElse("").equals(",")) { token(","); builder.forcedBreak(); // The ";" goes on its own line. } @@ -877,23 +935,79 @@ public boolean visitEnumDeclaration(ClassTree node) { return false; } + private 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; @@ -917,42 +1031,45 @@ public Void visitVariable(VariableTree node, Void unused) { return null; } - void visitVariables( + private void visitVariables( List fragments, DeclarationKind declarationKind, Direction annotationDirection) { if (fragments.size() == 1) { VariableTree fragment = fragments.get(0); + if (declarationKind == DeclarationKind.FIELD) { + recordMarkdownJavadocPosition(fragment); + } declareOne( declarationKind, annotationDirection, Optional.of(fragment.getModifiers()), fragment.getType(), - VarArgsOrNot.fromVariable(fragment), - /* varargsAnnotations= */ ImmutableList.of(), - fragment.getName(), + /* name= */ fragment.getName(), "", "=", - Optional.fromNullable(fragment.getInitializer()), + Optional.ofNullable(fragment.getInitializer()), Optional.of(";"), - /* receiverExpression= */ Optional.absent(), - Optional.fromNullable(variableFragmentDims(true, 0, fragment.getType()))); + /* receiverExpression= */ Optional.empty(), + Optional.ofNullable(variableFragmentDims(false, 0, fragment.getType()))); } else { declareMany(fragments, annotationDirection); } } - private TypeWithDims variableFragmentDims(boolean first, int leadingDims, Tree type) { + private static TypeWithDims variableFragmentDims( + boolean afterFirstToken, int leadingDims, Tree type) { if (type == null) { return null; } - if (first) { + if (!afterFirstToken) { return DimensionHelpers.extractDims(type, SortedDims.YES); } TypeWithDims dims = DimensionHelpers.extractDims(type, SortedDims.NO); return new TypeWithDims( - null, leadingDims > 0 ? dims.dims.subList(0, dims.dims.size() - leadingDims) : dims.dims); + null, + leadingDims > 0 ? dims.dims().subList(0, dims.dims().size() - leadingDims) : dims.dims()); } @Override @@ -974,15 +1091,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(); @@ -1039,11 +1156,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 { @@ -1067,7 +1184,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) { @@ -1088,20 +1205,61 @@ 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(); } visitName(node.getQualifiedIdentifier()); token(";"); - // TODO(cushon): remove this if https://bugs.openjdk.java.net/browse/JDK-8027682 is fixed - dropEmptyDeclarations(); 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 switch (importTree.getQualifiedIdentifier()) { + case IdentifierTree identifierTree -> identifierTree.getName(); + case MemberSelectTree memberSelectTree -> memberSelectTree.getIdentifier(); + case Tree tree -> throw new AssertionError(tree); + }; + } + @Override public Void visitBinary(BinaryTree node, Void unused) { sync(node); @@ -1134,7 +1292,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; @@ -1144,15 +1306,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; @@ -1179,14 +1341,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(")"); @@ -1200,14 +1365,14 @@ public Void visitLambdaExpression(LambdaExpressionTree node, Void unused) { } else { builder.breakOp(" "); } - if (node.getBody().getKind() == Tree.Kind.BLOCK) { - visitBlock( - (BlockTree) node.getBody(), - CollapseEmptyOrNot.YES, - AllowLeadingBlankLine.NO, - AllowTrailingBlankLine.NO); - } else { - scan(node.getBody(), null); + switch (node.getBody()) { + case BlockTree blockTree -> + visitBlock( + blockTree, + CollapseEmptyOrNot.YES, + AllowLeadingBlankLine.NO, + AllowTrailingBlankLine.NO); + case Tree expressionTree -> scan(expressionTree, null); } builder.close(); return null; @@ -1225,17 +1390,17 @@ public Void visitAnnotation(AnnotationTree node, Void unused) { token("@"); scan(node.getAnnotationType(), null); if (!node.getArguments().isEmpty()) { - builder.open(plusTwo); + builder.open(plusFour); token("("); builder.breakOp(); - boolean first = true; + boolean afterFirstToken = false; // Format the member value pairs one-per-line if any of them are // initialized with arrays. boolean hasArrayInitializer = Iterables.any(node.getArguments(), JavaInputAstVisitor::isArrayValue); for (ExpressionTree argument : node.getArguments()) { - if (!first) { + if (afterFirstToken) { token(","); if (hasArrayInitializer) { builder.forcedBreak(); @@ -1243,16 +1408,15 @@ public Void visitAnnotation(AnnotationTree node, Void unused) { builder.breakOp(" "); } } - if (argument instanceof AssignmentTree) { - visitAnnotationArgument((AssignmentTree) argument); + if (argument instanceof AssignmentTree assignmentTree) { + visitAnnotationArgument(assignmentTree); } else { scan(argument, null); } - first = false; + afterFirstToken = true; } - builder.breakOp(UNIFIED, "", minusTwo, /* optionalTag= */ Optional.absent()); + token(")"); builder.close(); - token(")", plusTwo); builder.close(); return null; @@ -1265,14 +1429,14 @@ public Void visitAnnotation(AnnotationTree node, Void unused) { } private static boolean isArrayValue(ExpressionTree argument) { - if (!(argument instanceof AssignmentTree)) { + if (!(argument instanceof AssignmentTree assignmentTree)) { return false; } - ExpressionTree expression = ((AssignmentTree) argument).getExpression(); - return expression instanceof NewArrayTree && ((NewArrayTree) expression).getType() == null; + ExpressionTree expression = assignmentTree.getExpression(); + return expression instanceof NewArrayTree newArrayTree && newArrayTree.getType() == null; } - public void visitAnnotationArgument(AssignmentTree node) { + private void visitAnnotationArgument(AssignmentTree node) { boolean isArrayInitializer = node.getExpression().getKind() == NEW_ARRAY; sync(node); builder.open(isArrayInitializer ? ZERO : plusFour); @@ -1291,30 +1455,35 @@ public void visitAnnotationArgument(AssignmentTree node) { @Override public Void visitAnnotatedType(AnnotatedTypeTree node, Void unused) { sync(node); - ExpressionTree base = node.getUnderlyingType(); - if (base instanceof MemberSelectTree) { - MemberSelectTree selectTree = (MemberSelectTree) base; - scan(selectTree.getExpression(), null); - token("."); - visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO); - builder.breakToFill(" "); - visit(selectTree.getIdentifier()); - } else if (base instanceof ArrayTypeTree) { - visitAnnotatedArrayType(node); - } else { - visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO); - builder.breakToFill(" "); - scan(base, null); + switch (node.getUnderlyingType()) { + case MemberSelectTree selectTree -> { + scan(selectTree.getExpression(), null); + token("."); + visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO); + builder.breakToFill(" "); + visit(selectTree.getIdentifier()); + } + case ArrayTypeTree unusedTree -> visitAnnotatedArrayType(node); + case ExpressionTree base -> { + visitAnnotations(node.getAnnotations(), BreakOrNot.NO, BreakOrNot.NO); + builder.breakToFill(" "); + scan(base, null); + } } return null; } @Override public Void visitMethod(MethodTree node, Void unused) { + recordMarkdownJavadocPosition(node); 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++) { @@ -1325,17 +1494,31 @@ public Void visitMethod(MethodTree node, Void unused) { } } } - builder.addAll( + List typeAnnotations = visitModifiers( - annotations, Direction.VERTICAL, /* declarationAnnotationBreak= */ Optional.absent())); + 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); + baseReturnType = extractedDims.node(); + dims = new ArrayDeque<>(extractedDims.dims()); + } else { + verticalAnnotations(typeAnnotations); + typeAnnotations = ImmutableList.of(); } builder.open(plusFour); @@ -1343,37 +1526,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); @@ -1384,7 +1576,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(); } @@ -1394,12 +1588,14 @@ public Void visitMethod(MethodTree node, Void unused) { builder.open(Indent.If.make(breakBeforeType, plusFour, ZERO)); builder.open(ZERO); { - if (!node.getParameters().isEmpty() || node.getReceiverParameter() != null) { - // Break before args. - builder.breakToFill(""); - visitFormals(Optional.fromNullable(node.getReceiverParameter()), node.getParameters()); + if (!isRecordConstructor) { + if (!node.getParameters().isEmpty() || node.getReceiverParameter() != null) { + // Break before args. + builder.breakToFill(""); + visitFormals(Optional.ofNullable(node.getReceiverParameter()), node.getParameters()); + } + token(")"); } - token(")"); if (dims != null) { maybeAddDims(dims); } @@ -1468,10 +1664,89 @@ private void methodBody(MethodTree node) { @Override public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { sync(node); + if (handleLogStatement(node)) { + return null; + } visitDot(node); return null; } + /** + * Special-cases log statements, to output: + * + *

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

Instead of: + * + *

{@code
+   * logger
+   *     .atInfo()
+   *     .log(
+   *         "Number of foos: %d, foos.size());
+   * }
+ */ + private boolean handleLogStatement(MethodInvocationTree node) { + Name methodName = getMethodName(node); + if (!methodName.contentEquals("log") && !methodName.contentEquals("logVarargs")) { + return false; + } + Deque parts = new ArrayDeque<>(); + ExpressionTree curr = node; + while (curr instanceof MethodInvocationTree method) { + parts.addFirst(method); + if (!LOG_METHODS.contains(getMethodName(method).toString())) { + return false; + } + curr = Trees.getMethodReceiver(method); + } + if (!(curr instanceof IdentifierTree)) { + return false; + } + parts.addFirst(curr); + visitDotWithPrefix( + ImmutableList.copyOf(parts), false, ImmutableList.of(parts.size() - 1), INDEPENDENT); + return true; + } + + private static final ImmutableSet LOG_METHODS = + ImmutableSet.of( + "at", + "atConfig", + "atDebug", + "atFine", + "atFiner", + "atFinest", + "atInfo", + "atMostEvery", + "atSevere", + "atWarning", + "every", + "log", + "logVarargs", + "perUnique", + "withCause", + "withStackTrace"); + + private static List handleStream(List parts) { + return indexes( + parts.stream(), + p -> { + if (!(p instanceof MethodInvocationTree methodInvocationTree)) { + return false; + } + Name name = getMethodName(methodInvocationTree); + return Stream.of("stream", "parallelStream", "toBuilder") + .anyMatch(name::contentEquals); + }) + .collect(toList()); + } + + private static Stream indexes(Stream stream, Predicate predicate) { + return Streams.mapWithIndex(stream, (x, i) -> predicate.apply(x) ? i : -1).filter(x -> x != -1); + } @Override public Void visitMemberSelect(MemberSelectTree node, Void unused) { @@ -1484,11 +1759,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(); } @@ -1496,6 +1784,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()) { @@ -1526,14 +1822,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(); @@ -1575,16 +1871,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; } @@ -1594,44 +1889,35 @@ private boolean ambiguousUnaryOperator(UnaryTree node, String operatorName) { return true; } + private JCTree.Tag unaryTag(ExpressionTree expression) { + return switch (expression) { + case UnaryTree unary -> ((JCTree) unary).getTag(); + case LiteralTree literal + when isUnaryMinusLiteral(getSourceForNode(literal, getCurrentPath())) -> + JCTree.Tag.MINUS; + default -> 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; } - public boolean visit(Name name) { + private boolean visit(Name name) { token(name.toString()); return false; } @@ -1649,7 +1935,7 @@ public Void visitReturn(ReturnTree node, Void unused) { } // TODO(cushon): is this worth special-casing? - boolean visitSingleMemberAnnotation(AnnotationTree node) { + private boolean visitSingleMemberAnnotation(AnnotationTree node) { if (node.getArguments().size() != 1) { return false; } @@ -1676,46 +1962,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; + } + + private 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 @@ -1753,27 +2090,25 @@ 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) { - VariableTree variableTree = (VariableTree) resource; + if (resource instanceof VariableTree variableTree) { + declareOne( DeclarationKind.PARAMETER, fieldAnnotationDirection(variableTree.getModifiers()), Optional.of(variableTree.getModifiers()), variableTree.getType(), - VarArgsOrNot.NO, - /* varargsAnnotations= */ ImmutableList.of(), - variableTree.getName(), + /* name= */ variableTree.getName(), "", "=", - Optional.fromNullable(variableTree.getInitializer()), - /* trailing= */ Optional.absent(), - /* receiverExpression= */ Optional.absent(), - /* typeWithDims= */ Optional.absent()); + Optional.ofNullable(variableTree.getInitializer()), + /* trailing= */ Optional.empty(), + /* receiverExpression= */ Optional.empty(), + /* typeWithDims= */ Optional.empty()); } else { // TODO(cushon): think harder about what to do with `try (resource1; resource2) {}` scan(resource, null); @@ -1782,7 +2117,7 @@ public Void visitTry(TryTree node, Void unused) { token(";"); builder.space(); } - first = false; + afterFirstToken = true; } if (builder.peekToken().equals(Optional.of(";"))) { token(";"); @@ -1819,16 +2154,13 @@ public Void visitTry(TryTree node, Void unused) { return null; } - public void visitClassDeclaration(ClassTree node) { + private void visitClassDeclaration(ClassTree node) { sync(node); - List breaks = - visitModifiers( - node.getModifiers(), - Direction.VERTICAL, - /* declarationAnnotationBreak= */ Optional.absent()); + typeDeclarationModifiers(node.getModifiers()); + List permitsTypes = node.getPermitsClause(); boolean hasSuperclassType = node.getExtendsClause() != null; boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty(); - builder.addAll(breaks); + boolean hasPermitsTypes = !permitsTypes.isEmpty(); token(node.getKind() == Tree.Kind.INTERFACE ? "interface" : "class"); builder.space(); visit(node.getSimpleName()); @@ -1840,7 +2172,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(" "); @@ -1848,22 +2180,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) { @@ -1886,15 +2206,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(); @@ -1944,19 +2264,19 @@ public Void visitWildcard(WildcardTree node, Void unused) { // Helper methods. /** Helper method for annotations. */ - void visitAnnotations( + private 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(" "); @@ -1964,6 +2284,14 @@ void visitAnnotations( } } + private void verticalAnnotations(List annotations) { + for (AnnotationTree annotation : annotations) { + builder.forcedBreak(); + scan(annotation, null); + builder.forcedBreak(); + } + } + /** Helper method for blocks. */ private void visitBlock( BlockTree node, @@ -2015,30 +2343,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; + 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( @@ -2051,12 +2380,21 @@ private void visitStatements(List statements) { } } + private 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( + private 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 @@ -2065,37 +2403,51 @@ public Void visitModifiers(ModifiersTree node, Void unused) { } /** Output combined modifiers and annotations and returns the trailing break. */ - private List visitModifiers( + @CheckReturnValue + private ImmutableList visitModifiers( ModifiersTree modifiersTree, Direction annotationsDirection, Optional declarationAnnotationBreak) { return visitModifiers( - modifiersTree.getAnnotations(), annotationsDirection, declarationAnnotationBreak); + modifiersTree, + modifiersTree.getAnnotations(), + annotationsDirection, + declarationAnnotationBreak); } - private List visitModifiers( + @CheckReturnValue + private 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(); @@ -2103,49 +2455,196 @@ private List visitModifiers( annotationsDirection.isVertical() ? forceBreakList(declarationAnnotationBreak) : breakList(declarationAnnotationBreak); - if (annotations.isEmpty() && !nextIsModifier()) { - return trailingBreak; + if (declarationModifiers.isEmpty()) { + builder.addAll(trailingBreak); + return splitModifiers.typeAnnotations(); } if (lastWasAnnotation) { builder.addAll(trailingBreak); } builder.open(ZERO); - first = true; - while (nextIsModifier() || !annotations.isEmpty()) { - if (!first) { - builder.addAll(breakFillList(Optional.absent())); - } - if (nextIsModifier()) { - token(builder.peekToken().get()); - } else { - scan(annotations.removeFirst(), null); - lastWasAnnotation = true; + afterFirstToken = false; + while (!declarationModifiers.isEmpty()) { + if (afterFirstToken) { + builder.addAll(breakFillList(Optional.empty())); } - first = false; + formatAnnotationOrModifier(declarationModifiers); + afterFirstToken = true; } builder.close(); - return breakFillList(Optional.absent()); - } - - boolean nextIsModifier() { - switch (builder.peekToken().get()) { - case "public": - case "protected": - case "private": - case "abstract": - case "static": - case "final": - case "transient": - case "volatile": - case "synchronized": - case "native": - case "strictfp": - case "default": - return true; - default: - return false; + builder.addAll(breakFillList(Optional.empty())); + return splitModifiers.typeAnnotations(); + } + + /** Represents an annotation or a modifier in a {@link ModifiersTree}. */ + @AutoOneOf(AnnotationOrModifier.Kind.class) + abstract static class AnnotationOrModifier implements Comparable { + enum Kind { + MODIFIER, + ANNOTATION + } + + abstract Kind getKind(); + + abstract AnnotationTree annotation(); + + abstract Input.Tok modifier(); + + static AnnotationOrModifier ofModifier(Input.Tok m) { + return AutoOneOf_JavaInputAstVisitor_AnnotationOrModifier.modifier(m); + } + + static AnnotationOrModifier ofAnnotation(AnnotationTree a) { + return AutoOneOf_JavaInputAstVisitor_AnnotationOrModifier.annotation(a); + } + + boolean isModifier() { + return getKind().equals(Kind.MODIFIER); + } + + boolean isAnnotation() { + return getKind().equals(Kind.ANNOTATION); + } + + int position() { + return switch (getKind()) { + case MODIFIER -> modifier().getPosition(); + case ANNOTATION -> getStartPosition(annotation()); + }; + } + + private static final Comparator COMPARATOR = + Comparator.comparingInt(AnnotationOrModifier::position); + + @Override + public int compareTo(AnnotationOrModifier o) { + return COMPARATOR.compare(this, o); + } + } + + /** + * The modifiers annotations for a declaration, grouped in to a prefix that contains all of the + * declaration annotations and modifiers, and a suffix of type annotations. + * + *

For examples like {@code @Deprecated public @Nullable Foo foo();}, this allows us to format + * {@code @Deprecated public} as declaration modifiers, and {@code @Nullable} as a type annotation + * on the return type. + */ + private record DeclarationModifiersAndTypeAnnotations( + ImmutableList declarationModifiers, + ImmutableList typeAnnotations) { + DeclarationModifiersAndTypeAnnotations { + requireNonNull(declarationModifiers, "declarationModifiers"); + requireNonNull(typeAnnotations, "typeAnnotations"); + } + + static DeclarationModifiersAndTypeAnnotations create( + ImmutableList declarationModifiers, + ImmutableList typeAnnotations) { + return new 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}. + */ + private 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); + } + } + + private boolean isTypeAnnotation(AnnotationTree annotationTree) { + Tree annotationType = annotationTree.getAnnotationType(); + return switch (annotationType) { + case IdentifierTree identifierTree -> + typeAnnotationSimpleNames.contains(identifierTree.getName()); + default -> false; + }; + } + + 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 @@ -2188,16 +2687,16 @@ private void visitUnionType(VariableTree declaration) { visitAndBreakModifiers( declaration.getModifiers(), Direction.HORIZONTAL, - /* declarationAnnotationBreak= */ Optional.absent()); + /* 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); } @@ -2208,17 +2707,15 @@ private void visitUnionType(VariableTree declaration) { declareOne( DeclarationKind.NONE, Direction.HORIZONTAL, - /* modifiers= */ Optional.absent(), + /* modifiers= */ Optional.empty(), last, - VarArgsOrNot.NO, - /* varargsAnnotations= */ ImmutableList.of(), - declaration.getName(), + /* name= */ declaration.getName(), /* op= */ "", "=", - Optional.fromNullable(declaration.getInitializer()), - /* trailing= */ Optional.absent(), - /* receiverExpression= */ Optional.absent(), - /* typeWithDims= */ Optional.absent()); + Optional.ofNullable(declaration.getInitializer()), + /* trailing= */ Optional.empty(), + /* receiverExpression= */ Optional.empty(), + /* typeWithDims= */ Optional.empty()); builder.close(); } @@ -2228,8 +2725,8 @@ private static void walkInfix( ExpressionTree expression, List operands, List operators) { - if (expression instanceof BinaryTree) { - BinaryTree binaryTree = (BinaryTree) expression; + if (expression instanceof BinaryTree binaryTree) { + if (precedence(binaryTree) == precedence) { walkInfix(precedence, binaryTree.getLeftOperand(), operands, operators); operators.add(operatorName(expression)); @@ -2248,38 +2745,36 @@ private void visitFormals( return; } builder.open(ZERO); - boolean first = true; + boolean afterFirstToken = false; if (receiver.isPresent()) { - // TODO(jdd): Use builders. + // TODO(user): Use builders. declareOne( DeclarationKind.PARAMETER, Direction.HORIZONTAL, Optional.of(receiver.get().getModifiers()), receiver.get().getType(), - VarArgsOrNot.NO, - /* varargsAnnotations= */ ImmutableList.of(), - receiver.get().getName(), + /* name= */ receiver.get().getName(), "", "", - /* initializer= */ Optional.absent(), - !parameters.isEmpty() ? Optional.of(",") : Optional.absent(), + /* initializer= */ Optional.empty(), + !parameters.isEmpty() ? Optional.of(",") : Optional.empty(), Optional.of(receiver.get().getNameExpression()), - /* typeWithDims= */ Optional.absent()); - first = false; + /* typeWithDims= */ Optional.empty()); + afterFirstToken = true; } for (int i = 0; i < parameters.size(); i++) { VariableTree parameter = parameters.get(i); - if (!first) { + if (afterFirstToken) { builder.breakOp(" "); } visitToDeclare( DeclarationKind.PARAMETER, Direction.HORIZONTAL, parameter, - /* initializer= */ Optional.absent(), + /* initializer= */ Optional.empty(), "=", - i < parameters.size() - 1 ? Optional.of(",") : /* a= */ Optional.absent()); - first = false; + i < parameters.size() - 1 ? Optional.of(",") : /* a= */ Optional.empty()); + afterFirstToken = true; } builder.close(); } @@ -2288,14 +2783,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; } } @@ -2308,6 +2803,7 @@ public Void visitIdentifier(IdentifierTree node, Void unused) { @Override public Void visitModule(ModuleTree node, Void unused) { + recordMarkdownJavadocPosition(node); for (AnnotationTree annotation : node.getAnnotations()) { scan(annotation, null); builder.forcedBreak(); @@ -2328,11 +2824,11 @@ public Void visitModule(ModuleTree node, Void unused) { builder.open(plusTwo); token("{"); builder.forcedBreak(); - Optional previousDirective = Optional.absent(); + Optional previousDirective = Optional.empty(); for (DirectiveTree directiveTree : node.getDirectives()) { markForPartialFormat(); builder.blankLineWanted( - previousDirective.transform(k -> !k.equals(directiveTree.getKind())).or(false) + previousDirective.map(k -> !k.equals(directiveTree.getKind())).orElse(false) ? BlankLineWanted.YES : BlankLineWanted.NO); builder.forcedBreak(); @@ -2359,14 +2855,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(); @@ -2425,17 +2921,18 @@ public Void visitUses(UsesTree node, Void unused) { /** Helper method for import declarations, names, and qualified names. */ private void visitName(Tree node) { Deque stack = new ArrayDeque<>(); - for (; node instanceof MemberSelectTree; node = ((MemberSelectTree) node).getExpression()) { - stack.addFirst(((MemberSelectTree) node).getIdentifier()); + while (node instanceof MemberSelectTree memberSelectTree) { + stack.addFirst(memberSelectTree.getIdentifier()); + node = memberSelectTree.getExpression(); } 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; } } @@ -2447,39 +2944,44 @@ private void visitToDeclare( String equals, Optional trailing) { sync(node); - boolean varargs = VarArgsOrNot.fromVariable(node).isYes(); - List varargsAnnotations = ImmutableList.of(); - Tree type = node.getType(); + Optional typeWithDims; + Tree type; + if (node.getType() != null) { + TypeWithDims extractedDims = DimensionHelpers.extractDims(node.getType(), SortedDims.YES); + typeWithDims = Optional.of(extractedDims); + type = extractedDims.node(); + } else { + typeWithDims = Optional.empty(); + type = null; + } declareOne( kind, annotationsDirection, Optional.of(node.getModifiers()), type, - VarArgsOrNot.valueOf(varargs), - varargsAnnotations, node.getName(), "", equals, initializer, trailing, - /* receiverExpression= */ Optional.absent(), - /* typeWithDims= */ Optional.absent()); + /* receiverExpression= */ Optional.empty(), + typeWithDims); } - /** Does not omit the leading '<', which should be associated with the type name. */ + /** Does not omit the leading {@code "<"}, which should be associated with the type name. */ private 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(); @@ -2493,7 +2995,7 @@ private void typeParametersRest( * * @param node0 the "." node */ - void visitDot(ExpressionTree node0) { + private void visitDot(ExpressionTree node0) { ExpressionTree node = node0; // collect a flattened list of "."-separated items @@ -2506,21 +3008,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); @@ -2549,9 +3049,11 @@ void visitDot(ExpressionTree node0) { } } + Set prefixes = new LinkedHashSet<>(); + // Check if the dot chain has a prefix that looks like a type name, so we can // treat the type name-shaped part as a single syntactic unit. - int prefixIndex = TypeNameClassifier.typePrefixLength(simpleNames(stack)); + TypeNameClassifier.typePrefixLength(simpleNames(stack)).ifPresent(prefixes::add); int invocationCount = 0; int firstInvocationIndex = -1; @@ -2587,23 +3089,22 @@ void visitDot(ExpressionTree node0) { // myField // .foo(); // - if (invocationCount == 1) { - prefixIndex = firstInvocationIndex; + if (invocationCount == 1 && firstInvocationIndex > 0) { + prefixes.add(firstInvocationIndex); } - if (prefixIndex == -1 && items.get(0) instanceof IdentifierTree) { - switch (((IdentifierTree) items.get(0)).getName().toString()) { - case "this": - case "super": - prefixIndex = 1; - break; - default: - break; + if (prefixes.isEmpty() && items.get(0) instanceof IdentifierTree identifierTree) { + switch (identifierTree.getName().toString()) { + case "this", "super" -> prefixes.add(1); + default -> {} } } - if (prefixIndex > 0) { - visitDotWithPrefix(items, needDot, prefixIndex); + List streamPrefixes = handleStream(items); + streamPrefixes.forEach(x -> prefixes.add(x.intValue())); + if (!prefixes.isEmpty()) { + visitDotWithPrefix( + items, needDot, prefixes, streamPrefixes.isEmpty() ? INDEPENDENT : UNIFIED); } else { visitRegularDot(items, needDot); } @@ -2695,22 +3196,30 @@ private boolean fillFirstArgument(ExpressionTree e, List items, * * @param items in the chain * @param needDot whether a leading dot is needed - * @param prefixIndex the index of the last item in the prefix + * @param prefixes the terminal indices of 'prefixes' of the expression that should be treated as + * a syntactic unit */ - private void visitDotWithPrefix(List items, boolean needDot, int prefixIndex) { + private void visitDotWithPrefix( + List items, + boolean needDot, + Collection prefixes, + FillMode prefixFillMode) { // Are there method invocations or field accesses after the prefix? - boolean trailingDereferences = prefixIndex >= 0 && prefixIndex < items.size() - 1; + boolean trailingDereferences = !prefixes.isEmpty() && getLast(prefixes) < items.size() - 1; builder.open(plusFour); - builder.open(trailingDereferences ? ZERO : ZERO); + for (int times = 0; times < prefixes.size(); times++) { + builder.open(ZERO); + } + Deque unconsumedPrefixes = new ArrayDeque<>(ImmutableSortedSet.copyOf(prefixes)); BreakTag nameTag = genSym(); for (int i = 0; i < items.size(); i++) { ExpressionTree e = items.get(i); if (needDot) { FillMode fillMode; - if (prefixIndex >= 0 && i <= prefixIndex) { - fillMode = FillMode.INDEPENDENT; + if (!unconsumedPrefixes.isEmpty() && i <= unconsumedPrefixes.peekFirst()) { + fillMode = prefixFillMode; } else { fillMode = FillMode.UNIFIED; } @@ -2720,8 +3229,9 @@ private void visitDotWithPrefix(List items, boolean needDot, int } BreakTag tyargTag = genSym(); dotExpressionUpToArgs(e, Optional.of(tyargTag)); - if (prefixIndex >= 0 && i == prefixIndex) { + if (!unconsumedPrefixes.isEmpty() && i == unconsumedPrefixes.peekFirst()) { builder.close(); + unconsumedPrefixes.removeFirst(); } Indent tyargIndent = Indent.If.make(tyargTag, plusFour, ZERO); @@ -2735,24 +3245,23 @@ private void visitDotWithPrefix(List items, boolean needDot, int } /** Returns the simple names of expressions in a "." chain. */ - private List simpleNames(Deque stack) { + private static ImmutableList simpleNames(Deque stack) { ImmutableList.Builder simpleNames = ImmutableList.builder(); OUTER: for (ExpressionTree expression : stack) { boolean isArray = expression.getKind() == ARRAY_ACCESS; expression = getArrayBase(expression); switch (expression.getKind()) { - case MEMBER_SELECT: - simpleNames.add(((MemberSelectTree) expression).getIdentifier().toString()); - break; - case IDENTIFIER: - simpleNames.add(((IdentifierTree) expression).getName().toString()); - break; - case METHOD_INVOCATION: + case MEMBER_SELECT -> + simpleNames.add(((MemberSelectTree) expression).getIdentifier().toString()); + case IDENTIFIER -> simpleNames.add(((IdentifierTree) expression).getName().toString()); + case METHOD_INVOCATION -> { simpleNames.add(getMethodName((MethodInvocationTree) expression).toString()); break OUTER; - default: + } + default -> { break OUTER; + } } if (isArray) { break OUTER; @@ -2764,44 +3273,42 @@ 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); } } /** - * Returns the base expression of an erray access, e.g. given {@code foo[0][0]} returns {@code + * Returns the base expression of an array access, e.g. given {@code foo[0][0]} returns {@code * foo}. */ - private ExpressionTree getArrayBase(ExpressionTree node) { - while (node instanceof ArrayAccessTree) { - node = ((ArrayAccessTree) node).getExpression(); + private static ExpressionTree getArrayBase(ExpressionTree node) { + while (node instanceof ArrayAccessTree arrayAccessTree) { + node = arrayAccessTree.getExpression(); } return node; } - private ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) { + private static @Nullable ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) { ExpressionTree select = methodInvocation.getMethodSelect(); - return select instanceof MemberSelectTree ? ((MemberSelectTree) select).getExpression() : null; + return select instanceof MemberSelectTree memberSelectTree + ? memberSelectTree.getExpression() + : null; } private void dotExpressionArgsAndParen( @@ -2809,14 +3316,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); } @@ -2840,10 +3346,9 @@ 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; + while (expression instanceof ArrayAccessTree array) { indices.addLast(array.getIndex()); expression = array.getExpression(); } @@ -2851,20 +3356,20 @@ private Deque getArrayIndices(ExpressionTree expression) { } /** Helper methods for method invocations. */ - void addTypeArguments(List typeArguments, Indent plusIndent) { + private void addTypeArguments(List typeArguments, Indent plusIndent) { if (typeArguments == null || typeArguments.isEmpty()) { return; } 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(">"); @@ -2878,18 +3383,18 @@ void addTypeArguments(List typeArguments, Indent plusIndent) { * @param arguments the arguments * @param plusIndent the extra indent for the arguments */ - void addArguments(List arguments, Indent plusIndent) { + private void addArguments(List arguments, Indent plusIndent) { builder.open(plusIndent); token("("); if (!arguments.isEmpty()) { if (arguments.size() % 2 == 0 && argumentsAreTabular(arguments) == 2) { builder.forcedBreak(); builder.open(ZERO); - boolean first = true; + 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(); } @@ -2899,7 +3404,7 @@ void addArguments(List arguments, Indent plusIndent) { builder.breakOp(" "); scan(argument1, null); builder.close(); - first = false; + afterFirstToken = true; } builder.close(); } else if (isFormatMethod(arguments)) { @@ -2923,15 +3428,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(); } @@ -2974,18 +3479,13 @@ 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(); - if (value instanceof String && FORMAT_SPECIFIER.matcher(value.toString()).find()) { + if (value instanceof String string && FORMAT_SPECIFIER.matcher(string).find()) { formatString[0] = true; } } @@ -3056,7 +3556,7 @@ private int argumentsAreTabular(List arguments) { return size0; } - static int rowLength(List row) { + private static int rowLength(List row) { int size = 0; for (ExpressionTree tree : row) { if (tree.getKind() != NEW_ARRAY) { @@ -3086,7 +3586,12 @@ 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). + switch (row.get(column)) { + case UnaryTree unary -> nodeTypes.add(unary.getExpression().getKind()); + case ExpressionTree expression -> nodeTypes.add(expression.getKind()); + } } for (Multiset.Entry nodeType : nodeTypes.entrySet()) { if (nodeType.getCount() >= atLeastM) { @@ -3098,20 +3603,19 @@ private static boolean expressionsAreParallel( // General helper functions. - enum DeclarationKind { + /** Kind of declaration. */ + private enum DeclarationKind { NONE, FIELD, PARAMETER } /** Declare one variable or variable-like thing. */ - int declareOne( + private int declareOne( DeclarationKind kind, Direction annotationsDirection, Optional modifiers, Tree type, - VarArgsOrNot isVarargs, - List varargsAnnotations, Name name, String op, String equals, @@ -3132,29 +3636,41 @@ 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); { - if (typeWithDims.isPresent() && typeWithDims.get().node != null) { - scan(typeWithDims.get().node, null); + visitAnnotations(annotations, BreakOrNot.NO, BreakOrNot.YES); + if (isVar) { + token("var"); + } else if (typeWithDims.isPresent() && typeWithDims.get().node() != null) { + scan(typeWithDims.get().node(), null); int totalDims = dims.size(); builder.open(plusFour); maybeAddDims(dims); @@ -3163,14 +3679,10 @@ int declareOne( } else { scan(type, null); } - if (isVarargs.isYes()) { - visitAnnotations(varargsAnnotations, BreakOrNot.YES, BreakOrNot.YES); - builder.op("..."); - } } builder.close(); - if (type != null) { + if (hasType) { builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO, Optional.of(typeBreak)); } @@ -3180,7 +3692,7 @@ int declareOne( if (receiverExpression.isPresent()) { scan(receiverExpression.get(), null); } else { - visit(name); + variableName(name); } builder.op(op); } @@ -3223,7 +3735,15 @@ int declareOne( return baseDims; } - private void maybeAddDims(Deque> annotations) { + private void variableName(Name name) { + if (name.isEmpty()) { + token("_"); + } else { + visit(name); + } + } + + private void maybeAddDims(Deque> annotations) { maybeAddDims(new ArrayDeque<>(), annotations); } @@ -3240,23 +3760,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 { @@ -3268,9 +3788,22 @@ private void maybeAddDims( } token("]"); lastWasAnnotation = false; - break; - default: + } + case "." -> { + if (!builder.peekToken().get().equals(".") || !builder.peekToken(1).get().equals(".")) { + return; + } + if (lastWasAnnotation) { + builder.breakToFill(" "); + } else { + builder.breakToFill(); + } + builder.op("..."); + lastWasAnnotation = false; + } + default -> { return; + } } } } @@ -3282,26 +3815,27 @@ private void declareMany(List fragments, Direction annotationDirec Tree type = fragments.get(0).getType(); visitAndBreakModifiers( - modifiers, annotationDirection, /* declarationAnnotationBreak= */ Optional.absent()); + modifiers, annotationDirection, /* declarationAnnotationBreak= */ Optional.empty()); builder.open(plusFour); builder.open(ZERO); TypeWithDims extractedDims = DimensionHelpers.extractDims(type, SortedDims.YES); - Deque> dims = new ArrayDeque<>(extractedDims.dims); - scan(extractedDims.node, null); + 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()); - dims = new ArrayDeque<>(fragmentDims.dims); + 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) { @@ -3313,10 +3847,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(";"); @@ -3324,7 +3858,7 @@ private void declareMany(List fragments, Direction annotationDirec } /** Add a list of declarations. */ - void addBodyDeclarations( + private void addBodyDeclarations( List bodyDeclarations, BracesOrNot braces, FirstDeclarationsOrNot first0) { if (bodyDeclarations.isEmpty()) { if (braces.isYes()) { @@ -3332,6 +3866,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(); } @@ -3382,6 +3922,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 @@ -3389,7 +3949,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); @@ -3403,22 +3964,38 @@ && getStartPosition(it.peek()) == start) { return fragments; } - /** Does this declaration have javadoc preceding it? */ - private boolean hasJavaDoc(Tree bodyDeclaration) { + private OptionalInt javadocPosition(Tree bodyDeclaration) { int position = ((JCTree) bodyDeclaration).getStartPosition(); Input.Token token = builder.getInput().getPositionTokenMap().get(position); - if (token != null) { - for (Input.Tok tok : token.getToksBefore()) { - if (tok.getText().startsWith("/**")) { - return true; - } + if (token == null) { + return OptionalInt.empty(); + } + var toksBefore = token.getToksBefore(); + // toksBefore is in source order. If there are several comments preceding bodyDeclaration, we + // want the last one that is a javadoc comment. + for (int i = toksBefore.size() - 1; i >= 0; i--) { + Input.Tok tok = toksBefore.get(i); + String text = tok.getText(); + // TODO: consider making earlier versions behave compatibly. Prior to Java 23, there are no + // markdown javadoc comments, and /// is just a regular // comment that happens to start with + // an additional slash. As of Java 23, /// is markdown javadoc, and all consecutive /// lines + // are part of the same javac token. To ensure consistent formatting before and after this + // change, we would have to merge /// lines in a compatible way, including when we are + // extracting their text to make a comment that might be reformatted. + if (text.startsWith("/**") || (Runtime.version().feature() >= 23 && text.startsWith("///"))) { + return OptionalInt.of(tok.getPosition()); } } - return false; + return OptionalInt.empty(); + } + + /** Does this declaration have javadoc preceding it? */ + private boolean hasJavaDoc(Tree bodyDeclaration) { + return javadocPosition(bodyDeclaration).isPresent(); } private static Optional getNextToken(Input input, int position) { - return Optional.fromNullable(input.getPositionTokenMap().get(position)); + return Optional.ofNullable(input.getPositionTokenMap().get(position)); } /** Does this list of trees end with the specified token? */ @@ -3434,28 +4011,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; @@ -3469,12 +4047,12 @@ private Direction fieldAnnotationDirection(ModifiersTree modifiers) { * * @param token the {@link String} to wrap in a {@link Doc.Token} */ - final void token(String token) { + private void token(String token) { builder.token( token, Doc.Token.RealOrImaginary.REAL, ZERO, - /* breakAndIndentTrailingComment= */ Optional.absent()); + /* breakAndIndentTrailingComment= */ Optional.empty()); } /** @@ -3483,12 +4061,12 @@ 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) { + private void token(String token, Indent plusIndentCommentsBefore) { builder.token( token, Doc.Token.RealOrImaginary.REAL, plusIndentCommentsBefore, - /* breakAndIndentTrailingComment= */ Optional.absent()); + /* breakAndIndentTrailingComment= */ Optional.empty()); } /** Emit a {@link Doc.Token}, and breaks and indents trailing javadoc or block comments. */ @@ -3509,7 +4087,7 @@ private void markForPartialFormat() { * * @param node the ASTNode holding the input position */ - final void sync(Tree node) { + private void sync(Tree node) { builder.sync(((JCTree) node).getStartPosition()); } @@ -3521,4 +4099,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 198cd9e97..6023065e3 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().or(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; } @@ -333,26 +337,17 @@ private Range expandToBreakableRegions(Range iRange) { public static String applyReplacements(String input, List replacements) { replacements = new ArrayList<>(replacements); - replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()).reversed()); + replacements.sort(comparing((Replacement r) -> r.replaceRange().lowerEndpoint()).reversed()); StringBuilder writer = new StringBuilder(input); for (Replacement replacement : replacements) { writer.replace( - replacement.getReplaceRange().lowerEndpoint(), - replacement.getReplaceRange().upperEndpoint(), - replacement.getReplacementString()); + replacement.replaceRange().lowerEndpoint(), + replacement.replaceRange().upperEndpoint(), + replacement.replacementString()); } return writer.toString(); } - /** The earliest position of any Tok in the Token, including leading whitespace. */ - public static int startPosition(Token token) { - int min = token.getTok().getPosition(); - for (Input.Tok tok : token.getToksBefore()) { - min = Math.min(min, tok.getPosition()); - } - return min; - } - /** The earliest non-whitespace Tok in the Token. */ public static Input.Tok startTok(Token token) { for (Input.Tok tok : token.getToksBefore()) { @@ -387,7 +382,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..93382330e 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavacTokens.java @@ -15,72 +15,48 @@ 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 private static final CharSequence EOF_COMMENT = "\n//EOF"; - /** An unprocessed input token, including whitespace and comments. */ - static class RawTok { - private final String stringVal; - private final TokenKind kind; - private final int pos; - private final int endPos; - - RawTok(String stringVal, TokenKind kind, int pos, int endPos) { - this.stringVal = stringVal; - this.kind = kind; - this.pos = pos; - this.endPos = endPos; - } - - /** The token kind, or {@code null} for whitespace and comments. */ - public TokenKind kind() { - return kind; - } - - /** The start position. */ - public int pos() { - return pos; - } - - /** The end position. */ - public int endPos() { - return endPos; - } - - /** The escaped string value of a literal, or {@code null} for other tokens. */ - public String stringVal() { - return stringVal; - } - } + /** + * An unprocessed input token, including whitespace and comments. + * + * @param stringVal the escaped string value of a literal, or {@code null} for other tokens. + * @param kind the token kind, or {@code null} for whitespace and comments. + * @param pos the start position. + * @param endPos the end position. + */ + record RawTok(String stringVal, TokenKind kind, int pos, int endPos) {} /** Lex the input and return a list of {@link RawTok}s. */ - public static ImmutableList getTokens( + static ImmutableList getTokens( String source, Context context, Set stopTokens) { if (source == null) { return ImmutableList.of(); } ScannerFactory fac = ScannerFactory.instance(context); char[] buffer = (source + EOF_COMMENT).toCharArray(); - Scanner scanner = - new AccessibleScanner(fac, new CommentSavingTokenizer(fac, buffer, buffer.length)); + 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,13 +64,13 @@ 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))); } tokens.add( - new RawTok(null, null, c.getSourcePos(0), c.getSourcePos(0) + c.getText().length())); - last = c.getSourcePos(0) + c.getText().length(); + new RawTok(null, null, c.getSourcePos(0), c.getSourcePos(0) + c.text().length())); + last = c.getSourcePos(0) + c.text().length(); } } if (stopTokens.contains(t.kind)) { @@ -120,46 +96,66 @@ 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 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 { - - private final int pos; - private final int endPos; - private final AccessibleReader reader; - private final CommentStyle style; - - private String text = null; - - public CommentWithTextAndPosition( - int pos, int endPos, AccessibleReader reader, CommentStyle style) { - this.pos = pos; - this.endPos = endPos; - this.reader = reader; - this.style = style; - } - + private record CommentWithTextAndPosition(int pos, int endPos, String text) { /** * Returns the source position of the character at index {@code index} in the comment text. * *

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) { + int getSourcePos(int index) { checkArgument( 0 <= index && index < (endPos - pos), "Expected %s in the range [0, %s)", @@ -168,47 +164,18 @@ 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()); + return String.format("Comment: '%s'", text()); } } - // Scanner(ScannerFactory, JavaTokenizer) is package-private - static class AccessibleScanner extends Scanner { - protected AccessibleScanner(ScannerFactory fac, JavaTokenizer tokenizer) { + // Scanner(ScannerFactory, JavaTokenizer) is protected + private static class AccessibleScanner extends Scanner { + 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 d94fe0c2c..bfe1ff51f 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,11 @@ 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()) + .reorderModifiers(parameters.reorderModifiers()) + .build(); if (parameters.stdin()) { return formatStdin(parameters, options); @@ -106,51 +131,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); + String input = new String(Files.readAllBytes(path), UTF_8); + cs.submit(new FormatFileCallable(parameters, path, input, options)); + files++; } catch (IOException e) { errWriter.println(fileName + ": could not read file: " + e.getMessage()); - return 1; + allOk = false; } - inputs.put(path, input); - results.put(path, executorService.submit(new FormatFileCallable(parameters, input, options))); } - boolean allOk = true; - for (Map.Entry> result : results.entrySet()) { - 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; + } + } + 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; } - boolean changed = !formatted.equals(inputs.get(path)); + String formatted = result.output(); + boolean changed = result.changed(); if (changed && parameters.setExitIfChanged()) { allOk = false; } @@ -173,6 +203,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; } @@ -185,9 +219,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; } @@ -198,18 +237,12 @@ 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; } /** Parses and validates command-line flags. */ - public static CommandLineOptions processArgs(String... args) throws UsageException { + static CommandLineOptions processArgs(String... args) throws UsageException { CommandLineOptions parameters; try { parameters = CommandLineOptionsParser.parse(Arrays.asList(args)); 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..140b5647d 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,74 @@ 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; + private record ModifierTokens(ImmutableList tokens, Modifier modifier) + implements Comparable { + + 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()))); + boolean isEmpty() { + return tokens.isEmpty() || modifier == null; + } + + 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 +111,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 +157,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. */ @@ -171,4 +234,6 @@ private static JavaInput applyReplacements( } return new JavaInput(sb.toString()); } + + private ModifierOrderer() {} } 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 ac7a24ea4..4b2211eae 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,62 +29,40 @@ 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 - * removed, and the references in javadoc are replaced with fully qualified names. + * Removes unused imports from a source file. Imports that are only used in javadoc are nevertheless + * kept. */ 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,61 +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()).equals(sep)) { + && contents.subSequence(endPosition, endPosition + sep.length()).toString().equals(sep)) { endPosition += sep.length(); } replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), ""); - // fully qualify any javadoc references with the same simple name as a deleted - // non-static import - if (!importTree.isStatic()) { - for (Range docRange : usedInJavadoc.get(simpleName)) { - if (docRange == null) { - continue; - } - String replaceWith = importTree.getQualifiedIdentifier().toString(); - replacements.put(docRange, replaceWith); - } - } } return replacements; } - private static String getSimpleName(JCImport importTree) { - return importTree.getQualifiedIdentifier() instanceof JCIdent - ? ((JCIdent) importTree.getQualifiedIdentifier()).getName().toString() - : ((JCFieldAccess) importTree.getQualifiedIdentifier()).getIdentifier().toString(); + private static String getSimpleName(JCTree importTree) { + return getQualifiedIdentifier(importTree).getIdentifier().toString(); } private static boolean isUnused( JCCompilationUnit unit, Set usedNames, Multimap> usedInJavadoc, - JCImport importTree, + JCTree importTree, String simpleName) { - String qualifier = - importTree.getQualifiedIdentifier() instanceof JCFieldAccess - ? ((JCFieldAccess) importTree.getQualifiedIdentifier()).getExpression().toString() - : null; + JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier(importTree); + String qualifier = qualifiedIdentifier.getExpression().toString(); if (qualifier.equals("java.lang")) { return true; } if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) { return true; } - if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess - && ((JCFieldAccess) importTree.getQualifiedIdentifier()) - .getIdentifier() - .contentEquals("*")) { + if (qualifiedIdentifier.getIdentifier().contentEquals("*")) { return false; } @@ -329,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 @@ -351,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..088f81597 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Replacement.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Replacement.java @@ -14,64 +14,38 @@ 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; +import com.google.errorprone.annotations.InlineMe; /** * Represents a range in the original source and replacement text for that range. * - *

google-java-format doesn't depend on AutoValue, to allow AutoValue to depend on - * google-java-format. + * @param replaceRange The range of characters in the original source to replace. + * @param replacementString The string to replace the range of characters with. */ -public class Replacement { +public record Replacement(Range replaceRange, String replacementString) { 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; + public Replacement { + checkNotNull(replaceRange, "Null replaceRange"); + checkNotNull(replacementString, "Null replacementString"); } - /** The range of characters in the original source to replace. */ + @InlineMe(replacement = "this.replaceRange()") public Range getReplaceRange() { - return replaceRange; + return replaceRange(); } - /** The string to replace the range of characters with. */ + @InlineMe(replacement = "this.replacementString()") public String getReplacementString() { - return replacementString; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof Replacement) { - Replacement that = (Replacement) o; - return replaceRange.equals(that.getReplaceRange()) - && replacementString.equals(that.getReplacementString()); - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(replaceRange, replacementString); + return replacementString(); } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java index 3827ef958..2b75eb4a1 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,20 +60,23 @@ 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 String createIndentationString(int indentationLevel) { + public SnippetFormatter() { + this(JavaFormatterOptions.defaultOptions()); + } + + public SnippetFormatter(JavaFormatterOptions formatterOptions) { + formatter = new Formatter(formatterOptions); + } + + private static String createIndentationString(int indentationLevel) { Preconditions.checkArgument( indentationLevel >= 0, "Indentation level cannot be less than zero. Given: %s", indentationLevel); - int spaces = indentationLevel * INDENTATION_SIZE; - StringBuilder buf = new StringBuilder(spaces); - for (int i = 0; i < spaces; i++) { - buf.append(' '); - } - return buf.toString(); + return " ".repeat(indentationLevel * INDENTATION_SIZE); } private static Range offsetRange(Range range, int offset) { @@ -87,7 +93,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 +120,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.replaceRange())) + .collect(toImmutableList()); } /** @@ -142,7 +143,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 +153,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 +166,38 @@ private SnippetWrapper snippetWrapper(SnippetKind kind, String source, int initi * Synthesize a dummy class around the code snippet provided by Eclipse. The dummy class is * correctly formatted -- the blocks use correct indentation, etc. */ - switch (kind) { - case COMPILATION_UNIT: - { - SnippetWrapper wrapper = new SnippetWrapper(); - for (int i = 1; i <= initialIndent; i++) { - wrapper.append("class Dummy {\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; - } - case CLASS_BODY_DECLARATIONS: - { - SnippetWrapper wrapper = new SnippetWrapper(); - for (int i = 1; i <= initialIndent; i++) { - wrapper.append("class Dummy {\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; + return switch (kind) { + case COMPILATION_UNIT, CLASS_BODY_DECLARATIONS -> { + SnippetWrapper wrapper = new SnippetWrapper(); + for (int i = 1; i <= initialIndent; i++) { + wrapper.append("class Dummy {\n").append(createIndentationString(i)); } - case STATEMENTS: - { - SnippetWrapper wrapper = new SnippetWrapper(); - wrapper.append("class Dummy {\n").append(createIndentationString(1)); - for (int i = 2; i <= initialIndent; i++) { - wrapper.append("{\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; + wrapper.appendSource(source); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + case STATEMENTS -> { + SnippetWrapper wrapper = new SnippetWrapper(); + wrapper.append("class Dummy {\n").append(createIndentationString(1)); + for (int i = 2; i <= initialIndent; i++) { + wrapper.append("{\n").append(createIndentationString(i)); } - case EXPRESSION: - { - SnippetWrapper wrapper = new SnippetWrapper(); - wrapper.append("class Dummy {\n").append(createIndentationString(1)); - for (int i = 2; i <= initialIndent; i++) { - wrapper.append("{\n").append(createIndentationString(i)); - } - wrapper.append("Object o = "); - wrapper.appendSource(source); - wrapper.append(";"); - wrapper.closeBraces(initialIndent); - return wrapper; + wrapper.appendSource(source); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + case EXPRESSION -> { + SnippetWrapper wrapper = new SnippetWrapper(); + wrapper.append("class Dummy {\n").append(createIndentationString(1)); + for (int i = 2; i <= initialIndent; i++) { + wrapper.append("{\n").append(createIndentationString(i)); } - default: - throw new IllegalArgumentException("Unknown snippet kind: " + kind); - } + wrapper.append("Object o = "); + wrapper.appendSource(source); + wrapper.append(";"); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + }; } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java new file mode 100644 index 000000000..52d55adba --- /dev/null +++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java @@ -0,0 +1,498 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Iterables.getLast; +import static com.google.googlejavaformat.java.Trees.getEndPosition; +import static com.google.googlejavaformat.java.Trees.getStartPosition; +import static java.lang.Math.min; +import static java.util.stream.Collectors.joining; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; +import com.google.common.collect.TreeRangeMap; +import com.google.googlejavaformat.Newlines; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Position; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; + +/** Wraps string literals that exceed the column limit. */ +public final class StringWrapper { + + private static final String TEXT_BLOCK_DELIMITER = "\"\"\""; + + /** Reflows long string literals in the given Java source code. */ + public static String wrap(String input, Formatter formatter) throws FormatterException { + return StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, input, formatter); + } + + /** + * Reflows string literals in the given Java source code that extend past the given column limit. + */ + static String wrap(final int columnLimit, String input, Formatter formatter) + throws FormatterException { + if (!needWrapping(columnLimit, input)) { + // fast path + return input; + } + + TreeRangeMap replacements = getReflowReplacements(columnLimit, input); + String firstPass = formatter.formatSource(input, replacements.asMapOfRanges().keySet()); + + if (!firstPass.equals(input)) { + // If formatting the replacement ranges resulted in a change, recalculate the replacements on + // the updated input. + input = firstPass; + replacements = getReflowReplacements(columnLimit, input); + } + + String result = applyReplacements(input, replacements); + + { + // We really don't want bugs in this pass to change the behaviour of programs we're + // formatting, so check that the pretty-printed AST is the same before and after reformatting. + String expected = parse(input, /* allowStringFolding= */ true).toString(); + String actual = parse(result, /* allowStringFolding= */ true).toString(); + if (!expected.equals(actual)) { + throw new FormatterException( + String.format( + "Something has gone terribly wrong. We planned to make the below formatting change," + + " but have aborted because it would unexpectedly change the AST.\n" + + "Please file a bug: " + + "https://github.com/google/google-java-format/issues/new" + + "\n\n=== Actual: ===\n%s\n=== Expected: ===\n%s\n", + actual, expected)); + } + } + + return result; + } + + private static TreeRangeMap getReflowReplacements( + int columnLimit, final String input) throws FormatterException { + return new Reflower(columnLimit, input).getReflowReplacements(); + } + + private static class Reflower { + + private final String input; + private final int columnLimit; + private final String separator; + private final JCTree.JCCompilationUnit unit; + private final Position.LineMap lineMap; + + Reflower(int columnLimit, String input) throws FormatterException { + this.columnLimit = columnLimit; + this.input = input; + this.separator = Newlines.guessLineSeparator(input); + this.unit = parse(input, /* allowStringFolding= */ false); + this.lineMap = unit.getLineMap(); + } + + TreeRangeMap getReflowReplacements() { + // Paths to string literals that extend past the column limit. + List longStringLiterals = new ArrayList<>(); + // Paths to text blocks to be re-indented. + List textBlocks = new ArrayList<>(); + new LongStringsAndTextBlockScanner(longStringLiterals, textBlocks) + .scan(new TreePath(unit), null); + TreeRangeMap replacements = TreeRangeMap.create(); + indentTextBlocks(replacements, textBlocks); + wrapLongStrings(replacements, longStringLiterals); + return replacements; + } + + private class LongStringsAndTextBlockScanner extends TreePathScanner { + + private final List longStringLiterals; + private final List textBlocks; + + LongStringsAndTextBlockScanner(List longStringLiterals, List textBlocks) { + this.longStringLiterals = longStringLiterals; + this.textBlocks = textBlocks; + } + + @Override + public Void visitLiteral(LiteralTree literalTree, Void aVoid) { + if (literalTree.getKind() != Kind.STRING_LITERAL) { + return null; + } + int pos = getStartPosition(literalTree); + if (input.substring(pos, min(input.length(), pos + 3)).equals(TEXT_BLOCK_DELIMITER)) { + textBlocks.add(literalTree); + return null; + } + Tree parent = getCurrentPath().getParentPath().getLeaf(); + if (parent instanceof MemberSelectTree memberSelectTree + && memberSelectTree.getExpression().equals(literalTree)) { + return null; + } + int endPosition = getEndPosition(literalTree, unit); + int lineEnd = endPosition; + while (Newlines.hasNewlineAt(input, lineEnd) == -1) { + lineEnd++; + } + if (lineMap.getColumnNumber(lineEnd) - 1 <= columnLimit) { + return null; + } + longStringLiterals.add(getCurrentPath()); + return null; + } + } + + private void indentTextBlocks( + TreeRangeMap replacements, List textBlocks) { + for (Tree tree : textBlocks) { + int startPosition = lineMap.getStartPosition(lineMap.getLineNumber(getStartPosition(tree))); + int endPosition = getEndPosition(tree, unit); + String text = input.substring(startPosition, endPosition); + int leadingWhitespace = CharMatcher.whitespace().negate().indexIn(text); + + // Find the source code of the text block with incidental whitespace removed. + // The first line of the text block is always """, and it does not affect incidental + // whitespace. + ImmutableList initialLines = text.lines().collect(toImmutableList()); + String stripped = initialLines.stream().skip(1).collect(joining(separator)).stripIndent(); + ImmutableList lines = stripped.lines().collect(toImmutableList()); + boolean deindent = + getLast(initialLines).stripTrailing().length() + == getLast(lines).stripTrailing().length(); + + String prefix = deindent ? "" : " ".repeat(leadingWhitespace); + + StringBuilder output = new StringBuilder(prefix).append(initialLines.get(0).stripLeading()); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + String trimmed = line.stripTrailing(); + output.append(separator); + if (!trimmed.isEmpty()) { + // Don't add incidental leading whitespace to empty lines + output.append(prefix); + } + if (i == lines.size() - 1) { + String withoutDelimiter = + trimmed + .substring(0, trimmed.length() - TEXT_BLOCK_DELIMITER.length()) + .stripTrailing(); + if (!withoutDelimiter.stripLeading().isEmpty()) { + output.append(withoutDelimiter).append('\\').append(separator).append(prefix); + } + // If the trailing line is just """, indenting it more than the prefix of incidental + // whitespace has no effect, and results in a javac text-blocks warning that 'trailing + // white space will be removed'. + output.append(TEXT_BLOCK_DELIMITER); + } else { + output.append(line); + } + } + replacements.put(Range.closedOpen(startPosition, endPosition), output.toString()); + } + } + + private void wrapLongStrings( + TreeRangeMap replacements, List longStringLiterals) { + for (TreePath path : longStringLiterals) { + // Find the outermost contiguous enclosing concatenation expression + TreePath enclosing = path; + while (enclosing.getParentPath().getLeaf().getKind() == Kind.PLUS) { + enclosing = enclosing.getParentPath(); + } + // Is the literal being wrapped the first in a chain of concatenation expressions? + // i.e. `ONE + TWO + THREE` + // We need this information to handle continuation indents. + AtomicBoolean first = new AtomicBoolean(false); + // Finds the set of string literals in the concat expression that includes the one that + // needs + // to be wrapped. + List flat = flatten(input, unit, path, enclosing, first); + // Zero-indexed start column + int startColumn = lineMap.getColumnNumber(getStartPosition(flat.get(0))) - 1; + + // Handling leaving trailing non-string tokens at the end of the literal, + // e.g. the trailing `);` in `foo("...");`. + int end = getEndPosition(getLast(flat), unit); + int lineEnd = end; + while (Newlines.hasNewlineAt(input, lineEnd) == -1) { + lineEnd++; + } + int trailing = lineEnd - end; + + // Get the original source text of the string literals, excluding `"` and `+`. + ImmutableList components = stringComponents(input, unit, flat); + replacements.put( + Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(getLast(flat), unit)), + reflow(separator, columnLimit, startColumn, trailing, components, first.get())); + } + } + } + + /** + * Returns the source text of the given string literal trees, excluding the leading and trailing + * double-quotes and the `+` operator. + */ + private static ImmutableList stringComponents( + String input, JCTree.JCCompilationUnit unit, List flat) { + ImmutableList.Builder result = ImmutableList.builder(); + StringBuilder piece = new StringBuilder(); + for (Tree tree : flat) { + // adjust for leading and trailing double quotes + String text = input.substring(getStartPosition(tree) + 1, getEndPosition(tree, unit) - 1); + int start = 0; + for (int idx = 0; idx < text.length(); idx++) { + if (CharMatcher.whitespace().matches(text.charAt(idx))) { + // continue below + } else if (hasEscapedWhitespaceAt(text, idx) != -1) { + // continue below + } else if (hasEscapedNewlineAt(text, idx) != -1) { + int length; + while ((length = hasEscapedNewlineAt(text, idx)) != -1) { + idx += length; + } + } else { + continue; + } + piece.append(text, start, idx); + result.add(piece.toString()); + piece = new StringBuilder(); + start = idx; + } + if (piece.length() > 0) { + result.add(piece.toString()); + piece = new StringBuilder(); + } + if (start < text.length()) { + piece.append(text, start, text.length()); + } + } + if (piece.length() > 0) { + result.add(piece.toString()); + } + return result.build(); + } + + private static int hasEscapedWhitespaceAt(String input, int idx) { + if (input.startsWith("\\t", idx)) { + return 2; + } + return -1; + } + + private static int hasEscapedNewlineAt(String input, int idx) { + int offset = 0; + if (input.startsWith("\\r", idx)) { + offset += 2; + } + if (input.startsWith("\\n", idx)) { + offset += 2; + } + return offset > 0 ? offset : -1; + } + + /** + * Reflows the given source text, trying to split on word boundaries. + * + * @param separator the line separator + * @param columnLimit the number of columns to wrap at + * @param startColumn the column position of the beginning of the original text + * @param trailing extra space to leave after the last line, to accommodate a ; or ) + * @param components the text to reflow. This is a list of “words” of a single literal. Its first + * and last quotes have been stripped + * @param first0 true if the text includes the beginning of its enclosing concat chain + */ + private static String reflow( + String separator, + int columnLimit, + int startColumn, + int trailing, + ImmutableList components, + boolean first0) { + // We have space between the start column and the limit to output the first line. + // Reserve two spaces for the start and end quotes. + int width = columnLimit - startColumn - 2; + Deque input = new ArrayDeque<>(components); + List lines = new ArrayList<>(); + boolean first = first0; + while (!input.isEmpty()) { + int length = 0; + List line = new ArrayList<>(); + // If we know this is going to be the last line, then remove a bit of width to account for the + // trailing characters. + if (totalLengthLessThanOrEqual(input, width)) { + // This isn’t quite optimal, but arguably good enough. See b/179561701 + width -= trailing; + } + while (!input.isEmpty() && (length <= 4 || (length + input.peekFirst().length()) <= width)) { + String text = input.removeFirst(); + line.add(text); + length += text.length(); + if (text.endsWith("\\n") || text.endsWith("\\r")) { + break; + } + } + if (line.isEmpty()) { + line.add(input.removeFirst()); + } + // add the split line to the output, and process whatever's left + lines.add(String.join("", line)); + if (first) { + width -= 6; // subsequent lines have a four-space continuation indent and a `+ ` + first = false; + } + } + + return lines.stream() + .collect( + joining( + "\"" + separator + Strings.repeat(" ", startColumn + (first0 ? 4 : -2)) + "+ \"", + "\"", + "\"")); + } + + private static boolean totalLengthLessThanOrEqual(Iterable input, int length) { + int total = 0; + for (String s : input) { + total += s.length(); + if (total > length) { + return false; + } + } + return true; + } + + /** + * Flattens the given binary expression tree, and extracts the subset that contains the given path + * and any adjacent nodes that are also string literals. + */ + private static List flatten( + String input, + JCTree.JCCompilationUnit unit, + TreePath path, + TreePath parent, + AtomicBoolean firstInChain) { + List flat = new ArrayList<>(); + + // flatten the expression tree with a pre-order traversal + ArrayDeque todo = new ArrayDeque<>(); + todo.add(parent.getLeaf()); + while (!todo.isEmpty()) { + Tree first = todo.removeFirst(); + if (first.getKind() == Tree.Kind.PLUS) { + BinaryTree bt = (BinaryTree) first; + todo.addFirst(bt.getRightOperand()); + todo.addFirst(bt.getLeftOperand()); + } else { + flat.add(first); + } + } + + int idx = flat.indexOf(path.getLeaf()); + Verify.verify(idx != -1); + + // walk outwards from the leaf for adjacent string literals to also reflow + int startIdx = idx; + int endIdx = idx + 1; + while (startIdx > 0 + && flat.get(startIdx - 1).getKind() == Tree.Kind.STRING_LITERAL + && noComments(input, unit, flat.get(startIdx - 1), flat.get(startIdx))) { + startIdx--; + } + while (endIdx < flat.size() + && flat.get(endIdx).getKind() == Tree.Kind.STRING_LITERAL + && noComments(input, unit, flat.get(endIdx - 1), flat.get(endIdx))) { + endIdx++; + } + + firstInChain.set(startIdx == 0); + return ImmutableList.copyOf(flat.subList(startIdx, endIdx)); + } + + private static boolean noComments( + String input, JCTree.JCCompilationUnit unit, Tree one, Tree two) { + return STRING_CONCAT_DELIMITER.matchesAllOf( + input.subSequence(getEndPosition(one, unit), getStartPosition(two))); + } + + private static final CharMatcher STRING_CONCAT_DELIMITER = + CharMatcher.whitespace().or(CharMatcher.anyOf("\"+")); + + /** + * Returns true if any lines in the given Java source exceed the column limit, or contain a {@code + * """} that could indicate a text block. + */ + private static boolean needWrapping(int columnLimit, String input) { + // TODO(cushon): consider adding Newlines.lineIterable? + Iterator it = Newlines.lineIterator(input); + while (it.hasNext()) { + String line = it.next(); + if (line.length() > columnLimit || line.contains(TEXT_BLOCK_DELIMITER)) { + return true; + } + } + return false; + } + + /** Parses the given Java source. */ + private static JCTree.JCCompilationUnit parse(String source, boolean allowStringFolding) + throws FormatterException { + List> errorDiagnostics = new ArrayList<>(); + Context context = new Context(); + JCTree.JCCompilationUnit unit = + Trees.parse(context, errorDiagnostics, allowStringFolding, source); + if (!errorDiagnostics.isEmpty()) { + // error handling is done during formatting + throw FormatterException.fromJavacDiagnostics(errorDiagnostics); + } + return unit; + } + + /** Applies replacements to the given string. */ + private static String applyReplacements( + String javaInput, TreeRangeMap replacementMap) throws FormatterException { + // process in descending order so the replacement ranges aren't perturbed if any replacements + // differ in size from the input + Map, String> ranges = replacementMap.asDescendingMapOfRanges(); + if (ranges.isEmpty()) { + return javaInput; + } + StringBuilder sb = new StringBuilder(javaInput); + for (Map.Entry, String> entry : ranges.entrySet()) { + Range range = entry.getKey(); + sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue()); + } + return sb.toString(); + } + + private StringWrapper() {} +} diff --git a/core/src/main/java/com/google/googlejavaformat/java/Trees.java b/core/src/main/java/com/google/googlejavaformat/java/Trees.java index 69b954c4f..a2f349e71 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,44 @@ package com.google.googlejavaformat.java; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +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.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +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; +import org.jspecify.annotations.Nullable; /** Utilities for working with {@link Tree}s. */ class Trees { @@ -44,8 +67,17 @@ 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. */ + static int getEndPosition(Tree tree, CompilationUnitTree unit) { + try { + return (int) GET_END_POS_HANDLE.invokeExact((JCTree) tree, (JCCompilationUnit) unit); + } catch (Throwable e) { + Throwables.throwIfUnchecked(e); + throw new AssertionError(e); + } } /** Returns the source text for the node. */ @@ -62,15 +94,19 @@ static String getSourceForNode(Tree node, TreePath path) { /** Returns the simple name of a (possibly qualified) method invocation expression. */ static Name getMethodName(MethodInvocationTree methodInvocation) { ExpressionTree select = methodInvocation.getMethodSelect(); - return select instanceof MemberSelectTree - ? ((MemberSelectTree) select).getIdentifier() - : ((IdentifierTree) select).getName(); + return switch (select) { + case MemberSelectTree memberSelect -> memberSelect.getIdentifier(); + case IdentifierTree identifier -> identifier.getName(); + default -> throw new AssertionError(select); + }; } /** Returns the receiver of a qualified method invocation expression, or {@code null}. */ - static ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) { + static @Nullable ExpressionTree getMethodReceiver(MethodInvocationTree methodInvocation) { ExpressionTree select = methodInvocation.getMethodSelect(); - return select instanceof MemberSelectTree ? ((MemberSelectTree) select).getExpression() : null; + return select instanceof MemberSelectTree memberSelectTree + ? memberSelectTree.getExpression() + : null; } /** Returns the string name of an operator, including assignment and compound assignment. */ @@ -92,27 +128,120 @@ static int precedence(ExpressionTree expression) { return TreeInfo.opPrec(((JCTree) expression).getTag()); } - /** - * Returns the enclosing type declaration (class, enum, interface, or annotation) for the given - * path. - */ - static ClassTree getEnclosingTypeDeclaration(TreePath path) { - for (; path != null; path = path.getParentPath()) { - switch (path.getLeaf().getKind()) { - case CLASS: - case ENUM: - case INTERFACE: - case ANNOTATED_TYPE: - return (ClassTree) path.getLeaf(); - default: - break; - } - } - throw new AssertionError(); - } - /** Skips a single parenthesized tree. */ static ExpressionTree skipParen(ExpressionTree node) { return ((ParenthesizedTree) node).getExpression(); } + + 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; + try { + parser = + newParser( + parserFactory, + javaInput, + /* keepDocComments= */ true, + /* keepEndPos= */ true, + /* keepLineMap= */ true); + } catch (Throwable e) { + Throwables.throwIfUnchecked(e); + throw new AssertionError(e); + } + JCCompilationUnit unit = parser.parseCompilationUnit(); + unit.sourcefile = source; + return unit; + } + + private static JavacParser newParser( + ParserFactory parserFactory, + CharSequence source, + boolean keepDocComments, + boolean keepEndPos, + boolean keepLineMap) { + if (END_POS_TABLE_CLASS != null) { + return parserFactory.newParser(source, keepDocComments, keepEndPos, keepLineMap); + } + return parserFactory.newParser( + source, keepDocComments, keepLineMap, /* parseModuleInfo */ false); + } + + 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"); + } + + private static final @Nullable Class END_POS_TABLE_CLASS = getEndPosTableClass(); + + private static @Nullable Class getEndPosTableClass() { + try { + return Class.forName("com.sun.tools.javac.tree.EndPosTable"); + } catch (ClassNotFoundException e) { + // JDK versions after https://bugs.openjdk.org/browse/JDK-8372948 + return null; + } + } + + private static final MethodHandle GET_END_POS_HANDLE = getEndPosMethodHandle(); + + private static MethodHandle getEndPosMethodHandle() { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + if (END_POS_TABLE_CLASS == null) { + try { + // (tree, unit) -> tree.getEndPosition() + return MethodHandles.dropArguments( + lookup.findVirtual(JCTree.class, "getEndPosition", MethodType.methodType(int.class)), + 1, + JCCompilationUnit.class); + } catch (ReflectiveOperationException e1) { + throw new LinkageError(e1.getMessage(), e1); + } + } + try { + // (tree, unit) -> tree.getEndPosition(unit.endPositions) + return MethodHandles.filterArguments( + lookup.findVirtual( + JCTree.class, + "getEndPosition", + MethodType.methodType(int.class, END_POS_TABLE_CLASS)), + 1, + lookup + .findVarHandle(JCCompilationUnit.class, "endPositions", END_POS_TABLE_CLASS) + .toMethodHandle(VarHandle.AccessMode.GET)); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java index b12265905..b1e07a76f 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java +++ b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java @@ -16,9 +16,10 @@ import com.google.common.base.Verify; import java.util.List; +import java.util.Optional; /** Heuristics for classifying qualified names as types. */ -public final class TypeNameClassifier { +final class TypeNameClassifier { private TypeNameClassifier() {} @@ -29,20 +30,17 @@ private enum TyParseState { START(false) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - // if we see an UpperCamel later, assume this was a class - // e.g. com.google.FOO.Bar - return TyParseState.AMBIGUOUS; - case LOWER_CAMEL: - return TyParseState.REJECT; - case LOWERCASE: - // could be a package - return TyParseState.START; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE -> + // if we see an UpperCamel later, assume this was a class + // e.g. com.google.FOO.Bar + TyParseState.AMBIGUOUS; + case LOWER_CAMEL -> TyParseState.REJECT; + case LOWERCASE -> + // could be a package + TyParseState.START; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }, @@ -50,15 +48,10 @@ public TyParseState next(JavaCaseFormat n) { TYPE(true) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - case LOWER_CAMEL: - case LOWERCASE: - return TyParseState.FIRST_STATIC_MEMBER; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE, LOWER_CAMEL, LOWERCASE -> TyParseState.FIRST_STATIC_MEMBER; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }, @@ -82,16 +75,11 @@ public TyParseState next(JavaCaseFormat n) { AMBIGUOUS(false) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - return AMBIGUOUS; - case LOWER_CAMEL: - case LOWERCASE: - return TyParseState.REJECT; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE -> AMBIGUOUS; + case LOWER_CAMEL, LOWERCASE -> TyParseState.REJECT; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }; @@ -101,12 +89,12 @@ public TyParseState next(JavaCaseFormat n) { this.isSingleUnit = isSingleUnit; } - public boolean isSingleUnit() { + boolean isSingleUnit() { return isSingleUnit; } /** Transition function. */ - public abstract TyParseState next(JavaCaseFormat n); + abstract TyParseState next(JavaCaseFormat n); } /** @@ -121,23 +109,23 @@ public boolean isSingleUnit() { *

  • com.google.ClassName.InnerClass.staticMemberName * */ - static int typePrefixLength(List nameParts) { + static Optional typePrefixLength(List nameParts) { TyParseState state = TyParseState.START; - int typeLength = -1; + Optional typeLength = Optional.empty(); for (int i = 0; i < nameParts.size(); i++) { state = state.next(JavaCaseFormat.from(nameParts.get(i))); if (state == TyParseState.REJECT) { break; } if (state.isSingleUnit()) { - typeLength = i; + typeLength = Optional.of(i); } } return typeLength; } /** Case formats used in Java identifiers. */ - public enum JavaCaseFormat { + enum JavaCaseFormat { UPPERCASE, LOWERCASE, UPPER_CAMEL, @@ -163,7 +151,7 @@ static JavaCaseFormat from(String name) { hasLowercase |= Character.isLowerCase(c); } if (firstUppercase) { - return hasLowercase ? UPPER_CAMEL : UPPERCASE; + return (hasLowercase || name.length() == 1) ? UPPER_CAMEL : UPPERCASE; } else { return hasUppercase ? LOWER_CAMEL : LOWERCASE; } diff --git a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java index 77cdf6521..f9c2eb80d 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/UsageException.java +++ b/core/src/main/java/com/google/googlejavaformat/java/UsageException.java @@ -16,62 +16,64 @@ import static com.google.common.base.Preconditions.checkNotNull; -import com.google.common.base.Joiner; - /** Checked exception class for formatter command-line usage errors. */ final class UsageException extends Exception { - private static final Joiner NEWLINE_JOINER = Joiner.on(System.lineSeparator()); + private static final String DOCS_LINK = + "https://github.com/google/google-java-format"; + + private static final String USAGE = +""" + +Usage: google-java-format [options] file(s) - private static final String[] DOCS_LINK = { - "https://github.com/google/google-java-format", - }; +Options: + -i, -r, -replace, --replace + Send formatted output back to files, not stdout. + - + Format stdin -> stdout + --assume-filename, -assume-filename + File name to use for diagnostics when formatting standard input (default is ). + --aosp, -aosp, -a + Use AOSP style instead of Google Style (4-space indentation). + --fix-imports-only + Fix import order and remove any unused imports, but do no other formatting. + --skip-sorting-imports + Do not fix the import order. Unused imports will still be removed. + --skip-removing-unused-imports + Do not remove unused imports. Imports will still be sorted. + --skip-reflowing-long-strings + Do not reflow string literals that exceed the column limit. + --skip-javadoc-formatting + Do not reformat javadoc. + --skip-reordering-modifiers + Do not reorder modifiers into the JLS-recommended order. + --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, e.g. the first 5 lines are 1:5 (1-based; default is all). + --offset, -offset + Character offset to format (0-based; default is all). + --length, -length + Character length to format. + --help, -help, -h + Print this usage statement. + --version, -version, -v + Print the version. + @ + Read options and filenames from file. - private static final String[] USAGE = { - "", - "Usage: google-java-format [options] file(s)", - "", - "Options:", - " -i, -r, -replace, --replace", - " Send formatted output back to files, not stdout.", - " -", - " Format stdin -> stdout", - " --assume-filename, -assume-filename", - " File name to use for diagnostics when formatting standard input (default is ).", - " --aosp, -aosp, -a", - " Use AOSP style instead of Google Style (4-space indentation).", - " --fix-imports-only", - " Fix import order and remove any unused imports, but do no other formatting.", - " --skip-sorting-imports", - " Do not fix the import order. Unused imports will still be removed.", - " --skip-removing-unused-imports", - " Do not remove unused imports. Imports will still be sorted.", - " --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.", - " --length, -length", - " Character length to format.", - " --lines, -lines, --line, -line", - " Line range(s) to format, like 5:10 (1-based; default is all).", - " --offset, -offset", - " Character offset to format (0-based; default is all).", - " --help, -help, -h", - " Print this usage statement.", - " --version, -version, -v", - " Print the version.", - " @", - " Read options and filenames from file.", - "", - }; +"""; - private static final String[] ADDITIONAL_USAGE = { - "If -i is given with -, the result is sent to stdout.", - "The --lines, --offset, and --length flags may be given more than once.", - "The --offset and --length flags must be given an equal number of times.", - "If --lines, --offset, or --length are given, only one file (or -) may be given." - }; + private static final String ADDITIONAL_USAGE = +""" +If -i is given with -, the result is sent to stdout. +The --lines, --offset, and --length flags may be given more than once. +The --offset and --length flags must be given an equal number of times. +If --lines, --offset, or --length are given, only one file (or -) may be given. +"""; UsageException() { super(buildMessage(null)); @@ -86,19 +88,15 @@ private static String buildMessage(String message) { if (message != null) { builder.append(message).append('\n'); } - appendLines(builder, USAGE); - appendLines(builder, ADDITIONAL_USAGE); - appendLines(builder, new String[] {""}); - appendLine(builder, Main.versionString()); - appendLines(builder, DOCS_LINK); + appendText(builder, USAGE); + appendText(builder, ADDITIONAL_USAGE); + appendText(builder, "\n"); + appendText(builder, Main.versionString()); + appendText(builder, DOCS_LINK); return builder.toString(); } - private static void appendLine(StringBuilder builder, String line) { - builder.append(line).append(System.lineSeparator()); - } - - private static void appendLines(StringBuilder builder, String[] lines) { - NEWLINE_JOINER.appendTo(builder, lines).append(System.lineSeparator()); + private static void appendText(StringBuilder builder, String text) { + builder.append(text.replace("\n", System.lineSeparator())); } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java index 7c7894754..e8da9fac9 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java +++ b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingFiler.java @@ -18,13 +18,14 @@ import com.google.googlejavaformat.java.Formatter; import java.io.IOException; -import javax.annotation.Nullable; import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.tools.FileObject; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** * A decorating {@link Filer} implementation which formats Java source files with a {@link @@ -37,8 +38,28 @@ public final class FormattingFiler implements Filer { private final Formatter formatter = new Formatter(); private final Messager messager; - /** @param delegate filer to decorate */ - public FormattingFiler(Filer delegate) { + /** + * Create a new {@link FormattingFiler}. + * + * @param processingEnv the processing environment + */ + public static Filer create(ProcessingEnvironment processingEnv) { + Filer delegate = processingEnv.getFiler(); + if (processingEnv.getOptions().containsKey("experimental_turbine_hjar")) { + return delegate; + } + return new FormattingFiler(delegate, processingEnv.getMessager()); + } + + /** + * Create a new {@link FormattingFiler}. + * + * @param delegate filer to decorate + * @deprecated prefer {@link #create(ProcessingEnvironment)} + */ + @Deprecated + public + FormattingFiler(Filer delegate) { this(delegate, null); } @@ -48,8 +69,11 @@ public FormattingFiler(Filer delegate) { * * @param delegate filer to decorate * @param messager to log warnings to + * @deprecated prefer {@link #create(ProcessingEnvironment)} */ - public FormattingFiler(Filer delegate, @Nullable Messager messager) { + @Deprecated + public + FormattingFiler(Filer delegate, @Nullable Messager messager) { this.delegate = checkNotNull(delegate); this.messager = messager; } diff --git a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java index 65b420f16..b65258b1a 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java +++ b/core/src/main/java/com/google/googlejavaformat/java/filer/FormattingJavaFileObject.java @@ -22,11 +22,11 @@ import com.google.googlejavaformat.java.FormatterException; import java.io.IOException; import java.io.Writer; -import javax.annotation.Nullable; import javax.annotation.processing.Messager; import javax.tools.Diagnostic; import javax.tools.ForwardingJavaFileObject; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /** A {@link JavaFileObject} decorator which {@linkplain Formatter formats} source code. */ final class FormattingJavaFileObject extends ForwardingJavaFileObject { @@ -58,6 +58,11 @@ public void write(char[] chars, int start, int end) throws IOException { stringBuilder.append(chars, start, end - start); } + @Override + public void write(String string) throws IOException { + stringBuilder.append(string); + } + @Override public void flush() throws IOException {} diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java index 8fbd49f9f..273bdcd33 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/CharStream.java @@ -14,7 +14,6 @@ package com.google.googlejavaformat.java.javadoc; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import java.util.regex.Matcher; @@ -27,42 +26,48 @@ * characters from tryConsume? -- but it is convenient for the lexer. */ final class CharStream { - String remaining; - int toConsume; + private final String input; + private int position; + private int tokenEnd = -1; // Negative value means no token, and will cause an exception if used. CharStream(String input) { - this.remaining = checkNotNull(input); + this.input = checkNotNull(input); } boolean tryConsume(String expected) { - if (!remaining.startsWith(expected)) { + if (!input.startsWith(expected, position)) { return false; } - toConsume = expected.length(); + tokenEnd = position + expected.length(); return true; } - /* + /** + * Tries to consume characters from the current position that match the given pattern. + * * @param pattern the pattern to search for, which must be anchored to match only at position 0 */ boolean tryConsumeRegex(Pattern pattern) { - Matcher matcher = pattern.matcher(remaining); - if (!matcher.find()) { + Matcher matcher = pattern.matcher(input).region(position, input.length()); + if (!matcher.lookingAt()) { return false; } - checkArgument(matcher.start() == 0); - toConsume = matcher.end(); + tokenEnd = matcher.end(); return true; } String readAndResetRecorded() { - String result = remaining.substring(0, toConsume); - remaining = remaining.substring(toConsume); - toConsume = 0; // TODO(cpovirk): Set this to a bogus value here and in the constructor. + String result = input.substring(position, tokenEnd); + position = tokenEnd; + tokenEnd = -1; return result; } boolean isExhausted() { - return remaining.isEmpty(); + return position == input.length(); + } + + int position() { + return position; } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java index b8d36e6e6..7e2a4e0bd 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 @@ -14,15 +14,49 @@ package com.google.googlejavaformat.java.javadoc; +import static com.google.common.base.Preconditions.checkState; import static com.google.googlejavaformat.java.javadoc.JavadocLexer.lex; -import static com.google.googlejavaformat.java.javadoc.Token.Type.BR_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; import static java.util.regex.Pattern.CASE_INSENSITIVE; import static java.util.regex.Pattern.compile; +import static java.util.stream.Collectors.joining; +import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableList; -import com.google.googlejavaformat.java.JavaFormatterOptions; import com.google.googlejavaformat.java.javadoc.JavadocLexer.LexException; +import com.google.googlejavaformat.java.javadoc.Token.BeginJavadoc; +import com.google.googlejavaformat.java.javadoc.Token.BlockquoteCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.BlockquoteOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.BrTag; +import com.google.googlejavaformat.java.javadoc.Token.CodeCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.CodeOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.EndJavadoc; +import com.google.googlejavaformat.java.javadoc.Token.FooterJavadocTagStart; +import com.google.googlejavaformat.java.javadoc.Token.ForcedNewline; +import com.google.googlejavaformat.java.javadoc.Token.HeaderCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.HeaderOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.HtmlComment; +import com.google.googlejavaformat.java.javadoc.Token.ListCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.ListItemCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.ListItemOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.ListOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.Literal; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanEnd; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanStart; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownFencedCodeBlock; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownHardLineBreak; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownTable; +import com.google.googlejavaformat.java.javadoc.Token.MoeBeginStripComment; +import com.google.googlejavaformat.java.javadoc.Token.MoeEndStripComment; +import com.google.googlejavaformat.java.javadoc.Token.OptionalLineBreak; +import com.google.googlejavaformat.java.javadoc.Token.ParagraphCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.ParagraphOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.PreCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.PreOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.SnippetBegin; +import com.google.googlejavaformat.java.javadoc.Token.SnippetEnd; +import com.google.googlejavaformat.java.javadoc.Token.TableCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.TableOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.Whitespace; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,101 +70,78 @@ * single blank line if it's empty. */ public final class JavadocFormatter { + + static final int MAX_LINE_LENGTH = 100; + /** - * Formats the given Javadoc comment, which must start with ∕✱✱ and end with ✱∕. The output will - * start and end with the same characters. + * Formats the given Javadoc comment. A classic Javadoc comment must start with ∕✱✱ and end with + * ✱∕, and the output will start and end with the same characters. A Markdown Javadoc comment + * consists of lines each of which starts with ///, and the output will also consist of such + * lines. */ - public static String formatJavadoc(String input, int blockIndent, JavaFormatterOptions options) { + public static String formatJavadoc(String input, int blockIndent) { + boolean classicJavadoc = + switch (input) { + case String s when s.startsWith("/**") -> true; + case String s when s.startsWith("///") -> false; + default -> + throw new IllegalArgumentException("Input does not start with /** or ///: " + input); + }; + String inputForLexer = classicJavadoc ? input : ("///" + markdownCommentText(input)); ImmutableList tokens; try { - tokens = lex(input); + tokens = lex(inputForLexer, classicJavadoc); } catch (LexException e) { return input; } - String result = render(tokens, blockIndent, options); - return makeSingleLineIfPossible(blockIndent, result, options); + String result = render(tokens, blockIndent, classicJavadoc); + if (classicJavadoc) { + result = makeSingleLineIfPossible(blockIndent, result); + } + return result; } - private static String render(List input, int blockIndent, JavaFormatterOptions options) { - JavadocWriter output = new JavadocWriter(blockIndent, options); + private static String render(List input, int blockIndent, boolean classicJavadoc) { + JavadocWriter output = new JavadocWriter(blockIndent, classicJavadoc); for (Token token : input) { - switch (token.getType()) { - case BEGIN_JAVADOC: - output.writeBeginJavadoc(); - break; - case END_JAVADOC: + switch (token) { + case BeginJavadoc unused -> output.writeBeginJavadoc(); + case EndJavadoc unused -> { 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 FooterJavadocTagStart t -> output.writeFooterJavadocTagStart(t); + case SnippetBegin t -> output.writeSnippetBegin(t); + case SnippetEnd t -> output.writeSnippetEnd(t); + case ListOpenTag t -> output.writeListOpen(t); + case ListCloseTag t -> output.writeListClose(t); + case ListItemOpenTag t -> output.writeListItemOpen(t); + case HeaderOpenTag t -> output.writeHeaderOpen(t); + case HeaderCloseTag t -> output.writeHeaderClose(t); + case ParagraphOpenTag t -> output.writeParagraphOpen(standardizePToken(t)); + case BlockquoteOpenTag t -> output.writeBlockquoteOpenOrClose(t); + case BlockquoteCloseTag t -> output.writeBlockquoteOpenOrClose(t); + case PreOpenTag t -> output.writePreOpen(t); + case PreCloseTag t -> output.writePreClose(t); + case CodeOpenTag t -> output.writeCodeOpen(t); + case CodeCloseTag t -> output.writeCodeClose(t); + case TableOpenTag t -> output.writeTableOpen(t); + case TableCloseTag t -> output.writeTableClose(t); + case MoeBeginStripComment t -> output.requestMoeBeginStripComment(t); + case MoeEndStripComment t -> output.writeMoeEndStripComment(t); + case HtmlComment t -> output.writeHtmlComment(t); + case BrTag t -> output.writeBr(standardizeBrToken(t)); + case Whitespace unused -> output.requestWhitespace(); + case ForcedNewline unused -> output.writeLineBreakNoAutoIndent(); + case MarkdownHardLineBreak unused -> output.writeMarkdownHardLineBreak(); + case Literal t -> output.writeLiteral(t); + case MarkdownFencedCodeBlock t -> output.writeMarkdownFencedCodeBlock(t); + case MarkdownTable t -> output.writeMarkdownTable(t); + case ListItemCloseTag unused -> {} + case OptionalLineBreak unused -> {} + case ParagraphCloseTag unused -> {} + case MarkdownCodeSpanStart unused -> {} + case MarkdownCodeSpanEnd unused -> {} } } throw new AssertionError(); @@ -141,20 +152,20 @@ private static String render(List input, int blockIndent, JavaFormatterOp * should include them as part of its own postprocessing? Or even the writer could make sense. */ - private static Token standardizeBrToken(Token token) { + private static BrTag standardizeBrToken(BrTag token) { return standardize(token, STANDARD_BR_TOKEN); } - private static Token standardizePToken(Token token) { + private static ParagraphOpenTag standardizePToken(ParagraphOpenTag token) { return standardize(token, STANDARD_P_TOKEN); } - private static Token standardize(Token token, Token standardToken) { - return SIMPLE_TAG_PATTERN.matcher(token.getValue()).matches() ? standardToken : token; + private static T standardize(T token, T standardToken) { + return SIMPLE_TAG_PATTERN.matcher(token.value()).matches() ? standardToken : token; } - private static final Token STANDARD_BR_TOKEN = new Token(BR_TAG, "
    "); - private static final Token STANDARD_P_TOKEN = new Token(PARAGRAPH_OPEN_TAG, "

    "); + private static final BrTag STANDARD_BR_TOKEN = new BrTag("
    "); + private static final ParagraphOpenTag STANDARD_P_TOKEN = new ParagraphOpenTag("

    "); private static final Pattern SIMPLE_TAG_PATTERN = compile("^<\\w+\\s*/?\\s*>", CASE_INSENSITIVE); private static final Pattern ONE_CONTENT_LINE_PATTERN = compile(" */[*][*]\n *[*] (.*)\n *[*]/"); @@ -163,17 +174,55 @@ private static Token standardize(Token token, Token standardToken) { * Returns the given string or a one-line version of it (e.g., "∕✱✱ Tests for foos. ✱∕") if it * fits on one line. */ - private static String makeSingleLineIfPossible( - int blockIndent, String input, JavaFormatterOptions options) { - int oneLinerContentLength = options.maxLineLength() - "/** */".length() - blockIndent; + private static String makeSingleLineIfPossible(int blockIndent, String input) { Matcher matcher = ONE_CONTENT_LINE_PATTERN.matcher(input); - if (matcher.matches() && matcher.group(1).isEmpty()) { - return "/** */"; - } else if (matcher.matches() && matcher.group(1).length() <= oneLinerContentLength) { - return "/** " + matcher.group(1) + " */"; + if (matcher.matches()) { + String line = matcher.group(1); + if (line.isEmpty()) { + return "/** */"; + } else if (oneLineJavadoc(line, blockIndent)) { + return "/** " + line + " */"; + } } return input; } + private static boolean oneLineJavadoc(String line, int blockIndent) { + int oneLinerContentLength = MAX_LINE_LENGTH - "/** */".length() - blockIndent; + if (line.length() > oneLinerContentLength) { + return false; + } + // If the javadoc contains only a tag, use multiple lines to encourage writing a summary + // fragment, unless it's /** @hide */. + if (line.startsWith("@") && !line.equals("@hide")) { + return false; + } + return true; + } + + private static final CharMatcher NOT_SPACE_OR_TAB = CharMatcher.noneOf(" \t"); + + /** + * Returns the given string with the leading /// and any common leading whitespace removed from + * each line. The resultant string can then be fed to a standard Markdown parser. + */ + private static String markdownCommentText(String input) { + List lines = + input + .lines() + .peek(line -> checkState(line.contains("///"), "Line does not contain ///: %s", line)) + .map(line -> line.substring(line.indexOf("///") + 3)) + .toList(); + int leadingSpace = + lines.stream() + .filter(line -> NOT_SPACE_OR_TAB.matchesAnyOf(line)) + .mapToInt(NOT_SPACE_OR_TAB::indexIn) + .min() + .orElse(0); + return lines.stream() + .map(line -> line.length() < leadingSpace ? "" : line.substring(leadingSpace)) + .collect(joining("\n")); + } + 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..ab89e5d1c 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 @@ -18,33 +18,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Verify.verify; import static com.google.common.collect.Iterators.peekingIterator; -import static com.google.googlejavaformat.java.javadoc.Token.Type.BEGIN_JAVADOC; -import static com.google.googlejavaformat.java.javadoc.Token.Type.BLOCKQUOTE_CLOSE_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.BLOCKQUOTE_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.BR_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.CODE_CLOSE_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.CODE_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.END_JAVADOC; -import static com.google.googlejavaformat.java.javadoc.Token.Type.FOOTER_JAVADOC_TAG_START; -import static com.google.googlejavaformat.java.javadoc.Token.Type.FORCED_NEWLINE; -import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_CLOSE_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.HTML_COMMENT; -import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_CLOSE_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_CLOSE_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.LITERAL; -import static com.google.googlejavaformat.java.javadoc.Token.Type.MOE_BEGIN_STRIP_COMMENT; -import static com.google.googlejavaformat.java.javadoc.Token.Type.MOE_END_STRIP_COMMENT; -import static com.google.googlejavaformat.java.javadoc.Token.Type.OPTIONAL_LINE_BREAK; -import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_CLOSE_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_CLOSE_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.PRE_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_CLOSE_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.TABLE_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.WHITESPACE; import static java.lang.String.format; import static java.util.regex.Pattern.CASE_INSENSITIVE; import static java.util.regex.Pattern.DOTALL; @@ -52,26 +25,72 @@ import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.PeekingIterator; -import com.google.googlejavaformat.java.javadoc.Token.Type; +import com.google.googlejavaformat.java.javadoc.Token.BeginJavadoc; +import com.google.googlejavaformat.java.javadoc.Token.BlockquoteCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.BlockquoteOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.BrTag; +import com.google.googlejavaformat.java.javadoc.Token.CodeCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.CodeOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.EndJavadoc; +import com.google.googlejavaformat.java.javadoc.Token.FooterJavadocTagStart; +import com.google.googlejavaformat.java.javadoc.Token.ForcedNewline; +import com.google.googlejavaformat.java.javadoc.Token.HeaderCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.HeaderOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.HtmlComment; +import com.google.googlejavaformat.java.javadoc.Token.ListCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.ListItemCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.ListItemOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.ListOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.Literal; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanEnd; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownCodeSpanStart; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownHardLineBreak; +import com.google.googlejavaformat.java.javadoc.Token.MoeBeginStripComment; +import com.google.googlejavaformat.java.javadoc.Token.MoeEndStripComment; +import com.google.googlejavaformat.java.javadoc.Token.OptionalLineBreak; +import com.google.googlejavaformat.java.javadoc.Token.ParagraphCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.ParagraphOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.PreCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.PreOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.SnippetBegin; +import com.google.googlejavaformat.java.javadoc.Token.SnippetEnd; +import com.google.googlejavaformat.java.javadoc.Token.TableCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.TableOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.Whitespace; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; +import java.util.function.Function; import java.util.regex.Pattern; /** Lexer for the Javadoc formatter. */ final class JavadocLexer { /** Takes a Javadoc comment, including ∕✱✱ and ✱∕, and returns tokens, including ∕✱✱ and ✱∕. */ - static ImmutableList lex(String input) throws LexException { - /* - * TODO(cpovirk): In theory, we should interpret Unicode escapes (yet output them in their - * original form). This would mean mean everything from an encoded ∕✱✱ to an encoded

     tag,
    -     * so we'll probably never bother.
    -     */
    -    input = stripJavadocBeginAndEnd(input);
    +  static ImmutableList lex(String input, boolean classicJavadoc) throws LexException {
         input = normalizeLineEndings(input);
    -    return new JavadocLexer(new CharStream(input)).generateTokens();
    +    MarkdownPositions markdownPositions;
    +    if (classicJavadoc) {
    +      /*
    +       * TODO(cpovirk): In theory, we should interpret Unicode escapes (yet output them in their
    +       * original form). This would mean mean everything from an encoded ∕✱✱ to an encoded 
    +       * tag, so we'll probably never bother.
    +       */
    +      input = stripJavadocBeginAndEnd(input);
    +      markdownPositions = MarkdownPositions.EMPTY;
    +    } else {
    +      checkArgument(input.startsWith("///"));
    +      input = input.substring("///".length());
    +      try {
    +        markdownPositions = MarkdownPositions.parse(input);
    +      } catch (UnsupportedOperationException e) {
    +        throw new LexException(e);
    +      }
    +    }
    +    return new JavadocLexer(new CharStream(input), markdownPositions, classicJavadoc)
    +        .generateTokens();
       }
     
       /** The lexer crashes on windows line endings, so for now just normalize to `\n`. */
    @@ -92,57 +111,153 @@ private static String stripJavadocBeginAndEnd(String input) {
         return input.substring("/**".length(), input.length() - "*/".length());
       }
     
    +  /**
    +   * An element of the nested contexts we might be in. For example, if we are inside {@code
    +   * 
    {@code ...}
    } then the stack of nested contexts would be {@code PRE} plus {@code + * CODE_CONTEXT}. + */ + enum NestingContext { + /** {@code
    ...
    }. */ + HTML_PRE_CONTEXT, + + /** {@code ...}. */ + HTML_CODE_CONTEXT, + + /** Markdown {@code `...`}. */ + MARKDOWN_CODE_CONTEXT, + + /** {@code ...
    }. */ + TABLE, + + /** {@code {@snippet ...}}. */ + SNIPPET_CONTEXT, + + /** Nested braces within one of the other contexts. */ + BRACE_CONTEXT, + + /** + * An inline tag such as {@code {@link ...}} or {@code {@code ...}}, but not {@code {@snippet + * ...}}. + */ + INLINE_TAG_CONTEXT + } + private final CharStream input; - private final NestingCounter braceDepth = new NestingCounter(); - private final NestingCounter preDepth = new NestingCounter(); - private final NestingCounter codeDepth = new NestingCounter(); - private final NestingCounter tableDepth = new NestingCounter(); + private final boolean classicJavadoc; + private final MarkdownPositions markdownPositions; + private final NestingStack contextStack = new NestingStack<>(); private boolean somethingSinceNewline; - private JavadocLexer(CharStream input) { + private JavadocLexer( + CharStream input, MarkdownPositions markdownPositions, boolean classicJavadoc) { this.input = checkNotNull(input); + this.markdownPositions = markdownPositions; + this.classicJavadoc = classicJavadoc; } private ImmutableList generateTokens() throws LexException { ImmutableList.Builder tokens = ImmutableList.builder(); - Token token = new Token(BEGIN_JAVADOC, "/**"); + Token token = new BeginJavadoc(classicJavadoc ? "/**" : "///"); tokens.add(token); while (!input.isExhausted()) { + boolean moreMarkdown; + do { + moreMarkdown = false; + // If there are one or more markdown tokens at the current position, consume their text and + // add them to the token list. If a token has non-empty text, consuming its text changes the + // position, so we need to start looking for markdown tokens at the new position. It is + // assumed that there are no other tokens (markdown or otherwise) in a non-empty text span + // covered by a markdown token. + for (Token markdownToken : markdownPositions.tokensAt(input.position())) { + // For `...`, we switch to MARKDOWN_CODE_CONTEXT for the duration of the span, and we + // change the start or end token to a Literal so it will get joined to adjacent Literal + // tokens. That prevents line breaks adjacent to the backticks in "foo`bar`baz", but still + // allows them at the spaces in "foo `bar` baz" or "foo` bar `baz". + switch (markdownToken) { + case MarkdownCodeSpanStart unused -> { + contextStack.push(NestingContext.MARKDOWN_CODE_CONTEXT); + markdownToken = new Literal(markdownToken.value()); + } + case MarkdownCodeSpanEnd unused -> { + contextStack.popUntil(NestingContext.MARKDOWN_CODE_CONTEXT); + markdownToken = new Literal(markdownToken.value()); + } + default -> {} + } + tokens.add(markdownToken); + if (!markdownToken.value().isEmpty()) { + boolean consumed = input.tryConsume(markdownToken.value()); + verify(consumed, "Did not consume markdown token: %s", markdownToken); + var unused = input.readAndResetRecorded(); + moreMarkdown = true; + } + } + } while (moreMarkdown); + if (input.isExhausted()) { + break; + } token = readToken(); tokens.add(token); } checkMatchingTags(); - token = new Token(END_JAVADOC, "*/"); + token = new EndJavadoc(classicJavadoc ? "*/" : ""); tokens.add(token); ImmutableList result = tokens.build(); result = joinAdjacentLiteralsAndAdjacentWhitespace(result); - result = inferParagraphTags(result); + if (classicJavadoc) { + result = inferParagraphTags(result); + } result = optionalizeSpacesAfterLinks(result); result = deindentPreCodeBlocks(result); return result; } private Token readToken() throws LexException { - Type type = consumeToken(); + Function tokenFactory = consumeToken(); String value = input.readAndResetRecorded(); - return new Token(type, value); + return tokenFactory.apply(value); } - private Type consumeToken() throws LexException { + private Function consumeToken() throws LexException { boolean preserveExistingFormatting = preserveExistingFormatting(); - if (input.tryConsumeRegex(NEWLINE_PATTERN)) { + Pattern newlinePattern = classicJavadoc ? CLASSIC_NEWLINE_PATTERN : MARKDOWN_NEWLINE_PATTERN; + if (input.tryConsumeRegex(newlinePattern)) { somethingSinceNewline = false; - return preserveExistingFormatting ? FORCED_NEWLINE : WHITESPACE; + return preserveExistingFormatting ? ForcedNewline::new : Whitespace::new; } else if (input.tryConsume(" ") || input.tryConsume("\t")) { // TODO(cpovirk): How about weird whitespace chars? Ideally we'd distinguish breaking vs. not. - // Returning LITERAL here prevent us from breaking a
     line. For more info, see LITERAL.
    -      return preserveExistingFormatting ? LITERAL : WHITESPACE;
    +      // Returning Literal here prevents us from breaking a 
     line. For more info, see Literal.
    +      return preserveExistingFormatting ? Literal::new : Whitespace::new;
    +    }
    +
    +    if (contextStack.contains(NestingContext.MARKDOWN_CODE_CONTEXT)) {
    +      // Consume one or more characters. We know the first character isn't a newline or space
    +      // because we've eliminated those possibilities, and it can't be the end of the `...` span
    +      // either because that would have caused us to pop MARKDOWN_CODE_CONTEXT from the stack. The
    +      // remaining characters being matched *could* be those things, so the regex stops at
    +      // whitespace or a backtick. The *first* character could be a backtick, in constructs like
    +      // `` `foo` ``, where the backticks adjacent to "foo" are part of the text of the code span.
    +      //
    +      // Backslash has no special meaning inside `...` so this code precedes the backslash code.
    +      verify(input.tryConsumeRegex(WORD_IN_CODE_SPAN_PATTERN));
    +      return Literal::new;
    +    }
    +    if (!classicJavadoc) {
    +      // Markdown backslash handling. \ at end of line, optionally followed by whitespace, is a hard
    +      // line break. \ elsewhere cancels any special meaning of the following character.
    +      if (input.tryConsumeRegex(MARKDOWN_HARD_LINE_BREAK_PATTERN)) {
    +        somethingSinceNewline = false;
    +        return MarkdownHardLineBreak::new;
    +      } else if (input.tryConsumeRegex(BACKSLASH_PLUS_CHARACTER_PATTERN)) {
    +        somethingSinceNewline = true;
    +        return Literal::new;
    +      }
         }
     
         /*
    @@ -154,99 +269,114 @@ private Type consumeToken() throws LexException {
         if (!somethingSinceNewline && input.tryConsumeRegex(FOOTER_TAG_PATTERN)) {
           checkMatchingTags();
           somethingSinceNewline = true;
    -      return FOOTER_JAVADOC_TAG_START;
    +      return FooterJavadocTagStart::new;
         }
         somethingSinceNewline = true;
     
    -    if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) {
    -      braceDepth.increment();
    -      return LITERAL;
    +    if (input.tryConsumeRegex(SNIPPET_TAG_OPEN_PATTERN)) {
    +      // {@snippet ...}
    +      if (contextStack.containsAny(BRACE_CONTEXTS)) {
    +        contextStack.push(NestingContext.BRACE_CONTEXT);
    +        return Literal::new;
    +      } else {
    +        contextStack.push(NestingContext.SNIPPET_CONTEXT);
    +        return SnippetBegin::new;
    +      }
    +    } else if (input.tryConsumeRegex(INLINE_TAG_OPEN_PATTERN)) {
    +      // {@foo ...}. We recognize this even in something like {@code {@foo ...}}, but it doesn't
    +      // make any difference.
    +      contextStack.push(NestingContext.INLINE_TAG_CONTEXT);
    +      return Literal::new;
         } else if (input.tryConsume("{")) {
    -      braceDepth.incrementIfPositive();
    -      return LITERAL;
    +      // A left brace that is not the start of {@foo}. We record the brace, for cases like
    +      // `{@code foo{bar}}`, where the second right brace is the end of the tag.
    +      if (contextStack.containsAny(BRACE_CONTEXTS)) {
    +        contextStack.push(NestingContext.BRACE_CONTEXT);
    +      }
    +      return Literal::new;
         } else if (input.tryConsume("}")) {
    -      braceDepth.decrementIfPositive();
    -      return LITERAL;
    +      var popped = contextStack.popIfIn(BRACE_CONTEXTS);
    +      if (popped == NestingContext.SNIPPET_CONTEXT) {
    +        return SnippetEnd::new;
    +      }
    +      return Literal::new;
         }
     
         // Inside an inline tag, don't do any HTML interpretation.
    -    if (braceDepth.isPositive()) {
    -      verify(input.tryConsumeRegex(LITERAL_PATTERN));
    -      return LITERAL;
    +    if (contextStack.containsAny(TAG_CONTEXTS)) {
    +      verify(input.tryConsumeRegex(literalPattern()));
    +      return Literal::new;
         }
     
         if (input.tryConsumeRegex(PRE_OPEN_PATTERN)) {
    -      preDepth.increment();
    -      return preserveExistingFormatting ? LITERAL : PRE_OPEN_TAG;
    +      contextStack.push(NestingContext.HTML_PRE_CONTEXT);
    +      return preserveExistingFormatting ? Literal::new : PreOpenTag::new;
         } else if (input.tryConsumeRegex(PRE_CLOSE_PATTERN)) {
    -      preDepth.decrementIfPositive();
    -      return preserveExistingFormatting() ? LITERAL : PRE_CLOSE_TAG;
    +      contextStack.popUntil(NestingContext.HTML_PRE_CONTEXT);
    +      return preserveExistingFormatting() ? Literal::new : PreCloseTag::new;
         }
     
         if (input.tryConsumeRegex(CODE_OPEN_PATTERN)) {
    -      codeDepth.increment();
    -      return preserveExistingFormatting ? LITERAL : CODE_OPEN_TAG;
    +      contextStack.push(NestingContext.HTML_CODE_CONTEXT);
    +      return preserveExistingFormatting ? Literal::new : CodeOpenTag::new;
         } else if (input.tryConsumeRegex(CODE_CLOSE_PATTERN)) {
    -      codeDepth.decrementIfPositive();
    -      return preserveExistingFormatting() ? LITERAL : CODE_CLOSE_TAG;
    +      contextStack.popUntil(NestingContext.HTML_CODE_CONTEXT);
    +      return preserveExistingFormatting() ? Literal::new : CodeCloseTag::new;
         }
     
         if (input.tryConsumeRegex(TABLE_OPEN_PATTERN)) {
    -      tableDepth.increment();
    -      return preserveExistingFormatting ? LITERAL : TABLE_OPEN_TAG;
    +      contextStack.push(NestingContext.TABLE);
    +      return preserveExistingFormatting ? Literal::new : TableOpenTag::new;
         } else if (input.tryConsumeRegex(TABLE_CLOSE_PATTERN)) {
    -      tableDepth.decrementIfPositive();
    -      return preserveExistingFormatting() ? LITERAL : TABLE_CLOSE_TAG;
    +      contextStack.popUntil(NestingContext.TABLE);
    +      return preserveExistingFormatting() ? Literal::new : TableCloseTag::new;
         }
     
         if (preserveExistingFormatting) {
    -      verify(input.tryConsumeRegex(LITERAL_PATTERN));
    -      return LITERAL;
    +      verify(input.tryConsumeRegex(literalPattern()));
    +      return Literal::new;
         }
     
         if (input.tryConsumeRegex(PARAGRAPH_OPEN_PATTERN)) {
    -      return PARAGRAPH_OPEN_TAG;
    +      return ParagraphOpenTag::new;
         } else if (input.tryConsumeRegex(PARAGRAPH_CLOSE_PATTERN)) {
    -      return PARAGRAPH_CLOSE_TAG;
    +      return ParagraphCloseTag::new;
         } else if (input.tryConsumeRegex(LIST_OPEN_PATTERN)) {
    -      return LIST_OPEN_TAG;
    +      return ListOpenTag::new;
         } else if (input.tryConsumeRegex(LIST_CLOSE_PATTERN)) {
    -      return LIST_CLOSE_TAG;
    +      return ListCloseTag::new;
         } else if (input.tryConsumeRegex(LIST_ITEM_OPEN_PATTERN)) {
    -      return LIST_ITEM_OPEN_TAG;
    +      return ListItemOpenTag::new;
         } else if (input.tryConsumeRegex(LIST_ITEM_CLOSE_PATTERN)) {
    -      return LIST_ITEM_CLOSE_TAG;
    +      return ListItemCloseTag::new;
         } else if (input.tryConsumeRegex(BLOCKQUOTE_OPEN_PATTERN)) {
    -      return BLOCKQUOTE_OPEN_TAG;
    +      return BlockquoteOpenTag::new;
         } else if (input.tryConsumeRegex(BLOCKQUOTE_CLOSE_PATTERN)) {
    -      return BLOCKQUOTE_CLOSE_TAG;
    +      return BlockquoteCloseTag::new;
         } else if (input.tryConsumeRegex(HEADER_OPEN_PATTERN)) {
    -      return HEADER_OPEN_TAG;
    +      return HeaderOpenTag::new;
         } else if (input.tryConsumeRegex(HEADER_CLOSE_PATTERN)) {
    -      return HEADER_CLOSE_TAG;
    +      return HeaderCloseTag::new;
         } else if (input.tryConsumeRegex(BR_PATTERN)) {
    -      return BR_TAG;
    +      return BrTag::new;
         } else if (input.tryConsumeRegex(MOE_BEGIN_STRIP_COMMENT_PATTERN)) {
    -      return MOE_BEGIN_STRIP_COMMENT;
    +      return MoeBeginStripComment::new;
         } else if (input.tryConsumeRegex(MOE_END_STRIP_COMMENT_PATTERN)) {
    -      return MOE_END_STRIP_COMMENT;
    +      return MoeEndStripComment::new;
         } else if (input.tryConsumeRegex(HTML_COMMENT_PATTERN)) {
    -      return HTML_COMMENT;
    -    } else if (input.tryConsumeRegex(LITERAL_PATTERN)) {
    -      return LITERAL;
    +      return HtmlComment::new;
    +    } else if (input.tryConsumeRegex(literalPattern())) {
    +      return Literal::new;
         }
         throw new AssertionError();
       }
     
       private boolean preserveExistingFormatting() {
    -    return preDepth.isPositive() || tableDepth.isPositive() || codeDepth.isPositive();
    +    return contextStack.containsAny(PRESERVE_FORMATTING_CONTEXTS);
       }
     
       private void checkMatchingTags() throws LexException {
    -    if (braceDepth.isPositive()
    -        || preDepth.isPositive()
    -        || tableDepth.isPositive()
    -        || codeDepth.isPositive()) {
    +    if (!contextStack.isEmpty()) {
           throw new LexException();
         }
       }
    @@ -255,7 +385,7 @@ private void checkMatchingTags() throws LexException {
        * Join together adjacent literal tokens, and join together adjacent whitespace tokens.
        *
        * 

    For literal tokens, this means something like {@code ["", "foo", ""] => - * ["foo"]}. See {@link #LITERAL_PATTERN} for discussion of why those tokens are separate + * ["foo"]}. See {@link #literalPattern()} for discussion of why those tokens are separate * to begin with. * *

    Whitespace tokens are treated analogously. We don't really "want" to join whitespace tokens, @@ -276,9 +406,8 @@ private static ImmutableList joinAdjacentLiteralsAndAdjacentWhitespace(Li StringBuilder accumulated = new StringBuilder(); for (PeekingIterator tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) { - if (tokens.peek().getType() == LITERAL) { - accumulated.append(tokens.peek().getValue()); - tokens.next(); + if (tokens.peek() instanceof Literal) { + accumulated.append(tokens.next().value()); continue; } @@ -289,30 +418,28 @@ private static ImmutableList joinAdjacentLiteralsAndAdjacentWhitespace(Li * it into a tag. */ - if (accumulated.length() == 0) { - output.add(tokens.peek()); - tokens.next(); + if (accumulated.isEmpty()) { + output.add(tokens.next()); continue; } StringBuilder seenWhitespace = new StringBuilder(); - while (tokens.peek().getType() == WHITESPACE) { - seenWhitespace.append(tokens.next().getValue()); + while (tokens.peek() instanceof Whitespace) { + seenWhitespace.append(tokens.next().value()); } - if (tokens.peek().getType() == LITERAL && tokens.peek().getValue().startsWith("@")) { + if (tokens.peek() instanceof Literal literal && literal.value().startsWith("@")) { // OK, we're in the case described above. accumulated.append(" "); - accumulated.append(tokens.peek().getValue()); - tokens.next(); + accumulated.append(tokens.next().value()); continue; } - output.add(new Token(LITERAL, accumulated.toString())); + output.add(new Literal(accumulated.toString())); accumulated.setLength(0); - if (seenWhitespace.length() > 0) { - output.add(new Token(WHITESPACE, seenWhitespace.toString())); + if (!seenWhitespace.isEmpty()) { + output.add(new Whitespace(seenWhitespace.toString())); } // We have another token coming, possibly of type OTHER. Leave it for the next iteration. @@ -336,15 +463,14 @@ private static ImmutableList inferParagraphTags(List input) { ImmutableList.Builder output = ImmutableList.builder(); for (PeekingIterator tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) { - if (tokens.peek().getType() == LITERAL) { + if (tokens.peek() instanceof Literal) { output.add(tokens.next()); - if (tokens.peek().getType() == WHITESPACE - && hasMultipleNewlines(tokens.peek().getValue())) { + if (tokens.peek() instanceof Whitespace && hasMultipleNewlines(tokens.peek().value())) { output.add(tokens.next()); - if (tokens.peek().getType() == LITERAL) { - output.add(new Token(PARAGRAPH_OPEN_TAG, "

    ")); + if (tokens.peek() instanceof Literal) { + output.add(new ParagraphOpenTag("

    ")); } } } else { @@ -374,11 +500,11 @@ private static ImmutableList optionalizeSpacesAfterLinks(List inpu ImmutableList.Builder output = ImmutableList.builder(); for (PeekingIterator tokens = peekingIterator(input.iterator()); tokens.hasNext(); ) { - if (tokens.peek().getType() == LITERAL && tokens.peek().getValue().matches("^href=[^>]*>")) { + if (tokens.peek() instanceof Literal && tokens.peek().value().matches("href=[^>]*>")) { output.add(tokens.next()); - if (tokens.peek().getType() == WHITESPACE) { - output.add(new Token(OPTIONAL_LINE_BREAK, tokens.next().getValue())); + if (tokens.peek() instanceof Whitespace) { + output.add(new OptionalLineBreak(tokens.next().value())); } } else { output.add(tokens.next()); @@ -400,20 +526,20 @@ 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) { + if (!(tokens.peek() instanceof PreOpenTag)) { output.add(tokens.next()); continue; } output.add(tokens.next()); List initialNewlines = new ArrayList<>(); - while (tokens.hasNext() && tokens.peek().getType() == FORCED_NEWLINE) { + while (tokens.hasNext() && tokens.peek() instanceof ForcedNewline) { initialNewlines.add(tokens.next()); } - if (tokens.peek().getType() != LITERAL - || !tokens.peek().getValue().matches("[ \t]*[{]@code")) { + if (!(tokens.peek() instanceof Literal) || !tokens.peek().value().matches("[ \t]*[{]@code")) { output.addAll(initialNewlines); output.add(tokens.next()); continue; @@ -427,15 +553,15 @@ private static ImmutableList deindentPreCodeBlocks(List input) { private static void deindentPreCodeBlock( ImmutableList.Builder output, PeekingIterator tokens) { Deque saved = new ArrayDeque<>(); - output.add(new Token(LITERAL, tokens.next().getValue().trim())); - while (tokens.hasNext() && tokens.peek().getType() != PRE_CLOSE_TAG) { + output.add(new Literal(tokens.next().value().trim())); + while (tokens.hasNext() && !(tokens.peek() instanceof PreCloseTag)) { Token token = tokens.next(); saved.addLast(token); } - while (!saved.isEmpty() && saved.peekFirst().getType() == FORCED_NEWLINE) { + while (!saved.isEmpty() && saved.peekFirst() instanceof ForcedNewline) { saved.removeFirst(); } - while (!saved.isEmpty() && saved.peekLast().getType() == FORCED_NEWLINE) { + while (!saved.isEmpty() && saved.peekLast() instanceof ForcedNewline) { saved.removeLast(); } if (saved.isEmpty()) { @@ -445,47 +571,68 @@ private static void deindentPreCodeBlock( // move the trailing `}` to its own line Token last = saved.peekLast(); boolean trailingBrace = false; - if (last.getType() == LITERAL && last.getValue().endsWith("}")) { + if (last instanceof Literal && last.value().endsWith("}")) { saved.removeLast(); if (last.length() > 1) { - saved.addLast( - new Token(LITERAL, last.getValue().substring(0, last.getValue().length() - 1))); - saved.addLast(new Token(FORCED_NEWLINE, null)); + saved.addLast(new Literal(last.value().substring(0, last.value().length() - 1))); + saved.addLast(new ForcedNewline(null)); } trailingBrace = true; } int trim = -1; for (Token token : saved) { - if (token.getType() == LITERAL) { - int idx = CharMatcher.isNot(' ').indexIn(token.getValue()); + if (token instanceof Literal) { + int idx = CharMatcher.isNot(' ').indexIn(token.value()); if (idx != -1 && (trim == -1 || idx < trim)) { trim = idx; } } } - output.add(new Token(FORCED_NEWLINE, "\n")); + output.add(new ForcedNewline("\n")); for (Token token : saved) { - if (token.getType() == LITERAL) { + if (token instanceof Literal) { output.add( - new Token( - LITERAL, - trim > 0 && token.length() > trim - ? token.getValue().substring(trim) - : token.getValue())); + new Literal( + trim > 0 && token.length() > trim ? token.value().substring(trim) : token.value())); } else { output.add(token); } } if (trailingBrace) { - output.add(new Token(LITERAL, "}")); + output.add(new Literal("}")); } else { - output.add(new Token(FORCED_NEWLINE, "\n")); + output.add(new ForcedNewline("\n")); } } + /** Contexts that imply that we should not do HTML interpretation. */ + private static final ImmutableSet TAG_CONTEXTS = + ImmutableSet.of(NestingContext.SNIPPET_CONTEXT, NestingContext.INLINE_TAG_CONTEXT); + + /** + * Contexts that are opened by a left brace and closed by a matching right brace. These are the + * ones where a nested left brace should open a nested context. + */ + private static final ImmutableSet BRACE_CONTEXTS = + ImmutableSet.of( + NestingContext.SNIPPET_CONTEXT, + NestingContext.INLINE_TAG_CONTEXT, + NestingContext.BRACE_CONTEXT); + + /** + * Contexts that preserve formatting, including line breaks and leading whitespace, within the + * context. + */ + private static final ImmutableSet PRESERVE_FORMATTING_CONTEXTS = + ImmutableSet.of( + NestingContext.HTML_PRE_CONTEXT, + NestingContext.TABLE, + NestingContext.HTML_CODE_CONTEXT, + NestingContext.SNIPPET_CONTEXT); + private static final CharMatcher NEWLINE = CharMatcher.is('\n'); private static boolean hasMultipleNewlines(String s) { @@ -499,18 +646,19 @@ private static boolean hasMultipleNewlines(String s) { * We'd remove the trailing whitespace later on (in JavaCommentsHelper.rewrite), but I feel safer * stripping it now: It otherwise might confuse our line-length count, which we use for wrapping. */ - private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n[ \t]*[*]?[ \t]?"); + private static final Pattern CLASSIC_NEWLINE_PATTERN = compile("[ \t]*\n[ \t]*[*]?[ \t]?"); + private static final Pattern MARKDOWN_NEWLINE_PATTERN = compile("[ \t]*\n[ \t]*"); // We ensure elsewhere that we match this only at the beginning of a line. // Only match tags that start with a lowercase letter, to avoid false matches on unescaped // annotations inside code blocks. // Match "@param " 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 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("^"); - private static final Pattern HTML_COMMENT_PATTERN = fullCommentPattern(); + compile(""); + private static final Pattern HTML_COMMENT_PATTERN = compile("", DOTALL); private static final Pattern PRE_OPEN_PATTERN = openTagPattern("pre"); private static final Pattern PRE_CLOSE_PATTERN = closeTagPattern("pre"); private static final Pattern CODE_OPEN_PATTERN = openTagPattern("code"); @@ -528,30 +676,48 @@ 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 INLINE_TAG_OPEN_PATTERN = compile("^[{]@\\w*"); + private static final Pattern SNIPPET_TAG_OPEN_PATTERN = compile("[{]@snippet\\b"); + private static final Pattern INLINE_TAG_OPEN_PATTERN = compile("[{]@\\w*"); + private static final Pattern WORD_IN_CODE_SPAN_PATTERN = compile(".[^ \t\n`]*"); + private static final Pattern MARKDOWN_HARD_LINE_BREAK_PATTERN = compile("\\\\[ \t]*\n"); + private static final Pattern BACKSLASH_PLUS_CHARACTER_PATTERN = compile("\\\\."); + /* * We exclude < so that we don't swallow following HTML tags. This lets us fix up "foo

    " (~400 - * hits in Google-internal code). We will join unnecessarily split "words" (like "foobar") - * in a later step. There's a similar story for braces. I'm not sure I actually need to exclude @ - * or *. TODO(cpovirk): Try removing them. + * hits in Google-internal code). * - * Thanks to the "rejoin" step in joinAdjacentLiteralsAndAdjacentWhitespace(), we could get away - * with matching only one character here. That would eliminate the need for the regex entirely. - * That might be faster or slower than what we do now. + * TODO(cpovirk): might not need to exclude @ or *. + */ + private static final Pattern CLASSIC_LITERAL_PATTERN = compile(".[^ \t\n@<{}*]*", DOTALL); + + /* + * Many characters have special meaning in Markdown. Rather than list them all, we'll just match + * a sequence of alphabetic characters. Even digits can have special meaning, for numbered lists. */ - private static final Pattern LITERAL_PATTERN = compile("^.[^ \t\n@<{}*]*", DOTALL); + private static final Pattern MARKDOWN_LITERAL_PATTERN = compile(".\\p{IsAlphabetic}*", DOTALL); - private static Pattern fullCommentPattern() { - return compile("^", DOTALL); + /** + * The pattern used for "literals", things that do not have any special formatting meaning. This + * doesn't have to be a maximal sequence of literal characters, since adjacent literals will be + * joined together in a later step. + */ + private Pattern literalPattern() { + return classicJavadoc ? CLASSIC_LITERAL_PATTERN : MARKDOWN_LITERAL_PATTERN; } private static Pattern openTagPattern(String namePattern) { - return compile(format("^<(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE); + return compile(format("<(?:%s)\\b[^>]*>", namePattern), CASE_INSENSITIVE); } private static Pattern closeTagPattern(String namePattern) { - return compile(format("^]*>", namePattern), CASE_INSENSITIVE); + return compile(format("]*>", namePattern), CASE_INSENSITIVE); } - static class LexException extends Exception {} + static class LexException extends Exception { + LexException() {} + + LexException(Throwable cause) { + super(cause); + } + } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java index cccec4f0a..53f6f1a29 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,22 +15,37 @@ package com.google.googlejavaformat.java.javadoc; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.Sets.immutableEnumSet; +import static com.google.common.collect.Comparators.max; import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.AUTO_INDENT; import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.NO_AUTO_INDENT; import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.BLANK_LINE; import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NEWLINE; import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE; import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE; -import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG; -import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Ordering; -import com.google.googlejavaformat.java.JavaFormatterOptions; -import com.google.googlejavaformat.java.javadoc.Token.Type; +import com.google.googlejavaformat.java.javadoc.Token.BrTag; +import com.google.googlejavaformat.java.javadoc.Token.CodeCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.CodeOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.FooterJavadocTagStart; +import com.google.googlejavaformat.java.javadoc.Token.HeaderCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.HeaderOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.HtmlComment; +import com.google.googlejavaformat.java.javadoc.Token.ListCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.ListItemOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.ListOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.Literal; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownFencedCodeBlock; +import com.google.googlejavaformat.java.javadoc.Token.MarkdownTable; +import com.google.googlejavaformat.java.javadoc.Token.MoeBeginStripComment; +import com.google.googlejavaformat.java.javadoc.Token.MoeEndStripComment; +import com.google.googlejavaformat.java.javadoc.Token.PreCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.PreOpenTag; +import com.google.googlejavaformat.java.javadoc.Token.SnippetBegin; +import com.google.googlejavaformat.java.javadoc.Token.SnippetEnd; +import com.google.googlejavaformat.java.javadoc.Token.StartOfLineToken; +import com.google.googlejavaformat.java.javadoc.Token.TableCloseTag; +import com.google.googlejavaformat.java.javadoc.Token.TableOpenTag; +import java.util.List; /** * Stateful object that accepts "requests" and "writes," producing formatted Javadoc. @@ -40,9 +55,13 @@ * are we inside?" */ final class JavadocWriter { + + private static final Literal BACKSLASH_LITERAL = new Literal("\\"); + private final int blockIndent; - private final JavaFormatterOptions options; + private final boolean classicJavadoc; private final StringBuilder output = new StringBuilder(); + /** * Whether we are inside an {@code

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