diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index daec3189..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "maven" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8a010f0..817cc758 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,9 @@ jobs: - os: ubuntu-latest java: 20-ea experimental: true + - os: ubuntu-latest + java: 21-ea + experimental: true runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} steps: diff --git a/core/pom.xml b/core/pom.xml index 3f84c174..9c414c92 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -104,13 +104,12 @@ https://docs.oracle.com/en/java/javase/11/docs/api + --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED,com.google.googlejavaformat + --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED,com.google.googlejavaformat --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED,com.google.googlejavaformat - --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED,com.google.googlejavaformat --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED,com.google.googlejavaformat --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED,com.google.googlejavaformat --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED,com.google.googlejavaformat - --add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED,com.google.googlejavaformat - --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED,com.google.googlejavaformat diff --git a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java index db431c04..a45e83b9 100644 --- a/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java +++ b/core/src/main/java/com/google/googlejavaformat/OpsBuilder.java @@ -159,7 +159,7 @@ public BlankLineWanted merge(BlankLineWanted other) { int depth = 0; /** Add an {@link Op}, and record open/close ops for later validation of unclosed levels. */ - private void add(Op op) { + public final void add(Op op) { if (op instanceof OpenOp) { depth++; } else if (op instanceof CloseOp) { diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java index 26b26f96..870c7147 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java +++ b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java @@ -14,40 +14,73 @@ package com.google.googlejavaformat.java; +import com.google.auto.value.AutoValue; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeSet; +import java.nio.file.Path; import java.util.concurrent.Callable; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Encapsulates information about a file to be formatted, including which parts of the file to * format. */ -class FormatFileCallable implements Callable { +class FormatFileCallable implements Callable { + + @AutoValue + abstract static class Result { + abstract @Nullable Path path(); + + abstract String input(); + + abstract @Nullable String output(); + + boolean changed() { + return !input().equals(output()); + } + + abstract @Nullable FormatterException exception(); + + static Result create( + @Nullable Path path, + String input, + @Nullable String output, + @Nullable FormatterException exception) { + return new AutoValue_FormatFileCallable_Result(path, input, output, exception); + } + } + + private final Path path; private final String input; private final CommandLineOptions parameters; private final JavaFormatterOptions options; public FormatFileCallable( - CommandLineOptions parameters, String input, JavaFormatterOptions options) { + CommandLineOptions parameters, Path path, String input, JavaFormatterOptions options) { + this.path = path; this.input = input; this.parameters = parameters; this.options = options; } @Override - public String call() throws FormatterException { - if (parameters.fixImportsOnly()) { - return fixImports(input); - } + public Result call() { + try { + if (parameters.fixImportsOnly()) { + return Result.create(path, input, fixImports(input), /* exception= */ null); + } - Formatter formatter = new Formatter(options); - String formatted = formatter.formatSource(input, characterRanges(input).asRanges()); - formatted = fixImports(formatted); - if (parameters.reflowLongStrings()) { - formatted = StringWrapper.wrap(options.maxLineLength(), formatted, formatter); + Formatter formatter = new Formatter(options); + String formatted = formatter.formatSource(input, characterRanges(input).asRanges()); + formatted = fixImports(formatted); + if (parameters.reflowLongStrings()) { + formatted = StringWrapper.wrap(options.style().maxLineLength(), formatted, formatter); + } + return Result.create(path, input, formatted, /* exception= */ null); + } catch (FormatterException e) { + return Result.create(path, input, /* output= */ null, e); } - return formatted; } private String fixImports(String input) throws FormatterException { diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java index ca5477f7..10fe6c7e 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java @@ -169,7 +169,7 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept builder.drain(); Doc doc = new DocBuilder().withOps(builder.build()).build(); doc.computeBreaks( - javaOutput.getCommentsHelper(), options.maxLineLength(), new Doc.State(+0, 0)); + javaOutput.getCommentsHelper(), options.style().maxLineLength(), new Doc.State(+0, 0)); doc.write(javaOutput); javaOutput.flush(); } @@ -263,7 +263,9 @@ public ImmutableList getFormatReplacements( // TODO(cushon): this is only safe because the modifier ordering doesn't affect whitespace, // and doesn't change the replacements that are output. This is not true in general for // 'de-linting' changes (e.g. import ordering). - javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges); + if (options.reorderModifiers()) { + javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges); + } String lineSeparator = Newlines.guessLineSeparator(input); JavaOutput javaOutput = diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java index da789c1b..cb1495f3 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaCommentsHelper.java @@ -123,8 +123,8 @@ private List wrapLineComments(List lines, int column0) { result.add(line); continue; } - while (line.length() + column0 > options.maxLineLength()) { - int idx = options.maxLineLength() - column0; + while (line.length() + column0 > options.style().maxLineLength()) { + int idx = options.style().maxLineLength() - column0; // only break on whitespace characters, and ignore the leading `// ` while (idx >= 2 && !CharMatcher.whitespace().matches(line.charAt(idx))) { idx--; diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java index b070793f..12d624d5 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaFormatterOptions.java @@ -14,6 +14,7 @@ package com.google.googlejavaformat.java; +import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.Immutable; /** @@ -27,7 +28,8 @@ * preferences, and in fact it would work directly against our primary goals. */ @Immutable -public class JavaFormatterOptions { +@AutoValue +public abstract class JavaFormatterOptions { static final int DEFAULT_MAX_LINE_LENGTH = 100; @@ -57,7 +59,7 @@ int indentationMultiplier() { return indentationMultiplier; } - int maxLineLength() { + public int maxLineLength() { return maxLineLength; } @@ -66,39 +68,24 @@ boolean alwaysStackFieldAnnotations() { } } - private final Style style; - private final boolean formatJavadoc; - - private JavaFormatterOptions(Style style, boolean formatJavadoc) { - this.style = style; - this.formatJavadoc = formatJavadoc; - } - - /** Returns the maximum formatted width */ - public int maxLineLength() { - return style.maxLineLength(); - } - /** Returns the multiplier for the unit of indent. */ public int indentationMultiplier() { - return style.indentationMultiplier(); + return style().indentationMultiplier(); } - public boolean formatJavadoc() { - return formatJavadoc; - } + public abstract boolean formatJavadoc(); + + public abstract boolean reorderModifiers(); /** Returns the code style. */ - public Style style() { - return style; - } + public abstract Style style(); /** * Returns true if field annotations should be unconditionally stacked. By default, annotation * direction is contextual. */ public boolean alwaysStackFieldAnnotations() { - return style.alwaysStackFieldAnnotations(); + return style().alwaysStackFieldAnnotations(); } /** Returns the default formatting options. */ @@ -108,28 +95,22 @@ public static JavaFormatterOptions defaultOptions() { /** Returns a builder for {@link JavaFormatterOptions}. */ public static Builder builder() { - return new Builder(); + return new AutoValue_JavaFormatterOptions.Builder() + .style(Style.GOOGLE) + .formatJavadoc(true) + .reorderModifiers(true); } /** A builder for {@link JavaFormatterOptions}. */ - public static class Builder { - private Style style = Style.GOOGLE; - private boolean formatJavadoc = true; + @AutoValue.Builder + public abstract static class Builder { - private Builder() {} + public abstract Builder style(Style style); - public Builder style(Style style) { - this.style = style; - return this; - } + public abstract Builder formatJavadoc(boolean formatJavadoc); - public Builder formatJavadoc(boolean formatJavadoc) { - this.formatJavadoc = formatJavadoc; - return this; - } + public abstract Builder reorderModifiers(boolean reorderModifiers); - public JavaFormatterOptions build() { - return new JavaFormatterOptions(style, formatJavadoc); - } + public abstract JavaFormatterOptions build(); } } 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 73e040d1..e3165051 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java @@ -285,7 +285,10 @@ private static ImmutableSetMultimap typeAnnotations() { ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder(); for (String annotation : ImmutableList.of( + "org.jspecify.annotations.NonNull", + "org.jspecify.annotations.Nullable", "org.jspecify.nullness.Nullable", + "org.checkerframework.checker.nullness.qual.NonNull", "org.checkerframework.checker.nullness.qual.Nullable")) { String simpleName = annotation.substring(annotation.lastIndexOf('.') + 1); result.put(simpleName, annotation); 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 c43a91ad..656b65c8 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java @@ -111,7 +111,7 @@ public void append(String text, Range range) { * there's a blank line here and it's a comment. */ BlankLineWanted wanted = blankLines.getOrDefault(lastK, BlankLineWanted.NO); - if (isComment(text) ? sawNewlines : wanted.wanted().orElse(sawNewlines)) { + if ((sawNewlines && isComment(text)) || wanted.wanted().orElse(sawNewlines)) { ++newlinesPending; } } 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 53ab8d64..6ccb77bc 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Main.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Main.java @@ -16,8 +16,10 @@ import static java.lang.Math.min; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Comparator.comparing; import com.google.common.io.ByteStreams; +import com.google.common.util.concurrent.MoreExecutors; import com.google.googlejavaformat.FormatterDiagnostic; import java.io.IOError; import java.io.IOException; @@ -27,13 +29,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.Collections; +import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; /** The main class for the Java formatter CLI. */ public final class Main { @@ -41,10 +45,8 @@ public final class Main { private static final String STDIN_FILENAME = ""; static final String versionString() { - return "sg-java-format: Version " - + GoogleJavaFormatVersion.version() - + ", forked from\ngoogle-java-format: Version " - + GoogleJavaFormatVersion.googleVersion(); + return "sg-java-format: Version 5" + + ", forked from\ngoogle-java-format: Version v1.16.0"; } private final PrintWriter outWriter; @@ -81,7 +83,8 @@ static int main(PrintWriter out, PrintWriter err, String... args) { return formatter.format(args); } catch (UsageException e) { err.print(e.getMessage()); - return 0; + // We return exit code 2 to differentiate usage issues from code formatting issues. + return 2; } finally { err.flush(); out.flush(); @@ -122,50 +125,55 @@ private int formatFiles(CommandLineOptions parameters, JavaFormatterOptions opti int numThreads = min(MAX_THREADS, parameters.files().size()); ExecutorService executorService = Executors.newFixedThreadPool(numThreads); - Map inputs = new LinkedHashMap<>(); - Map> results = new LinkedHashMap<>(); + ExecutorCompletionService cs = + new ExecutorCompletionService<>(executorService); boolean allOk = true; + int files = 0; for (String fileName : parameters.files()) { if (!fileName.endsWith(".java")) { errWriter.println("Skipping non-Java file: " + fileName); continue; } Path path = Paths.get(fileName); - String input; try { - input = new String(Files.readAllBytes(path), UTF_8); - inputs.put(path, input); - results.put( - path, executorService.submit(new FormatFileCallable(parameters, input, options))); + String input = new String(Files.readAllBytes(path), UTF_8); + cs.submit(new FormatFileCallable(parameters, path, input, options)); + files++; } catch (IOException e) { errWriter.println(fileName + ": could not read file: " + e.getMessage()); allOk = false; } } - for (Map.Entry> result : results.entrySet()) { - Path path = result.getKey(); - String formatted; + List results = new ArrayList<>(); + while (files > 0) { try { - formatted = result.getValue().get(); + files--; + results.add(cs.take().get()); } catch (InterruptedException e) { errWriter.println(e.getMessage()); allOk = false; continue; } catch (ExecutionException e) { - if (e.getCause() instanceof FormatterException) { - for (FormatterDiagnostic diagnostic : ((FormatterException) e.getCause()).diagnostics()) { - errWriter.println(path + ":" + diagnostic); - } - } else { - errWriter.println(path + ": error: " + e.getCause().getMessage()); - e.getCause().printStackTrace(errWriter); + errWriter.println("error: " + e.getCause().getMessage()); + e.getCause().printStackTrace(errWriter); + allOk = false; + continue; + } + } + Collections.sort(results, comparing(FormatFileCallable.Result::path)); + for (FormatFileCallable.Result result : results) { + Path path = result.path(); + if (result.exception() != null) { + for (FormatterDiagnostic diagnostic : result.exception().diagnostics()) { + errWriter.println(path + ":" + diagnostic); } allOk = false; continue; } - boolean changed = !formatted.equals(inputs.get(path)); + String formatted = result.output(); + boolean changed = result.changed(); if (changed && parameters.setExitIfChanged()) { allOk = false; } @@ -188,6 +196,10 @@ private int formatFiles(CommandLineOptions parameters, JavaFormatterOptions opti outWriter.write(formatted); } } + if (!MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(5))) { + errWriter.println("Failed to shut down ExecutorService"); + allOk = false; + } return allOk ? 0 : 1; } @@ -200,9 +212,16 @@ private int formatStdin(CommandLineOptions parameters, JavaFormatterOptions opti } String stdinFilename = parameters.assumeFilename().orElse(STDIN_FILENAME); boolean ok = true; - try { - String output = new FormatFileCallable(parameters, input, options).call(); - boolean changed = !input.equals(output); + FormatFileCallable.Result result = + new FormatFileCallable(parameters, null, input, options).call(); + if (result.exception() != null) { + for (FormatterDiagnostic diagnostic : result.exception().diagnostics()) { + errWriter.println(stdinFilename + ":" + diagnostic); + } + ok = false; + } else { + String output = result.output(); + boolean changed = result.changed(); if (changed && parameters.setExitIfChanged()) { ok = false; } @@ -213,12 +232,6 @@ private int formatStdin(CommandLineOptions parameters, JavaFormatterOptions opti } else { outWriter.write(output); } - } catch (FormatterException e) { - for (FormatterDiagnostic diagnostic : e.diagnostics()) { - errWriter.println(stdinFilename + ":" + diagnostic); - } - ok = false; - // TODO(cpovirk): Catch other types of exception (as we do in the formatFiles case). } return ok ? 0 : 1; } diff --git a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java index ecb30eed..a0fc2f54 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java +++ b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java @@ -49,7 +49,6 @@ import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.tree.JCTree.JCFieldAccess; -import com.sun.tools.javac.tree.JCTree.JCIdent; import com.sun.tools.javac.tree.JCTree.JCImport; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.Log; @@ -293,9 +292,7 @@ private static RangeMap buildReplacements( } private static String getSimpleName(JCImport importTree) { - return importTree.getQualifiedIdentifier() instanceof JCIdent - ? ((JCIdent) importTree.getQualifiedIdentifier()).getName().toString() - : ((JCFieldAccess) importTree.getQualifiedIdentifier()).getIdentifier().toString(); + return getQualifiedIdentifier(importTree).getIdentifier().toString(); } private static boolean isUnused( @@ -304,18 +301,15 @@ private static boolean isUnused( Multimap> usedInJavadoc, JCImport importTree, String simpleName) { - String qualifier = - ((JCFieldAccess) importTree.getQualifiedIdentifier()).getExpression().toString(); + JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier(importTree); + String qualifier = qualifiedIdentifier.getExpression().toString(); if (qualifier.equals("java.lang")) { return true; } if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) { return true; } - if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess - && ((JCFieldAccess) importTree.getQualifiedIdentifier()) - .getIdentifier() - .contentEquals("*")) { + if (qualifiedIdentifier.getIdentifier().contentEquals("*")) { return false; } @@ -328,6 +322,15 @@ private static boolean isUnused( return true; } + private static JCFieldAccess getQualifiedIdentifier(JCImport 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); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + /** Applies the replacements to the given source, and re-format any edited javadoc. */ private static String applyReplacements(String source, RangeMap replacements) { // save non-empty fixed ranges for reformatting after fixes are applied 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 e74de285..0f45a9d9 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 @@ -374,7 +374,7 @@ private void writeNewline(AutoIndent autoIndent) { appendSpaces(blockIndent + 1); output.append("*"); appendSpaces(1); - remainingOnLine = options.maxLineLength() - blockIndent - 3; + remainingOnLine = options.style().maxLineLength() - blockIndent - 3; if (autoIndent == AUTO_INDENT) { appendSpaces(innerIndent()); remainingOnLine -= innerIndent(); diff --git a/core/src/test/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProviderTest.java b/core/src/test/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProviderTest.java index 21de9d74..66767df6 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProviderTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/GoogleJavaFormatToolProviderTest.java @@ -46,7 +46,7 @@ public void testUsageOutputAfterLoadingViaToolName() { int result = format.run(new PrintWriter(out, true), new PrintWriter(err, true), "--help"); - assertThat(result).isEqualTo(0); + assertThat(result).isEqualTo(2); String usage = err.toString(); 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 ccac3861..7420ac4b 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java @@ -54,13 +54,12 @@ public class MainTest { private static final ImmutableList ADD_EXPORTS = ImmutableList.of( + "--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.main=ALL-UNNAMED", "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"); + "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"); @Test public void testUsageOutput() { @@ -134,7 +133,7 @@ public void testMain() throws Exception { process.waitFor(); String err = new String(ByteStreams.toByteArray(process.getErrorStream()), UTF_8); assertThat(err).contains("Usage: sg-java-format"); - assertThat(process.exitValue()).isEqualTo(0); + assertThat(process.exitValue()).isEqualTo(2); } // end to end javadoc formatting test @@ -615,4 +614,27 @@ public void noFormatJavadoc() throws Exception { assertThat(main.format("--skip-javadoc-formatting", "-")).isEqualTo(0); assertThat(out.toString()).isEqualTo(joiner.join(input)); } + + @Test + public void reorderModifiersOptionTest() throws Exception { + String[] input = { + "class Test {", // + " static public void main(String... args) {}", + "}", + "", + }; + String[] fixed = { + "class Test {", // + " public static void main(String... args) {}", + "}", + "", + }; + String source = joiner.join(input); + assertThat(new Formatter(JavaFormatterOptions.builder().build()).formatSource(source)) + .isEqualTo(joiner.join(fixed)); + assertThat( + new Formatter(JavaFormatterOptions.builder().reorderModifiers(false).build()) + .formatSource(source)) + .isEqualTo(source); + } } diff --git a/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java b/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java index b1142b3b..1750049d 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/PartialFormattingTest.java @@ -679,6 +679,7 @@ public void noTokensOnLine() throws Exception { String input = lines( " package com.google.googlejavaformat.java;", + "", "/*", " * Copyright 2015 Google Inc.", " *", diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input index 7317f17b..86e46d50 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.input @@ -1,6 +1,6 @@ public class B20844369 { private static final String ID_PATTERN = - // TODO(user): add min/max lengths for the numbers here, e.g. android ID + // TODO(daw): add min/max lengths for the numbers here, e.g. android ID "(?:(?\\d+)\\+)?" // optional Android ID + "(?\\d+)" // type + ":" diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output index 62f9721b..982dc2b5 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B20844369.output @@ -1,6 +1,6 @@ public class B20844369 { private static final String ID_PATTERN = - // TODO(user): add min/max lengths for the numbers here, e.g. android ID + // TODO(daw): add min/max lengths for the numbers here, e.g. android ID "(?:(?\\d+)\\+)?" // optional Android ID + "(?\\d+)" // type + ":" diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.output index b1e33e90..3d591f8f 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.output +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/B27246427.output @@ -6,6 +6,7 @@ enum TrailingComment { /** a */ Object a; + /** b */ Object b; } diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.input b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.input index 9dc34266..d012d17a 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.input +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.input @@ -1,5 +1,12 @@ class Fields { + int a = 1; + int b = 1; + + int c = 1; + /** Javadoc */ + int d = 1; + int x = 1; int y = 1; diff --git a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.output b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.output index 376e8b62..81ebd2ac 100644 --- a/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.output +++ b/core/src/test/resources/com/google/googlejavaformat/java/testdata/Fields.output @@ -1,5 +1,13 @@ class Fields { + int a = 1; + int b = 1; + + int c = 1; + + /** Javadoc */ + int d = 1; + int x = 1; int y = 1; diff --git a/google-README.md b/google-README.md index 2d53f8ae..4fe0e212 100644 --- a/google-README.md +++ b/google-README.md @@ -44,14 +44,25 @@ presented when you first open a project offering to do this for you.) To enable it by default in new projects, use `File→Other Settings→Default Settings...`. -When enabled, it will replace the normal `Reformat Code` action, which can be -triggered from the `Code` menu or with the Ctrl-Alt-L (by default) keyboard -shortcut. +When enabled, it will replace the normal `Reformat Code` and `Optimize Imports` +actions. -The import ordering is not handled by this plugin, unfortunately. To fix the -import order, download the -[IntelliJ Java Google Style file](https://raw.githubusercontent.com/google/styleguide/gh-pages/intellij-java-google-style.xml) -and import it into File→Settings→Editor→Code Style. +#### IntelliJ JRE Config + +The google-java-format plugin uses some internal classes that aren't available +without extra configuration. To use the plugin, go to `Help→Edit Custom VM +Options...` and paste in these lines: + +``` +--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +``` + +Once you've done that, restart the IDE. ### Eclipse @@ -94,11 +105,12 @@ following JVM flags are required when running on JDK 16 and newer, due to [JEP 396: Strongly Encapsulate JDK Internals by Default](https://openjdk.java.net/jeps/396): ``` ---add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED ---add-exports jdk.compiler/com.sun.tools.javac.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 +--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED ``` #### Maven diff --git a/idea_plugin/build.gradle b/idea_plugin/build.gradle deleted file mode 100644 index 294d77e7..00000000 --- a/idea_plugin/build.gradle +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2017 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. - */ - -plugins { - id "org.jetbrains.intellij" version "1.4.0" -} - -repositories { - mavenCentral() -} - -ext { - googleJavaFormatVersion = "1.15.0" -} - -apply plugin: "org.jetbrains.intellij" -apply plugin: "java" - -sourceCompatibility = JavaVersion.VERSION_11 -targetCompatibility = JavaVersion.VERSION_11 - -intellij { - pluginName = "google-java-format" - plugins = ["java"] - version = "221.3427-EAP-CANDIDATE-SNAPSHOT" -} - -patchPluginXml { - pluginDescription = "Formats source code using the google-java-format tool. This version of " + - "the plugin uses version ${googleJavaFormatVersion} of the tool." - version.set("${googleJavaFormatVersion}.0") - sinceBuild = "203" - untilBuild = "" -} - -publishPlugin { - token = project.ext.properties.jetbrainsPluginRepoToken -} - -sourceSets { - main { - java.srcDir "src" - resources.srcDir "resources" - } -} - -dependencies { - implementation "com.google.googlejavaformat:google-java-format:${googleJavaFormatVersion}" -} diff --git a/idea_plugin/build.gradle.kts b/idea_plugin/build.gradle.kts new file mode 100644 index 00000000..474c24d7 --- /dev/null +++ b/idea_plugin/build.gradle.kts @@ -0,0 +1,66 @@ +/* + * Copyright 2017 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. + */ + +plugins { id("org.jetbrains.intellij") version "1.13.3" } + +apply(plugin = "org.jetbrains.intellij") + +apply(plugin = "java") + +repositories { mavenCentral() } + +val googleJavaFormatVersion = "1.16.0" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +intellij { + pluginName.set("google-java-format") + plugins.set(listOf("java")) + version.set("2021.3") +} + +tasks { + patchPluginXml { + version.set("${googleJavaFormatVersion}.2") + sinceBuild.set("213") + untilBuild.set("") + } + + publishPlugin { + val jetbrainsPluginRepoToken: String by project + token.set(jetbrainsPluginRepoToken) + } + + withType().configureEach { + jvmArgs( + "--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", + ) + } +} + +dependencies { + implementation("com.google.googlejavaformat:google-java-format:${googleJavaFormatVersion}") + testImplementation("junit:junit:4.13.2") + testImplementation("com.google.truth:truth:1.1.3") +} diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java b/idea_plugin/src/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java deleted file mode 100644 index af5da957..00000000 --- a/idea_plugin/src/com/google/googlejavaformat/intellij/CodeStyleManagerDecorator.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright 2015 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.formatting.FormattingMode; -import com.intellij.lang.ASTNode; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.fileTypes.FileType; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.codeStyle.ChangedRangesInfo; -import com.intellij.psi.codeStyle.CodeStyleManager; -import com.intellij.psi.codeStyle.DocCommentSettings; -import com.intellij.psi.codeStyle.FormattingModeAwareIndentAdjuster; -import com.intellij.psi.codeStyle.Indent; -import com.intellij.util.IncorrectOperationException; -import com.intellij.util.ThrowableRunnable; -import java.util.Collection; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.jetbrains.annotations.NotNull; - -/** - * Decorates the {@link CodeStyleManager} abstract class by delegating to a concrete implementation - * instance (likely IntelliJ's default instance). - */ -@SuppressWarnings("deprecation") -class CodeStyleManagerDecorator extends CodeStyleManager - implements FormattingModeAwareIndentAdjuster { - - private final CodeStyleManager delegate; - - CodeStyleManagerDecorator(CodeStyleManager delegate) { - this.delegate = delegate; - } - - CodeStyleManager getDelegate() { - return delegate; - } - - @Override - public @NotNull Project getProject() { - return delegate.getProject(); - } - - @Override - public @NotNull PsiElement reformat(@NotNull PsiElement element) - throws IncorrectOperationException { - return delegate.reformat(element); - } - - @Override - public @NotNull PsiElement reformat(@NotNull PsiElement element, boolean canChangeWhiteSpacesOnly) - throws IncorrectOperationException { - return delegate.reformat(element, canChangeWhiteSpacesOnly); - } - - @Override - public PsiElement reformatRange(@NotNull PsiElement element, int startOffset, int endOffset) - throws IncorrectOperationException { - return delegate.reformatRange(element, startOffset, endOffset); - } - - @Override - public PsiElement reformatRange( - @NotNull PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly) - throws IncorrectOperationException { - return delegate.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly); - } - - @Override - public void reformatText(@NotNull PsiFile file, int startOffset, int endOffset) - throws IncorrectOperationException { - delegate.reformatText(file, startOffset, endOffset); - } - - @Override - public void reformatText(@NotNull PsiFile file, @NotNull Collection ranges) - throws IncorrectOperationException { - delegate.reformatText(file, ranges); - } - - @Override - public void reformatTextWithContext( - @NotNull PsiFile psiFile, @NotNull ChangedRangesInfo changedRangesInfo) - throws IncorrectOperationException { - delegate.reformatTextWithContext(psiFile, changedRangesInfo); - } - - @Override - public void reformatTextWithContext( - @NotNull PsiFile file, @NotNull Collection ranges) - throws IncorrectOperationException { - delegate.reformatTextWithContext(file, ranges); - } - - @Override - public void adjustLineIndent(@NotNull PsiFile file, TextRange rangeToAdjust) - throws IncorrectOperationException { - delegate.adjustLineIndent(file, rangeToAdjust); - } - - @Override - public int adjustLineIndent(@NotNull PsiFile file, int offset) - throws IncorrectOperationException { - return delegate.adjustLineIndent(file, offset); - } - - @Override - public int adjustLineIndent(@NotNull Document document, int offset) { - return delegate.adjustLineIndent(document, offset); - } - - public void scheduleIndentAdjustment(@NotNull Document document, int offset) { - delegate.scheduleIndentAdjustment(document, offset); - } - - @Override - public boolean isLineToBeIndented(@NotNull PsiFile file, int offset) { - return delegate.isLineToBeIndented(file, offset); - } - - @Override - @Nullable - public String getLineIndent(@NotNull PsiFile file, int offset) { - return delegate.getLineIndent(file, offset); - } - - @Override - @Nullable - public String getLineIndent(@NotNull PsiFile file, int offset, FormattingMode mode) { - return delegate.getLineIndent(file, offset, mode); - } - - @Override - @Nullable - public String getLineIndent(@NotNull Document document, int offset) { - return delegate.getLineIndent(document, offset); - } - - @Override - public Indent getIndent(String text, FileType fileType) { - return delegate.getIndent(text, fileType); - } - - @Override - public String fillIndent(Indent indent, FileType fileType) { - return delegate.fillIndent(indent, fileType); - } - - @Override - public Indent zeroIndent() { - return delegate.zeroIndent(); - } - - @Override - public void reformatNewlyAddedElement(@NotNull ASTNode block, @NotNull ASTNode addedElement) - throws IncorrectOperationException { - delegate.reformatNewlyAddedElement(block, addedElement); - } - - @Override - public boolean isSequentialProcessingAllowed() { - return delegate.isSequentialProcessingAllowed(); - } - - @Override - public void performActionWithFormatterDisabled(Runnable r) { - delegate.performActionWithFormatterDisabled(r); - } - - @Override - public void performActionWithFormatterDisabled(ThrowableRunnable r) - throws T { - delegate.performActionWithFormatterDisabled(r); - } - - @Override - public T performActionWithFormatterDisabled(Computable r) { - return delegate.performActionWithFormatterDisabled(r); - } - - @Override - public int getSpacing(@NotNull PsiFile file, int offset) { - return delegate.getSpacing(file, offset); - } - - @Override - public int getMinLineFeeds(@NotNull PsiFile file, int offset) { - return delegate.getMinLineFeeds(file, offset); - } - - @Override - public void runWithDocCommentFormattingDisabled( - @NotNull PsiFile file, @NotNull Runnable runnable) { - delegate.runWithDocCommentFormattingDisabled(file, runnable); - } - - @Override - public @NotNull DocCommentSettings getDocCommentSettings(@NotNull PsiFile file) { - return delegate.getDocCommentSettings(file); - } - - // From FormattingModeAwareIndentAdjuster - - /** Uses same fallback as {@link CodeStyleManager#getCurrentFormattingMode}. */ - @Override - public FormattingMode getCurrentFormattingMode() { - if (delegate instanceof FormattingModeAwareIndentAdjuster) { - return ((FormattingModeAwareIndentAdjuster) delegate).getCurrentFormattingMode(); - } - return FormattingMode.REFORMAT; - } - - @Override - public int adjustLineIndent( - final @NotNull Document document, final int offset, FormattingMode mode) - throws IncorrectOperationException { - if (delegate instanceof FormattingModeAwareIndentAdjuster) { - return ((FormattingModeAwareIndentAdjuster) delegate) - .adjustLineIndent(document, offset, mode); - } - return offset; - } - - @Override - public void scheduleReformatWhenSettingsComputed(@NotNull PsiFile file) { - delegate.scheduleReformatWhenSettingsComputed(file); - } -} diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java b/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java deleted file mode 100644 index a5e69c93..00000000 --- a/idea_plugin/src/com/google/googlejavaformat/intellij/FormatterUtil.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2015 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 static com.google.common.base.Preconditions.checkState; - -import com.google.common.collect.BoundType; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Range; -import com.google.googlejavaformat.java.Formatter; -import com.google.googlejavaformat.java.FormatterException; -import com.intellij.openapi.util.TextRange; -import java.util.Collection; -import java.util.Map; -import java.util.stream.Collectors; - -final class FormatterUtil { - - private FormatterUtil() {} - - static Map getReplacements( - Formatter formatter, String text, Collection ranges) { - try { - ImmutableMap.Builder replacements = ImmutableMap.builder(); - formatter - .getFormatReplacements(text, toRanges(ranges)) - .forEach( - replacement -> - replacements.put( - toTextRange(replacement.getReplaceRange()), - replacement.getReplacementString())); - return replacements.build(); - } catch (FormatterException e) { - return ImmutableMap.of(); - } - } - - private static Collection> toRanges(Collection textRanges) { - return textRanges.stream() - .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset())) - .collect(Collectors.toList()); - } - - private static TextRange toTextRange(Range range) { - checkState( - range.lowerBoundType().equals(BoundType.CLOSED) - && range.upperBoundType().equals(BoundType.OPEN)); - return new TextRange(range.lowerEndpoint(), range.upperEndpoint()); - } -} diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java deleted file mode 100644 index 3d56743e..00000000 --- a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatCodeStyleManager.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2015 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 static java.util.Comparator.comparing; - -import com.google.common.collect.ImmutableList; -import com.google.googlejavaformat.java.Formatter; -import com.google.googlejavaformat.java.JavaFormatterOptions; -import com.google.googlejavaformat.java.JavaFormatterOptions.Style; -import com.intellij.ide.highlighter.JavaFileType; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.codeStyle.ChangedRangesInfo; -import com.intellij.psi.codeStyle.CodeStyleManager; -import com.intellij.psi.impl.CheckUtil; -import com.intellij.util.IncorrectOperationException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -import org.jetbrains.annotations.NotNull; - -/** - * A {@link CodeStyleManager} implementation which formats .java files with google-java-format. - * Formatting of all other types of files is delegated to IntelliJ's default implementation. - */ -class GoogleJavaFormatCodeStyleManager extends CodeStyleManagerDecorator { - - public GoogleJavaFormatCodeStyleManager(@NotNull CodeStyleManager original) { - super(original); - } - - @Override - public void reformatText(@NotNull PsiFile file, int startOffset, int endOffset) - throws IncorrectOperationException { - if (overrideFormatterForFile(file)) { - formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset))); - } else { - super.reformatText(file, startOffset, endOffset); - } - } - - @Override - public void reformatText(@NotNull PsiFile file, @NotNull Collection ranges) - throws IncorrectOperationException { - if (overrideFormatterForFile(file)) { - formatInternal(file, ranges); - } else { - super.reformatText(file, ranges); - } - } - - @Override - public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info) - throws IncorrectOperationException { - List ranges = new ArrayList<>(); - if (info.insertedRanges != null) { - ranges.addAll(info.insertedRanges); - } - ranges.addAll(info.allChangedRanges); - reformatTextWithContext(file, ranges); - } - - @Override - public void reformatTextWithContext( - @NotNull PsiFile file, @NotNull Collection ranges) { - if (overrideFormatterForFile(file)) { - formatInternal(file, ranges); - } else { - super.reformatTextWithContext(file, ranges); - } - } - - @Override - public PsiElement reformatRange( - @NotNull PsiElement element, - int startOffset, - int endOffset, - boolean canChangeWhiteSpacesOnly) { - // Only handle elements that are PsiFile for now -- otherwise we need to search for some - // element within the file at new locations given the original startOffset and endOffsets - // to serve as the return value. - PsiFile file = element instanceof PsiFile ? (PsiFile) element : null; - if (file != null && canChangeWhiteSpacesOnly && overrideFormatterForFile(file)) { - formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset))); - return file; - } else { - return super.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly); - } - } - - /** Return whether this formatter can handle formatting the given file. */ - private boolean overrideFormatterForFile(PsiFile file) { - return JavaFileType.INSTANCE.equals(file.getFileType()) - && GoogleJavaFormatSettings.getInstance(getProject()).isEnabled(); - } - - private void formatInternal(PsiFile file, Collection ranges) { - ApplicationManager.getApplication().assertWriteAccessAllowed(); - PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject()); - documentManager.commitAllDocuments(); - CheckUtil.checkWritable(file); - - Document document = documentManager.getDocument(file); - - if (document == null) { - return; - } - // If there are postponed PSI changes (e.g., during a refactoring), just abort. - // If we apply them now, then the incoming text ranges may no longer be valid. - if (documentManager.isDocumentBlockedByPsi(document)) { - return; - } - - format(document, ranges); - } - - /** - * Format the ranges of the given document. - * - *

