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/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. 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..36899b6 --- /dev/null +++ b/app/src/main/java/com/diffplug/spotless/cli/steps/RemoveUnusedImports.java @@ -0,0 +1,65 @@ +/* + * 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 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.RemoveUnusedImportsStep; + +import picocli.CommandLine; + +@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..92a2ee4 --- /dev/null +++ b/app/src/test/java/com/diffplug/spotless/cli/steps/RemoveUnusedImportsTest.java @@ -0,0 +1,76 @@ +/* + * 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 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