Skip to content

Commit e8d321d

Browse files
authored
feat: add formatter clean-that (#5)
2 parents a3a4104 + fe2aeb7 commit e8d321d

File tree

11 files changed

+540
-0
lines changed

11 files changed

+540
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Added
1515

1616
- Commit, tag and push the choco source files to the chocolatey-bucket repository during the release process
17+
- Added formatter [`clean-that`](https://github.com/diffplug/spotless/tree/main/plugin-gradle#cleanthat)
1718

1819
## [0.1.1] - 2025-06-02
1920

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ or apply the formatting to the files.
153153
154154
Available formatting steps:
155155
clang-format Runs clang-format
156+
clean-that CleanThat enables automatic refactoring of Java code.
156157
format-annotations Corrects line break formatting of type annotations in
157158
java files.
158159
google-java-format Runs google java format
@@ -183,6 +184,7 @@ Possible exit codes:
183184
Spotless CLI supports the following formatter steps in alphabetical order:
184185

185186
- [clang-format](#clang-format)
187+
- [clean-that](#clean-that)
186188
- [format-annotations](#format-annotations)
187189
- [google-java-format](#google-java-format)
188190
- [license-header](#license-header)
@@ -240,6 +242,73 @@ spotless --target '**/src/**/*.cpp' clang-format --clang-version=20.1.2 --style=
240242
> [!IMPORTANT]
241243
> Running a clang-format step requires a working installation of the clang-format binary.
242244
245+
### clean-that
246+
247+
<!---freshmark ctshields
248+
output = [
249+
link(shield('CleanThat version', 'clean-that', '{{libs.versions.native.include.cleanThat}}', 'blue'), 'https://github.com/solven-eu/cleanthat'),
250+
].join('\n')
251+
-->
252+
253+
[![CleanThat version](https://img.shields.io/badge/clean--that-2.23-blue.svg)](https://github.com/solven-eu/cleanthat)
254+
255+
<!---freshmark /ctshields -->
256+
257+
Cleanthat is a project enabling automatic code cleaning, from formatting to refactoring.
258+
259+
To see usage instructions for the clean-that formatter, run: `spotless clean-that --help`
260+
261+
<!---freshmark usage_clean_that
262+
output =
263+
'```\n' +
264+
{{usage.clean-that.array}}.join('\n') +
265+
'\n```';
266+
-->
267+
268+
```
269+
Usage: spotless clean-that [-dDhV] [-s=<sourceCompatibility>] [-a[=mutator[,
270+
mutator...]...]]... [-e[=mutator[,mutator...]...]]...
271+
CleanThat enables automatic refactoring of Java code.
272+
-a, --add-mutator[=mutator[,mutator...]...]
273+
Add a mutator to the list of mutators to use. Mutators are
274+
the individual refactoring steps CleanThat applies. A list
275+
of available mutators can be found in the "Additional Info"
276+
section.
277+
-d, --use-default-mutators
278+
Use the default mutators provided by CleanThat. Default
279+
mutators are: <SafeAndConsensual>.
280+
(default: true)
281+
-D, --include-draft-mutators
282+
Include draft mutators in the list of mutators to use. Draft
283+
mutators are experimental and may not be fully tested or
284+
stable.
285+
(default: false)
286+
-e, --exclude-mutator[=mutator[,mutator...]...]
287+
Remove a mutator from the list of mutators to use. This might
288+
make sense for composite mutators
289+
-h, --help Show this help message and exit.
290+
-s, --source-compatibility=<sourceCompatibility>
291+
The source JDK version to use for the CleanThat mutators.
292+
This is used to determine the Java language features
293+
available.
294+
(default: 1.8)
295+
-V, --version Print version information and exit.
296+
297+
✅ This step supports the following file type: Java
298+
299+
🌎 Additional info:
300+
* https://github.com/solven-eu/cleanthat
301+
* https://github.com/solven-eu/cleanthat/blob/master/MUTATORS.generated.MD
302+
```
303+
304+
<!---freshmark /usage_clean_that -->
305+
306+
Example usage:
307+
308+
```shell
309+
spotless --target '**/src/**/*.java' clean-that --exclude-mutator=StreamAnyMatch
310+
```
311+
243312
### format-annotations
244313

245314
In Java, type annotations should be on the same line as the type that they qualify. This formatter fixes this for you.

app/src/main/java/com/diffplug/spotless/cli/SpotlessCLI.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import com.diffplug.spotless.cli.logging.output.LoggingConfigurer;
4545
import com.diffplug.spotless.cli.logging.output.Output;
4646
import com.diffplug.spotless.cli.steps.ClangFormat;
47+
import com.diffplug.spotless.cli.steps.CleanThat;
4748
import com.diffplug.spotless.cli.steps.FormatAnnotations;
4849
import com.diffplug.spotless.cli.steps.GoogleJavaFormat;
4950
import com.diffplug.spotless.cli.steps.LicenseHeader;
@@ -94,6 +95,7 @@
9495
subcommandsRepeatable = true,
9596
subcommands = {
9697
ClangFormat.class,
98+
CleanThat.class,
9799
FormatAnnotations.class,
98100
GoogleJavaFormat.class,
99101
LicenseHeader.class,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2025 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.cli.steps;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.Objects;
22+
23+
import org.jetbrains.annotations.NotNull;
24+
25+
import com.diffplug.spotless.FormatterStep;
26+
import com.diffplug.spotless.cli.core.SpotlessActionContext;
27+
import com.diffplug.spotless.cli.help.AdditionalInfoLinks;
28+
import com.diffplug.spotless.cli.help.OptionConstants;
29+
import com.diffplug.spotless.cli.help.SupportedFileTypes;
30+
import com.diffplug.spotless.java.CleanthatJavaStep;
31+
32+
import picocli.CommandLine;
33+
34+
@CommandLine.Command(name = "clean-that", description = "CleanThat enables automatic refactoring of Java code.")
35+
@SupportedFileTypes("Java")
36+
@AdditionalInfoLinks({
37+
"https://github.com/solven-eu/cleanthat",
38+
"https://github.com/solven-eu/cleanthat/blob/master/MUTATORS.generated.MD"
39+
})
40+
public class CleanThat extends SpotlessFormatterStep {
41+
42+
public static final String DEFAULT_MUTATORS = String.join(", ", CleanthatJavaStep.defaultMutators());
43+
44+
static {
45+
// workaround for dynamic property resolution in help messages
46+
System.setProperty("usage.cleanthat.defaultMutators", DEFAULT_MUTATORS);
47+
}
48+
49+
@CommandLine.Option(
50+
names = {"--use-default-mutators", "-d"},
51+
defaultValue = "true",
52+
description =
53+
"Use the default mutators provided by CleanThat. Default mutators are: <${usage.cleanthat.defaultMutators}>."
54+
+ OptionConstants.DEFAULT_VALUE_SUFFIX)
55+
boolean useDefaultMutators;
56+
57+
@CommandLine.Option(
58+
names = {"--add-mutator", "-a"},
59+
arity = "0..*",
60+
split = OptionConstants.OPTION_LIST_SPLIT,
61+
paramLabel = "mutator",
62+
description =
63+
"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. ")
64+
List<String> addMutators;
65+
66+
@CommandLine.Option(
67+
names = {"--exclude-mutator", "-e"},
68+
arity = "0..*",
69+
split = OptionConstants.OPTION_LIST_SPLIT,
70+
paramLabel = "mutator",
71+
description =
72+
"Remove a mutator from the list of mutators to use. This might make sense for composite mutators")
73+
List<String> excludeMutators;
74+
75+
@CommandLine.Option(
76+
names = {"--include-draft-mutators", "-D"},
77+
defaultValue = "false",
78+
description =
79+
"Include draft mutators in the list of mutators to use. Draft mutators are experimental and may not be fully tested or stable."
80+
+ OptionConstants.DEFAULT_VALUE_SUFFIX)
81+
boolean includeDraftMutators;
82+
83+
@CommandLine.Option(
84+
names = {"--source-compatibility", "-s"},
85+
defaultValue = "1.8",
86+
description =
87+
"The source JDK version to use for the CleanThat mutators. This is used to determine the Java language features available."
88+
+ OptionConstants.DEFAULT_VALUE_SUFFIX)
89+
String sourceCompatibility;
90+
91+
@Override
92+
public @NotNull List<FormatterStep> prepareFormatterSteps(SpotlessActionContext context) {
93+
return Collections.singletonList(CleanthatJavaStep.create(
94+
CleanthatJavaStep.defaultGroupArtifact(),
95+
CleanthatJavaStep.defaultVersion(),
96+
this.sourceCompatibility,
97+
includedMutators(),
98+
excludedMutators(),
99+
this.includeDraftMutators,
100+
context.provisioner()));
101+
}
102+
103+
private List<String> includedMutators() {
104+
List<String> mutators = new ArrayList<>();
105+
if (useDefaultMutators) {
106+
mutators.addAll(CleanthatJavaStep.defaultMutators());
107+
}
108+
if (addMutators != null) {
109+
mutators.addAll(addMutators.stream()
110+
.filter(Objects::nonNull)
111+
.map(String::trim)
112+
.filter(s -> !s.isEmpty())
113+
.toList());
114+
}
115+
return mutators;
116+
}
117+
118+
private List<String> excludedMutators() {
119+
if (excludeMutators == null) {
120+
return Collections.emptyList();
121+
}
122+
return excludeMutators.stream()
123+
.filter(Objects::nonNull)
124+
.map(String::trim)
125+
.filter(s -> !s.isEmpty())
126+
.toList();
127+
}
128+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2025 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.cli.steps;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import com.diffplug.spotless.cli.CLIIntegrationHarness;
24+
import com.diffplug.spotless.tag.CliNativeTest;
25+
import com.diffplug.spotless.tag.CliProcessTest;
26+
27+
@CliProcessTest
28+
@CliNativeTest
29+
public class CleanThatTest extends CLIIntegrationHarness {
30+
31+
@Test
32+
void itRunsWithDefaultOptions() {
33+
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");
34+
35+
cliRunner().withTargets("Test.java").withStep(CleanThat.class).run();
36+
37+
assertFile("Test.java").notSameSasResource("java/cleanthat/MultipleMutators.dirty.test");
38+
selfie().expectResource("Test.java").toMatchDisk();
39+
}
40+
41+
@Test
42+
void itLetsDisableDefaultMutators() {
43+
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");
44+
45+
cliRunner()
46+
.withTargets("Test.java")
47+
.withStep(CleanThat.class)
48+
.withOption("--use-default-mutators", "false")
49+
.run();
50+
51+
assertFile("Test.java").sameAsResource("java/cleanthat/MultipleMutators.dirty.test");
52+
}
53+
54+
@Test
55+
void itLetsEnableSpecificMutators() {
56+
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");
57+
58+
cliRunner()
59+
.withTargets("Test.java")
60+
.withStep(CleanThat.class)
61+
.withOption("--use-default-mutators", "false")
62+
.withOption("--add-mutator", "LiteralsFirstInComparisons")
63+
.run();
64+
65+
assertFile("Test.java").notSameSasResource("java/cleanthat/MultipleMutators.dirty.test");
66+
selfie().expectResource("Test.java").toMatchDisk();
67+
}
68+
69+
@Test
70+
void itLetsDisableSpecificMutators() {
71+
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");
72+
73+
cliRunner()
74+
.withTargets("Test.java")
75+
.withStep(CleanThat.class)
76+
.withOption("--exclude-mutator", "StreamAnyMatch")
77+
.run();
78+
79+
assertFile("Test.java").sameAsResource("java/cleanthat/MultipleMutators.dirty.test");
80+
}
81+
82+
@Test
83+
void itLetsEnableDraftMutators() throws IOException {
84+
File file1 = setFile("Test.java")
85+
.toResource("java/cleanthat/MultipleMutators.dirty.test")
86+
.getFile();
87+
File file2 = setFile("Test2.java")
88+
.toResource("java/cleanthat/MultipleMutators.dirty.test")
89+
.getFile();
90+
91+
cliRunner()
92+
.withTargets("Test.java")
93+
.withStep(CleanThat.class)
94+
.withOption("--add-mutator", "RemoveAllToClearCollection")
95+
.run();
96+
97+
var result = cliRunner()
98+
.withTargets("Test2.java")
99+
.withStep(CleanThat.class)
100+
.withOption("--include-draft-mutators", "true")
101+
.withOption("--add-mutator", "RemoveAllToClearCollection")
102+
.run();
103+
104+
assertFile("Test.java").notSameSasResource("java/cleanthat/MultipleMutators.dirty.test");
105+
selfie().expectResource("Test.java").toMatchDisk("excluding draft mutators");
106+
selfie().expectResource("Test2.java").toMatchDisk("including draft mutators");
107+
108+
// these outcomes should be different, but they are not, problably a upstream issue in CleanThat
109+
// assertFile(file1).notSameAsFile(file2);
110+
}
111+
112+
@Test
113+
void itCanExecuteAllMutators() {
114+
setFile("Test.java").toResource("java/cleanthat/MultipleMutators.dirty.test");
115+
116+
cliRunner()
117+
.withTargets("Test.java")
118+
.withStep(CleanThat.class)
119+
.withOption("--add-mutator", "AllIncludingDraftSingleMutators")
120+
.withOption("--include-draft-mutators", "true")
121+
.run();
122+
123+
selfie().expectResource("Test.java").toMatchDisk();
124+
}
125+
}

0 commit comments

Comments
 (0)