Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7bb9db8
Add WIP
Fossur Mar 31, 2025
73e9efc
Add licences
Fossur Mar 31, 2025
a950992
Clean up recipe naming, Update inherited recipes to visitor style inh…
Fossur Mar 31, 2025
dd8f3ac
Add usages to yaml
Fossur Mar 31, 2025
d946fc0
Fix tests to not use full runtime classpath
Fossur Apr 1, 2025
c85975e
Fix examples and descriptions
Fossur Apr 1, 2025
40eb375
Apply suggestions from code review
timtebeek Apr 1, 2025
255b8c7
Apply formatter on tests
timtebeek Apr 7, 2025
a46e244
Apply formatter on tests
timtebeek Apr 7, 2025
b30d0ab
Restore package declarations
timtebeek Apr 7, 2025
eeb4d31
Fix extended visitors to inline
Fossur Apr 8, 2025
0a790c1
Merge branch 'superclass-modifying-recipes' of https://github.com/Fos…
Fossur Apr 8, 2025
a46101e
Remove method stub creator
Fossur Apr 14, 2025
4de524e
Update tests
Fossur Apr 14, 2025
9e21ec8
Merge branch 'main' into superclass-modifying-recipes
timtebeek Apr 18, 2025
6f2b944
Apply suggestions from code review
timtebeek Apr 18, 2025
92ae561
Merge branch 'main' into superclass-modifying-recipes
timtebeek Apr 28, 2025
08fb545
Polish `RemoveUnnecessarySuperCalls`
timtebeek Apr 28, 2025
b4fa74e
Polish `RemoveSuperTypeVisitor`
timtebeek Apr 28, 2025
294e6ad
Place the documentation examples first
timtebeek Apr 28, 2025
b139441
Remove `RemoveSuperTypeByPackage` as it's likely to make unchecked ch…
timtebeek Apr 28, 2025
fb67197
No need to handle interfaces just yet
timtebeek Apr 28, 2025
8f8e060
Trim down `RemoveUnnecessaryOverride`
timtebeek Apr 28, 2025
9ae951b
Use `RemoveAnnotationVisitor` to not change formatting
timtebeek Apr 28, 2025
128758b
Comment out `ee.taltech.example.AbstractJpaDAO` reference for now
timtebeek Apr 28, 2025
dac7cbf
Polish `ChangeSuperType`
timtebeek Apr 28, 2025
86502ca
Update examples.yml
timtebeek Apr 28, 2025
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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<p align="center">
<p align="center">

<a href="https://docs.openrewrite.org">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/openrewrite/rewrite/raw/main/doc/logo-oss-dark.svg">
Expand All @@ -23,10 +24,13 @@

### What is this?

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

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