Overriding methods will need to modify the document with the result of the external - * formatter (usually using {@link #performReplacements(Document, Map)}). - */ - private void format(Document document, Collection ranges) { - Style style = GoogleJavaFormatSettings.getInstance(getProject()).getStyle(); - Formatter formatter = new Formatter(JavaFormatterOptions.builder().style(style).build()); - performReplacements( - document, FormatterUtil.getReplacements(formatter, document.getText(), ranges)); - } - - private void performReplacements( - final Document document, final Map replacements) { - - if (replacements.isEmpty()) { - return; - } - - TreeMap sorted = new TreeMap<>(comparing(TextRange::getStartOffset)); - sorted.putAll(replacements); - WriteCommandAction.runWriteCommandAction( - getProject(), - () -> { - for (Entry entry : sorted.descendingMap().entrySet()) { - document.replaceString( - entry.getKey().getStartOffset(), entry.getKey().getEndOffset(), entry.getValue()); - } - PsiDocumentManager.getInstance(getProject()).commitDocument(document); - }); - } -} diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java b/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java deleted file mode 100644 index c6067360..00000000 --- a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatInstaller.java +++ /dev/null @@ -1,57 +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 static com.google.common.base.Preconditions.checkState; - -import com.intellij.ide.plugins.IdeaPluginDescriptor; -import com.intellij.ide.plugins.PluginManagerCore; -import com.intellij.openapi.extensions.PluginId; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManagerListener; -import com.intellij.psi.codeStyle.CodeStyleManager; -import com.intellij.serviceContainer.ComponentManagerImpl; -import org.jetbrains.annotations.NotNull; - -/** - * A component that replaces the default IntelliJ {@link CodeStyleManager} with one that formats via - * google-java-format. - */ -final class GoogleJavaFormatInstaller implements ProjectManagerListener { - - @Override - public void projectOpened(@NotNull Project project) { - installFormatter(project); - } - - private static void installFormatter(Project project) { - CodeStyleManager currentManager = CodeStyleManager.getInstance(project); - - if (currentManager instanceof GoogleJavaFormatCodeStyleManager) { - currentManager = ((GoogleJavaFormatCodeStyleManager) currentManager).getDelegate(); - } - - setManager(project, new GoogleJavaFormatCodeStyleManager(currentManager)); - } - - private static void setManager(Project project, CodeStyleManager newManager) { - ComponentManagerImpl platformComponentManager = (ComponentManagerImpl) project; - IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PluginId.getId("google-java-format")); - checkState(plugin != null, "Couldn't locate our own PluginDescriptor."); - platformComponentManager.registerServiceInstance(CodeStyleManager.class, newManager, plugin); - } -} diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form similarity index 100% rename from idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.form diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java similarity index 100% rename from idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatConfigurable.java diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingService.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingService.java new file mode 100644 index 00000000..9d2d7a59 --- /dev/null +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingService.java @@ -0,0 +1,141 @@ +/* + * Copyright 2023 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.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Range; +import com.google.googlejavaformat.java.Formatter; +import com.google.googlejavaformat.java.FormatterException; +import com.google.googlejavaformat.java.JavaFormatterOptions; +import com.google.googlejavaformat.java.JavaFormatterOptions.Style; +import com.intellij.formatting.service.AsyncDocumentFormattingService; +import com.intellij.formatting.service.AsyncFormattingRequest; +import com.intellij.ide.highlighter.JavaFileType; +import com.intellij.lang.ImportOptimizer; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiFile; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import org.jetbrains.annotations.NotNull; + +/** Uses {@code google-java-format} to reformat code. */ +public class GoogleJavaFormatFormattingService extends AsyncDocumentFormattingService { + + public static final ImmutableSet IMPORT_OPTIMIZERS = + ImmutableSet.of(new GoogleJavaFormatImportOptimizer()); + + @Override + protected FormattingTask createFormattingTask(AsyncFormattingRequest request) { + Project project = request.getContext().getProject(); + + if (!JreConfigurationChecker.checkJreConfiguration(project)) { + return null; + } + + Style style = GoogleJavaFormatSettings.getInstance(project).getStyle(); + Formatter formatter = createFormatter(style, request.canChangeWhitespaceOnly()); + return new GoogleJavaFormatFormattingTask(formatter, request); + } + + @Override + protected String getNotificationGroupId() { + return Notifications.PARSING_ERROR_NOTIFICATION_GROUP; + } + + @Override + protected String getName() { + return "google-java-format"; + } + + private static Formatter createFormatter(Style style, boolean canChangeWhiteSpaceOnly) { + JavaFormatterOptions.Builder optBuilder = JavaFormatterOptions.builder().style(style); + if (canChangeWhiteSpaceOnly) { + optBuilder.formatJavadoc(false).reorderModifiers(false); + } + return new Formatter(optBuilder.build()); + } + + @Override + public @NotNull Set getFeatures() { + return Set.of(Feature.FORMAT_FRAGMENTS, Feature.OPTIMIZE_IMPORTS); + } + + @Override + public boolean canFormat(@NotNull PsiFile file) { + return JavaFileType.INSTANCE.equals(file.getFileType()) + && GoogleJavaFormatSettings.getInstance(file.getProject()).isEnabled(); + } + + @Override + public @NotNull Set getImportOptimizers(@NotNull PsiFile file) { + return IMPORT_OPTIMIZERS; + } + + private static final class GoogleJavaFormatFormattingTask implements FormattingTask { + private final Formatter formatter; + private final AsyncFormattingRequest request; + + private GoogleJavaFormatFormattingTask(Formatter formatter, AsyncFormattingRequest request) { + this.formatter = formatter; + this.request = request; + } + + @Override + public void run() { + try { + String formattedText = formatter.formatSource(request.getDocumentText(), toRanges(request)); + request.onTextReady(formattedText); + } catch (FormatterException e) { + request.onError( + Notifications.PARSING_ERROR_TITLE, + Notifications.parsingErrorMessage(request.getContext().getContainingFile().getName())); + } + } + + private static Collection> toRanges(AsyncFormattingRequest request) { + if (isWholeFile(request)) { + // The IDE sometimes passes invalid ranges when the file is unsaved before invoking the + // formatter. So this is a workaround for that issue. + return ImmutableList.of(Range.closedOpen(0, request.getDocumentText().length())); + } + return request.getFormattingRanges().stream() + .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset())) + .collect(ImmutableList.toImmutableList()); + } + + private static boolean isWholeFile(AsyncFormattingRequest request) { + List ranges = request.getFormattingRanges(); + return ranges.size() == 1 + && ranges.get(0).getStartOffset() == 0 + // using greater than or equal because ranges are sometimes passed inaccurately + && ranges.get(0).getEndOffset() >= request.getDocumentText().length(); + } + + @Override + public boolean isRunUnderProgress() { + return true; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java new file mode 100644 index 00000000..498c8852 --- /dev/null +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2023 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.google.common.util.concurrent.Runnables; +import com.google.googlejavaformat.java.FormatterException; +import com.google.googlejavaformat.java.ImportOrderer; +import com.google.googlejavaformat.java.JavaFormatterOptions; +import com.google.googlejavaformat.java.RemoveUnusedImports; +import com.intellij.ide.highlighter.JavaFileType; +import com.intellij.lang.ImportOptimizer; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +/** Uses {@code google-java-format} to optimize imports. */ +public class GoogleJavaFormatImportOptimizer implements ImportOptimizer { + + @Override + public boolean supports(@NotNull PsiFile file) { + return JavaFileType.INSTANCE.equals(file.getFileType()) + && GoogleJavaFormatSettings.getInstance(file.getProject()).isEnabled(); + } + + @Override + public @NotNull Runnable processFile(@NotNull PsiFile file) { + Project project = file.getProject(); + + if (!JreConfigurationChecker.checkJreConfiguration(file.getProject())) { + return Runnables.doNothing(); + } + + PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); + Document document = documentManager.getDocument(file); + + if (document == null) { + return Runnables.doNothing(); + } + + JavaFormatterOptions.Style style = GoogleJavaFormatSettings.getInstance(project).getStyle(); + + String text; + try { + text = + ImportOrderer.reorderImports( + RemoveUnusedImports.removeUnusedImports(document.getText()), style); + } catch (FormatterException e) { + Notifications.displayParsingErrorNotification(project, file.getName()); + return Runnables.doNothing(); + } + + return () -> document.setText(text); + } +} diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java similarity index 90% rename from idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java index 1e92a4bd..ee187c00 100644 --- a/idea_plugin/src/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatSettings.java @@ -18,7 +18,6 @@ import com.google.googlejavaformat.java.JavaFormatterOptions; import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.project.Project; @@ -30,10 +29,16 @@ storages = {@Storage("google-java-format.xml")}) class GoogleJavaFormatSettings implements PersistentStateComponent { + private final Project project; + private State state = new State(); + GoogleJavaFormatSettings(Project project) { + this.project = project; + } + static GoogleJavaFormatSettings getInstance(Project project) { - return ServiceManager.getService(project, GoogleJavaFormatSettings.class); + return project.getService(GoogleJavaFormatSettings.class); } @Nullable @@ -56,6 +61,9 @@ void setEnabled(boolean enabled) { } void setEnabled(EnabledState enabled) { + if (enabled.equals(EnabledState.ENABLED)) { + JreConfigurationChecker.checkJreConfiguration(project); + } state.enabled = enabled; } diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java similarity index 79% rename from idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java index 1906347f..940def65 100644 --- a/idea_plugin/src/com/google/googlejavaformat/intellij/InitialConfigurationProjectManagerListener.java +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/InitialConfigurationStartupActivity.java @@ -21,22 +21,24 @@ import com.intellij.notification.NotificationGroupManager; import com.intellij.notification.NotificationType; import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManagerListener; +import com.intellij.openapi.startup.StartupActivity; import org.jetbrains.annotations.NotNull; -final class InitialConfigurationProjectManagerListener implements ProjectManagerListener { +final class InitialConfigurationStartupActivity implements StartupActivity.Background { private static final String NOTIFICATION_TITLE = "Enable google-java-format"; private static final NotificationGroup NOTIFICATION_GROUP = NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_TITLE); @Override - public void projectOpened(@NotNull Project project) { + 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); } } @@ -47,11 +49,12 @@ private void displayNewUserNotification(Project project, GoogleJavaFormatSetting NOTIFICATION_TITLE, "The google-java-format plugin is disabled by default. " + "Enable for this project.", - NotificationType.INFORMATION, - (n, e) -> { - settings.setEnabled(true); - n.expire(); - }); + 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 new file mode 100644 index 00000000..5084b6a3 --- /dev/null +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/JreConfigurationChecker.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023 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.google.common.base.Suppliers; +import com.intellij.ide.ui.IdeUiService; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import java.util.function.Supplier; + +class JreConfigurationChecker { + + private static final Supplier hasAccess = + Suppliers.memoize(JreConfigurationChecker::checkJreConfiguration); + private static final Logger logger = Logger.getInstance(JreConfigurationChecker.class); + + private final Project project; + + public JreConfigurationChecker(Project project) { + this.project = project; + } + + static boolean checkJreConfiguration(Project project) { + var success = hasAccess.get(); + if (!success) { + project.getService(JreConfigurationChecker.class).displayConfigurationErrorNotification(); + } + return success; + } + + /** + * Determine whether the JRE is configured to work with the google-java-format plugin. If not, + * display a notification with instructions and return false. + */ + private static boolean checkJreConfiguration() { + try { + return testClassAccess( + "com.sun.tools.javac.api.JavacTrees", + "com.sun.tools.javac.code.Flags", + "com.sun.tools.javac.file.JavacFileManager", + "com.sun.tools.javac.parser.JavacParser", + "com.sun.tools.javac.tree.JCTree", + "com.sun.tools.javac.util.Log"); + } catch (ClassNotFoundException e) { + logger.error("Error checking jre configuration for google-java-format", e); + return false; + } + } + + private static boolean testClassAccess(String... classNames) throws ClassNotFoundException { + for (String className : classNames) { + if (!testClassAccess(className)) { + return false; + } + } + return true; + } + + private static boolean testClassAccess(String className) throws ClassNotFoundException { + Class klass = Class.forName(className); + return klass + .getModule() + // isExported returns true if the package is either open or exported. Either one is + // sufficient + // to run the google-java-format code (even though the documentation specifies --add-opens). + .isExported( + klass.getPackageName(), + JreConfigurationChecker.class.getClassLoader().getUnnamedModule()); + } + + private void displayConfigurationErrorNotification() { + Notification notification = + 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.", + 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.notify(project); + } +} diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/Notifications.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/Notifications.java new file mode 100644 index 00000000..d32aa98b --- /dev/null +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/Notifications.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 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.formatting.service.FormattingNotificationService; +import com.intellij.openapi.project.Project; + +class Notifications { + + static final String PARSING_ERROR_NOTIFICATION_GROUP = "google-java-format parsing error"; + static final String PARSING_ERROR_TITLE = PARSING_ERROR_NOTIFICATION_GROUP; + + static String parsingErrorMessage(String filename) { + return "google-java-format failed. Does " + filename + " have syntax errors?"; + } + + static void displayParsingErrorNotification(Project project, String filename) { + FormattingNotificationService.getInstance(project) + .reportError( + Notifications.PARSING_ERROR_NOTIFICATION_GROUP, + Notifications.PARSING_ERROR_TITLE, + Notifications.parsingErrorMessage(filename)); + } +} diff --git a/idea_plugin/src/com/google/googlejavaformat/intellij/UiFormatterStyle.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/UiFormatterStyle.java similarity index 100% rename from idea_plugin/src/com/google/googlejavaformat/intellij/UiFormatterStyle.java rename to idea_plugin/src/main/java/com/google/googlejavaformat/intellij/UiFormatterStyle.java diff --git a/idea_plugin/resources/META-INF/plugin.xml b/idea_plugin/src/main/resources/META-INF/plugin.xml similarity index 61% rename from idea_plugin/resources/META-INF/plugin.xml rename to idea_plugin/src/main/resources/META-INF/plugin.xml index 42d5f3b3..42011a20 100644 --- a/idea_plugin/resources/META-INF/plugin.xml +++ b/idea_plugin/src/main/resources/META-INF/plugin.xml @@ -22,13 +22,29 @@ Google - - com.intellij.java + com.intellij.modules.java + com.intellij.modules.lang + com.intellij.modules.platform + + + This plugin requires additional IDE configuration. For more information, + read + the documentation. + ]]> +

1.16.0.2
+
Disable AD_HOC_FORMATTING, which should stop the formatter from running so often when it wasn't specifically requested. +
1.16.0.1
+
When the plugin isn't configured correctly, show the error on every + format command. Previously it was only being shown at startup and going + unnoticed. +
1.16.0.0
+
Updated to use google-java-format 1.16.0.
+
Use the new IDE formatting APIs for a simplified plugin.
+
Optimize Imports now uses google-java-format.
1.15.0.0
Updated to use google-java-format 1.15.0.
1.14.0.0
@@ -62,23 +78,23 @@ ]]> - - - - - + + + + + diff --git a/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingServiceTest.java b/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingServiceTest.java new file mode 100644 index 00000000..fec086c6 --- /dev/null +++ b/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatFormattingServiceTest.java @@ -0,0 +1,250 @@ +/* + * Copyright 2023 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 static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.googlejavaformat.intellij.GoogleJavaFormatSettings.State; +import com.google.googlejavaformat.java.Formatter; +import com.google.googlejavaformat.java.JavaFormatterOptions; +import com.google.googlejavaformat.java.JavaFormatterOptions.Style; +import com.intellij.formatting.service.AsyncFormattingRequest; +import com.intellij.formatting.service.FormattingService; +import com.intellij.formatting.service.FormattingServiceUtil; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.testFramework.ExtensionTestUtil; +import com.intellij.testFramework.fixtures.IdeaProjectTestFixture; +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory; +import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture; +import com.intellij.testFramework.fixtures.JavaTestFixtureFactory; +import com.intellij.testFramework.fixtures.TestFixtureBuilder; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class GoogleJavaFormatFormattingServiceTest { + private JavaCodeInsightTestFixture fixture; + private GoogleJavaFormatSettings settings; + private DelegatingFormatter delegatingFormatter; + + @Before + public void setUp() throws Exception { + TestFixtureBuilder projectBuilder = + IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder(getClass().getName()); + fixture = + JavaTestFixtureFactory.getFixtureFactory() + .createCodeInsightFixture(projectBuilder.getFixture()); + fixture.setUp(); + + delegatingFormatter = new DelegatingFormatter(); + ExtensionTestUtil.maskExtensions( + FormattingService.EP_NAME, + ImmutableList.of(delegatingFormatter), + fixture.getProjectDisposable()); + + settings = GoogleJavaFormatSettings.getInstance(fixture.getProject()); + State resetState = new State(); + resetState.setEnabled("true"); + settings.loadState(resetState); + } + + @After + public void tearDown() throws Exception { + fixture.tearDown(); + } + + @Test + public void defaultFormatSettings() throws Exception { + PsiFile file = + createPsiFile( + "com/foo/FormatTest.java", + "package com.foo;", + "public class FormatTest {", + "static final String CONST_STR = \"Hello\";", + "}"); + String origText = file.getText(); + CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject()); + WriteCommandAction.runWriteCommandAction( + file.getProject(), () -> manager.reformatText(file, 0, file.getTextLength())); + + assertThat(file.getText()).isEqualTo(new Formatter().formatSource(origText)); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + @Test + public void aospStyle() throws Exception { + settings.setStyle(Style.AOSP); + PsiFile file = + createPsiFile( + "com/foo/FormatTest.java", + "package com.foo;", + "public class FormatTest {", + "static final String CONST_STR = \"Hello\";", + "}"); + String origText = file.getText(); + CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject()); + WriteCommandAction.runWriteCommandAction( + file.getProject(), () -> manager.reformatText(file, 0, file.getTextLength())); + + assertThat(file.getText()) + .isEqualTo( + new Formatter(JavaFormatterOptions.builder().style(Style.AOSP).build()) + .formatSource(origText)); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + @Test + public void canChangeWhitespaceOnlyDoesNotReorderModifiers() throws Exception { + settings.setStyle(Style.GOOGLE); + PsiFile file = + createPsiFile( + "com/foo/FormatTest.java", + "package com.foo;", + "public class FormatTest {", + "final static String CONST_STR = \"Hello\";", + "}"); + CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject()); + var offset = file.getText().indexOf("final static"); + WriteCommandAction.runWriteCommandAction( + file.getProject(), + () -> + FormattingServiceUtil.formatElement( + file.findElementAt(offset), /* canChangeWhitespaceOnly= */ true)); + + // In non-whitespace mode, this would flip the order of these modifiers. (Also check for leading + // spaces to make sure the formatter actually ran. + assertThat(file.getText()).containsMatch(" final static"); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + @Test + public void canChangeWhitespaceOnlyDoesNotReformatJavadoc() throws Exception { + settings.setStyle(Style.GOOGLE); + PsiFile file = + createPsiFile( + "com/foo/FormatTest.java", + "package com.foo;", + "public class FormatTest {", + "/**", + " * hello", + " */", + "static final String CONST_STR = \"Hello\";", + "}"); + CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject()); + var offset = file.getText().indexOf("hello"); + WriteCommandAction.runWriteCommandAction( + file.getProject(), + () -> + FormattingServiceUtil.formatElement( + file.findElementAt(offset), /* canChangeWhitespaceOnly= */ true)); + + // In non-whitespace mode, this would join the Javadoc into a single line. + assertThat(file.getText()).containsMatch(" \\* hello"); + // Also check for leading spaces to make sure the formatter actually ran. (Technically, this is + // outside the range that we asked to be formatted, but gjf will still format it.) + assertThat(file.getText()).containsMatch(" static final"); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + @Test + public void canChangeNonWhitespaceReordersModifiers() throws Exception { + settings.setStyle(Style.GOOGLE); + PsiFile file = + createPsiFile( + "com/foo/FormatTest.java", + "package com.foo;", + "public class FormatTest {", + "final static String CONST_STR = \"Hello\";", + "}"); + CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject()); + var offset = file.getText().indexOf("final static"); + WriteCommandAction.runWriteCommandAction( + file.getProject(), + () -> + FormattingServiceUtil.formatElement( + file.findElementAt(offset), /* canChangeWhitespaceOnly= */ false)); + + assertThat(file.getText()).containsMatch("static final"); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + @Test + public void canChangeNonWhitespaceReformatsJavadoc() throws Exception { + settings.setStyle(Style.GOOGLE); + PsiFile file = + createPsiFile( + "com/foo/FormatTest.java", + "package com.foo;", + "public class FormatTest {", + "/**", + " * hello", + " */", + "static final String CONST_STR = \"Hello\";", + "}"); + CodeStyleManager manager = CodeStyleManager.getInstance(file.getProject()); + var offset = file.getText().indexOf("hello"); + WriteCommandAction.runWriteCommandAction( + file.getProject(), + () -> + FormattingServiceUtil.formatElement( + file.findElementAt(offset), /* canChangeWhitespaceOnly= */ false)); + + assertThat(file.getText()).containsMatch("/\\*\\* hello \\*/"); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + private PsiFile createPsiFile(String path, String... contents) throws IOException { + VirtualFile virtualFile = + fixture.getTempDirFixture().createFile(path, String.join("\n", contents)); + fixture.configureFromExistingVirtualFile(virtualFile); + PsiFile psiFile = fixture.getFile(); + assertThat(psiFile).isNotNull(); + return psiFile; + } + + private static final class DelegatingFormatter extends GoogleJavaFormatFormattingService { + + private boolean invoked = false; + + private boolean wasInvoked() { + return invoked; + } + + @Override + protected FormattingTask createFormattingTask(AsyncFormattingRequest request) { + FormattingTask delegateTask = super.createFormattingTask(request); + return new FormattingTask() { + @Override + public boolean cancel() { + return delegateTask.cancel(); + } + + @Override + public void run() { + invoked = true; + delegateTask.run(); + } + }; + } + } +} diff --git a/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizerTest.java b/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizerTest.java new file mode 100644 index 00000000..ad9fe274 --- /dev/null +++ b/idea_plugin/src/test/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizerTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2023 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 static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.googlejavaformat.intellij.GoogleJavaFormatSettings.State; +import com.intellij.codeInsight.actions.OptimizeImportsProcessor; +import com.intellij.formatting.service.FormattingService; +import com.intellij.lang.ImportOptimizer; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.ExtensionTestUtil; +import com.intellij.testFramework.fixtures.IdeaProjectTestFixture; +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory; +import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture; +import com.intellij.testFramework.fixtures.JavaTestFixtureFactory; +import com.intellij.testFramework.fixtures.TestFixtureBuilder; +import java.io.IOException; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class GoogleJavaFormatImportOptimizerTest { + private JavaCodeInsightTestFixture fixture; + private DelegatingFormatter delegatingFormatter; + + @Before + public void setUp() throws Exception { + TestFixtureBuilder projectBuilder = + IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder(getClass().getName()); + fixture = + JavaTestFixtureFactory.getFixtureFactory() + .createCodeInsightFixture(projectBuilder.getFixture()); + fixture.setUp(); + + delegatingFormatter = new DelegatingFormatter(); + ExtensionTestUtil.maskExtensions( + FormattingService.EP_NAME, + ImmutableList.of(delegatingFormatter), + fixture.getProjectDisposable()); + + var settings = GoogleJavaFormatSettings.getInstance(fixture.getProject()); + State resetState = new State(); + resetState.setEnabled("true"); + settings.loadState(resetState); + } + + @After + public void tearDown() throws Exception { + fixture.tearDown(); + } + + @Test + public void removesUnusedImports() throws Exception { + PsiFile file = + createPsiFile( + "com/foo/ImportTest.java", + "package com.foo;", + "import java.util.List;", + "import java.util.ArrayList;", + "import java.util.Map;", + "public class ImportTest {", + "static final Map map;", + "}"); + OptimizeImportsProcessor processor = new OptimizeImportsProcessor(file.getProject(), file); + WriteCommandAction.runWriteCommandAction( + file.getProject(), + () -> { + processor.run(); + PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments(); + }); + + assertThat(file.getText()).doesNotContain("List"); + assertThat(file.getText()).contains("import java.util.Map;"); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + @Test + public void reordersImports() throws Exception { + PsiFile file = + createPsiFile( + "com/foo/ImportTest.java", + "package com.foo;", + "import java.util.List;", + "import java.util.ArrayList;", + "import java.util.Map;", + "public class ImportTest {", + "static final ArrayList arrayList;", + "static final List list;", + "static final Map map;", + "}"); + OptimizeImportsProcessor processor = new OptimizeImportsProcessor(file.getProject(), file); + WriteCommandAction.runWriteCommandAction( + file.getProject(), + () -> { + processor.run(); + PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments(); + }); + + assertThat(file.getText()) + .contains( + "import java.util.ArrayList;\n" + + "import java.util.List;\n" + + "import java.util.Map;\n"); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + private PsiFile createPsiFile(String path, String... contents) throws IOException { + VirtualFile virtualFile = + fixture.getTempDirFixture().createFile(path, String.join("\n", contents)); + fixture.configureFromExistingVirtualFile(virtualFile); + PsiFile psiFile = fixture.getFile(); + assertThat(psiFile).isNotNull(); + return psiFile; + } + + private static final class DelegatingFormatter extends GoogleJavaFormatFormattingService { + + private boolean invoked = false; + + private boolean wasInvoked() { + return invoked; + } + + @Override + public @NotNull Set getImportOptimizers(@NotNull PsiFile file) { + return super.getImportOptimizers(file).stream().map(this::wrap).collect(toImmutableSet()); + } + + private ImportOptimizer wrap(ImportOptimizer delegate) { + return new ImportOptimizer() { + @Override + public boolean supports(@NotNull PsiFile file) { + return delegate.supports(file); + } + + @Override + public @NotNull Runnable processFile(@NotNull PsiFile file) { + return () -> { + invoked = true; + delegate.processFile(file).run(); + }; + } + }; + } + } +} diff --git a/pom.xml b/pom.xml index 33794e5c..4a55220c 100644 --- a/pom.xml +++ b/pom.xml @@ -235,15 +235,12 @@ jdk.compiler/com.sun.tools.javac.api jdk.compiler/com.sun.tools.javac.code jdk.compiler/com.sun.tools.javac.file jdk.compiler/com.sun.tools.javac.main jdk.compiler/com.sun.tools.javac.model jdk.compiler/com.sun.tools.javac.parser jdk.compiler/com.sun.tools.javac.processing jdk.compiler/com.sun.tools.javac.tree jdk.compiler/com.sun.tools.javac.util - jdk.compiler/com.sun.tools.javac.code jdk.compiler/com.sun.tools.javac.comp