Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- 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)

## [0.1.1] - 2025-06-02

Expand Down
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ or apply the formatting to the files.

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
Expand Down Expand Up @@ -183,6 +184,7 @@ Possible exit codes:
Spotless CLI supports the following formatter steps in alphabetical order:

- [clang-format](#clang-format)
- [clean-that](#clean-that)
- [format-annotations](#format-annotations)
- [google-java-format](#google-java-format)
- [license-header](#license-header)
Expand Down Expand Up @@ -240,6 +242,73 @@ spotless --target '**/src/**/*.cpp' clang-format --clang-version=20.1.2 --style=
> [!IMPORTANT]
> Running a clang-format step requires a working installation of the clang-format binary.

### clean-that

<!---freshmark ctshields
output = [
link(shield('CleanThat version', 'clean-that', '{{libs.versions.native.include.cleanThat}}', 'blue'), 'https://github.com/solven-eu/cleanthat'),
].join('\n')
-->

[![CleanThat version](https://img.shields.io/badge/clean--that-2.23-blue.svg)](https://github.com/solven-eu/cleanthat)

<!---freshmark /ctshields -->

Cleanthat is a project enabling automatic code cleaning, from formatting to refactoring.

To see usage instructions for the clean-that formatter, run: `spotless clean-that --help`

<!---freshmark usage_clean_that
output =
'```\n' +
{{usage.clean-that.array}}.join('\n') +
'\n```';
-->

```
Usage: spotless clean-that [-dDhV] [-s=<sourceCompatibility>] [-a[=mutator[,
mutator...]...]]... [-e[=mutator[,mutator...]...]]...
CleanThat enables automatic refactoring of Java code.
-a, --add-mutator[=mutator[,mutator...]...]
Add a mutator to the list of mutators to use. Mutators are
the individual refactoring steps CleanThat applies. A list
of available mutators can be found in the "Additional Info"
section.
-d, --use-default-mutators
Use the default mutators provided by CleanThat. Default
mutators are: <SafeAndConsensual>.
(default: true)
-D, --include-draft-mutators
Include draft mutators in the list of mutators to use. Draft
mutators are experimental and may not be fully tested or
stable.
(default: false)
-e, --exclude-mutator[=mutator[,mutator...]...]
Remove a mutator from the list of mutators to use. This might
make sense for composite mutators
-h, --help Show this help message and exit.
-s, --source-compatibility=<sourceCompatibility>
The source JDK version to use for the CleanThat mutators.
This is used to determine the Java language features
available.
(default: 1.8)
-V, --version Print version information and exit.

✅ This step supports the following file type: Java

🌎 Additional info:
* https://github.com/solven-eu/cleanthat
* https://github.com/solven-eu/cleanthat/blob/master/MUTATORS.generated.MD
```

<!---freshmark /usage_clean_that -->

Example usage:

```shell
spotless --target '**/src/**/*.java' clean-that --exclude-mutator=StreamAnyMatch
```

### format-annotations

In Java, type annotations should be on the same line as the type that they qualify. This formatter fixes this for you.
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.diffplug.spotless.cli.logging.output.LoggingConfigurer;
import com.diffplug.spotless.cli.logging.output.Output;
import com.diffplug.spotless.cli.steps.ClangFormat;
import com.diffplug.spotless.cli.steps.CleanThat;
import com.diffplug.spotless.cli.steps.FormatAnnotations;
import com.diffplug.spotless.cli.steps.GoogleJavaFormat;
import com.diffplug.spotless.cli.steps.LicenseHeader;
Expand Down Expand Up @@ -94,6 +95,7 @@
subcommandsRepeatable = true,
subcommands = {
ClangFormat.class,
CleanThat.class,
FormatAnnotations.class,
GoogleJavaFormat.class,
LicenseHeader.class,
Expand Down
128 changes: 128 additions & 0 deletions app/src/main/java/com/diffplug/spotless/cli/steps/CleanThat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

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 picocli.CommandLine;

@CommandLine.Command(name = "clean-that", description = "CleanThat enables automatic refactoring of Java code.")
@SupportedFileTypes("Java")
@AdditionalInfoLinks({
"https://github.com/solven-eu/cleanthat",
"https://github.com/solven-eu/cleanthat/blob/master/MUTATORS.generated.MD"
})
public class CleanThat extends SpotlessFormatterStep {

public static final String DEFAULT_MUTATORS = String.join(", ", CleanthatJavaStep.defaultMutators());

static {
// workaround for dynamic property resolution in help messages
System.setProperty("usage.cleanthat.defaultMutators", DEFAULT_MUTATORS);
}

@CommandLine.Option(
names = {"--use-default-mutators", "-d"},
defaultValue = "true",
description =
"Use the default mutators provided by CleanThat. Default mutators are: <${usage.cleanthat.defaultMutators}>."
+ OptionConstants.DEFAULT_VALUE_SUFFIX)
boolean useDefaultMutators;

@CommandLine.Option(
names = {"--add-mutator", "-a"},
arity = "0..*",
split = OptionConstants.OPTION_LIST_SPLIT,
paramLabel = "mutator",
description =
"Add a mutator to the list of mutators to use. Mutators are the individual refactoring steps CleanThat applies. A list of available mutators can be found in the \"Additional Info\" section. ")
List<String> addMutators;

@CommandLine.Option(
names = {"--exclude-mutator", "-e"},
arity = "0..*",
split = OptionConstants.OPTION_LIST_SPLIT,
paramLabel = "mutator",
description =
"Remove a mutator from the list of mutators to use. This might make sense for composite mutators")
List<String> excludeMutators;

@CommandLine.Option(
names = {"--include-draft-mutators", "-D"},
defaultValue = "false",
description =
"Include draft mutators in the list of mutators to use. Draft mutators are experimental and may not be fully tested or stable."
+ OptionConstants.DEFAULT_VALUE_SUFFIX)
boolean includeDraftMutators;

@CommandLine.Option(
names = {"--source-compatibility", "-s"},
defaultValue = "1.8",
description =
"The source JDK version to use for the CleanThat mutators. This is used to determine the Java language features available."
+ OptionConstants.DEFAULT_VALUE_SUFFIX)
String sourceCompatibility;

@Override
public @NotNull List<FormatterStep> prepareFormatterSteps(SpotlessActionContext context) {
return Collections.singletonList(CleanthatJavaStep.create(
CleanthatJavaStep.defaultGroupArtifact(),
CleanthatJavaStep.defaultVersion(),
this.sourceCompatibility,
includedMutators(),
excludedMutators(),
this.includeDraftMutators,
context.provisioner()));
}

private List<String> includedMutators() {
List<String> mutators = new ArrayList<>();
if (useDefaultMutators) {
mutators.addAll(CleanthatJavaStep.defaultMutators());
}
if (addMutators != null) {
mutators.addAll(addMutators.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList());
}
return mutators;
}

private List<String> excludedMutators() {
if (excludeMutators == null) {
return Collections.emptyList();
}
return excludeMutators.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList();
}
}
125 changes: 125 additions & 0 deletions app/src/test/java/com/diffplug/spotless/cli/steps/CleanThatTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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.io.File;
import java.io.IOException;

import org.junit.jupiter.api.Test;

import com.diffplug.spotless.cli.CLIIntegrationHarness;
import com.diffplug.spotless.tag.CliNativeTest;
import com.diffplug.spotless.tag.CliProcessTest;

@CliProcessTest
@CliNativeTest
public class CleanThatTest extends CLIIntegrationHarness {

@Test
void itRunsWithDefaultOptions() {
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");

cliRunner().withTargets("Test.java").withStep(CleanThat.class).run();

assertFile("Test.java").notSameSasResource("java/cleanthat/MultipleMutators.dirty.test");
selfie().expectResource("Test.java").toMatchDisk();
}

@Test
void itLetsDisableDefaultMutators() {
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");

cliRunner()
.withTargets("Test.java")
.withStep(CleanThat.class)
.withOption("--use-default-mutators", "false")
.run();

assertFile("Test.java").sameAsResource("java/cleanthat/MultipleMutators.dirty.test");
}

@Test
void itLetsEnableSpecificMutators() {
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");

cliRunner()
.withTargets("Test.java")
.withStep(CleanThat.class)
.withOption("--use-default-mutators", "false")
.withOption("--add-mutator", "LiteralsFirstInComparisons")
.run();

assertFile("Test.java").notSameSasResource("java/cleanthat/MultipleMutators.dirty.test");
selfie().expectResource("Test.java").toMatchDisk();
}

@Test
void itLetsDisableSpecificMutators() {
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");

cliRunner()
.withTargets("Test.java")
.withStep(CleanThat.class)
.withOption("--exclude-mutator", "StreamAnyMatch")
.run();

assertFile("Test.java").sameAsResource("java/cleanthat/MultipleMutators.dirty.test");
}

@Test
void itLetsEnableDraftMutators() throws IOException {
File file1 = setFile("Test.java")
.toResource("java/cleanthat/MultipleMutators.dirty.test")
.getFile();
File file2 = setFile("Test2.java")
.toResource("java/cleanthat/MultipleMutators.dirty.test")
.getFile();

cliRunner()
.withTargets("Test.java")
.withStep(CleanThat.class)
.withOption("--add-mutator", "RemoveAllToClearCollection")
.run();

var result = cliRunner()
.withTargets("Test2.java")
.withStep(CleanThat.class)
.withOption("--include-draft-mutators", "true")
.withOption("--add-mutator", "RemoveAllToClearCollection")
.run();

assertFile("Test.java").notSameSasResource("java/cleanthat/MultipleMutators.dirty.test");
selfie().expectResource("Test.java").toMatchDisk("excluding draft mutators");
selfie().expectResource("Test2.java").toMatchDisk("including draft mutators");

// these outcomes should be different, but they are not, problably a upstream issue in CleanThat
// assertFile(file1).notSameAsFile(file2);
}

@Test
void itCanExecuteAllMutators() {
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");

cliRunner()
.withTargets("Test.java")
.withStep(CleanThat.class)
.withOption("--add-mutator", "AllIncludingDraftSingleMutators")
.withOption("--include-draft-mutators", "true")
.run();

selfie().expectResource("Test.java").toMatchDisk();
}
}
Loading
Loading