diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c623efe38..735cccaf7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,16 +29,16 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - java: [21, 17, 11] + 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: 21 + java: 25 experimental: false - os: windows-latest - java: 21 + java: 25 experimental: false - os: ubuntu-latest java: EA @@ -80,7 +80,7 @@ jobs: # 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-20.04, macos-latest, windows-latest] + os: [ubuntu-22.04, macos-latest, windows-latest] runs-on: ${{ matrix.os }} continue-on-error: true steps: @@ -93,7 +93,7 @@ jobs: - name: "Set up GraalVM ${{ matrix.java }}" uses: graalvm/setup-graalvm@v1 with: - java-version: "21" + java-version: "25" distribution: "graalvm-community" github-token: ${{ secrets.GITHUB_TOKEN }} native-image-job-reports: "true" @@ -114,10 +114,10 @@ jobs: steps: - name: "Check out repository" uses: actions/checkout@v4 - - name: "Set up JDK 17" + - name: "Set up JDK 21" uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: "zulu" cache: "maven" server-id: sonatype-nexus-snapshots diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2ce2beae..43dd0ed29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: java-version: 21 distribution: "zulu" cache: "maven" - server-id: sonatype-nexus-staging + server-id: central server-username: CI_DEPLOY_USERNAME server-password: CI_DEPLOY_PASSWORD gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} @@ -83,16 +83,18 @@ jobs: 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-20.04, macos-latest, windows-latest] + 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-20.04":"linux-x86-64", "macos-latest":"darwin-arm64", "windows-latest":"windows-x86-64"}')[matrix.os]}} + 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" @@ -100,7 +102,7 @@ jobs: - name: "Set up GraalVM" uses: graalvm/setup-graalvm@v1 with: - java-version: "21" + java-version: "25" distribution: "graalvm-community" github-token: ${{ secrets.GITHUB_TOKEN }} native-image-job-reports: "true" diff --git a/.mvn/jvm.config b/.mvn/jvm.config index 504456f90..2e8f1d41f 100644 --- a/.mvn/jvm.config +++ b/.mvn/jvm.config @@ -8,3 +8,6 @@ --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/README.md b/README.md index 83a524bbd..a5c400122 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,25 @@ and run it with: java -jar /path/to/google-java-format-${GJF_VERSION?}-all-deps.jar [files...] ``` +Note that it uses the `jdk.compiler` module to parse the Java source code. The +`java` binary version used must therefore be from a JDK (not JRE) with a version +equal to or newer than the Java language version of the files being formatted. +The minimum Java version can be found in `core/pom.xml` (currently Java 17). An +alternative is to use the available GraalVM based native binaries instead. + The formatter can act on whole files, on limited lines (`--lines`), on specific offsets (`--offset`), passing through to standard-out (default) or altered in-place (`--replace`). +Option `--help` will print full usage details; including built-in documentation +about other flags, such as `--aosp`, `--fix-imports-only`, +`--skip-sorting-imports`, `--skip-removing-unused-import`, +`--skip-reflowing-long-strings`, `--skip-javadoc-formatting`, or the `--dry-run` +and `--set-exit-if-changed`. + +Using `@` reads options and filenames from a file, instead of +arguments. + To reformat changed lines in a specific patch, use [`google-java-format-diff.py`](https://github.com/google/google-java-format/blob/master/scripts/google-java-format-diff.py). @@ -73,9 +88,33 @@ 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 diff --git a/core/pom.xml b/core/pom.xml index c3753a150..3b106cf71 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -93,7 +93,7 @@ maven-javadoc-plugin - 11 + 17 UTF-8 UTF-8 UTF-8 @@ -215,66 +215,14 @@ org.apache.maven.plugins maven-compiler-plugin - 11 - 11 + 17 + 17 - - jdk11 - - [11,17) - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - **/Java17InputAstVisitor.java - **/Java21InputAstVisitor.java - - - - - maven-javadoc-plugin - - com.google.googlejavaformat.java.java17 - com.google.googlejavaformat.java.java21 - - - - - - - jdk17 - - [17,21) - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - **/Java21InputAstVisitor.java - - - - - maven-javadoc-plugin - - com.google.googlejavaformat.java.java21 - - - - - native diff --git a/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java b/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java index be7f8a6ca..252da5bdf 100644 --- a/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java +++ b/core/src/main/java/com/google/googlejavaformat/FormatterDiagnostic.java @@ -49,7 +49,7 @@ public int line() { } /** - * Returns the 0-indexed column number on which the error occurred, or {@code -1} if the error + * Returns the 1-indexed column number on which the error occurred, or {@code -1} if the error * does not have a column. */ public int column() { @@ -61,14 +61,14 @@ public String message() { return message; } + @Override public String toString() { StringBuilder sb = new StringBuilder(); if (lineNumber >= 0) { sb.append(lineNumber).append(':'); } if (column >= 0) { - // internal column numbers are 0-based, but diagnostics use 1-based indexing by convention - sb.append(column + 1).append(':'); + sb.append(column).append(':'); } if (lineNumber >= 0 || column >= 0) { sb.append(' '); diff --git a/core/src/main/java/com/google/googlejavaformat/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 a45e83b9e..7f0fabb34 100644 --- a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java +++ b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java @@ -38,7 +38,7 @@ */ 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(); @@ -57,7 +57,7 @@ public int actualSize(int position, int 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(); @@ -316,7 +316,7 @@ public final void guessToken(String token) { token, Doc.Token.RealOrImaginary.IMAGINARY, ZERO, - /* breakAndIndentTrailingComment= */ Optional.empty()); + /* breakAndIndentTrailingComment= */ Optional.empty()); } public final void token( @@ -359,7 +359,7 @@ public final void op(String op) { op.substring(i, i + 1), Doc.Token.RealOrImaginary.REAL, ZERO, - /* breakAndIndentTrailingComment= */ Optional.empty()); + /* breakAndIndentTrailingComment= */ Optional.empty()); } } @@ -427,7 +427,7 @@ public final void breakToFill(String flat) { * @param plusIndent extra indent if taken */ public final void breakOp(Doc.FillMode fillMode, String flat, Indent plusIndent) { - breakOp(fillMode, flat, plusIndent, /* optionalTag= */ Optional.empty()); + breakOp(fillMode, flat, plusIndent, /* optionalTag= */ Optional.empty()); } /** diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java index d5480a790..e794a1815 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java +++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptions.java @@ -14,291 +14,121 @@ 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.common.collect.RangeSet; -import com.google.common.collect.TreeRangeSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.Optional; /** * Command line options for google-java-format. * - *

google-java-format doesn't depend on AutoValue, to allow AutoValue to depend on - * google-java-format. + * @param files The files to format. + * @param inPlace Format files in place. + * @param lines Line ranges to format. + * @param offsets Character offsets for partial formatting, paired with {@code lengths}. + * @param lengths Partial formatting region lengths, paired with {@code offsets}. + * @param aosp Use AOSP style instead of Google Style (4-space indentation). + * @param version Print the version. + * @param help Print usage information. + * @param stdin Format input from stdin. + * @param fixImportsOnly Fix imports, but do no formatting. + * @param sortImports Sort imports. + * @param removeUnusedImports Remove unused imports. + * @param dryRun Print the paths of the files whose contents would change if the formatter were run + * normally. + * @param setExitIfChanged Return exit code 1 if there are any formatting changes. + * @param assumeFilename Return the name to use for diagnostics when formatting standard input. */ -final class CommandLineOptions { - - private final ImmutableList files; - private final boolean inPlace; - private final ImmutableRangeSet lines; - private final ImmutableList offsets; - private final ImmutableList lengths; - private final boolean aosp; - private final boolean version; - private final boolean help; - private final boolean stdin; - private final boolean fixImportsOnly; - private final boolean sortImports; - private final boolean removeUnusedImports; - private final boolean dryRun; - private final boolean setExitIfChanged; - private final Optional assumeFilename; - private final boolean reflowLongStrings; - private final boolean formatJavadoc; - - 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) { - this.files = files; - this.inPlace = inPlace; - this.lines = lines; - this.offsets = offsets; - this.lengths = lengths; - this.aosp = aosp; - this.version = version; - this.help = help; - this.stdin = stdin; - this.fixImportsOnly = fixImportsOnly; - this.sortImports = sortImports; - this.removeUnusedImports = removeUnusedImports; - this.dryRun = dryRun; - this.setExitIfChanged = setExitIfChanged; - this.assumeFilename = assumeFilename; - this.reflowLongStrings = reflowLongStrings; - this.formatJavadoc = formatJavadoc; - } - - /** The files to format. */ - ImmutableList files() { - return files; - } - - /** Format files in place. */ - boolean inPlace() { - return inPlace; - } - - /** Line ranges to format. */ - ImmutableRangeSet lines() { - return lines; - } - - /** Character offsets for partial formatting, paired with {@code lengths}. */ - ImmutableList offsets() { - return offsets; - } - - /** Partial formatting region lengths, paired with {@code offsets}. */ - ImmutableList lengths() { - return lengths; - } - - /** Use AOSP style instead of Google Style (4-space indentation). */ - boolean aosp() { - return aosp; - } - - /** Print the version. */ - boolean version() { - return version; - } - - /** Print usage information. */ - boolean help() { - return help; - } - - /** Format input from stdin. */ - boolean stdin() { - return stdin; - } - - /** Fix imports, but do no formatting. */ - boolean fixImportsOnly() { - return fixImportsOnly; - } - - /** Sort imports. */ - boolean sortImports() { - return sortImports; - } - - /** Remove unused imports. */ - boolean removeUnusedImports() { - return removeUnusedImports; - } - - /** - * Print the paths of the files whose contents would change if the formatter were run normally. - */ - boolean dryRun() { - return dryRun; - } - - /** Return exit code 1 if there are any formatting changes. */ - boolean setExitIfChanged() { - return setExitIfChanged; - } - - /** Return the name to use for diagnostics when formatting standard input. */ - Optional assumeFilename() { - return assumeFilename; - } - - boolean reflowLongStrings() { - return reflowLongStrings; - } +record CommandLineOptions( + ImmutableList files, + boolean inPlace, + ImmutableRangeSet lines, + ImmutableList offsets, + ImmutableList lengths, + boolean aosp, + boolean version, + boolean help, + boolean stdin, + boolean fixImportsOnly, + boolean sortImports, + boolean removeUnusedImports, + boolean dryRun, + boolean setExitIfChanged, + Optional assumeFilename, + boolean reflowLongStrings, + boolean formatJavadoc) { /** Returns true if partial formatting was selected. */ boolean isSelection() { return !lines().isEmpty() || !offsets().isEmpty() || !lengths().isEmpty(); } - boolean formatJavadoc() { - return formatJavadoc; - } - static Builder builder() { - return new Builder(); + return new AutoBuilder_CommandLineOptions_Builder() + .sortImports(true) + .removeUnusedImports(true) + .reflowLongStrings(true) + .formatJavadoc(true) + .aosp(false) + .version(false) + .help(false) + .stdin(false) + .fixImportsOnly(false) + .dryRun(false) + .setExitIfChanged(false) + .inPlace(false); } - static class Builder { + @AutoBuilder + interface Builder { - private final ImmutableList.Builder files = ImmutableList.builder(); - private final RangeSet lines = TreeRangeSet.create(); - private final ImmutableList.Builder offsets = ImmutableList.builder(); - private final ImmutableList.Builder lengths = ImmutableList.builder(); - private boolean inPlace = false; - private boolean aosp = false; - private boolean version = false; - private boolean help = false; - private boolean stdin = false; - private boolean fixImportsOnly = false; - private boolean sortImports = true; - private boolean removeUnusedImports = true; - private boolean dryRun = false; - private boolean setExitIfChanged = false; - private Optional assumeFilename = Optional.empty(); - private boolean reflowLongStrings = true; - private boolean formatJavadoc = true; + ImmutableList.Builder filesBuilder(); - ImmutableList.Builder filesBuilder() { - return files; - } + Builder inPlace(boolean inPlace); - Builder inPlace(boolean inPlace) { - this.inPlace = inPlace; - return this; - } + Builder lines(ImmutableRangeSet lines); - RangeSet linesBuilder() { - return lines; - } + ImmutableList.Builder offsetsBuilder(); - Builder addOffset(Integer offset) { - offsets.add(offset); + @CanIgnoreReturnValue + default Builder addOffset(Integer offset) { + offsetsBuilder().add(offset); return this; } - Builder addLength(Integer length) { - lengths.add(length); - return this; - } + ImmutableList.Builder lengthsBuilder(); - Builder aosp(boolean aosp) { - this.aosp = aosp; + @CanIgnoreReturnValue + default Builder addLength(Integer length) { + lengthsBuilder().add(length); return this; } - Builder version(boolean version) { - this.version = version; - return this; - } + Builder aosp(boolean aosp); - Builder help(boolean help) { - this.help = help; - return this; - } + Builder version(boolean version); - Builder stdin(boolean stdin) { - this.stdin = stdin; - return this; - } + Builder help(boolean help); - Builder fixImportsOnly(boolean fixImportsOnly) { - this.fixImportsOnly = fixImportsOnly; - return this; - } + Builder stdin(boolean stdin); - Builder sortImports(boolean sortImports) { - this.sortImports = sortImports; - return this; - } + Builder fixImportsOnly(boolean fixImportsOnly); - Builder removeUnusedImports(boolean removeUnusedImports) { - this.removeUnusedImports = removeUnusedImports; - return this; - } + Builder sortImports(boolean sortImports); - Builder dryRun(boolean dryRun) { - this.dryRun = dryRun; - return this; - } + Builder removeUnusedImports(boolean removeUnusedImports); - Builder setExitIfChanged(boolean setExitIfChanged) { - this.setExitIfChanged = setExitIfChanged; - return this; - } + Builder dryRun(boolean dryRun); - Builder assumeFilename(String assumeFilename) { - this.assumeFilename = Optional.of(assumeFilename); - return this; - } + Builder setExitIfChanged(boolean setExitIfChanged); - Builder reflowLongStrings(boolean reflowLongStrings) { - this.reflowLongStrings = reflowLongStrings; - return this; - } + Builder assumeFilename(String assumeFilename); - Builder formatJavadoc(boolean formatJavadoc) { - this.formatJavadoc = formatJavadoc; - return this; - } + Builder reflowLongStrings(boolean reflowLongStrings); - CommandLineOptions build() { - return new CommandLineOptions( - files.build(), - inPlace, - ImmutableRangeSet.copyOf(lines), - offsets.build(), - lengths.build(), - aosp, - version, - help, - stdin, - fixImportsOnly, - sortImports, - removeUnusedImports, - dryRun, - setExitIfChanged, - assumeFilename, - reflowLongStrings, - formatJavadoc); - } + Builder formatJavadoc(boolean formatJavadoc); + + CommandLineOptions build(); } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java index 52a5e05d4..f5ce703e8 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java +++ b/core/src/main/java/com/google/googlejavaformat/java/CommandLineOptionsParser.java @@ -18,8 +18,10 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableRangeSet; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -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("-")) { @@ -61,74 +66,28 @@ static CommandLineOptions parse(Iterable options) { } // NOTE: update usage information in UsageException when new flags are added switch (flag) { - case "-i": - case "-r": - case "-replace": - case "--replace": - optionsBuilder.inPlace(true); - break; - case "--lines": - case "-lines": - case "--line": - case "-line": - parseRangeSet(optionsBuilder.linesBuilder(), getValue(flag, it, value)); - break; - case "--offset": - case "-offset": - optionsBuilder.addOffset(parseInteger(it, flag, value)); - break; - case "--length": - case "-length": - optionsBuilder.addLength(parseInteger(it, flag, value)); - break; - case "--aosp": - case "-aosp": - case "-a": - optionsBuilder.aosp(true); - break; - case "--version": - case "-version": - case "-v": - optionsBuilder.version(true); - break; - case "--help": - case "-help": - case "-h": - optionsBuilder.help(true); - break; - case "--fix-imports-only": - optionsBuilder.fixImportsOnly(true); - break; - case "--skip-sorting-imports": - optionsBuilder.sortImports(false); - break; - case "--skip-removing-unused-imports": - optionsBuilder.removeUnusedImports(false); - break; - case "--skip-reflowing-long-strings": - optionsBuilder.reflowLongStrings(false); - break; - case "--skip-javadoc-formatting": - optionsBuilder.formatJavadoc(false); - break; - case "-": - optionsBuilder.stdin(true); - break; - case "-n": - case "--dry-run": - optionsBuilder.dryRun(true); - break; - case "--set-exit-if-changed": - optionsBuilder.setExitIfChanged(true); - break; - case "-assume-filename": - case "--assume-filename": - optionsBuilder.assumeFilename(getValue(flag, it, value)); - break; - default: - throw new IllegalArgumentException("unexpected flag: " + flag); + case "-i", "-r", "-replace", "--replace" -> optionsBuilder.inPlace(true); + case "--lines", "-lines", "--line", "-line" -> + parseRangeSet(linesBuilder, getValue(flag, it, value)); + case "--offset", "-offset" -> optionsBuilder.addOffset(parseInteger(it, flag, value)); + case "--length", "-length" -> optionsBuilder.addLength(parseInteger(it, flag, value)); + case "--aosp", "-aosp", "-a" -> optionsBuilder.aosp(true); + case "--version", "-version", "-v" -> optionsBuilder.version(true); + case "--help", "-help", "-h" -> optionsBuilder.help(true); + case "--fix-imports-only" -> optionsBuilder.fixImportsOnly(true); + case "--skip-sorting-imports" -> optionsBuilder.sortImports(false); + case "--skip-removing-unused-imports" -> optionsBuilder.removeUnusedImports(false); + case "--skip-reflowing-long-strings" -> optionsBuilder.reflowLongStrings(false); + case "--skip-javadoc-formatting" -> optionsBuilder.formatJavadoc(false); + case "-" -> optionsBuilder.stdin(true); + case "-n", "--dry-run" -> optionsBuilder.dryRun(true); + case "--set-exit-if-changed" -> optionsBuilder.setExitIfChanged(true); + case "-assume-filename", "--assume-filename" -> + optionsBuilder.assumeFilename(getValue(flag, it, value)); + default -> throw new IllegalArgumentException("unexpected flag: " + flag); } } + optionsBuilder.lines(ImmutableRangeSet.copyOf(linesBuilder)); return optionsBuilder.build(); } @@ -169,17 +128,18 @@ private static void parseRangeSet(RangeSet result, String ranges) { */ 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 4bd19bee3..3bf4793c3 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java +++ b/core/src/main/java/com/google/googlejavaformat/java/DimensionHelpers.java @@ -106,19 +106,18 @@ private static Iterable> reorderBySourcePosition( * int}. */ private static Tree extractDims(Deque> dims, Tree node) { - switch (node.getKind()) { - case ARRAY_TYPE: - return extractDims(dims, ((ArrayTypeTree) node).getType()); - case ANNOTATED_TYPE: + return switch (node.getKind()) { + case ARRAY_TYPE -> extractDims(dims, ((ArrayTypeTree) node).getType()); + case ANNOTATED_TYPE -> { AnnotatedTypeTree annotatedTypeTree = (AnnotatedTypeTree) node; if (annotatedTypeTree.getUnderlyingType().getKind() != Tree.Kind.ARRAY_TYPE) { - return node; + yield node; } node = extractDims(dims, annotatedTypeTree.getUnderlyingType()); dims.addFirst(ImmutableList.copyOf(annotatedTypeTree.getAnnotations())); - return node; - default: - return node; - } + yield node; + } + default -> node; + }; } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java index 5aa7a1233..ff87eaab7 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java @@ -14,10 +14,8 @@ package com.google.googlejavaformat.java; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; @@ -31,25 +29,14 @@ import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.Op; import com.google.googlejavaformat.OpsBuilder; -import com.sun.tools.javac.file.JavacFileManager; -import com.sun.tools.javac.parser.JavacParser; -import com.sun.tools.javac.parser.ParserFactory; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.util.Context; -import com.sun.tools.javac.util.Log; -import com.sun.tools.javac.util.Options; -import java.io.IOError; import java.io.IOException; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.tools.Diagnostic; -import javax.tools.DiagnosticCollector; -import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; -import javax.tools.SimpleJavaFileObject; -import javax.tools.StandardLocation; /** * This is google-java-format, a new Java formatter that follows the Google Java Style Guide quite @@ -112,56 +99,18 @@ 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"); - Options.instance(context).put("--enable-preview", "true"); - JCCompilationUnit unit; - JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8); - try { - fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of()); - } catch (IOException e) { - // impossible - throw new IOError(e); - } - SimpleJavaFileObject source = - new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) { - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - return javaInput.getText(); - } - }; - Log.instance(context).useSource(source); - ParserFactory parserFactory = ParserFactory.instance(context); - JavacParser parser = - parserFactory.newParser( - javaInput.getText(), - /* keepDocComments= */ true, - /* keepEndPos= */ true, - /* keepLineMap= */ true); - unit = parser.parseCompilationUnit(); - unit.sourcefile = source; + List> errorDiagnostics = new ArrayList<>(); + JCCompilationUnit unit = + Trees.parse( + context, errorDiagnostics, /* allowStringFolding= */ false, javaInput.getText()); javaInput.setCompilationUnit(unit); - Iterable> errorDiagnostics = - Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic); - if (!Iterables.isEmpty(errorDiagnostics)) { + if (!errorDiagnostics.isEmpty()) { throw FormatterException.fromJavacDiagnostics(errorDiagnostics); } OpsBuilder builder = new OpsBuilder(javaInput, javaOutput); // Output the compilation unit. - JavaInputAstVisitor visitor; - if (Runtime.version().feature() >= 21) { - visitor = - createVisitor( - "com.google.googlejavaformat.java.java21.Java21InputAstVisitor", builder, options); - } else if (Runtime.version().feature() >= 17) { - visitor = - createVisitor( - "com.google.googlejavaformat.java.java17.Java17InputAstVisitor", builder, options); - } else { - visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier()); - } + JavaInputAstVisitor visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier()); visitor.scan(unit, null); builder.sync(javaInput.getText().length()); builder.drain(); @@ -171,31 +120,13 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept javaOutput.flush(); } - private static JavaInputAstVisitor createVisitor( - final String className, final OpsBuilder builder, final JavaFormatterOptions options) { - try { - return Class.forName(className) - .asSubclass(JavaInputAstVisitor.class) - .getConstructor(OpsBuilder.class, int.class) - .newInstance(builder, options.indentationMultiplier()); - } catch (ReflectiveOperationException e) { - throw new LinkageError(e.getMessage(), e); - } - } - 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"); } /** 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 808916c2c..7fe67073a 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java +++ b/core/src/main/java/com/google/googlejavaformat/java/FormatterException.java @@ -14,12 +14,15 @@ 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 java.util.regex.Pattern; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; @@ -46,13 +49,33 @@ public List diagnostics() { } public static FormatterException fromJavacDiagnostics( - Iterable> diagnostics) { + List> diagnostics) { return new FormatterException( - Iterables.transform(diagnostics, FormatterException::toFormatterDiagnostic)); + diagnostics.stream() + .map(FormatterException::toFormatterDiagnostic) + .collect(toImmutableList())); } private static FormatterDiagnostic toFormatterDiagnostic(Diagnostic input) { return FormatterDiagnostic.create( (int) input.getLineNumber(), (int) input.getColumnNumber(), input.getMessage(ENGLISH)); } + + public String formatDiagnostics(String path, String input) { + List lines = Splitter.on(NEWLINE_PATTERN).splitToList(input); + StringBuilder sb = new StringBuilder(); + for (FormatterDiagnostic diagnostic : diagnostics()) { + sb.append(path).append(":").append(diagnostic).append(System.lineSeparator()); + int line = diagnostic.line(); + int column = diagnostic.column(); + if (line != -1 && column != -1) { + sb.append(CharMatcher.breakingWhitespace().trimTrailingFrom(lines.get(line - 1))) + .append(System.lineSeparator()); + sb.append(" ".repeat(column - 1)).append('^').append(System.lineSeparator()); + } + } + return sb.toString(); + } + + private static final Pattern NEWLINE_PATTERN = Pattern.compile("\\R"); } diff --git a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java index dcbaea172..375a3289b 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/ImportOrderer.java @@ -122,18 +122,24 @@ private String reorderImports() throws FormatterException { /** * A {@link Comparator} that orders {@link Import}s by Google Style, defined at * https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing. + * + *

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

