diff --git a/CHANGES.md b/CHANGES.md index eb93716703..6bff21096a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Fixed - GrEclipse no longer leaves a `file%3A...` directory in the working directory. ([#2019](https://github.com/diffplug/spotless/issues/2019)) +- `RemoveUnusedImportsStep` and `GoogleJavaFormatStep` no longer remove imports that are only used by links in Markdown doc comments (`///`, JDK 23+). ([#3161](https://github.com/diffplug/spotless/issues/3161)) ## [4.11.0] - 2026-11-02 ### Added diff --git a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java index 859d728151..ab0e1d6e4f 100644 --- a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java +++ b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java @@ -26,7 +26,6 @@ import com.google.googlejavaformat.java.ImportOrderer; import com.google.googlejavaformat.java.JavaFormatterOptions; import com.google.googlejavaformat.java.JavaFormatterOptions.Style; -import com.google.googlejavaformat.java.RemoveUnusedImports; import com.google.googlejavaformat.java.StringWrapper; import com.diffplug.spotless.FormatterFunc; @@ -116,7 +115,7 @@ private static JavaFormatterOptions.Builder disableCleanup(JavaFormatterOptions. @Nonnull public String apply(@Nonnull String input) throws Exception { String formatted = formatter.formatSource(input); - String removedUnused = RemoveUnusedImports.removeUnusedImports(formatted); + String removedUnused = GoogleJavaFormatUnusedImports.removeUnusedImports(formatted); String sortedImports = ImportOrderer.reorderImports(removedUnused, reorderImports ? formatterStyle : Style.GOOGLE); return reflowLongStrings(sortedImports); } diff --git a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatRemoveUnusedImporterFormatterFunc.java b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatRemoveUnusedImporterFormatterFunc.java index de21a18795..852765c1cd 100644 --- a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatRemoveUnusedImporterFormatterFunc.java +++ b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatRemoveUnusedImporterFormatterFunc.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ import javax.annotation.Nonnull; -import com.google.googlejavaformat.java.RemoveUnusedImports; - import com.diffplug.spotless.FormatterFunc; public class GoogleJavaFormatRemoveUnusedImporterFormatterFunc implements FormatterFunc { @@ -35,6 +33,6 @@ public GoogleJavaFormatRemoveUnusedImporterFormatterFunc(@Nonnull String version @Override @Nonnull public String apply(@Nonnull String input) throws Exception { - return RemoveUnusedImports.removeUnusedImports(input); + return GoogleJavaFormatUnusedImports.removeUnusedImports(input); } } diff --git a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatUnusedImports.java b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatUnusedImports.java new file mode 100644 index 0000000000..d2bc23bc42 --- /dev/null +++ b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatUnusedImports.java @@ -0,0 +1,49 @@ +/* + * Copyright 2026 DiffPlug + * + * 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.diffplug.spotless.glue.java; + +import javax.annotation.Nonnull; + +import com.google.googlejavaformat.java.FormatterException; +import com.google.googlejavaformat.java.RemoveUnusedImports; +import com.sun.source.util.DocTrees; + +final class GoogleJavaFormatUnusedImports { + private GoogleJavaFormatUnusedImports() {} + + /** + * Calls {@link RemoveUnusedImports#removeUnusedImports(String)} with javac's class loader as + * the context class loader. + *

+ * On JDK 23+, javac parses Markdown doc comments ({@code ///}) with a transformer from the + * {@code jdk.internal.md} module, which it looks up with {@link java.util.ServiceLoader} on the + * context class loader. Gradle and Maven run Spotless with a context class loader that cannot + * see that module, so javac silently fell back to treating Markdown doc comments as plain text, + * and imports used only by their links were removed + * (https://github.com/diffplug/spotless/issues/3161). + */ + @Nonnull + static String removeUnusedImports(@Nonnull String input) throws FormatterException { + Thread thread = Thread.currentThread(); + ClassLoader originalClassLoader = thread.getContextClassLoader(); + thread.setContextClassLoader(DocTrees.class.getClassLoader()); + try { + return RemoveUnusedImports.removeUnusedImports(input); + } finally { + thread.setContextClassLoader(originalClassLoader); + } + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 5aaef36403..29742423f6 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Fixed - GrEclipse no longer leaves a `file%3A...` directory in the working directory. ([#2019](https://github.com/diffplug/spotless/issues/2019)) +- `removeUnusedImports()` and `googleJavaFormat()` no longer remove imports that are only used by links in Markdown doc comments (`///`, JDK 23+). ([#3161](https://github.com/diffplug/spotless/issues/3161)) ## [8.11.0] - 2026-11-02 ### Added diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 4081c6671e..19003c5ec5 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Fixed - GrEclipse no longer leaves a `file%3A...` directory in the working directory. ([#2019](https://github.com/diffplug/spotless/issues/2019)) +- `` and `` no longer remove imports that are only used by links in Markdown doc comments (`///`, JDK 23+). ([#3161](https://github.com/diffplug/spotless/issues/3161)) ## [3.11.0] - 2026-11-02 ### Added diff --git a/testlib/src/main/resources/java/removeunusedimports/MarkdownJavadocFormatted.test b/testlib/src/main/resources/java/removeunusedimports/MarkdownJavadocFormatted.test new file mode 100644 index 0000000000..98c64c0a5a --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/MarkdownJavadocFormatted.test @@ -0,0 +1,6 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.function.Predicate; + +/// Markdown doc comment linking to [ArrayList#add] and [Collection#removeIf(Predicate)]. +class MarkdownJavadoc {} diff --git a/testlib/src/main/resources/java/removeunusedimports/MarkdownJavadocUnformatted.test b/testlib/src/main/resources/java/removeunusedimports/MarkdownJavadocUnformatted.test new file mode 100644 index 0000000000..3eb095cc80 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/MarkdownJavadocUnformatted.test @@ -0,0 +1,7 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.function.Predicate; + +/// Markdown doc comment linking to [ArrayList#add] and [Collection#removeIf(Predicate)]. +class MarkdownJavadoc {} diff --git a/testlib/src/test/java/com/diffplug/spotless/java/RemoveUnusedImportsStep_withGoogleJavaFormatTest.java b/testlib/src/test/java/com/diffplug/spotless/java/RemoveUnusedImportsStep_withGoogleJavaFormatTest.java index bd63822e63..a644d3cbfc 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/RemoveUnusedImportsStep_withGoogleJavaFormatTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/RemoveUnusedImportsStep_withGoogleJavaFormatTest.java @@ -54,6 +54,27 @@ void moduleImports() throws Exception { .testResource("java/removeunusedimports/ModuleImportWithUnusedUnformatted.test", "java/removeunusedimports/ModuleImportWithUnusedFormatted.test"); } + /** + * Markdown doc comments (JDK 23+) are only parsed by javac if it can find the Markdown parser + * in the {@code jdk.internal.md} module through the context class loader. Gradle and Maven run + * Spotless with a context class loader which cannot see that module, simulated here with the + * platform class loader (https://github.com/diffplug/spotless/issues/3161). + */ + @Test + @EnabledForJreRange(min = JRE.JAVA_23) + void markdownJavadoc() throws Exception { + FormatterStep step = RemoveUnusedImportsStep.create(RemoveUnusedImportsStep.GJF, TestProvisioner.mavenCentral()); + Thread thread = Thread.currentThread(); + ClassLoader originalClassLoader = thread.getContextClassLoader(); + thread.setContextClassLoader(ClassLoader.getPlatformClassLoader()); + try { + StepHarness.forStep(step) + .testResource("java/removeunusedimports/MarkdownJavadocUnformatted.test", "java/removeunusedimports/MarkdownJavadocFormatted.test"); + } finally { + thread.setContextClassLoader(originalClassLoader); + } + } + @Test void equality() throws Exception { new SerializableEqualityTester() {