LifecycleMethodAdapter.java
/*******************************************************************************
* Copyright (c) 2025, 2026 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.Block;
import org.eclipse.jdt.core.dom.ExpressionStatement;
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.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.Type;
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.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.AstProcessorBuilder;
import org.sandbox.jdt.internal.common.ReferenceHolder;
/**
* Adapts JUnit 4 {@code ExternalResource} lifecycle methods to Jupiter callback
* methods without weakening their checked-exception or inheritance semantics.
*/
public final class LifecycleMethodAdapter {
private LifecycleMethodAdapter() {
throw new UnsupportedOperationException("Utility class"); //$NON-NLS-1$
}
/**
* Renames the selected lifecycle methods and adapts their callback contract.
*/
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);
}
}
}
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);
setPublicVisibilityIfProtected(method, rewriteToUse, ast, group);
adaptSuperLifecycleCalls(oldMethodName, newMethodName, method, rewriteToUse, ast, group,
directlyExtendsExternalResource(node));
replaceThrowsThrowableWithException(method, rewriteToUse, ast, group);
rewriteToUse.replace(method.getName(), ast.newSimpleName(newMethodName), group);
ensureExtensionContextParameter(method, rewriteToUse, ast, group, importRewriteToUse);
if (rewriteToUse != globalRewrite) {
DocumentHelper.createChangeForRewrite(
org.sandbox.jdt.internal.corext.util.ASTNavigationUtils.findCompilationUnit(node), rewriteToUse);
}
}
/**
* Compatibility entry point used by existing callers. Source hierarchy calls
* are renamed and receive the callback context.
*/
public static void adaptSuperBeforeCalls(String oldMethodName, String newMethodName,
MethodDeclaration method, ASTRewrite rewriter, AST ast, TextEditGroup group) {
adaptSuperLifecycleCalls(oldMethodName, newMethodName, method, rewriter, ast, group, false);
}
private static void adaptSuperLifecycleCalls(String oldMethodName, String newMethodName,
MethodDeclaration method, ASTRewrite rewriter, AST ast, TextEditGroup group,
boolean directExternalResourceSubclass) {
ReferenceHolder<String, Object> holder= ReferenceHolder.create();
AstProcessorBuilder.with(holder)
.onSuperMethodInvocation((node, ignored) -> {
if (!oldMethodName.equals(node.getName().getIdentifier())) {
return true;
}
if (directExternalResourceSubclass && node.getParent() instanceof ExpressionStatement statement) {
// ExternalResource itself disappears from the hierarchy. Its empty
// before()/after() implementation therefore has no Jupiter equivalent.
rewriter.remove(statement, group);
} else {
// A source superclass is migrated as part of the same closed plan, so
// preserve the explicit lifecycle chaining.
rewriter.replace(node.getName(), ast.newSimpleName(newMethodName), group);
addContextArgumentIfMissing(node, rewriter, ast, group);
}
return true;
})
.build(method);
}
/** Adds an {@code ExtensionContext} parameter when it is not already present. */
public static void ensureExtensionContextParameter(MethodDeclaration method, ASTRewrite rewrite, AST ast,
TextEditGroup group, ImportRewrite importRewrite) {
boolean hasExtensionContext= method.parameters().stream()
.anyMatch(param -> param instanceof SingleVariableDeclaration declaration
&& isExtensionContext(declaration, ORG_JUNIT_JUPITER_API_EXTENSION_EXTENSION_CONTEXT))
|| rewrite.getListRewrite(method, MethodDeclaration.PARAMETERS_PROPERTY).getRewrittenList().stream()
.anyMatch(param -> param instanceof SingleVariableDeclaration declaration
&& EXTENSION_CONTEXT.equals(declaration.getType().toString()));
if (hasExtensionContext) {
return;
}
SingleVariableDeclaration parameter= ast.newSingleVariableDeclaration();
parameter.setType(ast.newSimpleType(ast.newName(EXTENSION_CONTEXT)));
parameter.setName(ast.newSimpleName(VARIABLE_NAME_CONTEXT));
rewrite.getListRewrite(method, MethodDeclaration.PARAMETERS_PROPERTY).insertLast(parameter, group);
importRewrite.addImport(ORG_JUNIT_JUPITER_API_EXTENSION_EXTENSION_CONTEXT);
}
/**
* Historical compatibility method. Jupiter callbacks may throw
* {@link Exception}, but not arbitrary {@link Throwable}; replacing the type is
* therefore required to keep checked calls in real Eclipse fixtures compilable.
*/
public static void removeThrowsThrowable(MethodDeclaration method, ASTRewrite rewriter, TextEditGroup group) {
replaceThrowsThrowableWithException(method, rewriter, method.getAST(), group);
}
private static void replaceThrowsThrowableWithException(MethodDeclaration method, ASTRewrite rewriter,
AST ast, TextEditGroup group) {
boolean alreadyThrowsException= method.thrownExceptionTypes().stream()
.anyMatch(exception -> exception instanceof Type type
&& isType(type, "java.lang.Exception", "Exception")); //$NON-NLS-1$ //$NON-NLS-2$
ListRewrite exceptions= rewriter.getListRewrite(method,
MethodDeclaration.THROWN_EXCEPTION_TYPES_PROPERTY);
for (Object exceptionObject : method.thrownExceptionTypes()) {
if (!(exceptionObject instanceof Type exceptionType)
|| !isType(exceptionType, "java.lang.Throwable", "Throwable")) { //$NON-NLS-1$ //$NON-NLS-2$
continue;
}
if (alreadyThrowsException) {
exceptions.remove(exceptionType, group);
} else {
exceptions.replace(exceptionType,
ast.newSimpleType(ast.newSimpleName("Exception")), group); //$NON-NLS-1$
}
}
}
private static boolean isType(Type type, String qualifiedName, String sourceName) {
ITypeBinding binding= type.resolveBinding();
return binding != null ? qualifiedName.equals(binding.getErasure().getQualifiedName())
: sourceName.equals(type.toString()) || qualifiedName.equals(type.toString());
}
/** Changes protected callback implementations to the required public visibility. */
public static void setPublicVisibilityIfProtected(MethodDeclaration method, ASTRewrite rewrite, AST ast,
TextEditGroup group) {
for (Object modifierObject : method.modifiers()) {
if (modifierObject instanceof Modifier modifier && modifier.isProtected()) {
rewrite.getListRewrite(method, MethodDeclaration.MODIFIERS2_PROPERTY).replace(modifier,
ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD), group);
return;
}
}
}
/** Creates a callback method while removing obsolete direct super lifecycle calls. */
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));
SingleVariableDeclaration parameter= ast.newSingleVariableDeclaration();
parameter.setType(ast.newSimpleType(ast.newName(paramType)));
parameter.setName(ast.newSimpleName(VARIABLE_NAME_CONTEXT));
method.parameters().add(parameter);
if (oldBody != null) {
Block newBody= (Block) ASTNode.copySubtree(ast, oldBody);
removeSuperLifecycleCalls(newBody);
method.setBody(newBody);
}
return method;
}
private static void removeSuperLifecycleCalls(Block body) {
ReferenceHolder<String, Object> holder= ReferenceHolder.create();
AstProcessorBuilder.with(holder)
.onSuperMethodInvocation((node, ignored) -> {
String methodName= node.getName().getIdentifier();
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)) {
ASTNode parent= node.getParent();
if (parent instanceof ExpressionStatement) {
parent.delete();
}
}
return true;
})
.build(body);
}
private static boolean directlyExtendsExternalResource(TypeDeclaration type) {
ITypeBinding binding= type.resolveBinding();
ITypeBinding superclass= binding == null ? null : binding.getSuperclass();
return superclass != null && ORG_JUNIT_RULES_EXTERNAL_RESOURCE
.equals(superclass.getErasure().getQualifiedName());
}
private static boolean isLifecycleMethod(MethodDeclaration method, String methodName) {
return methodName.equals(method.getName().getIdentifier());
}
private static void addContextArgumentIfMissing(ASTNode node, ASTRewrite rewriter, AST ast,
TextEditGroup group) {
ListRewrite arguments;
if (node instanceof MethodInvocation) {
arguments= rewriter.getListRewrite(node, MethodInvocation.ARGUMENTS_PROPERTY);
} else if (node instanceof SuperMethodInvocation) {
arguments= rewriter.getListRewrite(node, SuperMethodInvocation.ARGUMENTS_PROPERTY);
} else {
return;
}
boolean hasContextArgument= arguments.getRewrittenList().stream()
.anyMatch(argument -> argument instanceof SimpleName name
&& VARIABLE_NAME_CONTEXT.equals(name.getIdentifier()));
if (!hasContextArgument) {
arguments.insertFirst(ast.newSimpleName(VARIABLE_NAME_CONTEXT), group);
}
}
private static boolean isExtensionContext(SingleVariableDeclaration parameter, String className) {
ITypeBinding binding= parameter.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);
}
}