Skip to content

Commit f53ded7

Browse files
authored
Introduce cleanthat refactorer (#1560)
2 parents 3226d2b + fdee03c commit f53ded7

File tree

20 files changed

+656
-4
lines changed

20 files changed

+656
-4
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
### Added
14+
* CleanThat Java Refactorer ([#???](https://github.com/diffplug/spotless/pull/???))
1315

1416
## [2.34.1] - 2023-02-05
1517
### Changes

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ lib('java.PalantirJavaFormatStep') +'{{yes}} | {{yes}}
8484
lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
8585
extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
8686
lib('java.FormatAnnotationsStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
87+
lib('java.CleanthatJavaStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
8788
lib('json.gson.GsonStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
8889
lib('json.JacksonJsonStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
8990
lib('json.JsonSimpleStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
@@ -131,6 +132,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}}
131132
| [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
132133
| [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
133134
| [`java.FormatAnnotationsStep`](lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
135+
| [`java.CleanthatJavaStep`](lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
134136
| [`json.gson.GsonStep`](lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
135137
| [`json.JacksonJsonStep`](lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
136138
| [`json.JsonSimpleStep`](lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |

lib/build.gradle

+11-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ def NEEDS_GLUE = [
1616
'diktat',
1717
'scalafmt',
1818
'jackson',
19-
'gson'
19+
'gson',
20+
'cleanthat'
2021
]
2122
for (glue in NEEDS_GLUE) {
2223
sourceSets.register(glue) {
@@ -39,6 +40,12 @@ versionCompatibility {
3940
]
4041
targetSourceSetName = 'ktlint'
4142
}
43+
namespaces.register('Cleanthat') {
44+
versions = [
45+
'2.1',
46+
]
47+
targetSourceSetName = 'cleanthat'
48+
}
4249
}
4350
}
4451

@@ -100,6 +107,9 @@ dependencies {
100107
flexmarkCompileOnly 'com.vladsch.flexmark:flexmark-all:0.62.2'
101108

102109
gsonCompileOnly 'com.google.code.gson:gson:2.10.1'
110+
111+
cleanthatCompileOnly 'io.github.solven-eu.cleanthat:java:2.1'
112+
compatCleanthat2Dot1CompileAndTestOnly 'io.github.solven-eu.cleanthat:java:2.1'
103113
}
104114

105115
// we'll hold the core lib to a high standard
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2023 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.glue.java;
17+
18+
import java.io.IOException;
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.List;
22+
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
import com.diffplug.spotless.FormatterFunc;
27+
28+
import eu.solven.cleanthat.config.pojo.CleanthatEngineProperties;
29+
import eu.solven.cleanthat.config.pojo.SourceCodeProperties;
30+
import eu.solven.cleanthat.engine.java.IJdkVersionConstants;
31+
import eu.solven.cleanthat.engine.java.refactorer.JavaRefactorer;
32+
import eu.solven.cleanthat.engine.java.refactorer.JavaRefactorerProperties;
33+
import eu.solven.cleanthat.formatter.LineEnding;
34+
35+
/**
36+
* The glue for CleanThat: it is build over the version in build.gradle, but at runtime it will be executed over
37+
* the version loaded in JarState, which is by default defined in com.diffplug.spotless.java.CleanthatJavaStep#JVM_SUPPORT
38+
*/
39+
public class JavaCleanthatRefactorerFunc implements FormatterFunc {
40+
private static final Logger LOGGER = LoggerFactory.getLogger(JavaCleanthatRefactorerFunc.class);
41+
42+
private String jdkVersion;
43+
private List<String> included;
44+
private List<String> excluded;
45+
46+
public JavaCleanthatRefactorerFunc(String jdkVersion, List<String> included, List<String> excluded) {
47+
this.jdkVersion = jdkVersion == null ? IJdkVersionConstants.JDK_8 : jdkVersion;
48+
this.included = included == null ? Collections.emptyList() : included;
49+
this.excluded = excluded == null ? Collections.emptyList() : excluded;
50+
}
51+
52+
public JavaCleanthatRefactorerFunc() {
53+
this(IJdkVersionConstants.JDK_8, Arrays.asList(JavaRefactorerProperties.WILDCARD), Arrays.asList());
54+
}
55+
56+
@Override
57+
public String apply(String input) throws Exception {
58+
// https://stackoverflow.com/questions/1771679/difference-between-threads-context-class-loader-and-normal-classloader
59+
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
60+
try {
61+
// Ensure CleanThat main Thread has its custom classLoader while executing its refactoring
62+
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
63+
return doApply(input);
64+
} finally {
65+
// Restore the originalClassLoader
66+
Thread.currentThread().setContextClassLoader(originalClassLoader);
67+
}
68+
}
69+
70+
private String doApply(String input) throws InterruptedException, IOException {
71+
// call some API that uses reflection without taking ClassLoader param
72+
CleanthatEngineProperties engineProperties = CleanthatEngineProperties.builder().engineVersion(jdkVersion).build();
73+
74+
// Spotless will push us LF content
75+
engineProperties.setSourceCode(SourceCodeProperties.builder().lineEnding(LineEnding.LF).build());
76+
77+
JavaRefactorerProperties refactorerProperties = new JavaRefactorerProperties();
78+
79+
refactorerProperties.setIncluded(included);
80+
refactorerProperties.setExcluded(excluded);
81+
82+
JavaRefactorer refactorer = new JavaRefactorer(engineProperties, refactorerProperties);
83+
84+
LOGGER.debug("Processing sourceJdk={} included={} excluded={}", jdkVersion, included, excluded);
85+
LOGGER.debug("Available mutators: {}", JavaRefactorer.getAllIncluded());
86+
87+
// Spotless calls steps always with LF eol.
88+
return refactorer.doFormat(input, LineEnding.LF);
89+
}
90+
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2023 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.java;
17+
18+
import java.io.IOException;
19+
import java.io.Serializable;
20+
import java.lang.reflect.Constructor;
21+
import java.lang.reflect.Method;
22+
import java.util.List;
23+
import java.util.Objects;
24+
25+
import com.diffplug.spotless.FormatterFunc;
26+
import com.diffplug.spotless.FormatterStep;
27+
import com.diffplug.spotless.JarState;
28+
import com.diffplug.spotless.Jvm;
29+
import com.diffplug.spotless.Provisioner;
30+
31+
/**
32+
* Enables CleanThat as a SpotLess step.
33+
*
34+
* @author Benoit Lacelle
35+
*/
36+
// https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md#how-to-add-a-new-formatterstep
37+
public final class CleanthatJavaStep {
38+
39+
private static final String NAME = "cleanthat";
40+
private static final String MAVEN_COORDINATE = "io.github.solven-eu.cleanthat:java";
41+
42+
// CleanThat changelog is available at https://github.com/solven-eu/cleanthat/blob/master/CHANGES.MD
43+
private static final Jvm.Support<String> JVM_SUPPORT = Jvm.<String> support(NAME).add(11, "2.1");
44+
45+
// prevent direct instantiation
46+
private CleanthatJavaStep() {}
47+
48+
/** Creates a step which apply default CleanThat mutators. */
49+
public static FormatterStep create(Provisioner provisioner) {
50+
return create(defaultVersion(), provisioner);
51+
}
52+
53+
/** Creates a step which apply default CleanThat mutators. */
54+
public static FormatterStep create(String version, Provisioner provisioner) {
55+
return create(MAVEN_COORDINATE, version, defaultSourceJdk(), defaultExcludedMutators(), defaultMutators(), provisioner);
56+
}
57+
58+
public static String defaultSourceJdk() {
59+
// see IJdkVersionConstants.JDK_7
60+
// https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#source
61+
// 1.7 is the default for 'maven-compiler-plugin' since 3.9.0
62+
return "1.7";
63+
}
64+
65+
public static List<String> defaultExcludedMutators() {
66+
return List.of();
67+
}
68+
69+
/**
70+
* By default, we include all available rules
71+
* @return
72+
*/
73+
public static List<String> defaultMutators() {
74+
// see JavaRefactorerProperties.WILDCARD
75+
return List.of("*");
76+
}
77+
78+
/** Creates a step which apply selected CleanThat mutators. */
79+
public static FormatterStep create(String groupArtifact,
80+
String version,
81+
String sourceJdkVersion,
82+
List<String> excluded,
83+
List<String> included,
84+
Provisioner provisioner) {
85+
Objects.requireNonNull(groupArtifact, "groupArtifact");
86+
if (groupArtifact.chars().filter(ch -> ch == ':').count() != 1) {
87+
throw new IllegalArgumentException("groupArtifact must be in the form 'groupId:artifactId'. it was: " + groupArtifact);
88+
}
89+
Objects.requireNonNull(version, "version");
90+
Objects.requireNonNull(provisioner, "provisioner");
91+
return FormatterStep.createLazy(NAME,
92+
() -> new JavaRefactorerState(NAME, groupArtifact, version, sourceJdkVersion, excluded, included, provisioner),
93+
JavaRefactorerState::createFormat);
94+
}
95+
96+
/** Get default formatter version */
97+
public static String defaultVersion() {
98+
return JVM_SUPPORT.getRecommendedFormatterVersion();
99+
}
100+
101+
public static String defaultGroupArtifact() {
102+
return MAVEN_COORDINATE;
103+
}
104+
105+
static final class JavaRefactorerState implements Serializable {
106+
private static final long serialVersionUID = 1L;
107+
108+
final JarState jarState;
109+
final String stepName;
110+
final String version;
111+
112+
final String sourceJdkVersion;
113+
final List<String> included;
114+
final List<String> excluded;
115+
116+
JavaRefactorerState(String stepName, String version, Provisioner provisioner) throws IOException {
117+
this(stepName, MAVEN_COORDINATE, version, defaultSourceJdk(), defaultExcludedMutators(), defaultMutators(), provisioner);
118+
}
119+
120+
JavaRefactorerState(String stepName,
121+
String groupArtifact,
122+
String version,
123+
String sourceJdkVersion,
124+
List<String> included,
125+
List<String> excluded,
126+
Provisioner provisioner) throws IOException {
127+
JVM_SUPPORT.assertFormatterSupported(version);
128+
ModuleHelper.doOpenInternalPackagesIfRequired();
129+
this.jarState = JarState.from(groupArtifact + ":" + version, provisioner);
130+
this.stepName = stepName;
131+
this.version = version;
132+
133+
this.sourceJdkVersion = sourceJdkVersion;
134+
this.included = included;
135+
this.excluded = excluded;
136+
}
137+
138+
@SuppressWarnings("PMD.UseProperClassLoader")
139+
FormatterFunc createFormat() {
140+
ClassLoader classLoader = jarState.getClassLoader();
141+
142+
Object formatter;
143+
Method formatterMethod;
144+
try {
145+
Class<?> formatterClazz = classLoader.loadClass("com.diffplug.spotless.glue.java.JavaCleanthatRefactorerFunc");
146+
Constructor<?> formatterConstructor = formatterClazz.getConstructor(String.class, List.class, List.class);
147+
148+
formatter = formatterConstructor.newInstance(sourceJdkVersion, included, excluded);
149+
formatterMethod = formatterClazz.getMethod("apply", String.class);
150+
} catch (ReflectiveOperationException e) {
151+
throw new IllegalStateException("Issue executing the formatter", e);
152+
}
153+
return JVM_SUPPORT.suggestLaterVersionOnError(version, input -> {
154+
return (String) formatterMethod.invoke(formatter, input);
155+
});
156+
}
157+
158+
}
159+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2023 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.glue.java;
17+
18+
import org.assertj.core.api.Assertions;
19+
import org.junit.jupiter.api.Test;
20+
21+
import eu.solven.cleanthat.engine.java.refactorer.JavaRefactorer;
22+
23+
public class JavaCleanthatRefactorerFuncTest {
24+
@Test
25+
public void testMutatorsDetection() {
26+
Assertions.assertThat(JavaRefactorer.getAllIncluded()).isNotEmpty();
27+
}
28+
}

plugin-gradle/CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* CleanThat Java Refactorer ([#1560](https://github.com/diffplug/spotless/pull/1560))
68

79
## [6.14.1] - 2023-02-05
810
### Fixed

plugin-gradle/README.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
5454
- [**Quickstart**](#quickstart)
5555
- [Requirements](#requirements)
5656
- **Languages**
57-
- [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations))
57+
- [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat))
5858
- [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy))
5959
- [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [prettier](#prettier))
6060
- [Scala](#scala) ([scalafmt](#scalafmt))
@@ -154,6 +154,9 @@ spotless {
154154
155155
removeUnusedImports()
156156
157+
// Cleanthat will refactor your code, but it may break your style: apply it before your formatter
158+
cleanthat() // has its own section below
159+
157160
// Choose one of these formatters.
158161
googleJavaFormat() // has its own section below
159162
eclipse() // has its own section below
@@ -257,6 +260,23 @@ You can use `addTypeAnnotation()` and `removeTypeAnnotation()` to override its d
257260

258261
You can make a pull request to add new annotations to Spotless's default list.
259262

263+
### cleanthat
264+
265+
[homepage](https://github.com/solven-eu/cleanthat). CleanThat enables automatic refactoring of Java code. [ChangeLog](https://github.com/solven-eu/cleanthat/blob/master/CHANGES.MD)
266+
267+
```gradle
268+
spotless {
269+
java {
270+
cleanthat()
271+
// optional: you can specify a specific version and/or config file
272+
cleanthat()
273+
.groupArtifact('1.7') // default is 'io.github.solven-eu.cleanthat:java'
274+
.version('2.1') // You may force a past of -SNAPSHOT
275+
.sourceCompatibility('1.7') // default is '1.7'
276+
.addMutator('your.custom.MagicMutator')
277+
.excludeMutator('UseCollectionIsEmpty')
278+
```
279+
260280

261281
<a name="applying-to-groovy-source"></a>
262282

0 commit comments

Comments
 (0)