Skip to content

Commit a54064d

Browse files
Fossurtimtebeekgithub-actions[bot]
authored
Superclass modifying recipes (#3)
* Add WIP * Add licences * Clean up recipe naming, Update inherited recipes to visitor style inheritance * Add usages to yaml * Fix tests to not use full runtime classpath * Fix examples and descriptions * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply formatter on tests * Apply formatter on tests * Restore package declarations * Fix extended visitors to inline * Remove method stub creator * Update tests * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Polish `RemoveUnnecessarySuperCalls` * Polish `RemoveSuperTypeVisitor` * Place the documentation examples first * Remove `RemoveSuperTypeByPackage` as it's likely to make unchecked changes * No need to handle interfaces just yet * Trim down `RemoveUnnecessaryOverride` * Use `RemoveAnnotationVisitor` to not change formatting * Comment out `ee.taltech.example.AbstractJpaDAO` reference for now * Polish `ChangeSuperType` * Update examples.yml --------- Co-authored-by: Tim te Beek <timtebeek@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent 4db1511 commit a54064d

File tree

14 files changed

+1611
-5
lines changed

14 files changed

+1611
-5
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<p align="center">
1+
<p align="center">
2+
23
<a href="https://docs.openrewrite.org">
34
<picture>
45
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/openrewrite/rewrite/raw/main/doc/logo-oss-dark.svg">
@@ -23,10 +24,13 @@
2324

2425
### What is this?
2526

26-
This project implements a [Rewrite module](https://github.com/openrewrite/rewrite) that applies best practices and migrations for those projects using the [Dropwizard framework](https://dropwizard.io/).
27+
This project implements a [Rewrite module](https://github.com/openrewrite/rewrite) that applies best practices and
28+
migrations for those projects using the [Dropwizard framework](https://dropwizard.io/).
2729

2830
Browse [a selection of recipes available through this module in the recipe catalog](https://docs.openrewrite.org/recipes/java/dropwizard).
2931

3032
## Contributing
3133

32-
We appreciate all types of contributions. See the [contributing guide](https://github.com/openrewrite/.github/blob/main/CONTRIBUTING.md) for detailed instructions on how to get started.
34+
We appreciate all types of contributions. See
35+
the [contributing guide](https://github.com/openrewrite/.github/blob/main/CONTRIBUTING.md) for detailed instructions on
36+
how to get started.

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ dependencies {
2626
testImplementation("org.openrewrite:rewrite-test")
2727

2828
testRuntimeOnly("io.dropwizard.metrics:metrics-annotation:4.1.+")
29+
testRuntimeOnly("io.dropwizard.metrics:metrics-healthchecks:4.1.+")
2930
testRuntimeOnly("org.springframework.boot:spring-boot-starter-actuator:2.5.+")
3031
testRuntimeOnly("javax.persistence:javax.persistence-api:2.2")
3132
testRuntimeOnly("org.projectlombok:lombok:1.18.+")
33+
testRuntimeOnly("net.sourceforge.argparse4j:argparse4j:0.9.0")
3234
}
3335

3436
recipeDependencies {
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
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+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
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 org.openrewrite.java.dropwizard.method;
17+
18+
19+
import lombok.EqualsAndHashCode;
20+
import lombok.Value;
21+
import org.openrewrite.ExecutionContext;
22+
import org.openrewrite.Option;
23+
import org.openrewrite.Recipe;
24+
import org.openrewrite.TreeVisitor;
25+
import org.openrewrite.java.JavaIsoVisitor;
26+
import org.openrewrite.java.JavaParser;
27+
import org.openrewrite.java.JavaTemplate;
28+
import org.openrewrite.java.ShortenFullyQualifiedTypeReferences;
29+
import org.openrewrite.java.tree.Expression;
30+
import org.openrewrite.java.tree.J;
31+
import org.openrewrite.java.tree.JavaType;
32+
import org.openrewrite.java.tree.TypeTree;
33+
34+
import static java.lang.Boolean.TRUE;
35+
import static org.openrewrite.java.tree.TypeUtils.asFullyQualified;
36+
import static org.openrewrite.java.tree.TypeUtils.isOfClassType;
37+
38+
@Value
39+
@EqualsAndHashCode(callSuper = false)
40+
public class ChangeSuperType extends Recipe {
41+
42+
@Option(displayName = "Target class",
43+
description = "The fully qualified name of the class whose superclass should be changed.",
44+
example = "com.myorg.MyClass")
45+
String targetClass;
46+
47+
@Option(displayName = "New superclass",
48+
description = "The fully qualified name of the new superclass to extend or interface to implement.",
49+
example = "com.myorg.NewSuperclass")
50+
String newSuperclass;
51+
52+
@Option(displayName = "Keep type parameters",
53+
description = "Whether to keep existing type parameters on the target class declaration.",
54+
required = false)
55+
Boolean keepTypeParameters;
56+
57+
@Option(displayName = "Convert to interface",
58+
description = "If the new supertype is an interface, setting this to true converts 'extends' to 'implements'.",
59+
required = false)
60+
Boolean convertToInterface;
61+
62+
@Option(displayName = "Remove unnecessary overrides",
63+
description = "Remove method Override annotations that override methods from the *old* superclass but are no longer necessary with the new superclass.",
64+
required = false)
65+
Boolean removeUnnecessaryOverrides;
66+
67+
@Override
68+
public String getDisplayName() {
69+
return "Change superclass";
70+
}
71+
72+
@Override
73+
public String getDescription() {
74+
return "Changes the superclass of a specified class to a new superclass.";
75+
}
76+
77+
@Override
78+
public TreeVisitor<?, ExecutionContext> getVisitor() {
79+
return new JavaIsoVisitor<ExecutionContext>() {
80+
@Override
81+
public J.ClassDeclaration visitClassDeclaration(
82+
J.ClassDeclaration classDecl, ExecutionContext ctx) {
83+
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
84+
85+
if (cd.getExtends() == null || !isOfClassType(cd.getExtends().getType(), targetClass)) {
86+
return cd;
87+
}
88+
89+
String typeParams = getTypeParams(cd.getExtends());
90+
91+
maybeAddImport(newSuperclass);
92+
maybeRemoveImport(targetClass);
93+
94+
JavaTemplate extendsTemplate = JavaTemplate.builder(newSuperclass + typeParams)
95+
// TODO runtimeClasspath() might be different in other recipe run environments
96+
.javaParser(JavaParser.fromJavaVersion().classpath(JavaParser.runtimeClasspath()))
97+
.imports(newSuperclass)
98+
.contextSensitive()
99+
.build();
100+
101+
JavaType.FullyQualified newSuperType;
102+
103+
if (TRUE.equals(convertToInterface)) {
104+
cd = extendsTemplate.apply(updateCursor(cd), cd.getCoordinates().addImplementsClause());
105+
cd = cd.withExtends(null);
106+
TypeTree lastInterface = cd.getImplements().get(cd.getImplements().size() - 1);
107+
newSuperType = asFullyQualified(lastInterface.getType());
108+
} else {
109+
cd = extendsTemplate.apply(updateCursor(cd), cd.getCoordinates().replaceExtendsClause());
110+
newSuperType = asFullyQualified(cd.getExtends().getType());
111+
}
112+
113+
// If changing to a class that was not compiled initially (eg. from a new library)
114+
if (newSuperType == null) {
115+
newSuperType = asFullyQualified(JavaType.buildType(newSuperclass));
116+
}
117+
118+
cd = cd.withType(((JavaType.Class) cd.getType()).withSupertype(newSuperType));
119+
doAfterVisit(ShortenFullyQualifiedTypeReferences.modifyOnly(cd));
120+
doAfterVisit(new UpdateMethodTypesVisitor(cd.getType()));
121+
122+
if (TRUE.equals(removeUnnecessaryOverrides)) {
123+
doAfterVisit(new RemoveUnnecessaryOverride(false).getVisitor());
124+
}
125+
126+
doAfterVisit(new RemoveUnnecessarySuperCalls.RemoveUnnecessarySuperCallsVisitor());
127+
return cd;
128+
}
129+
130+
private String getTypeParams(TypeTree extendsType) {
131+
StringBuilder typeParams = new StringBuilder();
132+
133+
if (TRUE.equals(keepTypeParameters) && extendsType instanceof J.ParameterizedType) {
134+
J.ParameterizedType parameterizedType = (J.ParameterizedType) extendsType;
135+
136+
boolean hasParameters = false;
137+
typeParams.append('<');
138+
139+
for (Expression typeParameter : parameterizedType.getTypeParameters()) {
140+
if (hasParameters) {
141+
typeParams.append(", ");
142+
}
143+
typeParams.append(typeParameter.toString());
144+
hasParameters = true;
145+
}
146+
147+
if (hasParameters) {
148+
typeParams.append('>');
149+
} else {
150+
typeParams.setLength(0);
151+
}
152+
}
153+
154+
return typeParams.toString();
155+
}
156+
};
157+
}
158+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
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+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
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 org.openrewrite.java.dropwizard.method;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.*;
21+
import org.openrewrite.java.JavaIsoVisitor;
22+
import org.openrewrite.java.search.UsesType;
23+
import org.openrewrite.java.tree.J;
24+
import org.openrewrite.java.tree.JavaType;
25+
26+
import static org.openrewrite.java.tree.TypeUtils.isOfClassType;
27+
28+
@Value
29+
@EqualsAndHashCode(callSuper = false)
30+
public class RemoveSuperTypeByType extends Recipe {
31+
32+
@Option(displayName = "Fully qualified name of the superclass to remove",
33+
description = "Supertypes that match this name are to be removed",
34+
example = "io.dropwizard.Configuration")
35+
String typeToRemove;
36+
37+
@Override
38+
public String getDisplayName() {
39+
return "Remove supertype by fully qualified name matches";
40+
}
41+
42+
@Override
43+
public String getDescription() {
44+
return "Removes a specified type from class extends or implements clauses.";
45+
}
46+
47+
@Override
48+
public TreeVisitor<?, ExecutionContext> getVisitor() {
49+
return Preconditions.check(
50+
new UsesType<>(typeToRemove, false),
51+
new JavaIsoVisitor<ExecutionContext>() {
52+
@Override
53+
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
54+
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
55+
56+
if (cd.getExtends() != null && isOfClassType(cd.getExtends().getType(), typeToRemove)) {
57+
cd = cd.withExtends(null);
58+
JavaType.ShallowClass type = (JavaType.ShallowClass) JavaType.buildType("java.lang.Object");
59+
doAfterVisit(new UpdateMethodTypesVisitor(type));
60+
doAfterVisit(new RemoveUnnecessarySuperCalls.RemoveUnnecessarySuperCallsVisitor());
61+
}
62+
63+
return cd;
64+
}
65+
66+
}
67+
);
68+
}
69+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
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+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
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 org.openrewrite.java.dropwizard.method;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.jspecify.annotations.Nullable;
21+
import org.openrewrite.*;
22+
import org.openrewrite.java.AnnotationMatcher;
23+
import org.openrewrite.java.JavaIsoVisitor;
24+
import org.openrewrite.java.RemoveAnnotationVisitor;
25+
import org.openrewrite.java.search.UsesType;
26+
import org.openrewrite.java.service.AnnotationService;
27+
import org.openrewrite.java.tree.J;
28+
import org.openrewrite.java.tree.TypeUtils;
29+
30+
@Value
31+
@EqualsAndHashCode(callSuper = false)
32+
public class RemoveUnnecessaryOverride extends Recipe {
33+
34+
private static final AnnotationMatcher OVERRIDE_ANNOTATION = new AnnotationMatcher("@java.lang.Override");
35+
private static final RemoveAnnotationVisitor removeAnnotationVisitor = new RemoveAnnotationVisitor(OVERRIDE_ANNOTATION);
36+
37+
@Option(
38+
displayName = "Ignore methods in anonymous classes",
39+
description = "When enabled, ignore @Override annotations on methods in anonymous classes.",
40+
required = false)
41+
@Nullable
42+
Boolean ignoreAnonymousClassMethods;
43+
44+
@Override
45+
public String getDisplayName() {
46+
return "Remove unnecessary `@Override` annotations";
47+
}
48+
49+
@Override
50+
public String getDescription() {
51+
return "Removes `@Override` annotations from methods that don't actually override or implement any method. " +
52+
"This helps maintain clean code by removing incorrect annotations that could be misleading.";
53+
}
54+
55+
@Override
56+
public TreeVisitor<?, ExecutionContext> getVisitor() {
57+
return Preconditions.check(
58+
new UsesType<>("java.lang.Override", false),
59+
new JavaIsoVisitor<ExecutionContext>() {
60+
private Cursor getCursorToParentScope(Cursor cursor) {
61+
return cursor.dropParentUntil(is -> is instanceof J.NewClass || is instanceof J.ClassDeclaration);
62+
}
63+
64+
@Override
65+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
66+
J.MethodDeclaration m = super.visitMethodDeclaration(method, ctx);
67+
if (!m.isConstructor() &&
68+
service(AnnotationService.class).matches(getCursor(), OVERRIDE_ANNOTATION) &&
69+
!TypeUtils.isOverride(m.getMethodType()) &&
70+
!(Boolean.TRUE.equals(ignoreAnonymousClassMethods) &&
71+
getCursorToParentScope(getCursor()).getValue() instanceof J.NewClass)) {
72+
return removeAnnotationVisitor.visitMethodDeclaration(m, ctx);
73+
}
74+
return m;
75+
}
76+
});
77+
}
78+
}

0 commit comments

Comments
 (0)