From bd23f0c142ed5c20c1fdb4a59600aec45ae5a1a0 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Mon, 30 Jun 2025 21:11:09 +0200 Subject: [PATCH 1/3] feat: add remove-unused-import --- CHANGELOG.md | 1 + .../diffplug/spotless/cli/SpotlessCLI.java | 4 +- .../cli/steps/RemoveUnusedImports.java | 68 +++++++++++++++++++ .../cli/steps/RemoveUnusedImportsTest.java | 64 +++++++++++++++++ .../cli/steps/RemoveUnusedImportsTest.ss | 52 ++++++++++++++ .../cli/picocli/usage/DocumentedUsages.groovy | 3 +- .../diffplug/spotless/ResourceHarness.java | 37 +++++----- ...JavaCodeWithLicensePackageUnformatted.test | 17 +++++ 8 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java create mode 100644 app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java create mode 100644 app/src/test/resources/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.ss create mode 100644 testlib/src/main/resources/java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9acf0..36eccce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Commit, tag and push the choco source files to the chocolatey-bucket repository during the release process - Added formatter [`clean-that`](https://github.com/diffplug/spotless/tree/main/plugin-gradle#cleanthat) +- Added formatter [`remove-unused-imports`](https://github.com/diffplug/spotless/tree/main/plugin-gradle#removeunusedimports) ## [0.1.1] - 2025-06-02 diff --git a/app/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java b/app/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java index bfee0c1..965598f 100644 --- a/app/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java +++ b/app/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java @@ -50,6 +50,7 @@ import com.diffplug.spotless.cli.steps.LicenseHeader; import com.diffplug.spotless.cli.steps.PalantirJavaFormat; import com.diffplug.spotless.cli.steps.Prettier; +import com.diffplug.spotless.cli.steps.RemoveUnusedImports; import com.diffplug.spotless.cli.version.SpotlessCLIVersionProvider; import picocli.CommandLine; @@ -100,7 +101,8 @@ GoogleJavaFormat.class, LicenseHeader.class, PalantirJavaFormat.class, - Prettier.class + Prettier.class, + RemoveUnusedImports.class }) public class SpotlessCLI implements SpotlessAction, SpotlessCommand, SpotlessActionContextProvider { diff --git a/app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java b/app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java new file mode 100644 index 0000000..d559822 --- /dev/null +++ b/app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 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.cli.steps; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.cli.core.SpotlessActionContext; +import com.diffplug.spotless.cli.help.AdditionalInfoLinks; +import com.diffplug.spotless.cli.help.OptionConstants; +import com.diffplug.spotless.cli.help.SupportedFileTypes; +import com.diffplug.spotless.java.CleanthatJavaStep; +import com.diffplug.spotless.java.RemoveUnusedImportsStep; +import org.jetbrains.annotations.NotNull; +import picocli.CommandLine; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@CommandLine.Command(name = "remove-unused-imports", description = "Removes unused imports from Java files.") +@SupportedFileTypes("Java") +@AdditionalInfoLinks("https://github.com/diffplug/spotless/tree/main/plugin-gradle#removeunusedimports") +public class RemoveUnusedImports extends SpotlessFormatterStep { + + @CommandLine.Option( + names = {"--engine", "-e"}, + defaultValue = "GOOGLE_JAVA_FORMAT", + description = + "The backing engine to use for detecting and removing unused imports." + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX) + Engine engine; + + public enum Engine { + GOOGLE_JAVA_FORMAT { + @Override + String formatterName() { + return RemoveUnusedImportsStep.defaultFormatter(); + + } + }, + CLEAN_THAT { + @Override + String formatterName() { + return "cleanthat-javaparser-unnecessaryimport"; + } + }; + + + abstract String formatterName(); + } + + @Override + public @NotNull List prepareFormatterSteps(SpotlessActionContext context) { + return Collections.singletonList(RemoveUnusedImportsStep.create(engine.formatterName(), context.provisioner())); + } +} diff --git a/app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java b/app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java new file mode 100644 index 0000000..2f2c7aa --- /dev/null +++ b/app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java @@ -0,0 +1,64 @@ +package com.diffplug.spotless.cli.steps; + +import com.diffplug.spotless.cli.CLIIntegrationHarness; +import com.diffplug.spotless.tag.CliNativeTest; +import com.diffplug.spotless.tag.CliProcessTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@CliProcessTest +@CliNativeTest +class RemoveUnusedImportsTest extends CLIIntegrationHarness { + + @Test + void itRemovesUnusedImportsWithDefaultEngine() { + setFile("Java.java").toResource("java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test"); + + cliRunner() + .withTargets("Java.java") + .withStep(RemoveUnusedImports.class) + .run(); + + assertFile("Java.java") + .notSameSasResource("java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test") + .hasNotContent("Unused"); + + + selfie().expectResource("Java.java").toMatchDisk(); + } + + @Test + void itRemovesWithExplicitDefaultEngine() { + setFile("Java.java").toResource("java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test"); + + cliRunner() + .withTargets("Java.java") + .withStep(RemoveUnusedImports.class) + .withOption("--engine", "GOOGLE_JAVA_FORMAT") + .run(); + + assertFile("Java.java") + .notSameSasResource("java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test") + .hasNotContent("Unused"); + + selfie().expectResource("Java.java").toMatchDisk(); + } + + @Test + void itRemovesWithExplicitCleanThatEngine() { + setFile("Java.java").toResource("java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test"); + + cliRunner() + .withTargets("Java.java") + .withStep(RemoveUnusedImports.class) + .withOption("--engine", "CLEAN_THAT") + .run(); + + assertFile("Java.java") + .notSameSasResource("java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test") + .hasNotContent("Unused"); + + selfie().expectResource("Java.java").toMatchDisk(); + } +} diff --git a/app/src/test/resources/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.ss b/app/src/test/resources/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.ss new file mode 100644 index 0000000..3fbe6e3 --- /dev/null +++ b/app/src/test/resources/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.ss @@ -0,0 +1,52 @@ +╔═ itRemovesUnusedImportsWithDefaultEngine ═╗ +/* + * Some license stuff. + * Very official. + */ +package hello.world; + +import mylib.UsedB; +import mylib.UsedA; + +public class Java { +public static void main(String[] args) { +System.out.println("hello"); +UsedB.someMethod(); +UsedA.someMethod(); +} +} +╔═ itRemovesWithExplicitCleanThatEngine ═╗ +/* + * Some license stuff. + * Very official. + */ +package hello.world; + +import mylib.UsedB; +import mylib.UsedA; + +public class Java { +public static void main(String[] args) { +System.out.println("hello"); +UsedB.someMethod(); +UsedA.someMethod(); +} +} +╔═ itRemovesWithExplicitDefaultEngine ═╗ +/* + * Some license stuff. + * Very official. + */ +package hello.world; + +import mylib.UsedB; +import mylib.UsedA; + +public class Java { +public static void main(String[] args) { +System.out.println("hello"); +UsedB.someMethod(); +UsedA.someMethod(); +} +} +╔═ [end of file] ═╗ diff --git a/build-logic/src/main/groovy/com/diffplug/spotless/cli/picocli/usage/DocumentedUsages.groovy b/build-logic/src/main/groovy/com/diffplug/spotless/cli/picocli/usage/DocumentedUsages.groovy index 82e4233..3f6a62a 100644 --- a/build-logic/src/main/groovy/com/diffplug/spotless/cli/picocli/usage/DocumentedUsages.groovy +++ b/build-logic/src/main/groovy/com/diffplug/spotless/cli/picocli/usage/DocumentedUsages.groovy @@ -9,7 +9,8 @@ enum DocumentedUsages { GOOGLE_JAVA_FORMAT(), LICENSE_HEADER(), PALANTIR_JAVA_FORMAT(), - PRETTIER() + PRETTIER(), + REMOVE_UNUSED_IMPORTS(), private final String fileName diff --git a/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java b/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java index 17d3bd4..dd39cf8 100644 --- a/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java +++ b/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java @@ -201,47 +201,50 @@ private ReadAsserter(File file) { this.file = file; } - public void hasContent(String expected) { - hasContent(expected, StandardCharsets.UTF_8); + public ReadAsserter hasContent(String expected) { + return hasContent(expected, StandardCharsets.UTF_8); } - public void hasNotContent(String notExpected) { - notHasContent(notExpected, StandardCharsets.UTF_8); + public ReadAsserter hasNotContent(String notExpected) { + return notHasContent(notExpected, StandardCharsets.UTF_8); } - public void hasContent(String expected, Charset charset) { + public ReadAsserter hasContent(String expected, Charset charset) { assertThat(file).usingCharset(charset).hasContent(expected); + return this; } - public void notHasContent(String notExpected, Charset charset) { + public ReadAsserter notHasContent(String notExpected, Charset charset) { assertThat(file).usingCharset(charset).content().isNotEqualTo(notExpected); + return this; } - public void hasLines(String... lines) { - hasContent(String.join("\n", Arrays.asList(lines))); + public ReadAsserter hasLines(String... lines) { + return hasContent(String.join("\n", Arrays.asList(lines))); } - public void sameAsResource(String resource) { - hasContent(getTestResource(resource)); + public ReadAsserter sameAsResource(String resource) { + return hasContent(getTestResource(resource)); } - public void notSameSasResource(String resource) { - hasNotContent(getTestResource(resource)); + public ReadAsserter notSameSasResource(String resource) { + return hasNotContent(getTestResource(resource)); } - public void matches(Consumer> conditions) throws IOException { + public ReadAsserter matches(Consumer> conditions) throws IOException { String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); conditions.accept(assertThat(content)); + return this; } - public void sameAsFile(File otherFile) throws IOException { + public ReadAsserter sameAsFile(File otherFile) throws IOException { String otherFileContent = Files.readString(otherFile.toPath()); - hasContent(otherFileContent, StandardCharsets.UTF_8); + return hasContent(otherFileContent, StandardCharsets.UTF_8); } - public void notSameAsFile(File otherFile) throws IOException { + public ReadAsserter notSameAsFile(File otherFile) throws IOException { String otherFileContent = Files.readString(otherFile.toPath()); - notHasContent(otherFileContent, StandardCharsets.UTF_8); + return notHasContent(otherFileContent, StandardCharsets.UTF_8); } } diff --git a/testlib/src/main/resources/java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test b/testlib/src/main/resources/java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test new file mode 100644 index 0000000..615f5b4 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test @@ -0,0 +1,17 @@ +/* + * Some license stuff. + * Very official. + */ +package hello.world; + +import mylib.Unused; +import mylib.UsedB; +import mylib.UsedA; + +public class Java { +public static void main(String[] args) { +System.out.println("hello"); +UsedB.someMethod(); +UsedA.someMethod(); +} +} \ No newline at end of file From 85605ba30e50cecdd83d874b262b3a99f29c8c4a Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Wed, 30 Jul 2025 20:47:14 +0200 Subject: [PATCH 2/3] chore: work on remove unsed imports --- .../cli/steps/RemoveUnusedImports.java | 19 +++++++-------- .../cli/steps/RemoveUnusedImportsTest.java | 24 ++++++++++++++----- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java b/app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java index d559822..36899b6 100644 --- a/app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java +++ b/app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java @@ -15,20 +15,19 @@ */ package com.diffplug.spotless.cli.steps; +import java.util.Collections; +import java.util.List; + +import org.jetbrains.annotations.NotNull; + import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.cli.core.SpotlessActionContext; import com.diffplug.spotless.cli.help.AdditionalInfoLinks; import com.diffplug.spotless.cli.help.OptionConstants; import com.diffplug.spotless.cli.help.SupportedFileTypes; -import com.diffplug.spotless.java.CleanthatJavaStep; import com.diffplug.spotless.java.RemoveUnusedImportsStep; -import org.jetbrains.annotations.NotNull; -import picocli.CommandLine; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import picocli.CommandLine; @CommandLine.Command(name = "remove-unused-imports", description = "Removes unused imports from Java files.") @SupportedFileTypes("Java") @@ -38,8 +37,8 @@ public class RemoveUnusedImports extends SpotlessFormatterStep { @CommandLine.Option( names = {"--engine", "-e"}, defaultValue = "GOOGLE_JAVA_FORMAT", - description = - "The backing engine to use for detecting and removing unused imports." + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX) + description = "The backing engine to use for detecting and removing unused imports." + + OptionConstants.VALID_AND_DEFAULT_VALUES_SUFFIX) Engine engine; public enum Engine { @@ -47,7 +46,6 @@ public enum Engine { @Override String formatterName() { return RemoveUnusedImportsStep.defaultFormatter(); - } }, CLEAN_THAT { @@ -57,7 +55,6 @@ String formatterName() { } }; - abstract String formatterName(); } diff --git a/app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java b/app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java index 2f2c7aa..92a2ee4 100644 --- a/app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java +++ b/app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java @@ -1,9 +1,25 @@ +/* + * Copyright 2025 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.cli.steps; +import org.junit.jupiter.api.Test; + import com.diffplug.spotless.cli.CLIIntegrationHarness; import com.diffplug.spotless.tag.CliNativeTest; import com.diffplug.spotless.tag.CliProcessTest; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -15,16 +31,12 @@ class RemoveUnusedImportsTest extends CLIIntegrationHarness { void itRemovesUnusedImportsWithDefaultEngine() { setFile("Java.java").toResource("java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test"); - cliRunner() - .withTargets("Java.java") - .withStep(RemoveUnusedImports.class) - .run(); + cliRunner().withTargets("Java.java").withStep(RemoveUnusedImports.class).run(); assertFile("Java.java") .notSameSasResource("java/removeunusedimports/JavaCodeWithLicensePackageUnformatted.test") .hasNotContent("Unused"); - selfie().expectResource("Java.java").toMatchDisk(); } From 84d0b2b25da401179a3c4de37fa693b96dbdc956 Mon Sep 17 00:00:00 2001 From: Simon Gamma Date: Wed, 30 Jul 2025 20:39:06 +0200 Subject: [PATCH 3/3] docs: update readme for unused import --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8ad1d9c..f53598c 100644 --- a/README.md +++ b/README.md @@ -152,14 +152,15 @@ or apply the formatting to the files. -V, --version Print version information and exit. Available formatting steps: - clang-format Runs clang-format - clean-that CleanThat enables automatic refactoring of Java code. - format-annotations Corrects line break formatting of type annotations in - java files. - google-java-format Runs google java format - license-header Runs license header - palantir-java-format Runs palantir java format - prettier Runs prettier, the opinionated code formatter. + clang-format Runs clang-format + clean-that CleanThat enables automatic refactoring of Java code. + format-annotations Corrects line break formatting of type annotations in + java files. + google-java-format Runs google java format + license-header Runs license header + palantir-java-format Runs palantir java format + prettier Runs prettier, the opinionated code formatter. + remove-unused-imports Removes unused imports from Java files. Possible exit codes: 0 Successful formatting.