## Contributing

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.
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.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ dependencies {
testImplementation("org.openrewrite:rewrite-test")

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

recipeDependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.openrewrite.java.dropwizard.method;

import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.TypeTree;
import org.openrewrite.java.tree.TypeUtils;

import java.util.*;
import java.util.stream.Collectors;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.openrewrite.java.dropwizard.method.util.MethodStubCreator.buildMethodStub;
import static org.openrewrite.java.tree.Flag.Abstract;
import static org.openrewrite.java.tree.Flag.Default;
import static org.openrewrite.java.tree.TypeUtils.asFullyQualified;
import static org.openrewrite.java.tree.TypeUtils.isAssignableTo;

public class AddMissingAbstractMethods extends Recipe {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, and optionally, we already have AddMissingMethodImplementation, which is a little more explicit in it's use, but then saves us having to maintain two recipes for similar functionality. Please let me know your thoughts!

Copy link
Copy Markdown
Collaborator Author

@Fossur Fossur Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think I made this recipe before the other one existed, I wanted to make a recipe that can handle all kinds of type changes for different migration scenarios - For more of the overall context; I ran into an issue where entire method signatures change here. I realized that it's really a really a lot of work to start reworking existing methods into new forms, and much simpler to add scaffolding here - so I focused on the quality of life aspect here to automate is as much as I can. I didn't find a really good way of doing so though; I initially kinda hoped that I could just get the method information from the superclasses and kinda reput it into the classes that do the implementation, but that approach did not work, no convenient way to make the Method back into MethodDeclaration form - so I resorted to manually parsing parameters here. Ultimately, its your guys call here how you want to maintain this stuff.

Copy link
Copy Markdown
Collaborator Author

@Fossur Fossur Apr 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this would necessarily interfere with the other recipe, as ideally this could be turned on/off at will, but I understand the maintenance concerns - so your call here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the AddAbstractMethods from the PR here, still waiting for feedback on the other stuff

@Override
public String getDisplayName() {
return "Add missing abstract methods";
}

@Override
public String getDescription() {
return "Implements missing abstract methods from superclasses and interfaces.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddMissingMethodsVisitor();
}

public static class AddMissingMethodsVisitor extends JavaIsoVisitor<ExecutionContext> {
private static List<JavaType.Method> getAbstractMethods(JavaType.FullyQualified implType) {
if (implType.getMethods() == null) {
return Collections.emptyList();
}

return implType.getMethods().stream()
.filter(m -> m.hasFlags(Abstract))
.filter(m -> !m.hasFlags(Default))
.collect(Collectors.toList());
}

private static Set<String> getObjectClassMethods() {
Set<String> objectMethodNames = new HashSet<>();
objectMethodNames.add("equals");
objectMethodNames.add("hashCode");
objectMethodNames.add("toString");
objectMethodNames.add("clone");
objectMethodNames.add("finalize");
objectMethodNames.add("getClass");
objectMethodNames.add("notify");
objectMethodNames.add("notifyAll");
objectMethodNames.add("wait");
return objectMethodNames;
}

@Override
public J.ClassDeclaration visitClassDeclaration(
J.ClassDeclaration classDecl, ExecutionContext ctx) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);

if (isNull(cd.getType())) {
return cd;
}

List<JavaType.Method> abstractMethods = new ArrayList<>();

JavaType.FullyQualified extendsType =
asFullyQualified(nonNull(cd.getExtends()) ? cd.getExtends().getType() : null);

if (nonNull(extendsType)) {
abstractMethods.addAll(getAbstractMethods(extendsType));
}

if (nonNull(cd.getImplements())) {
for (TypeTree impl : cd.getImplements()) {
JavaType.FullyQualified implType = asFullyQualified(impl.getType());
if (nonNull(implType)) {
abstractMethods.addAll(getAbstractMethods(implType));
}
}
}

List<JavaType.Method> missingMethods = findMissingAbstractMethods(cd, abstractMethods);

if (missingMethods.isEmpty()) {
return cd;
}

for (JavaType.Method missingMethod : missingMethods) {
cd = createMethod(cd, missingMethod);
}

return maybeAutoFormat(classDecl, cd, ctx);
}

public J.ClassDeclaration createMethod(J.ClassDeclaration cd, JavaType.Method methodType) {
if (methodType == null) {
return cd;
}

JavaTemplate template =
JavaTemplate.builder(buildMethodStub(methodType)).contextSensitive().build();

return template.apply(updateCursor(cd), cd.getBody().getCoordinates().lastStatement());
}

private List<JavaType.Method> findMissingAbstractMethods(
J.ClassDeclaration classDecl, List<JavaType.Method> candidateAbstractMethods) {
JavaType.FullyQualified classType = classDecl.getType();
if (classType == null) {
return Collections.emptyList();
}

// Get all method declarations from the class body
List<J.MethodDeclaration> existingMethodDecls =
classDecl.getBody().getStatements().stream()
.filter(statement -> statement instanceof J.MethodDeclaration)
.map(statement -> (J.MethodDeclaration) statement)
.collect(Collectors.toList());

List<JavaType.Method> missing = new ArrayList<>();
Set<String> objectMethodNames = getObjectClassMethods();

for (JavaType.Method abstractMethod : candidateAbstractMethods) {
if (objectMethodNames.contains(abstractMethod.getName())) {
continue;
}

boolean alreadyImplemented =
existingMethodDecls.stream()
.anyMatch(
methodDecl -> {
JavaType.Method methodType = methodDecl.getMethodType();
return methodType != null && signaturesMatch(methodType, abstractMethod);
});

if (!alreadyImplemented) {
missing.add(abstractMethod);
}
}

return missing;
}

private boolean signaturesMatch(JavaType.Method existing, JavaType.Method candidate) {
if (!existing.getName().equals(candidate.getName())) {
return false;
}

if (existing.getParameterTypes().size() != candidate.getParameterTypes().size()) {
return false;
}

// Then do a robust generic check
// We need the parent's JavaType.Parameterized if it's generic, e.g. the parent's
// "ComplexParent"
JavaType parentType = existing.getDeclaringType();
if (parentType instanceof JavaType.Parameterized) {
return methodMatchesGenericDefinition(
existing, candidate, (JavaType.Parameterized) parentType);
} else {
// fallback: compare them in the simpler, old-fashioned way
return isAssignableTo(existing.getReturnType(), candidate.getReturnType());
}
}

private boolean areTypesCompatible(JavaType type1, JavaType type2) {
if (type1 == null || type2 == null) {
return type1 == type2;
}

// Get fully qualified names if possible
String name1 = getFullyQualifiedName(type1);
String name2 = getFullyQualifiedName(type2);

if (name1 != null && name2 != null) {
// Handle primitive type variations (e.g., int vs java.lang.Integer)
name1 = normalizePrimitiveType(name1);
name2 = normalizePrimitiveType(name2);
return name1.equals(name2);
}

// Handle parameterized types
if (type1 instanceof JavaType.Parameterized && type2 instanceof JavaType.Parameterized) {
return areParameterizedTypesCompatible(
(JavaType.Parameterized) type1, (JavaType.Parameterized) type2);
}

// If we can't determine compatibility, err on the side of caution
return false;
}

private String getFullyQualifiedName(JavaType type) {
if (type instanceof JavaType.Primitive) {
return ((JavaType.Primitive) type).getKeyword();
}

JavaType.FullyQualified fq = TypeUtils.asFullyQualified(type);
return fq != null ? fq.getFullyQualifiedName() : null;
}

private String normalizePrimitiveType(String typeName) {
Map<String, String> primitiveToWrapper = new HashMap<>();
primitiveToWrapper.put("int", "java.lang.Integer");
primitiveToWrapper.put("long", "java.lang.Long");
primitiveToWrapper.put("boolean", "java.lang.Boolean");
primitiveToWrapper.put("byte", "java.lang.Byte");
primitiveToWrapper.put("short", "java.lang.Short");
primitiveToWrapper.put("char", "java.lang.Character");
primitiveToWrapper.put("float", "java.lang.Float");
primitiveToWrapper.put("double", "java.lang.Double");

return primitiveToWrapper.getOrDefault(typeName, typeName);
}

private boolean areParameterizedTypesCompatible(
JavaType.Parameterized type1, JavaType.Parameterized type2) {
// Check base type compatibility
if (!areTypesCompatible(type1.getType(), type2.getType())) {
return false;
}

// Check type parameters
List<JavaType> params1 = type1.getTypeParameters();
List<JavaType> params2 = type2.getTypeParameters();

if (params1.size() != params2.size()) {
return false;
}

for (int i = 0; i < params1.size(); i++) {
if (!areTypesCompatible(params1.get(i), params2.get(i))) {
return false;
}
}

return true;
}

private boolean methodMatchesGenericDefinition(
JavaType.Method existing, JavaType.Method candidate, JavaType.Parameterized declaringType) {

// Compare parameter counts
if (existing.getParameterTypes().size() != candidate.getParameterTypes().size()) {
return false;
}

// Get type mappings from the parameterized type
Map<JavaType, JavaType> typeMapping = new HashMap<>();
JavaType.FullyQualified rawType = declaringType.getType();
if (rawType instanceof JavaType.Class) {
JavaType.Class classType = (JavaType.Class) rawType;
List<JavaType> typeVarNames = classType.getTypeParameters();
List<JavaType> typeArgs = declaringType.getTypeParameters();

for (int i = 0; i < typeVarNames.size() && i < typeArgs.size(); i++) {
typeMapping.put(typeVarNames.get(i), typeArgs.get(i));
}
}

// Check parameters
for (int i = 0; i < existing.getParameterTypes().size(); i++) {
JavaType existingParam = existing.getParameterTypes().get(i);
JavaType candidateParam = candidate.getParameterTypes().get(i);

if (candidateParam instanceof JavaType.GenericTypeVariable) {
JavaType.GenericTypeVariable genericParam = (JavaType.GenericTypeVariable) candidateParam;
String genericName = genericParam.getName();

// Check if this generic parameter has a mapping
if (typeMapping.containsKey(genericName)) {
JavaType mappedType = typeMapping.get(genericName);
if (!isAssignableTo(existingParam, mappedType)) {
return false;
}
} else {
// If no mapping exists, check bounds
List<JavaType> bounds = genericParam.getBounds();
if (!bounds.isEmpty() &&
!bounds.stream().allMatch(bound -> isAssignableTo(existingParam, bound))) {
return false;
}
}
} else if (!isAssignableTo(existingParam, candidateParam)) {
return false;
}
}

// Check return type
JavaType existingReturn = existing.getReturnType();
JavaType candidateReturn = candidate.getReturnType();

if (existingReturn instanceof JavaType.Parameterized &&
candidateReturn instanceof JavaType.Parameterized) {
JavaType.Parameterized existingParamReturn = (JavaType.Parameterized) existingReturn;
JavaType.Parameterized candidateParamReturn = (JavaType.Parameterized) candidateReturn;

// Compare the raw types (e.g., Optional)
if (!isAssignableTo(existingParamReturn.getType(), candidateParamReturn.getType())) {
return false;
}

// Compare type arguments
List<JavaType> existingTypeArgs = existingParamReturn.getTypeParameters();
List<JavaType> candidateTypeArgs = candidateParamReturn.getTypeParameters();

if (existingTypeArgs.size() != candidateTypeArgs.size()) {
return false;
}

for (int i = 0; i < existingTypeArgs.size(); i++) {
JavaType existingArg = existingTypeArgs.get(i);
JavaType candidateArg = candidateTypeArgs.get(i);

if (candidateArg instanceof JavaType.GenericTypeVariable) {
String genericName = ((JavaType.GenericTypeVariable) candidateArg).getName();
if (typeMapping.containsKey(genericName)) {
JavaType mappedType = typeMapping.get(genericName);
if (!isAssignableTo(existingArg, mappedType)) {
return false;
}
}
} else if (!isAssignableTo(existingArg, candidateArg)) {
return false;
}
}
} else {
return isAssignableTo(existingReturn, candidateReturn);
}

return true;
}
}
}
Loading
Loading