LifecycleMethodAdapter.java
/*******************************************************************************
* Copyright (c) 2025 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper.lib;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.*;
import java.util.List;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.text.edits.TextEditGroup;
/**
* Helper class for adapting JUnit lifecycle methods from JUnit 4 to JUnit 5.
* Handles method renaming, parameter additions, visibility changes, and exception cleanup.
*/
public final class LifecycleMethodAdapter {
// Private constructor to prevent instantiation
private LifecycleMethodAdapter() {
throw new UnsupportedOperationException("Utility class");
}
/**
* Updates lifecycle methods in a class: renames before() -> beforeEach(), after() -> afterEach().
* Also handles parameter updates and visibility changes as needed.
*
* @param node the type declaration containing lifecycle methods
* @param globalRewrite the AST rewriter
* @param ast the AST instance
* @param group the text edit group
* @param importRewrite the import rewriter
* @param methodbefore the old "before" method name
* @param methodafter the old "after" method name
* @param methodbeforeeach the new "beforeEach" method name
* @param methodaftereach the new "afterEach" method name
*/
public static void updateLifecycleMethodsInClass(TypeDeclaration node, ASTRewrite globalRewrite, AST ast,
TextEditGroup group, ImportRewrite importRewrite, String methodbefore, String methodafter,
String methodbeforeeach, String methodaftereach) {
for (MethodDeclaration method : node.getMethods()) {
if (isLifecycleMethod(method, methodbefore)) {
processLifecycleMethod(node, method, globalRewrite, ast, group, importRewrite, methodbefore, methodbeforeeach);
} else if (isLifecycleMethod(method, methodafter)) {
processLifecycleMethod(node, method, globalRewrite, ast, group, importRewrite, methodafter, methodaftereach);
}
}
}
/**
* Processes a lifecycle method by setting up the appropriate rewriters and applying changes.
*/
private static void processLifecycleMethod(TypeDeclaration node, MethodDeclaration method, ASTRewrite globalRewrite,
AST ast, TextEditGroup group, ImportRewrite importRewrite, String oldMethodName, String newMethodName) {
ASTRewrite rewriteToUse = getASTRewrite(node, ast, globalRewrite);
ImportRewrite importRewriteToUse = getImportRewrite(node, ast, importRewrite);
processMethod(method, rewriteToUse, ast, group, importRewriteToUse, oldMethodName, newMethodName);
if (rewriteToUse != globalRewrite) {
DocumentHelper.createChangeForRewrite(
org.sandbox.jdt.internal.corext.util.ASTNavigationUtils.findCompilationUnit(node), rewriteToUse);
}
}
/**
* Processes a single lifecycle method: renames it, adapts super calls, removes throws, ensures parameter.
*/
private static void processMethod(MethodDeclaration method, ASTRewrite rewriter, AST ast, TextEditGroup group,
ImportRewrite importRewriter, String methodname, String methodnamejunit5) {
setPublicVisibilityIfProtected(method, rewriter, ast, group);
adaptSuperBeforeCalls(methodname, methodnamejunit5, method, rewriter, ast, group);
removeThrowsThrowable(method, rewriter, group);
rewriter.replace(method.getName(), ast.newSimpleName(methodnamejunit5), group);
ensureExtensionContextParameter(method, rewriter, ast, group, importRewriter);
}
/**
* Renames super.before() and super.after() calls to match JUnit 5 lifecycle method names.
* Also ensures that the ExtensionContext parameter is passed to super calls.
*
* @param oldMethodName the old method name (e.g., "before")
* @param newMethodName the new method name (e.g., "beforeEach")
* @param method the method containing super calls to update
* @param rewriter the AST rewriter
* @param ast the AST instance
* @param group the text edit group
*/
public static void adaptSuperBeforeCalls(String oldMethodName, String newMethodName, MethodDeclaration method,
ASTRewrite rewriter, AST ast, TextEditGroup group) {
method.accept(new ASTVisitor() {
@Override
public boolean visit(SuperMethodInvocation node) {
if (oldMethodName.equals(node.getName().getIdentifier())) {
rewriter.replace(node.getName(), ast.newSimpleName(newMethodName), group);
addContextArgumentIfMissing(node, rewriter, ast, group);
}
return super.visit(node);
}
});
}
/**
* Ensures that a method declaration has an ExtensionContext parameter.
* Adds the parameter if missing. Used when converting ExternalResource lifecycle
* methods to JUnit 5 callback interface methods.
*
* @param method the method declaration to check and update
* @param rewrite the AST rewriter
* @param ast the AST instance
* @param group the text edit group
* @param importRewrite the import rewriter
*/
public static void ensureExtensionContextParameter(MethodDeclaration method, ASTRewrite rewrite, AST ast,
TextEditGroup group, ImportRewrite importRewrite) {
// Check if ExtensionContext parameter already exists (in AST or pending rewrites)
boolean hasExtensionContext = method.parameters().stream()
.anyMatch(param -> param instanceof SingleVariableDeclaration && isExtensionContext(
(SingleVariableDeclaration) param, ORG_JUNIT_JUPITER_API_EXTENSION_EXTENSION_CONTEXT))
|| rewrite.getListRewrite(method, MethodDeclaration.PARAMETERS_PROPERTY).getRewrittenList().stream()
.anyMatch(param -> param instanceof SingleVariableDeclaration
&& EXTENSION_CONTEXT.equals(((SingleVariableDeclaration) param).getType().toString()));
if (!hasExtensionContext) {
// Add ExtensionContext parameter
SingleVariableDeclaration newParam = ast.newSingleVariableDeclaration();
newParam.setType(ast.newSimpleType(ast.newName(EXTENSION_CONTEXT)));
newParam.setName(ast.newSimpleName(VARIABLE_NAME_CONTEXT));
ListRewrite listRewrite = rewrite.getListRewrite(method, MethodDeclaration.PARAMETERS_PROPERTY);
listRewrite.insertLast(newParam, group);
// Add import for ExtensionContext
importRewrite.addImport(ORG_JUNIT_JUPITER_API_EXTENSION_EXTENSION_CONTEXT);
}
}
/**
* Removes "throws Throwable" from a method declaration.
* JUnit 5 lifecycle methods don't need to declare Throwable.
*
* @param method the method declaration to modify
* @param rewriter the AST rewriter
* @param group the text edit group
*/
public static void removeThrowsThrowable(MethodDeclaration method, ASTRewrite rewriter, TextEditGroup group) {
List<?> thrownExceptionTypes = method.thrownExceptionTypes();
for (Object exceptionType : thrownExceptionTypes) {
if (exceptionType instanceof SimpleType) {
SimpleType exception = (SimpleType) exceptionType;
if ("Throwable".equals(exception.getName().getFullyQualifiedName())) {
ListRewrite listRewrite = rewriter.getListRewrite(method,
MethodDeclaration.THROWN_EXCEPTION_TYPES_PROPERTY);
listRewrite.remove(exception, group);
break; // Only one Throwable should be present
}
}
}
}
/**
* Changes a method's visibility from protected to public if needed.
* JUnit 5 callback methods must be public.
*
* @param method the method declaration to modify
* @param rewrite the AST rewriter
* @param ast the AST instance
* @param group the text edit group
*/
public static void setPublicVisibilityIfProtected(MethodDeclaration method, ASTRewrite rewrite, AST ast,
TextEditGroup group) {
// Iterate through modifiers and search for a protected modifier
for (Object modifier : method.modifiers()) {
if (modifier instanceof Modifier) {
Modifier mod = (Modifier) modifier;
if (mod.isProtected()) {
ListRewrite modifierRewrite = rewrite.getListRewrite(method, MethodDeclaration.MODIFIERS2_PROPERTY);
Modifier publicModifier = ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD);
modifierRewrite.replace(mod, publicModifier, group);
break; // Stop the loop as soon as the modifier is replaced
}
}
}
}
/**
* Creates a lifecycle callback method for JUnit 5 extension interfaces.
* Used when converting ExternalResource before()/after() methods to callback methods.
*
* @param ast the AST instance
* @param methodName the callback method name (e.g., "beforeEach", "afterEach")
* @param paramType the parameter type name (e.g., "ExtensionContext")
* @param oldBody the body from the original lifecycle method (will be copied and cleaned)
* @param group the text edit group
* @return the new method declaration
*/
public static MethodDeclaration createLifecycleCallbackMethod(AST ast, String methodName, String paramType,
Block oldBody, TextEditGroup group) {
MethodDeclaration method = ast.newMethodDeclaration();
method.setName(ast.newSimpleName(methodName));
method.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD));
method.setReturnType2(ast.newPrimitiveType(PrimitiveType.VOID));
// Add the ExtensionContext (or similar) parameter
SingleVariableDeclaration param = ast.newSingleVariableDeclaration();
param.setType(ast.newSimpleType(ast.newName(paramType)));
param.setName(ast.newSimpleName(VARIABLE_NAME_CONTEXT));
method.parameters().add(param);
// Copy the body from the old method and remove super calls to lifecycle methods
if (oldBody != null) {
Block newBody = (Block) ASTNode.copySubtree(ast, oldBody);
removeSuperLifecycleCalls(newBody);
method.setBody(newBody);
}
return method;
}
/**
* Removes super calls to lifecycle methods (before/after) from a method body.
* When converting ExternalResource to callback interfaces, super calls to lifecycle
* methods should be removed as the callback interfaces don't have such super methods.
*
* @param body the method body to clean
*/
private static void removeSuperLifecycleCalls(Block body) {
body.accept(new ASTVisitor() {
@Override
public boolean visit(SuperMethodInvocation node) {
String methodName = node.getName().getIdentifier();
// Remove super calls to lifecycle methods
if (METHOD_BEFORE.equals(methodName) || METHOD_AFTER.equals(methodName) ||
METHOD_BEFORE_EACH.equals(methodName) || METHOD_AFTER_EACH.equals(methodName) ||
METHOD_BEFORE_ALL.equals(methodName) || METHOD_AFTER_ALL.equals(methodName)) {
// Replace the super call with an empty statement by removing it from parent
ASTNode parent = node.getParent();
if (parent != null) {
parent.delete();
}
}
return super.visit(node);
}
});
}
/**
* Checks if a method is a lifecycle method with the given name.
*
* @param method the method declaration to check
* @param methodName the expected method name
* @return true if the method name matches
*/
private static boolean isLifecycleMethod(MethodDeclaration method, String methodName) {
return methodName.equals(method.getName().getIdentifier());
}
/**
* Adds the ExtensionContext parameter to method invocations if not already present.
* Used when migrating JUnit 4 lifecycle methods to JUnit 5 callback interfaces.
*
* @param node the method invocation node (MethodInvocation or SuperMethodInvocation)
* @param rewriter the AST rewriter
* @param ast the AST instance
* @param group the text edit group
*/
private static void addContextArgumentIfMissing(ASTNode node, ASTRewrite rewriter, AST ast, TextEditGroup group) {
ListRewrite argsRewrite;
if (node instanceof MethodInvocation) {
argsRewrite = rewriter.getListRewrite(node, MethodInvocation.ARGUMENTS_PROPERTY);
} else if (node instanceof SuperMethodInvocation) {
argsRewrite = rewriter.getListRewrite(node, SuperMethodInvocation.ARGUMENTS_PROPERTY);
} else {
return; // Only supports MethodInvocation and SuperMethodInvocation
}
// Check if context argument is already present
boolean hasContextArgument = argsRewrite.getRewrittenList().stream().anyMatch(
arg -> arg instanceof SimpleName && VARIABLE_NAME_CONTEXT.equals(((SimpleName) arg).getIdentifier()));
if (!hasContextArgument) {
argsRewrite.insertFirst(ast.newSimpleName(VARIABLE_NAME_CONTEXT), group);
}
}
private static boolean isExtensionContext(SingleVariableDeclaration param, String className) {
ITypeBinding binding = param.getType().resolveBinding();
return binding != null && className.equals(binding.getQualifiedName());
}
private static ASTRewrite getASTRewrite(ASTNode node, AST globalAST, ASTRewrite globalRewrite) {
return (node.getAST() == globalAST) ? globalRewrite : ASTRewrite.create(node.getAST());
}
private static ImportRewrite getImportRewrite(ASTNode node, AST globalAST, ImportRewrite globalImportRewrite) {
org.eclipse.jdt.core.dom.CompilationUnit compilationUnit = org.sandbox.jdt.internal.corext.util.ASTNavigationUtils
.findCompilationUnit(node);
return (node.getAST() == globalAST) ? globalImportRewrite : ImportRewrite.create(compilationUnit, true);
}
}