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 9aebf4641..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. */ @@ -245,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()) { @@ -301,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++; @@ -347,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/JavaInputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java index 8e8ac97cc..a2a32e79c 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java @@ -157,6 +157,7 @@ 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; @@ -1220,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(); @@ -1231,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()); 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..4035ce319 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java +++ b/core/src/main/java/com/google/googlejavaformat/java/RemoveUnusedImports.java @@ -16,6 +16,7 @@ package com.google.googlejavaformat.java; +import static com.google.common.base.Preconditions.checkArgument; import static java.lang.Math.max; import static java.nio.charset.StandardCharsets.UTF_8; @@ -67,6 +68,7 @@ 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 @@ -274,6 +276,9 @@ 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; @@ -322,10 +327,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/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java index 8e863a28d..a406487f3 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterIntegrationTest.java @@ -59,6 +59,7 @@ public class FormatterIntegrationTest { "I981", "I1020", "I1037") + .putAll(25, "ModuleImport") .build(); @Parameters(name = "{index}: {0}") 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/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); }