TestJUnit3Plugin.java
/*******************************************************************************
* Copyright (c) 2021, 2026 Carsten Hammer and others.
*
* 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
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.*;
import java.util.Set;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
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.Annotation;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
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.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.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.AstProcessorBuilder;
import org.sandbox.jdt.internal.common.HelperVisitorFactory;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.JUnitCleanUpFixCore;
import org.sandbox.jdt.internal.corext.fix.helper.lib.AbstractTool;
import org.sandbox.jdt.internal.corext.fix.helper.lib.JUnit3MigrationExclusions;
import org.sandbox.jdt.internal.corext.fix.helper.lib.JunitHolder;
import org.sandbox.jdt.internal.corext.util.AnnotationUtils;
/**
* Migrates only a narrowly proven, self-contained JUnit 3 {@code TestCase} to
* Jupiter. Hierarchy-driven or custom JUnit 3 execution models are deliberately
* rejected until they can be migrated by a coordinated project-wide planner.
*/
public class TestJUnit3Plugin extends AbstractTool<ReferenceHolder<Integer, JunitHolder>> {
private static final String JUNIT3_TEST_CASE= "junit.framework.TestCase"; //$NON-NLS-1$
private static final String JUNIT3_ASSERT= "junit.framework.Assert"; //$NON-NLS-1$
private static final Set<String> KNOWN_JUNIT3_ASSERTION_METHODS= Set.of(
"assertEquals", "assertArrayEquals", "assertTrue", "assertFalse", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
"assertNull", "assertNotNull", "assertSame", "assertNotSame", "fail"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
private static final Set<String> CUSTOM_EXECUTION_METHODS= Set.of(
"suite", "runTest", "runBare", "createResult", "countTestCases", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
"getName", "setName", "run"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
@Override
public void find(JUnitCleanUpFixCore fixcore, CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed) {
ReferenceHolder<Integer, JunitHolder> dataHolder= ReferenceHolder.createIndexed();
HelperVisitorFactory.callTypeDeclarationVisitor(JUNIT3_TEST_CASE, compilationUnit, dataHolder, nodesprocessed,
(visited, holder) -> processFoundNode(fixcore, operations, visited, holder, nodesprocessed));
}
private boolean processFoundNode(JUnitCleanUpFixCore fixcore,
Set<CompilationUnitRewriteOperationWithSourceRange> operations, TypeDeclaration node,
ReferenceHolder<Integer, JunitHolder> dataHolder, Set<ASTNode> nodesprocessed) {
if (nodesprocessed.contains(node) || !isSafeStandaloneCandidate(node)) {
return false;
}
nodesprocessed.add(node);
JunitHolder holder= new JunitHolder();
holder.setMinv(node);
dataHolder.put(dataHolder.size(), holder);
operations.add(fixcore.rewrite(dataHolder));
return false;
}
private boolean isSafeStandaloneCandidate(TypeDeclaration node) {
ITypeBinding binding= node.resolveBinding();
ITypeBinding superclass= binding == null ? null : binding.getSuperclass();
if (binding == null || superclass == null || !node.isPackageMemberTypeDeclaration()
|| !Modifier.isPublic(node.getModifiers()) || Modifier.isAbstract(node.getModifiers())
|| !JUNIT3_TEST_CASE.equals(superclass.getErasure().getQualifiedName())
|| hasJUnitAnnotation(node) || !(binding.getJavaElement() instanceof IType type)) {
return false;
}
if (JUnit3MigrationExclusions.isExcluded(binding)
|| hasKnownSubtypes(type) || hasAnyReferences(type) || hasUnsupportedTestCaseUsage(node)) {
return false;
}
boolean testFound= false;
for (MethodDeclaration method : node.getMethods()) {
if (method.isConstructor() || hasJUnitAnnotation(method)) {
return false;
}
String name= method.getName().getIdentifier();
if (CUSTOM_EXECUTION_METHODS.contains(name)) {
return false;
}
if (name.startsWith("test")) { //$NON-NLS-1$
if (!isTestMethod(method)) {
return false;
}
testFound= true;
} else if (("setUp".equals(name) || "tearDown".equals(name)) //$NON-NLS-1$ //$NON-NLS-2$
&& !isLifecycleMethod(method, name)) {
return false;
}
}
return testFound;
}
private boolean hasKnownSubtypes(IType type) {
try {
ITypeHierarchy hierarchy= type.newTypeHierarchy(null);
return hierarchy.getAllSubtypes(type).length != 0;
} catch (JavaModelException e) {
return true;
}
}
private boolean hasAnyReferences(IType type) {
SearchPattern pattern= SearchPattern.createPattern(type, IJavaSearchConstants.REFERENCES);
if (pattern == null) {
return true;
}
boolean[] referenced= { false };
try {
new SearchEngine().search(pattern,
new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() },
SearchEngine.createWorkspaceScope(), new SearchRequestor() {
@Override
public void acceptSearchMatch(SearchMatch match) {
referenced[0]= true;
}
}, null);
} catch (CoreException e) {
return true;
}
return referenced[0];
}
private boolean hasUnsupportedTestCaseUsage(TypeDeclaration node) {
boolean[] unsupported= { false };
node.accept(new ASTVisitor() {
@Override
public boolean visit(SuperMethodInvocation invocation) {
unsupported[0]= true;
return false;
}
@Override
public boolean visit(MethodInvocation invocation) {
String name= invocation.getName().getIdentifier();
IMethodBinding methodBinding= invocation.resolveMethodBinding();
if (methodBinding == null) {
unsupported[0]|= CUSTOM_EXECUTION_METHODS.contains(name);
return !unsupported[0];
}
ITypeBinding declaringClass= methodBinding.getDeclaringClass();
String declaringName= declaringClass == null ? "" //$NON-NLS-1$
: declaringClass.getErasure().getQualifiedName();
if ((JUNIT3_TEST_CASE.equals(declaringName) || JUNIT3_ASSERT.equals(declaringName))
&& !KNOWN_JUNIT3_ASSERTION_METHODS.contains(name)) {
unsupported[0]= true;
}
return !unsupported[0];
}
});
return unsupported[0];
}
private boolean hasJUnitAnnotation(BodyDeclaration declaration) {
for (Object modifier : declaration.modifiers()) {
if (modifier instanceof Annotation annotation) {
ITypeBinding annotationBinding= annotation.resolveTypeBinding();
String name= annotationBinding == null
? annotation.getTypeName().getFullyQualifiedName()
: annotationBinding.getQualifiedName();
if (name.startsWith("org.junit.") || name.startsWith("junit.framework.")) { //$NON-NLS-1$ //$NON-NLS-2$
return true;
}
}
}
return false;
}
private boolean isTestMethod(MethodDeclaration method) {
return !method.isConstructor() && method.getName().getIdentifier().startsWith("test") //$NON-NLS-1$
&& Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers())
&& method.parameters().isEmpty() && isVoidReturnType(method);
}
private boolean isLifecycleMethod(MethodDeclaration method, String expectedName) {
return expectedName.equals(method.getName().getIdentifier())
&& !Modifier.isStatic(method.getModifiers()) && !Modifier.isPrivate(method.getModifiers())
&& method.parameters().isEmpty() && isVoidReturnType(method);
}
@Override
protected void process2Rewrite(TextEditGroup group, ASTRewrite rewriter, AST ast, ImportRewrite importRewriter,
JunitHolder junitHolder) {
TypeDeclaration node= junitHolder.getTypeDeclaration();
Type superclass= node.getSuperclassType();
if (superclass != null) {
rewriter.remove(superclass, group);
importRewriter.removeImport(JUNIT3_TEST_CASE);
}
for (MethodDeclaration method : node.getMethods()) {
if (isLifecycleMethod(method, "setUp")) { //$NON-NLS-1$
convertToAnnotation(method, "BeforeEach", importRewriter, rewriter, ast, group); //$NON-NLS-1$
} else if (isLifecycleMethod(method, "tearDown")) { //$NON-NLS-1$
convertToAnnotation(method, "AfterEach", importRewriter, rewriter, ast, group); //$NON-NLS-1$
} else if (isTestMethod(method)) {
addAnnotationToMethod(method, "Test", importRewriter, rewriter, ast, group); //$NON-NLS-1$
}
if (method.getBody() != null) {
rewriteAssertions(method, rewriter, ast, group, importRewriter);
}
}
}
private void rewriteAssertions(MethodDeclaration method, ASTRewrite rewriter, AST ast,
TextEditGroup group, ImportRewrite importRewriter) {
ReferenceHolder<String, Object> holder= ReferenceHolder.create();
AstProcessorBuilder.with(holder)
.onMethodInvocation((node, ignored) -> {
boolean junitAssertion= false;
if (node.resolveMethodBinding() != null) {
String owner= node.resolveMethodBinding().getDeclaringClass().getQualifiedName();
junitAssertion= JUNIT3_ASSERT.equals(owner) || JUNIT3_TEST_CASE.equals(owner);
} else if (node.getExpression() == null) {
junitAssertion= KNOWN_JUNIT3_ASSERTION_METHODS.contains(node.getName().getIdentifier());
}
if (junitAssertion) {
reorderParameters(node, rewriter, group, ONEPARAM_ASSERTIONS, TWOPARAM_ASSERTIONS);
if (node.getExpression() != null) {
rewriter.set(node.getExpression(), SimpleName.IDENTIFIER_PROPERTY, "Assertions", group); //$NON-NLS-1$
} else {
rewriter.set(node, MethodInvocation.EXPRESSION_PROPERTY,
ast.newSimpleName("Assertions"), group); //$NON-NLS-1$
}
addImportForAssertion(node.getName().getIdentifier(), importRewriter);
}
return true;
})
.build(method);
}
private void addImportForAssertion(String assertionMethod, ImportRewrite importRewriter) {
if (KNOWN_JUNIT3_ASSERTION_METHODS.contains(assertionMethod)) {
importRewriter.addImport(ORG_JUNIT_JUPITER_API_ASSERTIONS);
}
}
private boolean isVoidReturnType(MethodDeclaration method) {
Type returnType= method.getReturnType2();
return returnType != null && returnType.isPrimitiveType()
&& PrimitiveType.VOID.equals(((org.eclipse.jdt.core.dom.PrimitiveType) returnType).getPrimitiveTypeCode());
}
private void convertToAnnotation(MethodDeclaration method, String annotation, ImportRewrite importRewrite,
ASTRewrite rewrite, AST ast, TextEditGroup group) {
removeOverrideAnnotation(method, rewrite, group);
addAnnotationToMethod(method, annotation, importRewrite, rewrite, ast, group);
}
private void removeOverrideAnnotation(MethodDeclaration method, ASTRewrite rewrite, TextEditGroup group) {
for (Object modifier : method.modifiers()) {
if (modifier instanceof Annotation annotation
&& "Override".equals(annotation.getTypeName().getFullyQualifiedName())) { //$NON-NLS-1$
rewrite.remove(annotation, group);
return;
}
}
}
private void addAnnotationToMethod(MethodDeclaration method, String annotation, ImportRewrite importRewrite,
ASTRewrite rewrite, AST ast, TextEditGroup group) {
ListRewrite modifiers= rewrite.getListRewrite(method, MethodDeclaration.MODIFIERS2_PROPERTY);
MarkerAnnotation newMarkerAnnotation= AnnotationUtils.createMarkerAnnotation(ast, annotation);
modifiers.insertFirst(newMarkerAnnotation, group);
importRewrite.addImport("org.junit.jupiter.api." + annotation); //$NON-NLS-1$
}
@Override
public String getPreview(boolean afterRefactoring) {
return afterRefactoring ? "import org.junit.jupiter.api.Test;\n" //$NON-NLS-1$
: "import junit.framework.TestCase;\n"; //$NON-NLS-1$
}
@Override
public String toString() {
return "TestCase"; //$NON-NLS-1$
}
}