Skip to content

Commit

Permalink
RenameBean Scanning Recipe Refactor (openrewrite#630)
Browse files Browse the repository at this point in the history
* Refactored the RenameBean recipe into a scanning recipe to allow renaming usages that occur outside the file where the bean is defined.

* Add missing language hints

* Remove unused `fromDeclaration` methods

---------

Co-authored-by: Hudson, Ryan <[email protected]>
Co-authored-by: Tim te Beek <[email protected]>
  • Loading branch information
3 people committed Nov 26, 2024
1 parent f897704 commit 309250e
Show file tree
Hide file tree
Showing 2 changed files with 822 additions and 721 deletions.
270 changes: 100 additions & 170 deletions src/main/java/org/openrewrite/java/spring/RenameBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,19 @@
import org.openrewrite.java.search.DeclaresType;
import org.openrewrite.java.search.FindAnnotations;
import org.openrewrite.java.search.UsesType;
import org.openrewrite.java.tree.*;
import org.openrewrite.java.service.AnnotationService;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.TypeUtils;

import java.util.*;

import static org.openrewrite.java.MethodMatcher.methodPattern;

@EqualsAndHashCode(callSuper = false)
@Value
public class RenameBean extends Recipe {
public class RenameBean extends ScanningRecipe<List<TreeVisitor<?, ExecutionContext>>> {

@Option(required = false, example = "foo.MyType")
@Nullable
Expand Down Expand Up @@ -74,90 +78,66 @@ public String getDescription() {
return "Renames a Spring bean, both declaration and references.";
}

/**
* @param methodDeclaration, which may or may not declare a bean
* @param newName, for the potential bean
* @return a recipe for this methodDeclaration if it declares a bean, or null if it does not declare a bean
*/
public static @Nullable RenameBean fromDeclaration(J.MethodDeclaration methodDeclaration, String newName) {
return methodDeclaration.getMethodType() == null ? null :
fromDeclaration(methodDeclaration, newName, methodDeclaration.getMethodType().getReturnType().toString());
@Override
public List<TreeVisitor<?, ExecutionContext>> getInitialValue(ExecutionContext ctx) {
return new ArrayList<>();
}

/**
* @param methodDeclaration, which may or may not declare a bean
* @param newName, for the potential bean
* @param type, to override the type field on the returned RenameBean instance
* @return a recipe for this methodDeclaration if it declares a bean, or null if it does not declare a bean
*/
public static @Nullable RenameBean fromDeclaration(J.MethodDeclaration methodDeclaration, String newName, @Nullable String type) {
BeanSearchResult beanSearchResult = isBean(methodDeclaration.getAllAnnotations(), BEAN_METHOD_ANNOTATIONS);
if (!beanSearchResult.isBean || methodDeclaration.getMethodType() == null) {
return null;
}
String beanName =
beanSearchResult.beanName != null ? beanSearchResult.beanName : methodDeclaration.getSimpleName();
return beanName.equals(newName) ? null : new RenameBean(type, beanName, newName);
}
@Override
public TreeVisitor<?, ExecutionContext> getScanner(List<TreeVisitor<?, ExecutionContext>> acc) {

/**
* @param classDeclaration, which may or may not declare a bean
* @param newName, for the potential bean
* @return a recipe for this classDeclaration if it declares a bean, or null if it does not declare a bean
*/
public static @Nullable RenameBean fromDeclaration(J.ClassDeclaration classDeclaration, String newName) {
return classDeclaration.getType() == null ? null :
fromDeclaration(classDeclaration, newName, classDeclaration.getType().toString());
}
return Preconditions.check(precondition(), new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
J.MethodDeclaration m = super.visitMethodDeclaration(method, ctx);

/**
* @param classDeclaration, which may or may not declare a bean
* @param newName, for the potential bean
* @param type, to override the type field on the returned RenameBean instance
* @return a recipe for this classDeclaration if it declares a bean, or null if it does not declare a bean
*/
public static @Nullable RenameBean fromDeclaration(J.ClassDeclaration classDeclaration, String newName, @Nullable String type) {
BeanSearchResult beanSearchResult = isBean(classDeclaration.getAllAnnotations(), BEAN_TYPE_ANNOTATIONS);
if (!beanSearchResult.isBean || classDeclaration.getType() == null) {
return null;
}
String beanName =
beanSearchResult.beanName != null ? beanSearchResult.beanName : classDeclaration.getSimpleName();
return beanName.equals(newName) ? null : new RenameBean(type, beanName, newName);
// handle beans named via methods
List<J.Annotation> allAnnotations = service(AnnotationService.class).getAllAnnotations(getCursor());
Expression beanNameExpression = getBeanNameExpression(allAnnotations, BEAN_METHOD_ANNOTATIONS);
if (beanNameExpression == null && isRelevantType(m.getMethodType().getReturnType()) && m.getSimpleName().equals(oldName)) {
acc.add(new ChangeMethodName(methodPattern(m), newName, true, false).getVisitor());
}

// handle annotation renames
acc.add(renameBeanAnnotations(BEAN_METHOD_ANNOTATIONS));
return m;
}

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

List<J.Annotation> allAnnotations = service(AnnotationService.class).getAllAnnotations(getCursor());
Expression beanNameExpression = getBeanNameExpression(allAnnotations, BEAN_TYPE_ANNOTATIONS);

// handle bean named via class name
if (beanNameExpression == null && isRelevantType(cd.getType()) && StringUtils.uncapitalize(cd.getSimpleName()).equals(oldName)) {
String newFullyQualifiedTypeName = cd.getType().getFullyQualifiedName()
.replaceAll("^((.+\\.)*)[^.]+$", "$1" + StringUtils.capitalize(newName));
acc.add(new ChangeType(cd.getType().getFullyQualifiedName(), newFullyQualifiedTypeName, false).getVisitor());
acc.add(new ChangeType(cd.getType().getFullyQualifiedName() + "Test", newFullyQualifiedTypeName + "Test", false).getVisitor());
}

// handle annotation renames
acc.add(renameBeanAnnotations(BEAN_TYPE_ANNOTATIONS));

return cd;
}
});
}

private static BeanSearchResult isBean(Collection<J.Annotation> annotations, Set<String> types) {
for (J.Annotation annotation : annotations) {
if (anyAnnotationMatches(annotation, types)) {
if (annotation.getArguments() != null && !annotation.getArguments().isEmpty()) {
for (Expression expr : annotation.getArguments()) {
if (expr instanceof J.Literal) {
return new BeanSearchResult(true, (String) ((J.Literal) expr).getValue());
}
J.Assignment beanNameAssignment = asBeanNameAssignment(expr);
if (beanNameAssignment != null) {
Expression assignmentExpr = beanNameAssignment.getAssignment();
if (assignmentExpr instanceof J.Literal) {
return new BeanSearchResult(true, (String) ((J.Literal) assignmentExpr).getValue());
} else if (assignmentExpr instanceof J.NewArray) {
List<Expression> initializers = ((J.NewArray) assignmentExpr).getInitializer();
if (initializers != null) {
for (Expression initExpr : initializers) {
// if multiple aliases, just take the first one
if (initExpr instanceof J.Literal) {
return new BeanSearchResult(true,
(String) ((J.Literal) initExpr).getValue());
}
}
}
}
}
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(List<TreeVisitor<?, ExecutionContext>> acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
J.CompilationUnit newCu = super.visitCompilationUnit(cu, ctx);
for (TreeVisitor<?, ExecutionContext> visitor : acc) {
newCu = (J.CompilationUnit) visitor.visit(newCu, ctx);
}
return new BeanSearchResult(true, null);
return newCu;
}
}
return new BeanSearchResult(false, null);
};
}

private static boolean anyAnnotationMatches(J.Annotation type, Set<String> types) {
Expand Down Expand Up @@ -190,111 +170,61 @@ private TreeVisitor<?, ExecutionContext> precondition() {
Preconditions.or(new UsesType<>(type, false), new DeclaresType<>(type));
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(precondition(), new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method,
ExecutionContext ctx) {
J.MethodDeclaration m = super.visitMethodDeclaration(method, ctx);

// handle bean declarations
if (m.getMethodType() != null && isRelevantType(m.getMethodType().getReturnType())) {
boolean maybeRenameMethodDeclaration = maybeRenameBean(m.getAllAnnotations(),
BEAN_METHOD_ANNOTATIONS);
if (maybeRenameMethodDeclaration && m.getSimpleName().equals(oldName)) {
doAfterVisit(new ChangeMethodName(methodPattern(m), newName, false, false).getVisitor());
private @Nullable Expression getBeanNameExpression(Collection<J.Annotation> annotations, Set<String> types) {
for (J.Annotation annotation : annotations) {
if (anyAnnotationMatches(annotation, types)) {
if (annotation.getArguments() != null && !annotation.getArguments().isEmpty()) {
for (Expression expr : annotation.getArguments()) {
if (expr instanceof J.Literal) {
return expr;
}
J.Assignment beanNameAssignment = asBeanNameAssignment(expr);
if (beanNameAssignment != null) {
return beanNameAssignment;
}
}
}

// handle bean references (method params)
for (Statement statement : m.getParameters()) {
renameMatchingQualifierAnnotations(statement);
}

return m;
}
}
return null;
}

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

// handle bean declarations
if (cd.getType() != null && isRelevantType(cd.getType())) {
boolean maybeRenameClass = maybeRenameBean(cd.getAllAnnotations(), BEAN_TYPE_ANNOTATIONS);
if (maybeRenameClass && StringUtils.uncapitalize(cd.getSimpleName()).equals(oldName)) {
String newFullyQualifiedTypeName = cd.getType().getFullyQualifiedName()
.replaceAll("^((.+\\.)*)[^.]+$", "$1" + StringUtils.capitalize(newName));
doAfterVisit(new ChangeType(cd.getType().getFullyQualifiedName(), newFullyQualifiedTypeName, false).getVisitor());
private TreeVisitor<J, ExecutionContext> renameBeanAnnotations(Set<String> types) {
return new JavaIsoVisitor<ExecutionContext>() {
private boolean annotationParentMatchesBeanType() {
if (getCursor().getParent() != null) {
Object annotationParent = getCursor().getParent().getValue();

if (annotationParent instanceof J.MethodDeclaration) {
return isRelevantType(((J.MethodDeclaration) annotationParent).getMethodType().getReturnType());
} else if (annotationParent instanceof J.ClassDeclaration) {
return isRelevantType(((J.ClassDeclaration) annotationParent).getType());
} else if (annotationParent instanceof J.VariableDeclarations) {
return isRelevantType(((J.VariableDeclarations) annotationParent).getType());
}
}

// handle bean references (fields)
for (Statement statement : cd.getBody().getStatements()) {
renameMatchingQualifierAnnotations(statement);
}

return cd;
return false;
}

private void renameMatchingQualifierAnnotations(Statement statement) {
if (statement instanceof J.VariableDeclarations) {
J.VariableDeclarations varDecls = (J.VariableDeclarations) statement;
for (J.VariableDeclarations.NamedVariable namedVar : varDecls.getVariables()) {
if (isRelevantType(namedVar.getType())) {
maybeRenameBean(varDecls.getAllAnnotations(), JUST_QUALIFIER);
break;
}
}
}
}
@Override
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
Expression beanNameExpression = getBeanNameExpression(Collections.singleton(annotation), types);

/**
* Checks for presence of a bean-like annotation in the list of annotations,
* and queues up a visitor to change that annotation's arguments if they match the oldName.
*
* @return true in the specific case where there are bean-like annotations,
* but they don't determine the name of the bean, and therefore the J element itself should be renamed
*/
private boolean maybeRenameBean(Collection<J.Annotation> annotations, Set<String> types) {
J.Annotation beanAnnotation = null;
J.Literal literalBeanName = null;
J.Assignment beanNameAssignment = null;
outer:
for (J.Annotation annotation : annotations) {
if (anyAnnotationMatches(annotation, types)) {
beanAnnotation = annotation;
if (beanAnnotation.getArguments() != null && !beanAnnotation.getArguments().isEmpty()) {
for (Expression expr : beanAnnotation.getArguments()) {
if (expr instanceof J.Literal) {
literalBeanName = (J.Literal) expr;
break outer;
}
beanNameAssignment = asBeanNameAssignment(expr);
if (beanNameAssignment != null) {
break outer;
}
}
}
}
}
if (beanAnnotation != null) {
if (literalBeanName != null) {
if (oldName.equals(literalBeanName.getValue())) {
doAfterVisit(renameBeanAnnotationValue(beanAnnotation));
if (beanNameExpression != null && annotationParentMatchesBeanType()) {
if (beanNameExpression instanceof J.Literal) {
if (oldName.equals(((J.Literal) beanNameExpression).getValue())) {
doAfterVisit(renameBeanAnnotationValue(annotation));
}
} else if (beanNameAssignment != null) {
} else if (beanNameExpression instanceof J.Assignment) {
J.Assignment beanNameAssignment = (J.Assignment) beanNameExpression;
if (contains(beanNameAssignment.getAssignment(), oldName)) {
doAfterVisit(renameBeanAnnotationValue(beanAnnotation, beanNameAssignment));
doAfterVisit(renameBeanAnnotationValue(annotation, beanNameAssignment));
}
} else {
return true;
}
}
return false;
return super.visitAnnotation(annotation, ctx);
}
});
};
}

private static J.@Nullable Assignment asBeanNameAssignment(Expression argumentExpression) {
Expand All @@ -310,8 +240,8 @@ private boolean maybeRenameBean(Collection<J.Annotation> annotations, Set<String
return null;
}

private TreeVisitor<J, ExecutionContext> renameBeanAnnotationValue(J.Annotation beanAnnotation,
J.Assignment beanNameAssignment) {
private TreeVisitor<J, ExecutionContext> renameBeanAnnotationValue(
J.Annotation beanAnnotation, J.Assignment beanNameAssignment) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
Expand Down
Loading

0 comments on commit 309250e

Please sign in to comment.