Module imports are not mentioned by Android Style, so we make an arbitrary choice about + * where to include them if they are present. */ private static final Comparator AOSP_IMPORT_COMPARATOR = - Comparator.comparing(Import::isStatic, trueFirst()) + Comparator.comparing(Import::importType) .thenComparing(Import::isAndroid, trueFirst()) .thenComparing(Import::isThirdParty, trueFirst()) .thenComparing(Import::isJava, trueFirst()) @@ -144,7 +150,7 @@ private String reorderImports() throws FormatterException { * Import}s based on Google style. */ private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) { - return prev.isStatic() && !curr.isStatic(); + return !prev.importType().equals(curr.importType()); } /** @@ -152,7 +158,7 @@ private static boolean shouldInsertBlankLineGoogle(Import prev, Import curr) { * Import}s based on AOSP style. */ private static boolean shouldInsertBlankLineAosp(Import prev, Import curr) { - if (prev.isStatic() && !curr.isStatic()) { + if (!prev.importType().equals(curr.importType())) { return true; } // insert blank line between "com.android" from "com.anythingelse" @@ -183,16 +189,22 @@ private ImportOrderer(String text, ImmutableList toks, Style style) { } } + enum ImportType { + STATIC, + MODULE, + NORMAL + } + /** An import statement. */ class Import { private final String imported; - private final boolean isStatic; private final String trailing; + private final ImportType importType; - Import(String imported, String trailing, boolean isStatic) { + Import(String imported, String trailing, ImportType importType) { this.imported = imported; this.trailing = trailing; - this.isStatic = isStatic; + this.importType = importType; } /** The name being imported, for example {@code java.util.List}. */ @@ -200,9 +212,9 @@ String imported() { return imported; } - /** True if this is {@code import static}. */ - boolean isStatic() { - return isStatic; + /** Returns the {@link ImportType}. */ + ImportType importType() { + return importType; } /** The top-level package of the import. */ @@ -218,13 +230,10 @@ boolean isAndroid() { /** True if this is a Java import per AOSP style. */ boolean isJava() { - switch (topLevel()) { - case "java": - case "javax": - return true; - default: - return false; - } + return switch (topLevel()) { + case "java", "javax" -> true; + default -> false; + }; } /** @@ -248,8 +257,10 @@ public boolean isThirdParty() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("import "); - if (isStatic()) { - sb.append("static "); + switch (importType) { + case STATIC -> sb.append("static "); + case MODULE -> sb.append("module "); + case NORMAL -> {} } sb.append(imported()).append(';'); if (trailing().trim().isEmpty()) { @@ -304,8 +315,13 @@ private ImportsAndIndex scanImports(int i) throws FormatterException { if (isSpaceToken(i)) { i++; } - boolean isStatic = tokenAt(i).equals("static"); - if (isStatic) { + ImportType importType = + switch (tokenAt(i)) { + case "static" -> ImportType.STATIC; + case "module" -> ImportType.MODULE; + default -> ImportType.NORMAL; + }; + if (!importType.equals(ImportType.NORMAL)) { i++; if (isSpaceToken(i)) { i++; @@ -350,7 +366,7 @@ private ImportsAndIndex scanImports(int i) throws FormatterException { // Extra semicolons are not allowed by the JLS but are accepted by javac. i++; } - imports.add(new Import(importedName, trailing.toString(), isStatic)); + imports.add(new Import(importedName, trailing.toString(), importType)); // Remember the position just after the import we just saw, before skipping blank lines. // If the next thing after the blank lines is not another import then we don't want to // include those blank lines in the text to be replaced. diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java index d34ecc43f..9526b892c 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java @@ -49,7 +49,11 @@ public String rewrite(Tok tok, int maxWidth, int 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); @@ -181,4 +185,3 @@ private static boolean javadocShaped(List lines) { return true; } } - 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 01c617776..cf525e86f 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java @@ -37,10 +37,13 @@ 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; @@ -53,6 +56,7 @@ 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 { @@ -362,9 +366,17 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept return text; } }); - DeferredDiagnosticHandler diagnostics = new DeferredDiagnosticHandler(log); + DeferredDiagnosticHandler diagnostics = deferredDiagnosticHandler(log); ImmutableList rawToks = JavacTokens.getTokens(text, context, stopTokens); - if (diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) { + Collection ds; + try { + @SuppressWarnings("unchecked") + var extraLocalForSuppression = (Collection) GET_DIAGNOSTICS.invoke(diagnostics); + ds = extraLocalForSuppression; + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + if (ds.stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) { return ImmutableList.of(new Tok(0, "", "", 0, 0, true, null)); // EOF } int kN = 0; @@ -471,6 +483,39 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept return ImmutableList.copyOf(toks); } + private static final Constructor + DEFERRED_DIAGNOSTIC_HANDLER_CONSTRUCTOR = getDeferredDiagnosticHandlerConstructor(); + + // Depending on the JDK version, we might have a static class whose constructor has an explicit + // Log parameter, or an inner class whose constructor has an *implicit* Log parameter. They are + // different at the source level, but look the same to reflection. + + private static Constructor getDeferredDiagnosticHandlerConstructor() { + try { + return DeferredDiagnosticHandler.class.getConstructor(Log.class); + } catch (NoSuchMethodException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static DeferredDiagnosticHandler deferredDiagnosticHandler(Log log) { + try { + return DEFERRED_DIAGNOSTIC_HANDLER_CONSTRUCTOR.newInstance(log); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static final Method GET_DIAGNOSTICS = getGetDiagnostics(); + + private static @Nullable Method getGetDiagnostics() { + try { + return DeferredDiagnosticHandler.class.getMethod("getDiagnostics"); + } catch (NoSuchMethodException e) { + throw new LinkageError(e.getMessage(), e); + } + } + private static int updateColumn(int columnI, String originalTokText) { Integer last = Iterators.getLast(Newlines.lineOffsetIterator(originalTokText)); if (last > 0) { @@ -516,20 +561,18 @@ private static ImmutableList buildTokens(List toks) { // TODO(cushon): find a better strategy. if (toks.get(k).isSlashStarComment()) { switch (tok.getText()) { - case "(": - case "<": - case ".": + case "(", "<", "." -> { break OUTER; - default: - break; + } + default -> {} } } if (toks.get(k).isJavadocComment()) { switch (tok.getText()) { - case ";": + case ";" -> { break OUTER; - default: - break; + } + default -> {} } } if (isParamComment(toks.get(k))) { 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 01f9a3e05..a2a32e79c 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; @@ -41,6 +42,7 @@ import static com.sun.source.tree.Tree.Kind.STRING_LITERAL; import static com.sun.source.tree.Tree.Kind.UNION_TYPE; import static com.sun.source.tree.Tree.Kind.VARIABLE; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import com.google.auto.value.AutoOneOf; @@ -70,6 +72,7 @@ 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; @@ -84,15 +87,20 @@ 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; @@ -120,11 +128,14 @@ 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; @@ -138,12 +149,15 @@ import com.sun.source.tree.VariableTree; import com.sun.source.tree.WhileLoopTree; import com.sun.source.tree.WildcardTree; +import com.sun.source.tree.YieldTree; import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.TreeInfo; import com.sun.tools.javac.tree.TreeScanner; +import java.lang.reflect.Method; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; @@ -351,6 +365,12 @@ private boolean inExpression() { @Override public Void scan(Tree tree, Void unused) { + // Pre-visit AST for preview features, since com.sun.source.tree.AnyPattern can't be + // accessed directly without --enable-preview. + if (tree instanceof JCTree.JCAnyPattern) { + visitJcAnyPattern((JCTree.JCAnyPattern) tree); + return null; + } inExpression.addLast(tree instanceof ExpressionTree || inExpression.peekLast()); int previous = builder.depth(); try { @@ -410,7 +430,17 @@ public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { return null; } - protected void handleModule(boolean afterFirstToken, CompilationUnitTree node) {} + protected void handleModule(boolean afterFirstToken, CompilationUnitTree node) { + ModuleTree module = node.getModule(); + if (module != null) { + if (afterFirstToken) { + builder.blankLineWanted(YES); + } + markForPartialFormat(); + visitModule(module, null); + builder.forcedBreak(); + } + } /** Skips over extra semi-colons at the top-level, or in a class member declaration lists. */ protected void dropEmptyDeclarations() { @@ -426,18 +456,11 @@ protected void dropEmptyDeclarations() { @Override public Void visitClass(ClassTree tree, Void unused) { switch (tree.getKind()) { - case ANNOTATION_TYPE: - visitAnnotationType(tree); - break; - case CLASS: - case INTERFACE: - visitClassDeclaration(tree); - break; - case ENUM: - visitEnumDeclaration(tree); - break; - default: - throw new AssertionError(tree.getKind()); + 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; } @@ -928,6 +951,69 @@ public boolean visitEnumDeclaration(ClassTree node) { return false; } + public void visitRecordDeclaration(ClassTree node) { + sync(node); + typeDeclarationModifiers(node.getModifiers()); + Verify.verify(node.getExtendsClause() == null); + boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty(); + token("record"); + builder.space(); + visit(node.getSimpleName()); + if (!node.getTypeParameters().isEmpty()) { + token("<"); + } + builder.open(plusFour); + { + if (!node.getTypeParameters().isEmpty()) { + typeParametersRest(node.getTypeParameters(), hasSuperInterfaceTypes ? plusFour : ZERO); + } + ImmutableList parameters = JavaInputAstVisitor.recordVariables(node); + token("("); + if (!parameters.isEmpty()) { + // Break before args. + builder.breakToFill(""); + } + // record headers can't declare receiver parameters + visitFormals(/* receiver= */ Optional.empty(), parameters); + token(")"); + if (hasSuperInterfaceTypes) { + builder.breakToFill(" "); + builder.open(node.getImplementsClause().size() > 1 ? plusFour : ZERO); + token("implements"); + builder.space(); + boolean afterFirstToken = false; + for (Tree superInterfaceType : node.getImplementsClause()) { + if (afterFirstToken) { + token(","); + builder.breakOp(" "); + } + scan(superInterfaceType, null); + afterFirstToken = true; + } + builder.close(); + } + } + builder.close(); + if (node.getMembers() == null) { + token(";"); + } else { + ImmutableList members = + node.getMembers().stream() + .filter(t -> (TreeInfo.flags((JCTree) t) & Flags.GENERATED_MEMBER) == 0) + .collect(toImmutableList()); + addBodyDeclarations(members, BracesOrNot.YES, FirstDeclarationsOrNot.YES); + } + dropEmptyDeclarations(); + } + + private static ImmutableList recordVariables(ClassTree node) { + return node.getMembers().stream() + .filter(JCTree.JCVariableDecl.class::isInstance) + .map(JCTree.JCVariableDecl.class::cast) + .filter(m -> (m.mods.flags & RECORD) == RECORD) + .collect(toImmutableList()); + } + @Override public Void visitMemberReference(MemberReferenceTree node, Void unused) { builder.open(plusFour); @@ -936,14 +1022,8 @@ public Void visitMemberReference(MemberReferenceTree node, Void unused) { 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; @@ -1141,6 +1221,10 @@ public Void visitImport(ImportTree node, Void unused) { sync(node); token("import"); builder.space(); + if (isModuleImport(node)) { + token("module"); + builder.space(); + } if (node.isStatic()) { token("static"); builder.space(); @@ -1152,6 +1236,27 @@ public Void visitImport(ImportTree node, Void unused) { return null; } + private static final @Nullable Method IS_MODULE_METHOD = getIsModuleMethod(); + + private static @Nullable Method getIsModuleMethod() { + try { + return ImportTree.class.getMethod("isModule"); + } catch (NoSuchMethodException ignored) { + return null; + } + } + + private static boolean isModuleImport(ImportTree importTree) { + if (IS_MODULE_METHOD == null) { + return false; + } + try { + return (boolean) IS_MODULE_METHOD.invoke(importTree); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + private void checkForTypeAnnotation(ImportTree node) { Name simpleName = getSimpleName(node); Collection wellKnownAnnotations = TYPE_ANNOTATIONS.get(simpleName.toString()); @@ -1199,7 +1304,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; @@ -1667,6 +1776,23 @@ public Void visitMemberSelect(MemberSelectTree node, Void unused) { public Void visitLiteral(LiteralTree node, Void unused) { sync(node); String sourceForNode = getSourceForNode(node, getCurrentPath()); + 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(); @@ -1762,11 +1888,10 @@ 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; + } } JCTree.Tag tag = unaryTag(node.getExpression()); if (tag == null) { @@ -1796,35 +1921,16 @@ && isUnaryMinusLiteral(getSourceForNode(expression, getCurrentPath()))) { 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; } @@ -1874,18 +1980,65 @@ public Void visitCase(CaseTree node, Void unused) { sync(node); markForPartialFormat(); builder.forcedBreak(); - if (node.getExpression() == null) { + 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); - token(":"); } else { 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; } @@ -2022,7 +2175,7 @@ public Void visitTry(TryTree node, Void unused) { public void visitClassDeclaration(ClassTree node) { sync(node); typeDeclarationModifiers(node.getModifiers()); - List permitsTypes = getPermitsClause(node); + List permitsTypes = node.getPermitsClause(); boolean hasSuperclassType = node.getExtendsClause() != null; boolean hasSuperInterfaceTypes = !node.getImplementsClause().isEmpty(); boolean hasPermitsTypes = !permitsTypes.isEmpty(); @@ -2208,15 +2361,16 @@ 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(); + } } } @@ -2372,13 +2526,10 @@ boolean isAnnotation() { } int position() { - switch (getKind()) { - case MODIFIER: - return modifier().getPosition(); - case ANNOTATION: - return getStartPosition(annotation()); - } - throw new AssertionError(); + return switch (getKind()) { + case MODIFIER -> modifier().getPosition(); + case ANNOTATION -> getStartPosition(annotation()); + }; } private static final Comparator COMPARATOR = @@ -2471,16 +2622,14 @@ DeclarationModifiersAndTypeAnnotations splitModifiers( private void formatAnnotationOrModifier(Deque modifiers) { AnnotationOrModifier modifier = modifiers.removeFirst(); switch (modifier.getKind()) { - case MODIFIER: + case MODIFIER -> { token(modifier.modifier().getText()); if (modifier.modifier().getText().equals("non")) { token(modifiers.removeFirst().modifier().getText()); token(modifiers.removeFirst().modifier().getText()); } - break; - case ANNOTATION: - scan(modifier.annotation(), null); - break; + } + case ANNOTATION -> scan(modifier.annotation(), null); } } @@ -2493,26 +2642,25 @@ boolean isTypeAnnotation(AnnotationTree annotationTree) { } private static boolean isModifier(String token) { - switch (token) { - case "public": - case "protected": - case "private": - case "abstract": - case "static": - case "final": - case "transient": - case "volatile": - case "synchronized": - case "native": - case "strictfp": - case "default": - case "sealed": - case "non": - case "-": - return true; - default: - return false; - } + return switch (token) { + case "public", + "protected", + "private", + "abstract", + "static", + "final", + "transient", + "volatile", + "synchronized", + "native", + "strictfp", + "default", + "sealed", + "non", + "-" -> + true; + default -> false; + }; } @Override @@ -2874,21 +3022,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); @@ -2963,12 +3109,8 @@ void visitDot(ExpressionTree node0) { if (prefixes.isEmpty() && items.get(0) instanceof IdentifierTree) { switch (((IdentifierTree) items.get(0)).getName().toString()) { - case "this": - case "super": - prefixes.add(1); - break; - default: - break; + case "this", "super" -> prefixes.add(1); + default -> {} } } @@ -3124,17 +3266,16 @@ private static ImmutableList simpleNames(Deque 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; @@ -3146,11 +3287,11 @@ private static ImmutableList 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); @@ -3160,13 +3301,9 @@ private void dotExpressionUpToArgs(ExpressionTree expression, Optional 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); } } @@ -3191,14 +3328,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); } @@ -3356,14 +3492,9 @@ public void scan(JCTree tree) { return; } switch (tree.getKind()) { - case STRING_LITERAL: - break; - case PLUS: - super.scan(tree); - break; - default: - stringLiteral[0] = false; - break; + case STRING_LITERAL -> {} + case PLUS -> super.scan(tree); + default -> stringLiteral[0] = false; } if (tree.getKind() == STRING_LITERAL) { Object value = ((LiteralTree) tree).getValue(); @@ -3619,7 +3750,11 @@ protected int declareOne( } protected void variableName(Name name) { - visit(name); + if (name.isEmpty()) { + token("_"); + } else { + visit(name); + } } private void maybeAddDims(Deque> annotations) { @@ -3643,7 +3778,7 @@ private void maybeAddDims( boolean lastWasAnnotation = false; while (builder.peekToken().isPresent()) { switch (builder.peekToken().get()) { - case "@": + case "@" -> { if (annotations.isEmpty()) { return; } @@ -3654,8 +3789,8 @@ private void maybeAddDims( builder.breakToFill(" "); visitAnnotations(dimAnnotations, BreakOrNot.NO, BreakOrNot.NO); lastWasAnnotation = true; - break; - case "[": + } + case "[" -> { if (lastWasAnnotation) { builder.breakToFill(" "); } else { @@ -3667,8 +3802,8 @@ private void maybeAddDims( } token("]"); lastWasAnnotation = false; - break; - case ".": + } + case "." -> { if (!builder.peekToken().get().equals(".") || !builder.peekToken(1).get().equals(".")) { return; } @@ -3679,9 +3814,10 @@ private void maybeAddDims( } builder.op("..."); lastWasAnnotation = false; - break; - default: + } + default -> { return; + } } } } @@ -3800,11 +3936,6 @@ protected void addBodyDeclarations( } } - /** Gets the permits clause for the given node. This is only available in Java 15 and later. */ - protected List getPermitsClause(ClassTree node) { - return ImmutableList.of(); - } - private void classDeclarationTypeList(String token, List types) { if (types.isEmpty()) { return; @@ -3966,4 +4097,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 656b65c83..ea3731131 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java @@ -140,7 +140,7 @@ public void append(String text, Range range) { if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { i++; } - // falls through + // falls through case '\n': spacesPending = new StringBuilder(); ++newlinesPending; 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 0845e0ec2..f1affa74d 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Main.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Main.java @@ -20,7 +20,6 @@ import com.google.common.io.ByteStreams; import com.google.common.util.concurrent.MoreExecutors; -import com.google.googlejavaformat.FormatterDiagnostic; import com.google.googlejavaformat.java.JavaFormatterOptions.Style; import java.io.IOError; import java.io.IOException; @@ -175,9 +174,7 @@ private int formatFiles(CommandLineOptions parameters, JavaFormatterOptions opti for (FormatFileCallable.Result result : results) { Path path = result.path(); if (result.exception() != null) { - for (FormatterDiagnostic diagnostic : result.exception().diagnostics()) { - errWriter.println(path + ":" + diagnostic); - } + errWriter.print(result.exception().formatDiagnostics(path.toString(), result.input())); allOk = false; continue; } @@ -224,9 +221,7 @@ private int formatStdin(CommandLineOptions parameters, JavaFormatterOptions opti FormatFileCallable.Result result = new FormatFileCallable(parameters, null, input, options).call(); if (result.exception() != null) { - for (FormatterDiagnostic diagnostic : result.exception().diagnostics()) { - errWriter.println(stdinFilename + ":" + diagnostic); - } + errWriter.print(result.exception().formatDiagnostics(stdinFilename, input)); ok = false; } else { String output = result.output(); 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 1b26f7d69..33ad13178 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java +++ b/core/src/main/java/com/google/googlejavaformat/java/ModifierOrderer.java @@ -207,41 +207,29 @@ private static void addTrivia(StringBuilder replacement, ImmutableList Modifier.PUBLIC; + case PROTECTED -> Modifier.PROTECTED; + case PRIVATE -> Modifier.PRIVATE; + case ABSTRACT -> Modifier.ABSTRACT; + case STATIC -> Modifier.STATIC; + case DEFAULT -> Modifier.DEFAULT; + + case FINAL -> Modifier.FINAL; + case TRANSIENT -> Modifier.TRANSIENT; + case VOLATILE -> Modifier.VOLATILE; + case SYNCHRONIZED -> Modifier.SYNCHRONIZED; + case NATIVE -> Modifier.NATIVE; + case STRICTFP -> Modifier.STRICTFP; + default -> + switch (token.getTok().getText()) { + case "sealed" -> Modifier.SEALED; + default -> null; + }; + }; } /** Applies replacements to the given string. */ diff --git a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java index 8c3cae319..18745a809 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java +++ b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java @@ -16,13 +16,12 @@ package com.google.googlejavaformat.java; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.googlejavaformat.java.Trees.getEndPosition; import static java.lang.Math.max; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.CharMatcher; import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.collect.Range; import com.google.common.collect.RangeMap; @@ -41,9 +40,6 @@ import com.sun.source.util.TreePathScanner; import com.sun.source.util.TreeScanner; import com.sun.tools.javac.api.JavacTrees; -import com.sun.tools.javac.file.JavacFileManager; -import com.sun.tools.javac.parser.JavacParser; -import com.sun.tools.javac.parser.ParserFactory; import com.sun.tools.javac.tree.DCTree; import com.sun.tools.javac.tree.DCTree.DCReference; import com.sun.tools.javac.tree.JCTree; @@ -51,22 +47,15 @@ import com.sun.tools.javac.tree.JCTree.JCFieldAccess; import com.sun.tools.javac.tree.JCTree.JCImport; import com.sun.tools.javac.util.Context; -import com.sun.tools.javac.util.Log; -import com.sun.tools.javac.util.Options; -import java.io.IOError; -import java.io.IOException; import java.lang.reflect.Method; -import java.net.URI; +import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.tools.Diagnostic; -import javax.tools.DiagnosticCollector; -import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; -import javax.tools.SimpleJavaFileObject; -import javax.tools.StandardLocation; +import org.jspecify.annotations.Nullable; /** * Removes unused imports from a source file. Imports that are only used in javadoc are also @@ -228,38 +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("--enable-preview", "true"); - Options.instance(context).put("allowStringFolding", "false"); - JCCompilationUnit unit; - JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8); - try { - fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of()); - } catch (IOException e) { - // impossible - throw new IOError(e); - } - SimpleJavaFileObject source = - new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) { - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - return javaInput; - } - }; - Log.instance(context).useSource(source); - ParserFactory parserFactory = ParserFactory.instance(context); - JavacParser parser = - parserFactory.newParser( - javaInput, - /* keepDocComments= */ true, - /* keepEndPos= */ true, - /* keepLineMap= */ true); - unit = parser.parseCompilationUnit(); - unit.sourcefile = source; - Iterable> 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); } @@ -274,12 +235,15 @@ private static RangeMap buildReplacements( Multimap> usedInJavadoc) { RangeMap replacements = TreeRangeMap.create(); 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); + int endPosition = getEndPosition(importTree, unit); endPosition = max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition); String sep = Newlines.guessLineSeparator(contents); if (endPosition + sep.length() < contents.length() @@ -322,10 +286,42 @@ 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) JCImport.class.getMethod("getQualifiedIdentifier").invoke(importTree); + 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); } 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 8d426b65d..60fd77ea8 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/SnippetFormatter.java @@ -60,9 +60,17 @@ public void closeBraces(int initialIndent) { } private static final int INDENTATION_SIZE = 2; - private final Formatter formatter = new Formatter(); + private final Formatter formatter; private static final CharMatcher NOT_WHITESPACE = CharMatcher.whitespace().negate(); + public SnippetFormatter() { + this(JavaFormatterOptions.defaultOptions()); + } + + public SnippetFormatter(JavaFormatterOptions formatterOptions) { + formatter = new Formatter(formatterOptions); + } + public String createIndentationString(int indentationLevel) { Preconditions.checkArgument( indentationLevel >= 0, @@ -163,53 +171,38 @@ private SnippetWrapper snippetWrapper(SnippetKind kind, String source, int initi * Synthesize a dummy class around the code snippet provided by Eclipse. The dummy class is * correctly formatted -- the blocks use correct indentation, etc. */ - switch (kind) { - case COMPILATION_UNIT: - { - SnippetWrapper wrapper = new SnippetWrapper(); - for (int i = 1; i <= initialIndent; i++) { - wrapper.append("class Dummy {\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; - } - case CLASS_BODY_DECLARATIONS: - { - SnippetWrapper wrapper = new SnippetWrapper(); - for (int i = 1; i <= initialIndent; i++) { - wrapper.append("class Dummy {\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; + return switch (kind) { + case COMPILATION_UNIT, CLASS_BODY_DECLARATIONS -> { + SnippetWrapper wrapper = new SnippetWrapper(); + for (int i = 1; i <= initialIndent; i++) { + wrapper.append("class Dummy {\n").append(createIndentationString(i)); } - case STATEMENTS: - { - SnippetWrapper wrapper = new SnippetWrapper(); - wrapper.append("class Dummy {\n").append(createIndentationString(1)); - for (int i = 2; i <= initialIndent; i++) { - wrapper.append("{\n").append(createIndentationString(i)); - } - wrapper.appendSource(source); - wrapper.closeBraces(initialIndent); - return wrapper; + wrapper.appendSource(source); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + case STATEMENTS -> { + SnippetWrapper wrapper = new SnippetWrapper(); + wrapper.append("class Dummy {\n").append(createIndentationString(1)); + for (int i = 2; i <= initialIndent; i++) { + wrapper.append("{\n").append(createIndentationString(i)); } - case EXPRESSION: - { - SnippetWrapper wrapper = new SnippetWrapper(); - wrapper.append("class Dummy {\n").append(createIndentationString(1)); - for (int i = 2; i <= initialIndent; i++) { - wrapper.append("{\n").append(createIndentationString(i)); - } - wrapper.append("Object o = "); - wrapper.appendSource(source); - wrapper.append(";"); - wrapper.closeBraces(initialIndent); - return wrapper; + wrapper.appendSource(source); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + case EXPRESSION -> { + SnippetWrapper wrapper = new SnippetWrapper(); + wrapper.append("class Dummy {\n").append(createIndentationString(1)); + for (int i = 2; i <= initialIndent; i++) { + wrapper.append("{\n").append(createIndentationString(i)); } - default: - throw new IllegalArgumentException("Unknown snippet kind: " + kind); - } + wrapper.append("Object o = "); + wrapper.appendSource(source); + wrapper.append(";"); + wrapper.closeBraces(initialIndent); + yield wrapper; + } + }; } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java index 6814054a2..22110ad8a 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java @@ -16,15 +16,15 @@ 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.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.joining; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.common.collect.TreeRangeMap; import com.google.googlejavaformat.Newlines; @@ -35,18 +35,9 @@ import com.sun.source.tree.Tree.Kind; import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; -import com.sun.tools.javac.file.JavacFileManager; -import com.sun.tools.javac.parser.JavacParser; -import com.sun.tools.javac.parser.ParserFactory; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Context; -import com.sun.tools.javac.util.Log; -import com.sun.tools.javac.util.Options; import com.sun.tools.javac.util.Position; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.lang.reflect.Method; -import java.net.URI; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -54,17 +45,14 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; import javax.tools.Diagnostic; -import javax.tools.DiagnosticCollector; -import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; -import javax.tools.SimpleJavaFileObject; -import javax.tools.StandardLocation; -import org.jspecify.annotations.Nullable; /** Wraps string literals that exceed the column limit. */ public final class StringWrapper { + + public static final String TEXT_BLOCK_DELIMITER = "\"\"\""; + /** Reflows long string literals in the given Java source code. */ public static String wrap(String input, Formatter formatter) throws FormatterException { return StringWrapper.wrap(Formatter.MAX_LINE_LENGTH, input, formatter); @@ -162,7 +150,7 @@ public Void visitLiteral(LiteralTree literalTree, Void aVoid) { return null; } int pos = getStartPosition(literalTree); - if (input.substring(pos, min(input.length(), pos + 3)).equals("\"\"\"")) { + if (input.substring(pos, min(input.length(), pos + 3)).equals(TEXT_BLOCK_DELIMITER)) { textBlocks.add(literalTree); return null; } @@ -171,7 +159,7 @@ public Void visitLiteral(LiteralTree literalTree, Void aVoid) { && ((MemberSelectTree) parent).getExpression().equals(literalTree)) { return null; } - int endPosition = getEndPosition(unit, literalTree); + int endPosition = getEndPosition(literalTree, unit); int lineEnd = endPosition; while (Newlines.hasNewlineAt(input, lineEnd) == -1) { lineEnd++; @@ -187,39 +175,44 @@ public Void visitLiteral(LiteralTree literalTree, Void aVoid) { private void indentTextBlocks( TreeRangeMap replacements, List textBlocks) { for (Tree tree : textBlocks) { - int startPosition = getStartPosition(tree); - int endPosition = getEndPosition(unit, tree); + 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 = stripIndent(initialLines.stream().skip(1).collect(joining(separator))); + String stripped = initialLines.stream().skip(1).collect(joining(separator)).stripIndent(); ImmutableList lines = stripped.lines().collect(toImmutableList()); - int deindent = - initialLines.get(1).stripTrailing().length() - lines.get(0).stripTrailing().length(); + boolean deindent = + getLast(initialLines).stripTrailing().length() + == getLast(lines).stripTrailing().length(); - int startColumn = lineMap.getColumnNumber(startPosition); - String prefix = - (deindent == 0 || lines.stream().anyMatch(x -> x.length() + startColumn > columnLimit)) - ? "" - : " ".repeat(startColumn - 1); + String prefix = deindent ? "" : " ".repeat(leadingWhitespace); - StringBuilder output = new StringBuilder("\"\"\""); + 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.stripLeading().stripTrailing(); + 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 && trimmed.equals("\"\"\"")) { - // If the trailing line is just """, indenting is more than the prefix of incidental + 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("\"\"\""); + output.append(TEXT_BLOCK_DELIMITER); } else { output.append(line); } @@ -249,7 +242,7 @@ private void wrapLongStrings( // Handling leaving trailing non-string tokens at the end of the literal, // e.g. the trailing `);` in `foo("...");`. - int end = getEndPosition(unit, getLast(flat)); + int end = getEndPosition(getLast(flat), unit); int lineEnd = end; while (Newlines.hasNewlineAt(input, lineEnd) == -1) { lineEnd++; @@ -259,36 +252,12 @@ private void wrapLongStrings( // Get the original source text of the string literals, excluding `"` and `+`. ImmutableList components = stringComponents(input, unit, flat); replacements.put( - Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(unit, getLast(flat))), + Range.closedOpen(getStartPosition(flat.get(0)), getEndPosition(getLast(flat), unit)), reflow(separator, columnLimit, startColumn, trailing, components, first.get())); } } } - private static final Method STRIP_INDENT = getStripIndent(); - - private static @Nullable Method getStripIndent() { - if (Runtime.version().feature() < 15) { - return null; - } - try { - return String.class.getMethod("stripIndent"); - } catch (NoSuchMethodException e) { - throw new LinkageError(e.getMessage(), e); - } - } - - private static String stripIndent(String input) { - if (STRIP_INDENT == null) { - return input; - } - try { - return (String) STRIP_INDENT.invoke(input); - } catch (ReflectiveOperationException e) { - throw new LinkageError(e.getMessage(), e); - } - } - /** * Returns the source text of the given string literal trees, excluding the leading and trailing * double-quotes and the `+` operator. @@ -299,7 +268,7 @@ private static ImmutableList stringComponents( StringBuilder piece = new StringBuilder(); for (Tree tree : flat) { // adjust for leading and trailing double quotes - String text = input.substring(getStartPosition(tree) + 1, getEndPosition(unit, tree) - 1); + String text = input.substring(getStartPosition(tree) + 1, getEndPosition(tree, unit) - 1); int start = 0; for (int idx = 0; idx < text.length(); idx++) { if (CharMatcher.whitespace().matches(text.charAt(idx))) { @@ -334,19 +303,21 @@ private static ImmutableList stringComponents( } static int hasEscapedWhitespaceAt(String input, int idx) { - return Stream.of("\\t") - .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1) - .filter(x -> x != -1) - .findFirst() - .orElse(-1); + if (input.startsWith("\\t", idx)) { + return 2; + } + return -1; } static int hasEscapedNewlineAt(String input, int idx) { - return Stream.of("\\r\\n", "\\r", "\\n") - .mapToInt(x -> input.startsWith(x, idx) ? x.length() : -1) - .filter(x -> x != -1) - .findFirst() - .orElse(-1); + int offset = 0; + if (input.startsWith("\\r", idx)) { + offset += 2; + } + if (input.startsWith("\\n", idx)) { + offset += 2; + } + return offset > 0 ? offset : -1; } /** @@ -378,7 +349,7 @@ private static String reflow( 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 (input.stream().mapToInt(String::length).sum() <= width) { + if (totalLengthLessThanOrEqual(input, width)) { // This isn’t quite optimal, but arguably good enough. See b/179561701 width -= trailing; } @@ -409,6 +380,17 @@ private static String reflow( "\"")); } + private static boolean totalLengthLessThanOrEqual(Iterable input, int length) { + int total = 0; + for (String s : input) { + total += s.length(); + if (total > length) { + return false; + } + } + return true; + } + /** * Flattens the given binary expression tree, and extracts the subset that contains the given path * and any adjacent nodes that are also string literals. @@ -459,20 +441,12 @@ && noComments(input, unit, flat.get(endIdx - 1), flat.get(endIdx))) { private static boolean noComments( String input, JCTree.JCCompilationUnit unit, Tree one, Tree two) { return STRING_CONCAT_DELIMITER.matchesAllOf( - input.subSequence(getEndPosition(unit, one), getStartPosition(two))); + input.subSequence(getEndPosition(one, unit), getStartPosition(two))); } public static final CharMatcher STRING_CONCAT_DELIMITER = CharMatcher.whitespace().or(CharMatcher.anyOf("\"+")); - private static int getEndPosition(JCTree.JCCompilationUnit unit, Tree tree) { - return ((JCTree) tree).getEndPosition(unit.endPositions); - } - - private static int getStartPosition(Tree tree) { - return ((JCTree) tree).getStartPosition(); - } - /** * Returns true if any lines in the given Java source exceed the column limit, or contain a {@code * """} that could indicate a text block. @@ -482,7 +456,7 @@ private static boolean needWrapping(int columnLimit, String input) { Iterator it = Newlines.lineIterator(input); while (it.hasNext()) { String line = it.next(); - if (line.length() > columnLimit || line.contains("\"\"\"")) { + if (line.length() > columnLimit || line.contains(TEXT_BLOCK_DELIMITER)) { return true; } } @@ -492,34 +466,11 @@ private static boolean needWrapping(int columnLimit, String input) { /** Parses the given Java source. */ private static JCTree.JCCompilationUnit parse(String source, boolean allowStringFolding) throws FormatterException { - DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + List> errorDiagnostics = new ArrayList<>(); Context context = new Context(); - context.put(DiagnosticListener.class, diagnostics); - Options.instance(context).put("--enable-preview", "true"); - Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding)); - JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8); - try { - fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - SimpleJavaFileObject sjfo = - new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) { - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) { - return source; - } - }; - Log.instance(context).useSource(sjfo); - ParserFactory parserFactory = ParserFactory.instance(context); - JavacParser parser = - parserFactory.newParser( - source, /* keepDocComments= */ true, /* keepEndPos= */ true, /* keepLineMap= */ true); - JCTree.JCCompilationUnit unit = parser.parseCompilationUnit(); - unit.sourcefile = sjfo; - Iterable> errorDiagnostics = - Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic); - if (!Iterables.isEmpty(errorDiagnostics)) { + JCTree.JCCompilationUnit unit = + Trees.parse(context, errorDiagnostics, allowStringFolding, source); + if (!errorDiagnostics.isEmpty()) { // error handling is done during formatting throw FormatterException.fromJavacDiagnostics(errorDiagnostics); } diff --git a/core/src/main/java/com/google/googlejavaformat/java/Trees.java b/core/src/main/java/com/google/googlejavaformat/java/Trees.java index 397dacae6..6b053771b 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Trees.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Trees.java @@ -14,7 +14,13 @@ package com.google.googlejavaformat.java; +import static com.google.googlejavaformat.java.Trees.getEndPosition; +import static com.google.googlejavaformat.java.Trees.getStartPosition; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; @@ -23,12 +29,26 @@ import com.sun.source.tree.ParenthesizedTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; +import com.sun.tools.javac.file.JavacFileManager; +import com.sun.tools.javac.parser.JavacParser; +import com.sun.tools.javac.parser.ParserFactory; import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.tree.Pretty; import com.sun.tools.javac.tree.TreeInfo; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Log; +import com.sun.tools.javac.util.Options; import java.io.IOError; import java.io.IOException; +import java.net.URI; +import java.util.List; import javax.lang.model.element.Name; +import javax.tools.Diagnostic; +import javax.tools.DiagnosticListener; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardLocation; /** Utilities for working with {@link Tree}s. */ class Trees { @@ -44,8 +64,12 @@ static int getStartPosition(Tree expression) { /** Returns the source end position of the node. */ static int getEndPosition(Tree expression, TreePath path) { - return ((JCTree) expression) - .getEndPosition(((JCTree.JCCompilationUnit) path.getCompilationUnit()).endPositions); + return getEndPosition(expression, path.getCompilationUnit()); + } + + /** Returns the source end position of the node. */ + public static int getEndPosition(Tree tree, CompilationUnitTree unit) { + return ((JCTree) tree).getEndPosition(((JCCompilationUnit) unit).endPositions); } /** Returns the source text for the node. */ @@ -99,13 +123,10 @@ static int precedence(ExpressionTree expression) { static ClassTree getEnclosingTypeDeclaration(TreePath path) { for (; path != null; path = path.getParentPath()) { switch (path.getLeaf().getKind()) { - case CLASS: - case ENUM: - case INTERFACE: - case ANNOTATED_TYPE: + case CLASS, ENUM, INTERFACE, ANNOTATED_TYPE -> { return (ClassTree) path.getLeaf(); - default: - break; + } + default -> {} } } throw new AssertionError(); @@ -115,4 +136,54 @@ static ClassTree getEnclosingTypeDeclaration(TreePath path) { static ExpressionTree skipParen(ExpressionTree node) { return ((ParenthesizedTree) node).getExpression(); } + + static JCCompilationUnit parse( + Context context, + List> errorDiagnostics, + boolean allowStringFolding, + String javaInput) { + DiagnosticListener diagnostics = + diagnostic -> { + if (errorDiagnostic(diagnostic)) { + errorDiagnostics.add(diagnostic); + } + }; + context.put(DiagnosticListener.class, diagnostics); + Options.instance(context).put("--enable-preview", "true"); + Options.instance(context).put("allowStringFolding", Boolean.toString(allowStringFolding)); + JavacFileManager fileManager = new JavacFileManager(context, /* register= */ true, UTF_8); + try { + fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of()); + } catch (IOException e) { + // impossible + throw new IOError(e); + } + SimpleJavaFileObject source = + new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) { + @Override + public String getCharContent(boolean ignoreEncodingErrors) { + return javaInput; + } + }; + Log.instance(context).useSource(source); + ParserFactory parserFactory = ParserFactory.instance(context); + JavacParser parser = + parserFactory.newParser( + javaInput, + /* keepDocComments= */ true, + /* keepEndPos= */ true, + /* keepLineMap= */ true); + JCCompilationUnit unit = parser.parseCompilationUnit(); + unit.sourcefile = source; + return unit; + } + + private static boolean errorDiagnostic(Diagnostic input) { + if (input.getKind() != Diagnostic.Kind.ERROR) { + return false; + } + // accept constructor-like method declarations that don't match the name of their + // enclosing class + return !input.getCode().equals("compiler.err.invalid.meth.decl.ret.type.req"); + } } diff --git a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java index 21fae5f03..83c09b3ee 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java +++ b/core/src/main/java/com/google/googlejavaformat/java/TypeNameClassifier.java @@ -30,20 +30,17 @@ private enum TyParseState { START(false) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - // if we see an UpperCamel later, assume this was a class - // e.g. com.google.FOO.Bar - return TyParseState.AMBIGUOUS; - case LOWER_CAMEL: - return TyParseState.REJECT; - case LOWERCASE: - // could be a package - return TyParseState.START; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE -> + // if we see an UpperCamel later, assume this was a class + // e.g. com.google.FOO.Bar + TyParseState.AMBIGUOUS; + case LOWER_CAMEL -> TyParseState.REJECT; + case LOWERCASE -> + // could be a package + TyParseState.START; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }, @@ -51,15 +48,10 @@ public TyParseState next(JavaCaseFormat n) { TYPE(true) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - case LOWER_CAMEL: - case LOWERCASE: - return TyParseState.FIRST_STATIC_MEMBER; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE, LOWER_CAMEL, LOWERCASE -> TyParseState.FIRST_STATIC_MEMBER; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }, @@ -83,16 +75,11 @@ public TyParseState next(JavaCaseFormat n) { AMBIGUOUS(false) { @Override public TyParseState next(JavaCaseFormat n) { - switch (n) { - case UPPERCASE: - return AMBIGUOUS; - case LOWER_CAMEL: - case LOWERCASE: - return TyParseState.REJECT; - case UPPER_CAMEL: - return TyParseState.TYPE; - } - throw new AssertionError(); + return switch (n) { + case UPPERCASE -> AMBIGUOUS; + case LOWER_CAMEL, LOWERCASE -> TyParseState.REJECT; + case UPPER_CAMEL -> TyParseState.TYPE; + }; } }; 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 ebdc8dfec..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 @@ -20,6 +20,7 @@ import java.io.IOException; 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; @@ -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/java17/Java17InputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/java17/Java17InputAstVisitor.java deleted file mode 100644 index a0037edb7..000000000 --- a/core/src/main/java/com/google/googlejavaformat/java/java17/Java17InputAstVisitor.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright 2020 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.googlejavaformat.java.java17; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.Iterables.getOnlyElement; - -import com.google.common.base.Verify; -import com.google.common.collect.ImmutableList; -import com.google.googlejavaformat.OpsBuilder; -import com.google.googlejavaformat.OpsBuilder.BlankLineWanted; -import com.google.googlejavaformat.java.JavaInputAstVisitor; -import com.sun.source.tree.BindingPatternTree; -import com.sun.source.tree.BlockTree; -import com.sun.source.tree.CaseLabelTree; -import com.sun.source.tree.CaseTree; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.InstanceOfTree; -import com.sun.source.tree.ModifiersTree; -import com.sun.source.tree.ModuleTree; -import com.sun.source.tree.SwitchExpressionTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.VariableTree; -import com.sun.source.tree.YieldTree; -import com.sun.tools.javac.code.Flags; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.JCTree.JCVariableDecl; -import com.sun.tools.javac.tree.TreeInfo; -import java.util.List; -import java.util.Optional; -import javax.lang.model.element.Name; - -/** - * Extends {@link JavaInputAstVisitor} with support for AST nodes that were added or modified in - * Java 17. - */ -public class Java17InputAstVisitor extends JavaInputAstVisitor { - - public Java17InputAstVisitor(OpsBuilder builder, int indentMultiplier) { - super(builder, indentMultiplier); - } - - @Override - protected void handleModule(boolean afterFirstToken, CompilationUnitTree node) { - ModuleTree module = node.getModule(); - if (module != null) { - if (afterFirstToken) { - builder.blankLineWanted(BlankLineWanted.YES); - } - markForPartialFormat(); - visitModule(module, null); - builder.forcedBreak(); - } - } - - @Override - protected List getPermitsClause(ClassTree node) { - return node.getPermitsClause(); - } - - @Override - public Void visitBindingPattern(BindingPatternTree node, Void unused) { - sync(node); - VariableTree variableTree = node.getVariable(); - visitBindingPattern( - variableTree.getModifiers(), variableTree.getType(), variableTree.getName()); - return null; - } - - private void visitBindingPattern(ModifiersTree modifiers, Tree type, Name name) { - builder.open(plusFour); - declareOne( - DeclarationKind.PARAMETER, - Direction.HORIZONTAL, - Optional.of(modifiers), - type, - name, - /* op= */ "", - /* equals= */ "", - /* initializer= */ Optional.empty(), - /* trailing= */ Optional.empty(), - /* receiverExpression= */ Optional.empty(), - /* typeWithDims= */ Optional.empty()); - builder.close(); - } - - @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 visitClass(ClassTree tree, Void unused) { - switch (tree.getKind()) { - case ANNOTATION_TYPE: - visitAnnotationType(tree); - break; - case CLASS: - case INTERFACE: - visitClassDeclaration(tree); - break; - case ENUM: - visitEnumDeclaration(tree); - break; - case RECORD: - visitRecordDeclaration(tree); - break; - default: - throw new AssertionError(tree.getKind()); - } - return null; - } - - public void visitRecordDeclaration(ClassTree node) { - sync(node); - 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 = 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 { - List 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(JCVariableDecl.class::isInstance) - .map(JCVariableDecl.class::cast) - .filter(m -> (m.mods.flags & RECORD) == RECORD) - .collect(toImmutableList()); - } - - @Override - public Void visitInstanceOf(InstanceOfTree node, Void unused) { - sync(node); - builder.open(plusFour); - scan(node.getExpression(), null); - builder.breakOp(" "); - builder.open(ZERO); - token("instanceof"); - builder.breakOp(" "); - if (node.getPattern() != null) { - scan(node.getPattern(), null); - } else { - scan(node.getType(), null); - } - builder.close(); - builder.close(); - return null; - } - - @Override - public Void visitCase(CaseTree node, Void unused) { - sync(node); - markForPartialFormat(); - builder.forcedBreak(); - 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) - && !node.getBody().getKind().equals(Tree.Kind.BLOCK) - ? plusFour - : ZERO); - if (isDefault) { - token("default", ZERO); - } else { - token("case", ZERO); - builder.open(labels.size() > 1 ? plusFour : ZERO); - builder.space(); - boolean afterFirstToken = false; - for (Tree expression : labels) { - if (afterFirstToken) { - token(","); - builder.breakOp(" "); - } - scan(expression, null); - afterFirstToken = true; - } - builder.close(); - } - - final ExpressionTree guard = getGuard(node); - if (guard != null) { - builder.space(); - token("when"); - builder.space(); - scan(guard, null); - } - - switch (node.getCaseKind()) { - case STATEMENT: - token(":"); - builder.open(plusTwo); - visitStatements(node.getStatements()); - builder.close(); - break; - case RULE: - builder.space(); - token("-"); - token(">"); - if (node.getBody().getKind() == Tree.Kind.BLOCK) { - 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.guessToken(";"); - break; - default: - throw new AssertionError(node.getCaseKind()); - } - builder.close(); - return null; - } - - protected ExpressionTree getGuard(final CaseTree node) { - return null; - } -} diff --git a/core/src/main/java/com/google/googlejavaformat/java/java21/Java21InputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/java21/Java21InputAstVisitor.java deleted file mode 100644 index 859c9c0cf..000000000 --- a/core/src/main/java/com/google/googlejavaformat/java/java21/Java21InputAstVisitor.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2023 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. - */ - -package com.google.googlejavaformat.java.java21; - -import com.google.googlejavaformat.OpsBuilder; -import com.google.googlejavaformat.java.java17.Java17InputAstVisitor; -import com.sun.source.tree.CaseTree; -import com.sun.source.tree.ConstantCaseLabelTree; -import com.sun.source.tree.DeconstructionPatternTree; -import com.sun.source.tree.DefaultCaseLabelTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.PatternCaseLabelTree; -import com.sun.source.tree.PatternTree; -import com.sun.source.tree.Tree; -import com.sun.tools.javac.tree.JCTree; -import javax.lang.model.element.Name; - -/** - * Extends {@link Java17InputAstVisitor} with support for AST nodes that were added or modified in - * Java 21. - */ -public class Java21InputAstVisitor extends Java17InputAstVisitor { - - public Java21InputAstVisitor(OpsBuilder builder, int indentMultiplier) { - super(builder, indentMultiplier); - } - - @Override - protected ExpressionTree getGuard(final CaseTree node) { - return node.getGuard(); - } - - @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; - } - - @Override - protected void variableName(Name name) { - if (name.isEmpty()) { - token("_"); - } else { - visit(name); - } - } - - @Override - public Void scan(Tree tree, Void unused) { - // Pre-visit AST for preview features, since com.sun.source.tree.AnyPattern can't be - // accessed directly without --enable-preview. - if (tree instanceof JCTree.JCAnyPattern) { - visitJcAnyPattern((JCTree.JCAnyPattern) tree); - return null; - } else { - return super.scan(tree, null); - } - } - - private void visitJcAnyPattern(JCTree.JCAnyPattern unused) { - token("_"); - } -} 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 4d45c9874..bb9f70040 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocFormatter.java @@ -57,88 +57,35 @@ private static String render(List input, int blockIndent) { JavadocWriter output = new JavadocWriter(blockIndent); for (Token token : input) { switch (token.getType()) { - case BEGIN_JAVADOC: - output.writeBeginJavadoc(); - break; - case END_JAVADOC: + case BEGIN_JAVADOC -> output.writeBeginJavadoc(); + case END_JAVADOC -> { output.writeEndJavadoc(); return output.toString(); - case FOOTER_JAVADOC_TAG_START: - output.writeFooterJavadocTagStart(token); - break; - case SNIPPET_BEGIN: - output.writeSnippetBegin(token); - break; - case SNIPPET_END: - output.writeSnippetEnd(token); - break; - case LIST_OPEN_TAG: - output.writeListOpen(token); - break; - case LIST_CLOSE_TAG: - output.writeListClose(token); - break; - case LIST_ITEM_OPEN_TAG: - output.writeListItemOpen(token); - break; - case HEADER_OPEN_TAG: - output.writeHeaderOpen(token); - break; - case HEADER_CLOSE_TAG: - output.writeHeaderClose(token); - break; - case PARAGRAPH_OPEN_TAG: - output.writeParagraphOpen(standardizePToken(token)); - break; - case BLOCKQUOTE_OPEN_TAG: - case BLOCKQUOTE_CLOSE_TAG: - output.writeBlockquoteOpenOrClose(token); - break; - case PRE_OPEN_TAG: - output.writePreOpen(token); - break; - case PRE_CLOSE_TAG: - output.writePreClose(token); - break; - case CODE_OPEN_TAG: - output.writeCodeOpen(token); - break; - case CODE_CLOSE_TAG: - output.writeCodeClose(token); - break; - case TABLE_OPEN_TAG: - output.writeTableOpen(token); - break; - case TABLE_CLOSE_TAG: - output.writeTableClose(token); - break; - case MOE_BEGIN_STRIP_COMMENT: - output.requestMoeBeginStripComment(token); - break; - case MOE_END_STRIP_COMMENT: - output.writeMoeEndStripComment(token); - break; - case HTML_COMMENT: - output.writeHtmlComment(token); - break; - case BR_TAG: - output.writeBr(standardizeBrToken(token)); - break; - case WHITESPACE: - output.requestWhitespace(); - break; - case FORCED_NEWLINE: - output.writeLineBreakNoAutoIndent(); - break; - case LITERAL: - output.writeLiteral(token); - break; - case PARAGRAPH_CLOSE_TAG: - case LIST_ITEM_CLOSE_TAG: - case OPTIONAL_LINE_BREAK: - break; - default: - throw new AssertionError(token.getType()); + } + case FOOTER_JAVADOC_TAG_START -> output.writeFooterJavadocTagStart(token); + case SNIPPET_BEGIN -> output.writeSnippetBegin(token); + case SNIPPET_END -> output.writeSnippetEnd(token); + case LIST_OPEN_TAG -> output.writeListOpen(token); + case LIST_CLOSE_TAG -> output.writeListClose(token); + case LIST_ITEM_OPEN_TAG -> output.writeListItemOpen(token); + case HEADER_OPEN_TAG -> output.writeHeaderOpen(token); + case HEADER_CLOSE_TAG -> output.writeHeaderClose(token); + case PARAGRAPH_OPEN_TAG -> output.writeParagraphOpen(standardizePToken(token)); + case BLOCKQUOTE_OPEN_TAG, BLOCKQUOTE_CLOSE_TAG -> output.writeBlockquoteOpenOrClose(token); + case PRE_OPEN_TAG -> output.writePreOpen(token); + case PRE_CLOSE_TAG -> output.writePreClose(token); + case CODE_OPEN_TAG -> output.writeCodeOpen(token); + case CODE_CLOSE_TAG -> output.writeCodeClose(token); + case TABLE_OPEN_TAG -> output.writeTableOpen(token); + case TABLE_CLOSE_TAG -> output.writeTableClose(token); + case MOE_BEGIN_STRIP_COMMENT -> output.requestMoeBeginStripComment(token); + case MOE_END_STRIP_COMMENT -> output.writeMoeEndStripComment(token); + case HTML_COMMENT -> output.writeHtmlComment(token); + case BR_TAG -> output.writeBr(standardizeBrToken(token)); + case WHITESPACE -> output.requestWhitespace(); + case FORCED_NEWLINE -> output.writeLineBreakNoAutoIndent(); + case LITERAL -> output.writeLiteral(token); + case PARAGRAPH_CLOSE_TAG, LIST_ITEM_CLOSE_TAG, OPTIONAL_LINE_BREAK -> {} } } throw new AssertionError(); 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 8a4100e45..5e6af1795 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/javadoc/JavadocWriter.java @@ -40,6 +40,7 @@ final class JavadocWriter { private final int blockIndent; private final StringBuilder output = new StringBuilder(); + /** * Whether we are inside an {@code

  • } element, excluding the case in which the {@code
  • } * contains a {@code
      } or {@code
        } that we are also inside -- unless of course we're diff --git a/core/src/main/resources/META-INF/native-image/reflect-config.json b/core/src/main/resources/META-INF/native-image/reflect-config.json index 4d30840f1..2c6580345 100644 --- a/core/src/main/resources/META-INF/native-image/reflect-config.json +++ b/core/src/main/resources/META-INF/native-image/reflect-config.json @@ -2,23 +2,5 @@ { "name": "com.sun.tools.javac.parser.UnicodeReader", "allDeclaredMethods": true - }, - { - "name": "com.google.googlejavaformat.java.java17.Java17InputAstVisitor", - "methods": [ - { - "name": "", - "parameterTypes": ["com.google.googlejavaformat.OpsBuilder", "int"] - } - ] - }, - { - "name": "com.google.googlejavaformat.java.java21.Java21InputAstVisitor", - "methods": [ - { - "name": "", - "parameterTypes": ["com.google.googlejavaformat.OpsBuilder", "int"] - } - ] } ] diff --git a/core/src/main/scripts/google-java-format.el b/core/src/main/scripts/google-java-format.el index 5df8a1396..1bb3ffdc2 100644 --- a/core/src/main/scripts/google-java-format.el +++ b/core/src/main/scripts/google-java-format.el @@ -2,8 +2,6 @@ ;; ;; Copyright 2015 Google, Inc. All Rights Reserved. ;; -;; Package-Requires: ((emacs "24")) -;; ;; Licensed under the Apache License, Version 2.0 (the "License"); ;; you may not use this file except in compliance with the License. ;; You may obtain a copy of the License at @@ -17,6 +15,8 @@ ;; limitations under the License. ;; Keywords: tools, Java +;; Version: 0.1.0 +;; Package-Requires: ((emacs "24")) ;;; Commentary: @@ -109,5 +109,4 @@ there is no region, then formats the current line." (defalias 'google-java-format 'google-java-format-region) (provide 'google-java-format) - ;;; google-java-format.el ends here diff --git a/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java b/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java index e5fbc9f5f..928ce0078 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/CommandLineFlagsTest.java @@ -21,9 +21,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** - * Tests for command-line flags. - */ +/** Tests for command-line flags. */ @RunWith(JUnit4.class) public class CommandLineFlagsTest { diff --git a/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java index fc966fac3..e05a37264 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/DiagnosticTest.java @@ -79,7 +79,7 @@ public void parseError() throws Exception { int result = main.format(path.toString()); assertThat(stdout.toString()).isEmpty(); - assertThat(stderr.toString()).contains("InvalidSyntax.java:2:29: error: expected"); + assertThat(stderr.toString()).contains("InvalidSyntax.java:2:28: error: expected"); assertThat(result).isEqualTo(1); } @@ -119,7 +119,7 @@ public void oneFileParseError() throws Exception { int result = main.format(pathOne.toString(), pathTwo.toString()); assertThat(stdout.toString()).isEqualTo(two); - assertThat(stderr.toString()).contains("One.java:1:13: error: reached end of file"); + assertThat(stderr.toString()).contains("One.java:1:12: error: reached end of file"); assertThat(result).isEqualTo(1); } @@ -141,7 +141,7 @@ public void oneFileParseErrorReplace() throws Exception { int result = main.format("-i", pathOne.toString(), pathTwo.toString()); assertThat(stdout.toString()).isEmpty(); - assertThat(stderr.toString()).contains("One.java:1:14: error: class, interface"); + assertThat(stderr.toString()).contains("One.java:1:13: error: class, interface"); assertThat(result).isEqualTo(1); // don't edit files with parse errors assertThat(Files.readAllLines(pathOne, UTF_8)).containsExactly("class One {}}"); @@ -164,7 +164,7 @@ public void parseError2() throws FormatterException, IOException, UsageException int exitCode = main.format(args); assertThat(exitCode).isEqualTo(1); - assertThat(err.toString()).contains("A.java:2:6: error: ';' expected"); + assertThat(err.toString()).contains("A.java:2:5: error: ';' expected"); } @Test @@ -179,7 +179,7 @@ public void parseErrorStdin() throws FormatterException, IOException, UsageExcep int exitCode = main.format(args); assertThat(exitCode).isEqualTo(1); - assertThat(err.toString()).contains(":2:6: error: ';' expected"); + assertThat(err.toString()).contains(":2:5: error: ';' expected"); } @Test @@ -198,7 +198,7 @@ public void lexError2() throws FormatterException, IOException, UsageException { int exitCode = main.format(args); assertThat(exitCode).isEqualTo(1); - assertThat(err.toString()).contains("A.java:2:5: error: unclosed character literal"); + assertThat(err.toString()).contains("A.java:2:4: error: unclosed character literal"); } @Test @@ -212,6 +212,6 @@ public void lexErrorStdin() throws FormatterException, IOException, UsageExcepti int exitCode = main.format(args); assertThat(exitCode).isEqualTo(1); - assertThat(err.toString()).contains(":2:5: error: unclosed character literal"); + assertThat(err.toString()).contains(":2:4: error: unclosed character literal"); } } diff --git a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java index 3e4e175e6..a406487f3 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java @@ -48,19 +48,6 @@ public class FormatterIntegrationTest { private static final ImmutableMultimap VERSIONED_TESTS = ImmutableMultimap.builder() - .putAll( - 14, - "I477", - "Records", - "RSLs", - "Var", - "ExpressionSwitch", - "I574", - "I594", - "SwitchComment") - .putAll(15, "I603") - .putAll(16, "I588", "Sealed") - .putAll(17, "I683", "I684", "I696") .putAll( 21, "SwitchGuardClause", @@ -72,6 +59,7 @@ public class FormatterIntegrationTest { "I981", "I1020", "I1037") + .putAll(25, "ModuleImport") .build(); @Parameters(name = "{index}: {0}") @@ -94,13 +82,9 @@ public static Iterable data() throws IOException { contents = CharStreams.toString(new InputStreamReader(stream, UTF_8)); } switch (extension) { - case "input": - inputs.put(baseName, contents); - break; - case "output": - outputs.put(baseName, contents); - break; - default: // fall out + case "input" -> inputs.put(baseName, contents); + case "output" -> outputs.put(baseName, contents); + default -> {} } } } @@ -144,6 +128,19 @@ public void format() { } } + @Test + public void idempotent() { + try { + Formatter formatter = new Formatter(); + String formatted = formatter.formatSource(input); + formatted = StringWrapper.wrap(formatted, formatter); + String reformatted = formatter.formatSource(formatted); + assertEquals("bad output for " + name, formatted, reformatted); + } catch (FormatterException e) { + fail(String.format("Formatter crashed on %s: %s", name, e.getMessage())); + } + } + @Test public void idempotentLF() { try { diff --git a/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java b/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java index 0b9dab26f..6772b42be 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/ImportOrdererTest.java @@ -820,6 +820,23 @@ public static Collection parameters() { "", "public class Blim {}", }, + }, + { + { + "import module java.base;", // + "import static java.lang.Math.min;", + "import java.util.List;", + "class Test {}", + }, + { + "import static java.lang.Math.min;", // + "", + "import module java.base;", + "", + "import java.util.List;", + "", + "class Test {}", + }, } }; ImmutableList.Builder builder = ImmutableList.builder(); diff --git a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java index aab8ec5d4..39d43c2f9 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/JavadocFormattingTest.java @@ -1084,7 +1084,7 @@ public void paragraphTag() { @Test public void xhtmlParagraphTag() { String[] input = { - "class Test {", + "class Test {", // " /**", " * hello

        world", " */", @@ -1093,7 +1093,7 @@ public void xhtmlParagraphTag() { "}", }; String[] expected = { - "class Test {", + "class Test {", // " /**", " * hello", " *", diff --git a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java index 42e12d860..2d9364082 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java @@ -307,7 +307,7 @@ public void importRemoveErrorParseError() throws Exception { new PrintWriter(err, true), new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8))); assertThat(main.format("-")).isEqualTo(1); - assertThat(err.toString()).contains(":4:3: error: class, interface"); + assertThat(err.toString()).contains(":4:2: error: class, interface"); } finally { Locale.setDefault(backupLocale); @@ -508,7 +508,7 @@ public void assumeFilename_error() throws Exception { new PrintWriter(err, true), new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8))); assertThat(main.format("--assume-filename=Foo.java", "-")).isEqualTo(1); - assertThat(err.toString()).contains("Foo.java:1:15: error: class, interface"); + assertThat(err.toString()).contains("Foo.java:1:14: error: class, interface"); } @Test @@ -635,4 +635,59 @@ public void reorderModifiersOptionTest() throws Exception { .formatSource(source)) .isEqualTo(source); } + + @Test + public void syntaxError() throws Exception { + Path path = testFolder.newFile("Test.java").toPath(); + String[] input = { + "class Test {", // + " void f(int package) {", + " int", + " }", + "}", + "", + }; + String source = joiner.join(input); + Files.writeString(path, source, UTF_8); + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in); + int errorCode = main.format(path.toAbsolutePath().toString()); + assertWithMessage("Error Code").that(errorCode).isEqualTo(1); + String[] expected = { + path + ":2:13: error: expected", + " void f(int package) {", + " ^", + path + ":3:5: error: not a statement", + " int", + " ^", + path + ":3:8: error: ';' expected", + " int", + " ^", + "", + }; + assertThat(err.toString()).isEqualTo(joiner.join(expected)); + } + + @Test + public void syntaxErrorBeginning() throws Exception { + Path path = testFolder.newFile("Test.java").toPath(); + String[] input = { + "error", // + }; + String source = joiner.join(input); + Files.writeString(path, source, UTF_8); + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in); + int errorCode = main.format(path.toAbsolutePath().toString()); + assertWithMessage("Error Code").that(errorCode).isEqualTo(1); + String[] expected = { + path + ":1:1: error: reached end of file while parsing", // + "error", + "^", + "", + }; + assertThat(err.toString()).isEqualTo(joiner.join(expected)); + } } diff --git a/core/src/test/java/com/google/googlejavaformat/java/ModifierOrdererTest.java b/core/src/test/java/com/google/googlejavaformat/java/ModifierOrdererTest.java index 0f01e9d3f..013d14358 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/ModifierOrdererTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/ModifierOrdererTest.java @@ -17,7 +17,6 @@ package com.google.googlejavaformat.java; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.TruthJUnit.assume; import com.google.common.base.Joiner; import com.google.common.collect.Range; @@ -107,7 +106,6 @@ public void whitespace() throws FormatterException { @Test public void sealedClass() throws FormatterException { - assume().that(Runtime.version().feature()).isAtLeast(16); assertThat(ModifierOrderer.reorderModifiers("non-sealed sealed public").getText()) .isEqualTo("public sealed non-sealed"); } diff --git a/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java b/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java index 675bc8884..d8b63ef3e 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/RemoveUnusedImportsTest.java @@ -17,7 +17,6 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.googlejavaformat.java.RemoveUnusedImports.removeUnusedImports; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import java.util.Collection; import org.junit.Test; @@ -255,14 +254,29 @@ public static Collection parameters() { "interface Test { private static void foo() {} }", }, }, + { + { + "import module java.base;", // + "import java.lang.Foo;", + "interface Test { private static void foo() {} }", + }, + { + "import module java.base;", // + "interface Test { private static void foo() {} }", + }, + }, }; ImmutableList.Builder builder = ImmutableList.builder(); for (String[][] inputAndOutput : inputsOutputs) { assertThat(inputAndOutput).hasLength(2); - String[] input = inputAndOutput[0]; - String[] output = inputAndOutput[1]; + String input = String.join("\n", inputAndOutput[0]) + "\n"; + String output = String.join("\n", inputAndOutput[1]) + "\n"; + if (input.contains("import module") && Runtime.version().feature() < 25) { + // TODO: cushon - remove this once the minimum supported JDK updates past 25 + continue; + } String[] parameters = { - Joiner.on('\n').join(input) + '\n', Joiner.on('\n').join(output) + '\n', + input, output, }; builder.add(parameters); } diff --git a/core/src/test/java/com/google/googlejavaformat/java/ReplacementTest.java b/core/src/test/java/com/google/googlejavaformat/java/ReplacementTest.java new file mode 100644 index 000000000..177af9f1f --- /dev/null +++ b/core/src/test/java/com/google/googlejavaformat/java/ReplacementTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2025 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.Range; +import com.google.common.testing.EqualsTester; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** {@link Replacement}Test */ +@RunWith(JUnit4.class) +public class ReplacementTest { + + @Test + public void testCreateWithValidInput() { + Replacement replacement = Replacement.create(3, 7, "replacementText"); + assertThat(replacement.getReplaceRange()).isEqualTo(Range.closedOpen(3, 7)); + assertThat(replacement.getReplacementString()).isEqualTo("replacementText"); + } + + @Test + public void invalidReplacementRange() { + assertThrows(IllegalArgumentException.class, () -> Replacement.create(-1, 5, "text")); + assertThrows(IllegalArgumentException.class, () -> Replacement.create(10, 5, "text")); + } + + @Test + public void testEqualsAndHashCode() { + new EqualsTester() + .addEqualityGroup(Replacement.create(0, 4, "abc"), Replacement.create(0, 4, "abc")) + .addEqualityGroup(Replacement.create(1, 4, "abc")) + .addEqualityGroup(Replacement.create(0, 4, "def")) + .testEquals(); + } +} diff --git a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java index 53fb54d91..5032ac97c 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperIntegrationTest.java @@ -365,6 +365,71 @@ public static Collection parameters() { "}" }, }, + { + { + "class T {", // + " String s = \"\\r\\rone\\rlong\\rincredibly\\runbroken\\rsentence\\rmoving\\rfrom\\r" + + " topic\\rto\\r topic\\rso\\rthat\\rno-one\\rhad\\ra\\rchance\\rto\\rinterrupt\";", + "}" + }, + { + "class T {", + " String s =", + " \"\\r\\r\"", + " + \"one\\r\"", + " + \"long\\r\"", + " + \"incredibly\\r\"", + " + \"unbroken\\r\"", + " + \"sentence\\r\"", + " + \"moving\\r\"", + " + \"from\\r\"", + " + \" topic\\r\"", + " + \"to\\r\"", + " + \" topic\\r\"", + " + \"so\\r\"", + " + \"that\\r\"", + " + \"no-one\\r\"", + " + \"had\\r\"", + " + \"a\\r\"", + " + \"chance\\r\"", + " + \"to\\r\"", + " + \"interrupt\";", + "}", + }, + }, + { + { + "class T {", // + " String s = \"\\r\\n\\r\\none\\r\\nlong\\r\\nincredibly\\r\\nunbroken\\r\\nsentence" + + "\\r\\nmoving\\r\\nfrom\\r\\n topic\\r\\nto\\r\\n topic\\r\\nso\\r\\nthat\\r\\n" + + "no-one\\r\\nhad\\r\\na\\r\\nchance\\r\\nto\\r\\ninterrupt\";", + "}" + }, + { + "class T {", + " String s =", + " \"\\r\\n\\r\\n\"", + " + \"one\\r\\n\"", + " + \"long\\r\\n\"", + " + \"incredibly\\r\\n\"", + " + \"unbroken\\r\\n\"", + " + \"sentence\\r\\n\"", + " + \"moving\\r\\n\"", + " + \"from\\r\\n\"", + " + \" topic\\r\\n\"", + " + \"to\\r\\n\"", + " + \" topic\\r\\n\"", + " + \"so\\r\\n\"", + " + \"that\\r\\n\"", + " + \"no-one\\r\\n\"", + " + \"had\\r\\n\"", + " + \"a\\r\\n\"", + " + \"chance\\r\\n\"", + " + \"to\\r\\n\"", + " + \"interrupt\";", + "}", + }, + }, }; return Arrays.stream(inputsAndOutputs) .map( diff --git a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java index fd176ed4e..a9ceeacb6 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java @@ -63,9 +63,10 @@ public void textBlock() throws Exception { " private String myString;", " private ReproBug() {", " String str =", - " \"\"\"", + "\"\"\"", "{\"sourceEndpoint\":\"ri.something.1-1.object-internal.1\",\"targetEndpoint" - + "\":\"ri.something.1-1.object-internal.2\",\"typeId\":\"typeId\"}\"\"\";", + + "\":\"ri.something.1-1.object-internal.2\",\"typeId\":\"typeId\"}\\", + "\"\"\";", " myString = str;", " }", "}"); @@ -173,6 +174,33 @@ public void textBlockSpaceTabMix() throws Exception { assertThat(actual).isEqualTo(expected); } + @Test + public void leadingBlankLine() throws Exception { + assumeTrue(Runtime.version().feature() >= 15); + String input = + lines( + "public class T {", + " String s =", + " \"\"\"", + "", + " lorem", + " ipsum", + " \"\"\";", + "}"); + String expected = + lines( + "public class T {", + " String s =", + " \"\"\"", + "", + " lorem", + " ipsum", + " \"\"\";", + "}"); + String actual = StringWrapper.wrap(100, input, new Formatter()); + assertThat(actual).isEqualTo(expected); + } + private static String lines(String... line) { return Joiner.on('\n').join(line) + '\n'; } diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B361077825.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B361077825.input new file mode 100644 index 000000000..c93942a5d --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B361077825.input @@ -0,0 +1,15 @@ +class T { + String a = + """ + # No implicit input file, because they can only be created outside a symbolic macro, + """; + + String b = + """ + # No implicit input file, because they can only be created outside a symbolic macro, + """; + String c = + """ + # No implicit input file, because they can only be created outside a symbolic macro, + """; +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B361077825.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B361077825.output new file mode 100644 index 000000000..c93942a5d --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B361077825.output @@ -0,0 +1,15 @@ +class T { + String a = + """ + # No implicit input file, because they can only be created outside a symbolic macro, + """; + + String b = + """ + # No implicit input file, because they can only be created outside a symbolic macro, + """; + String c = + """ + # No implicit input file, because they can only be created outside a symbolic macro, + """; +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B377585941.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B377585941.input new file mode 100644 index 000000000..bd3107be4 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B377585941.input @@ -0,0 +1,8 @@ +class T { + { + f( + /* foo */ """ + hello + """); + } +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B377585941.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B377585941.output new file mode 100644 index 000000000..bd3107be4 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B377585941.output @@ -0,0 +1,8 @@ +class T { + { + f( + /* foo */ """ + hello + """); + } +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B380299722.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B380299722.input new file mode 100644 index 000000000..8da9601c9 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B380299722.input @@ -0,0 +1,9 @@ +package com.helloworld; + +class Foo { + void foo() { + var bar = """ + bar\ + bar"""; + } +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B380299722.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B380299722.output new file mode 100644 index 000000000..397073147 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B380299722.output @@ -0,0 +1,11 @@ +package com.helloworld; + +class Foo { + void foo() { + var bar = + """ + bar\ + bar\ + """; + } +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B381242320.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B381242320.input new file mode 100644 index 000000000..4c5e12e53 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B381242320.input @@ -0,0 +1,11 @@ +class B381242320 { + @Deprecated() + public int deprecatedMethod() { + return 42; + } + + @Override() + public int hashCode() { + return 42; + } +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B381242320.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B381242320.output new file mode 100644 index 000000000..4c5e12e53 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B381242320.output @@ -0,0 +1,11 @@ +class B381242320 { + @Deprecated() + public int deprecatedMethod() { + return 42; + } + + @Override() + public int hashCode() { + return 42; + } +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1153.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1153.input new file mode 100644 index 000000000..e2b27dde1 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1153.input @@ -0,0 +1,15 @@ +class I1153 { + void f() { + //// (1) one + int one; + + //// (2) two + int two; + + //// (2.1) if we need to collect data using multiple different collectors, e.g. taxonomy and + //// ranges, or even two taxonomy facets that use different Category List Field, we can + //// use MultiCollectorManager, e.g.: + // TODO: This should be (2.1) two point one + int twoPointOne; + } +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1153.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1153.output new file mode 100644 index 000000000..e2b27dde1 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1153.output @@ -0,0 +1,15 @@ +class I1153 { + void f() { + //// (1) one + int one; + + //// (2) two + int two; + + //// (2.1) if we need to collect data using multiple different collectors, e.g. taxonomy and + //// ranges, or even two taxonomy facets that use different Category List Field, we can + //// use MultiCollectorManager, e.g.: + // TODO: This should be (2.1) two point one + int twoPointOne; + } +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1205.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1205.input new file mode 100644 index 000000000..8f8acaa16 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1205.input @@ -0,0 +1,7 @@ +public interface Foo { + + private static String foo = + """ + foo\ + bar """; +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1205.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1205.output new file mode 100644 index 000000000..c82862c09 --- /dev/null +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/I1205.output @@ -0,0 +1,8 @@ +public interface Foo { + + private static String foo = + """ + foo\ + bar\ + """; +} diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/RSLs.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/RSLs.input index 22aa8f2b2..002b3653a 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/RSLs.input +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/RSLs.input @@ -40,6 +40,11 @@ class RSLs { lorem ipsum """; + String l = + """ + foo +bar + baz"""; { f(""" lorem @@ -50,5 +55,39 @@ ipsum hello %s """ .formatted("world"); + f( + /* foo= */ """ + foo + """, + /* bar= */ """ + bar + """); + """ + hello + """.codePoints().forEach(System.err::println); + String s = + """ + foo + """ + + """ + bar + """; + String t = + """ +foo +""" + + """ +bar +"""; + String u = + stringVariableOne + + + """ + ... + """ + stringVariableTwo + + + """ + ... + """; } } diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/RSLs.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/RSLs.output index 5ca1fb8cc..af4a8a6fc 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/RSLs.output +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/RSLs.output @@ -23,7 +23,8 @@ class RSLs { """; String f = """ - ipsum"""; + ipsum\ + """; String g = """ lorem\ @@ -42,14 +43,20 @@ class RSLs { """; String j = """ -lorem -one long incredibly unbroken sentence moving from topic to topic so that no one had a chance to interrupt -ipsum -"""; + lorem + one long incredibly unbroken sentence moving from topic to topic so that no one had a chance to interrupt + ipsum + """; String k = - """ +""" lorem ipsum +"""; + String l = +""" + foo +bar + baz\ """; { @@ -64,5 +71,41 @@ ipsum hello %s """ .formatted("world"); + f( + /* foo= */ """ + foo + """, + /* bar= */ """ + bar + """); + """ + hello + """ + .codePoints() + .forEach(System.err::println); + String s = + """ + foo + """ + + """ + bar + """; + String t = +""" +foo +""" + + +""" +bar +"""; + String u = + stringVariableOne + + """ + ... + """ + + stringVariableTwo + + """ + ... + """; } } diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.input index 25df58096..0f4b485b8 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.input +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.input @@ -6,4 +6,68 @@ class SwitchGuardClause { default -> true; }; } + + { + switch (o) { + case TypeWithVeryVeryVeryVeryLongName variableWithVeryLongName when variableWithVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> { + System.err.println(); + } + default -> {} + } + switch (o) { + case TypeWithVeryVeryVeryVeryLongName + variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName when variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> { + System.err.println(); + } + default -> {} + } + switch (o) { + case TypeWithVeryVeryVeryVeryLongName variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + when variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> { + System.err.println(); + } + default -> {} + } + switch (o) { + case SwitchRecord( + int one, + int two, + int three, + int four, + int five, + int six, + int seven, + int eight, + int variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName) + when variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> { + System.err.println(); + } + default -> {} + } + switch (o) { + case SwitchRecord( + int one, + int two, + int three, + int four, + int five, + int six, + int seven, + int eight, + int variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName) + when variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> + System.err.println(); + default -> {} + } + } } diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.output index 25df58096..ac0961d03 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.output +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/SwitchGuardClause.output @@ -6,4 +6,70 @@ class SwitchGuardClause { default -> true; }; } + + { + switch (o) { + case TypeWithVeryVeryVeryVeryLongName variableWithVeryLongName + when variableWithVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> { + System.err.println(); + } + default -> {} + } + switch (o) { + case TypeWithVeryVeryVeryVeryLongName variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + when variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> { + System.err.println(); + } + default -> {} + } + switch (o) { + case TypeWithVeryVeryVeryVeryLongName + variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + when variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> { + System.err.println(); + } + default -> {} + } + switch (o) { + case SwitchRecord( + int one, + int two, + int three, + int four, + int five, + int six, + int seven, + int eight, + int variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName) + when variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> { + System.err.println(); + } + default -> {} + } + switch (o) { + case SwitchRecord( + int one, + int two, + int three, + int four, + int five, + int six, + int seven, + int eight, + int variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName) + when variableWithVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName + .methodWithVeryVeryVeryVeryLongNameReturnThis() + .methodWithVeryVeryVeryVeryLongNameReturnBoolean() -> + System.err.println(); + default -> {} + } + } } diff --git a/eclipse_plugin/META-INF/MANIFEST.MF b/eclipse_plugin/META-INF/MANIFEST.MF index 913245393..4170ad643 100644 --- a/eclipse_plugin/META-INF/MANIFEST.MF +++ b/eclipse_plugin/META-INF/MANIFEST.MF @@ -3,7 +3,7 @@ Bundle-ManifestVersion: 2 Bundle-Name: google-java-format Bundle-SymbolicName: google-java-format-eclipse-plugin;singleton:=true Bundle-Vendor: Google -Bundle-Version: 1.13.0 +Bundle-Version: 1.31.0 Bundle-RequiredExecutionEnvironment: JavaSE-11 Require-Bundle: org.eclipse.jdt.core;bundle-version="3.10.0", org.eclipse.jface, diff --git a/eclipse_plugin/plugin.xml b/eclipse_plugin/plugin.xml index 1fdfb6e41..e40b59aea 100644 --- a/eclipse_plugin/plugin.xml +++ b/eclipse_plugin/plugin.xml @@ -24,5 +24,10 @@ id="com.google.googlejavaformat.java.GoogleJavaFormatter" name="google-java-format"> + + diff --git a/eclipse_plugin/pom.xml b/eclipse_plugin/pom.xml index b2c6e368a..a5d8c23ae 100644 --- a/eclipse_plugin/pom.xml +++ b/eclipse_plugin/pom.xml @@ -22,7 +22,7 @@ com.google.googlejavaformat google-java-format-eclipse-plugin eclipse-plugin - 1.13.0 + 1.31.0 Google Java Format Plugin for Eclipse 4.5+ diff --git a/eclipse_plugin/src/com/google/googlejavaformat/java/AospJavaFormatter.java b/eclipse_plugin/src/com/google/googlejavaformat/java/AospJavaFormatter.java new file mode 100644 index 000000000..ac501fcb6 --- /dev/null +++ b/eclipse_plugin/src/com/google/googlejavaformat/java/AospJavaFormatter.java @@ -0,0 +1,22 @@ +/* + * Copyright 2025 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; + +/** Runs the Google Java formatter on the given code. */ +public class AospJavaFormatter extends JavaFormatterBase { + public AospJavaFormatter() { + super(JavaFormatterOptions.builder().style(JavaFormatterOptions.Style.AOSP).build()); + } +} diff --git a/eclipse_plugin/src/com/google/googlejavaformat/java/GoogleJavaFormatter.java b/eclipse_plugin/src/com/google/googlejavaformat/java/GoogleJavaFormatter.java index c300514be..5402b5f8b 100644 --- a/eclipse_plugin/src/com/google/googlejavaformat/java/GoogleJavaFormatter.java +++ b/eclipse_plugin/src/com/google/googlejavaformat/java/GoogleJavaFormatter.java @@ -14,131 +14,9 @@ package com.google.googlejavaformat.java; -import com.google.common.base.Preconditions; -import com.google.common.collect.Range; -import com.google.googlejavaformat.java.SnippetFormatter.SnippetKind; -import java.util.ArrayList; -import java.util.List; -import org.eclipse.jdt.core.dom.ASTParser; -import org.eclipse.jdt.core.formatter.CodeFormatter; -import org.eclipse.jface.text.IRegion; -import org.eclipse.jface.text.Region; -import org.eclipse.text.edits.MultiTextEdit; -import org.eclipse.text.edits.ReplaceEdit; -import org.eclipse.text.edits.TextEdit; - /** Runs the Google Java formatter on the given code. */ -public class GoogleJavaFormatter extends CodeFormatter { - - private static final int INDENTATION_SIZE = 2; - - @Override - public TextEdit format( - int kind, String source, int offset, int length, int indentationLevel, String lineSeparator) { - IRegion[] regions = new IRegion[] {new Region(offset, length)}; - return formatInternal(kind, source, regions, indentationLevel); - } - - @Override - public TextEdit format( - int kind, String source, IRegion[] regions, int indentationLevel, String lineSeparator) { - return formatInternal(kind, source, regions, indentationLevel); - } - - @Override - public String createIndentationString(int indentationLevel) { - Preconditions.checkArgument( - indentationLevel >= 0, - "Indentation level cannot be less than zero. Given: %s", - indentationLevel); - int spaces = indentationLevel * INDENTATION_SIZE; - StringBuilder buf = new StringBuilder(spaces); - for (int i = 0; i < spaces; i++) { - buf.append(' '); - } - return buf.toString(); - } - - /** Runs the Google Java formatter on the given source, with only the given ranges specified. */ - private TextEdit formatInternal(int kind, String source, IRegion[] regions, int initialIndent) { - try { - boolean includeComments = - (kind & CodeFormatter.F_INCLUDE_COMMENTS) == CodeFormatter.F_INCLUDE_COMMENTS; - kind &= ~CodeFormatter.F_INCLUDE_COMMENTS; - SnippetKind snippetKind; - switch (kind) { - case ASTParser.K_EXPRESSION: - snippetKind = SnippetKind.EXPRESSION; - break; - case ASTParser.K_STATEMENTS: - snippetKind = SnippetKind.STATEMENTS; - break; - case ASTParser.K_CLASS_BODY_DECLARATIONS: - snippetKind = SnippetKind.CLASS_BODY_DECLARATIONS; - break; - case ASTParser.K_COMPILATION_UNIT: - snippetKind = SnippetKind.COMPILATION_UNIT; - break; - default: - throw new IllegalArgumentException(String.format("Unknown snippet kind: %d", kind)); - } - List replacements = - new SnippetFormatter() - .format( - snippetKind, source, rangesFromRegions(regions), initialIndent, includeComments); - if (idempotent(source, regions, replacements)) { - // Do not create edits if there's no diff. - return null; - } - // Convert replacements to text edits. - return editFromReplacements(replacements); - } catch (IllegalArgumentException | FormatterException exception) { - // Do not format on errors. - return null; - } - } - - private List> rangesFromRegions(IRegion[] regions) { - List> ranges = new ArrayList<>(); - for (IRegion region : regions) { - ranges.add(Range.closedOpen(region.getOffset(), region.getOffset() + region.getLength())); - } - return ranges; - } - - /** @return {@code true} if input and output texts are equal, else {@code false}. */ - private boolean idempotent(String source, IRegion[] regions, List replacements) { - // This implementation only checks for single replacement. - if (replacements.size() == 1) { - Replacement replacement = replacements.get(0); - String output = replacement.getReplacementString(); - // Entire source case: input = output, nothing changed. - if (output.equals(source)) { - return true; - } - // Single region and single replacement case: if they are equal, nothing changed. - if (regions.length == 1) { - Range range = replacement.getReplaceRange(); - String snippet = source.substring(range.lowerEndpoint(), range.upperEndpoint()); - if (output.equals(snippet)) { - return true; - } - } - } - return false; - } - - private TextEdit editFromReplacements(List replacements) { - // Split the replacements that cross line boundaries. - TextEdit edit = new MultiTextEdit(); - for (Replacement replacement : replacements) { - Range replaceRange = replacement.getReplaceRange(); - edit.addChild( - new ReplaceEdit( - replaceRange.lowerEndpoint(), - replaceRange.upperEndpoint() - replaceRange.lowerEndpoint(), - replacement.getReplacementString())); - } - return edit; +public class GoogleJavaFormatter extends JavaFormatterBase { + public GoogleJavaFormatter() { + super(JavaFormatterOptions.defaultOptions()); } } diff --git a/eclipse_plugin/src/com/google/googlejavaformat/java/JavaFormatterBase.java b/eclipse_plugin/src/com/google/googlejavaformat/java/JavaFormatterBase.java new file mode 100644 index 000000000..abeb17297 --- /dev/null +++ b/eclipse_plugin/src/com/google/googlejavaformat/java/JavaFormatterBase.java @@ -0,0 +1,151 @@ +/* + * Copyright 2025 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Range; +import com.google.googlejavaformat.java.SnippetFormatter.SnippetKind; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.formatter.CodeFormatter; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; + +/** Runs the Google Java formatter on the given code. */ +public class JavaFormatterBase extends CodeFormatter { + + private static final int INDENTATION_SIZE = 2; + private final JavaFormatterOptions formatterOptions; + + JavaFormatterBase(JavaFormatterOptions formatterOptions) { + this.formatterOptions = formatterOptions; + } + + @Override + public TextEdit format( + int kind, String source, int offset, int length, int indentationLevel, String lineSeparator) { + IRegion[] regions = new IRegion[] {new Region(offset, length)}; + return formatInternal(kind, source, regions, indentationLevel); + } + + @Override + public TextEdit format( + int kind, String source, IRegion[] regions, int indentationLevel, String lineSeparator) { + return formatInternal(kind, source, regions, indentationLevel); + } + + @Override + public String createIndentationString(int indentationLevel) { + Preconditions.checkArgument( + indentationLevel >= 0, + "Indentation level cannot be less than zero. Given: %s", + indentationLevel); + int spaces = indentationLevel * INDENTATION_SIZE; + StringBuilder buf = new StringBuilder(spaces); + for (int i = 0; i < spaces; i++) { + buf.append(' '); + } + return buf.toString(); + } + + /** Runs the Google Java formatter on the given source, with only the given ranges specified. */ + private TextEdit formatInternal(int kind, String source, IRegion[] regions, int initialIndent) { + try { + boolean includeComments = + (kind & CodeFormatter.F_INCLUDE_COMMENTS) == CodeFormatter.F_INCLUDE_COMMENTS; + kind &= ~CodeFormatter.F_INCLUDE_COMMENTS; + SnippetKind snippetKind; + switch (kind) { + case ASTParser.K_EXPRESSION: + snippetKind = SnippetKind.EXPRESSION; + break; + case ASTParser.K_STATEMENTS: + snippetKind = SnippetKind.STATEMENTS; + break; + case ASTParser.K_CLASS_BODY_DECLARATIONS: + snippetKind = SnippetKind.CLASS_BODY_DECLARATIONS; + break; + case ASTParser.K_COMPILATION_UNIT: + snippetKind = SnippetKind.COMPILATION_UNIT; + break; + default: + throw new IllegalArgumentException(String.format("Unknown snippet kind: %d", kind)); + } + List replacements = + new SnippetFormatter(formatterOptions) + .format( + snippetKind, source, rangesFromRegions(regions), initialIndent, includeComments); + if (idempotent(source, regions, replacements)) { + // Do not create edits if there's no diff. + return null; + } + // Convert replacements to text edits. + return editFromReplacements(replacements); + } catch (IllegalArgumentException | FormatterException exception) { + // Do not format on errors. + return null; + } + } + + private List> rangesFromRegions(IRegion[] regions) { + List> ranges = new ArrayList<>(); + for (IRegion region : regions) { + ranges.add(Range.closedOpen(region.getOffset(), region.getOffset() + region.getLength())); + } + return ranges; + } + + /** + * @return {@code true} if input and output texts are equal, else {@code false}. + */ + private boolean idempotent(String source, IRegion[] regions, List replacements) { + // This implementation only checks for single replacement. + if (replacements.size() == 1) { + Replacement replacement = replacements.get(0); + String output = replacement.getReplacementString(); + // Entire source case: input = output, nothing changed. + if (output.equals(source)) { + return true; + } + // Single region and single replacement case: if they are equal, nothing changed. + if (regions.length == 1) { + Range range = replacement.getReplaceRange(); + String snippet = source.substring(range.lowerEndpoint(), range.upperEndpoint()); + if (output.equals(snippet)) { + return true; + } + } + } + return false; + } + + private TextEdit editFromReplacements(List replacements) { + // Split the replacements that cross line boundaries. + TextEdit edit = new MultiTextEdit(); + for (Replacement replacement : replacements) { + Range replaceRange = replacement.getReplaceRange(); + edit.addChild( + new ReplaceEdit( + replaceRange.lowerEndpoint(), + replaceRange.upperEndpoint() - replaceRange.lowerEndpoint(), + replacement.getReplacementString())); + } + return edit; + } +} diff --git a/idea_plugin/build.gradle.kts b/idea_plugin/build.gradle.kts index c7d1d4ba1..c529963f5 100644 --- a/idea_plugin/build.gradle.kts +++ b/idea_plugin/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.intellij.platform.gradle.TestFrameworkType /* * Copyright 2017 Google Inc. All Rights Reserved. * @@ -15,33 +14,44 @@ import org.jetbrains.intellij.platform.gradle.TestFrameworkType * limitations under the License. */ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + // https://github.com/JetBrains/intellij-platform-gradle-plugin/releases plugins { - id("org.jetbrains.intellij.platform") version "2.0.1" + id("org.jetbrains.intellij.platform") version "2.10.2" + // See https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#bundled-stdlib-versions + // This version of Kotlin will crash if your Gradle daemon is running under Java 25 (even if that + // isn't the JDK you're using to compile). So make sure to update JAVA_HOME and then + // `./gradlew --stop` + kotlin("jvm") version "2.0.21" } repositories { mavenCentral() - intellijPlatform { - defaultRepositories() - } + intellijPlatform { defaultRepositories() } } // https://github.com/google/google-java-format/releases -val googleJavaFormatVersion = "1.23.0" +val googleJavaFormatVersion = "1.31.0" +val pluginPatchVersion = "0" java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } +kotlin { jvmToolchain(21) } + intellijPlatform { pluginConfiguration { name = "google-java-format" - version = "${googleJavaFormatVersion}.0" + version = "${googleJavaFormatVersion}.${pluginPatchVersion}" ideaVersion { - sinceBuild = "223" + sinceBuild = "242" untilBuild = provider { null } } } @@ -53,39 +63,35 @@ intellijPlatform { } var gjfRequiredJvmArgs = - listOf( - "--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", - ) + listOf( + "--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", + ) -tasks { - runIde { - jvmArgumentProviders += CommandLineArgumentProvider { - gjfRequiredJvmArgs - } - } -} +tasks { runIde { jvmArgumentProviders += CommandLineArgumentProvider { gjfRequiredJvmArgs } } } -tasks { - withType().configureEach { - jvmArgs(gjfRequiredJvmArgs) - } -} +tasks { withType().configureEach { jvmArgs(gjfRequiredJvmArgs) } } dependencies { intellijPlatform { - intellijIdeaCommunity("2022.3") + intellijIdeaCommunity("2024.3") bundledPlugin("com.intellij.java") - instrumentationTools() testFramework(TestFrameworkType.Plugin.Java) } implementation("com.google.googlejavaformat:google-java-format:${googleJavaFormatVersion}") // https://mvnrepository.com/artifact/junit/junit testImplementation("junit:junit:4.13.2") // https://mvnrepository.com/artifact/com.google.truth/truth - testImplementation("com.google.truth:truth:1.4.4") + testImplementation("com.google.truth:truth:1.4.5") + implementation(kotlin("stdlib-jdk8")) } diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java index 759decc0f..3a98c2eb9 100644 --- a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java @@ -199,7 +199,9 @@ private void createUIComponents() { false)); } - /** @noinspection ALL */ + /** + * @noinspection ALL + */ public JComponent $$$getRootComponent$$$() { return panel; } diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationNotifier.kt b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationNotifier.kt new file mode 100644 index 000000000..d5d4c83da --- /dev/null +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationNotifier.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.googlejavaformat.intellij + +import com.intellij.notification.Notification +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity + +private class InitialConfigurationNotifier : ProjectActivity { + + companion object { + const val NOTIFICATION_TITLE: String = "Enable google-java-format" + } + + override suspend fun execute(project: Project) { + val settings = GoogleJavaFormatSettings.getInstance(project) + + if (settings.isUninitialized) { + settings.isEnabled = false + displayNewUserNotification(project, settings) + } else if (settings.isEnabled) { + JreConfigurationChecker.checkJreConfiguration(project) + } + } + + private fun displayNewUserNotification(project: Project?, settings: GoogleJavaFormatSettings) { + val groupManager = NotificationGroupManager.getInstance() + val group = groupManager.getNotificationGroup(NOTIFICATION_TITLE) + val notification = + Notification( + group.displayId, + NOTIFICATION_TITLE, + "The google-java-format plugin is disabled by default.", + NotificationType.INFORMATION + ) + notification.addAction( + object : NotificationAction("Enable for this project") { + override fun actionPerformed( + anActionEvent: AnActionEvent, notification: Notification + ) { + settings.isEnabled = true + notification.expire() + } + }) + notification.notify(project) + } +} diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java deleted file mode 100644 index 95e13d325..000000000 --- a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.googlejavaformat.intellij; - -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationGroup; -import com.intellij.notification.NotificationGroupManager; -import com.intellij.notification.NotificationType; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.startup.StartupActivity; -import org.jetbrains.annotations.NotNull; - -final class InitialConfigurationStartupActivity implements StartupActivity.Background { - - private static final String NOTIFICATION_TITLE = "Enable google-java-format"; - - @Override - public void runActivity(@NotNull Project project) { - GoogleJavaFormatSettings settings = GoogleJavaFormatSettings.getInstance(project); - - if (settings.isUninitialized()) { - settings.setEnabled(false); - displayNewUserNotification(project, settings); - } else if (settings.isEnabled()) { - JreConfigurationChecker.checkJreConfiguration(project); - } - } - - private void displayNewUserNotification(Project project, GoogleJavaFormatSettings settings) { - NotificationGroupManager groupManager = NotificationGroupManager.getInstance(); - NotificationGroup group = groupManager.getNotificationGroup(NOTIFICATION_TITLE); - Notification notification = - new Notification( - group.getDisplayId(), - NOTIFICATION_TITLE, - "The google-java-format plugin is disabled by default. " - + "Enable for this project.", - NotificationType.INFORMATION); - notification.setListener( - (n, e) -> { - settings.setEnabled(true); - n.expire(); - }); - notification.notify(project); - } -} diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/JreConfigurationChecker.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/JreConfigurationChecker.java index 5084b6a39..0bc3572e8 100644 --- a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/JreConfigurationChecker.java +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/JreConfigurationChecker.java @@ -17,12 +17,15 @@ package com.google.googlejavaformat.intellij; import com.google.common.base.Suppliers; -import com.intellij.ide.ui.IdeUiService; +import com.intellij.ide.BrowserUtil; import com.intellij.notification.Notification; +import com.intellij.notification.NotificationAction; import com.intellij.notification.NotificationType; +import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; class JreConfigurationChecker { @@ -89,15 +92,17 @@ private void displayConfigurationErrorNotification() { new Notification( "Configure JRE for google-java-format", "Configure the JRE for google-java-format", - "The google-java-format plugin needs additional configuration before it can be used. " - + "Follow the instructions here.", + "The google-java-format plugin needs additional configuration before it can be used.", NotificationType.INFORMATION); - notification.setListener( - (n, e) -> { - IdeUiService.getInstance() - .browse( - "https://github.com/google/google-java-format/blob/master/README.md#intellij-jre-config"); - n.expire(); + notification.addAction( + new NotificationAction("Follow the instructions here") { + @Override + public void actionPerformed( + @NotNull AnActionEvent anActionEvent, @NotNull Notification notification) { + BrowserUtil.browse( + "https://github.com/google/google-java-format/blob/master/README.md#intellij-jre-config"); + notification.expire(); + } }); notification.notify(project); } diff --git a/idea_plugin/src/main/resources/META-INF/plugin.xml b/idea_plugin/src/main/resources/META-INF/plugin.xml index 8d7574539..11d7d4877 100644 --- a/idea_plugin/src/main/resources/META-INF/plugin.xml +++ b/idea_plugin/src/main/resources/META-INF/plugin.xml @@ -35,6 +35,31 @@ ]]> +

        1.31.0.0
        +
        Updated to use google-java-format 1.31.0.
        +
        1.30.0.1
        +
        Add support for 2024.2 IDE versions, which were mistakenly excluded.
        +
        1.30.0.0
        +
        Updated to use google-java-format 1.30.0.
        +
        1.29.0.1
        +
        Remove uses of deprecated IntelliJ plugin APIs.
        +
        1.29.0.0
        +
        Updated to use google-java-format 1.29.0.
        +
        Minimum supported IntelliJ version is now 2024.3.
        +
        1.28.0.0
        +
        Updated to use google-java-format 1.28.0.
        +
        1.27.0.0
        +
        Updated to use google-java-format 1.27.0.
        +
        1.26.0.0
        +
        Updated to use google-java-format 1.26.0.
        +
        1.25.2.0
        +
        Updated to use google-java-format 1.25.2.
        +
        1.25.1.0
        +
        Updated to use google-java-format 1.25.1.
        +
        1.25.0.0
        +
        Updated to use google-java-format 1.25.0.
        +
        1.24.0.0
        +
        Updated to use google-java-format 1.24.0.
        1.23.0.0
        Updated to use google-java-format 1.23.0.
        Fix crashes in IntelliJ 2024.2 (Thanks, @nrayburn-tech!)
        @@ -96,7 +121,7 @@ - + 32.1.3-jre 1.4.0 1.0.0 - 2.28.0 + 2.45.0 1.9 1.0.1 3.6.3 3.2.1 + 0.8.0 @@ -196,6 +197,7 @@ -XDcompilePolicy=simple + --should-stop=ifError=FLOW -Xplugin:ErrorProne --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED @@ -300,14 +302,8 @@ sonatype-nexus-snapshots - Sonatype Nexus Snapshots - https://oss.sonatype.org/content/repositories/snapshots/ + https://central.sonatype.com/repository/maven-snapshots/ - - sonatype-nexus-staging - Nexus Release Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - @@ -315,6 +311,15 @@ sonatype-oss-release + + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} + true + + central + + org.apache.maven.plugins maven-source-plugin