SuiteMethodJUnitPlugin.java
/*******************************************************************************
* Copyright (c) 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.ANNOTATION_SELECT_CLASSES;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ANNOTATION_SUITE;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ORG_JUNIT_JUPITER_SUITE;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ORG_JUNIT_PLATFORM_SUITE_API_SELECT_CLASSES;
import java.util.List;
import java.util.Set;
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.ArrayInitializer;
import org.eclipse.jdt.core.dom.CompilationUnit;
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.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeLiteral;
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.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.text.edits.TextEditGroup;
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.JUnit3SuiteModel;
import org.sandbox.jdt.internal.corext.fix.helper.lib.JunitHolder;
/**
* Migrates JUnit 3 {@code public static Test suite()} aggregators to the
* JUnit Platform suite model.
*
* <p>The aggregator pattern is dominant in {@code org.eclipse.jdt.ui.tests*}
* ({@code AllTests}, {@code *TestSuite}). Only aggregators whose selected
* classes are provable from the source are migrated; everything else is left
* untouched, matching the fail-closed contract of the JUnit 3 hierarchy planner.
*
* <pre>
* public class AllTests { @Suite
* public static Test suite() { → @SelectClasses({ FooTest.class, BarTest.class })
* TestSuite suite= new TestSuite(); public class AllTests {
* suite.addTestSuite(FooTest.class); }
* suite.addTestSuite(BarTest.class);
* return suite;
* }
* }
* </pre>
*/
public class SuiteMethodJUnitPlugin extends AbstractTool<ReferenceHolder<Integer, JunitHolder>> {
@Override
public void find(JUnitCleanUpFixCore fixcore, CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed) {
compilationUnit.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
MethodDeclaration suite= findMigratableSuiteMethod(node, nodesprocessed);
if (suite == null) {
return true;
}
JUnit3SuiteModel.Result model= JUnit3SuiteModel.analyze(suite);
if (!model.supported()) {
return true;
}
nodesprocessed.add(node);
nodesprocessed.add(suite);
ReferenceHolder<Integer, JunitHolder> dataHolder= ReferenceHolder.createIndexed();
JunitHolder holder= new JunitHolder();
holder.setMinv(suite);
holder.setAdditionalInfo(model.selectedTypes());
dataHolder.put(dataHolder.size(), holder);
operations.add(fixcore.rewrite(dataHolder));
return true;
}
});
}
private MethodDeclaration findMigratableSuiteMethod(TypeDeclaration node, Set<ASTNode> nodesprocessed) {
if (nodesprocessed.contains(node) || !(node.getParent() instanceof CompilationUnit)
|| node.getSuperclassType() != null || node.isInterface() || hasJUnitAnnotation(node)
|| isExcludedOrTestCase(node)) {
return null;
}
MethodDeclaration suite= null;
for (MethodDeclaration method : node.getMethods()) {
if (JUnit3SuiteModel.isSuiteBuilder(method)) {
suite= method;
} else if (!method.isConstructor()) {
// Any additional behavior in an aggregator is not represented by @Suite.
return null;
}
}
if (suite == null || nodesprocessed.contains(suite) || node.getFields().length != 0
|| node.getTypes().length != 0) {
return null;
}
return suite;
}
/**
* Rejects aggregators that are test cases themselves or derive from a base
* type whose execution contract must not be migrated.
*/
private boolean isExcludedOrTestCase(TypeDeclaration node) {
ITypeBinding binding= node.resolveBinding();
if (binding == null) {
return node.getSuperclassType() != null;
}
if (JUnit3MigrationExclusions.isExcluded(binding)) {
return true;
}
for (ITypeBinding current= binding.getSuperclass(); current != null; current= current.getSuperclass()) {
if ("junit.framework.TestCase".equals(current.getErasure().getQualifiedName())) { //$NON-NLS-1$
return true;
}
}
return false;
}
private boolean hasJUnitAnnotation(TypeDeclaration node) {
for (Object modifier : node.modifiers()) {
if (!(modifier instanceof Annotation annotation)) {
continue;
}
ITypeBinding binding= annotation.resolveTypeBinding();
String name= binding == null || binding.isRecovered()
? annotation.getTypeName().getFullyQualifiedName()
: binding.getQualifiedName();
if (name.startsWith("org.junit.") || name.startsWith("junit.")) { //$NON-NLS-1$ //$NON-NLS-2$
return true;
}
}
return false;
}
@Override
protected void process2Rewrite(TextEditGroup group, ASTRewrite rewriter, AST ast, ImportRewrite importRewriter,
JunitHolder junitHolder) {
if (!(junitHolder.getMinv() instanceof MethodDeclaration suite)
|| !(suite.getParent() instanceof TypeDeclaration type)
|| !(junitHolder.getAdditionalInfo() instanceof List<?> selectedTypes)
|| selectedTypes.isEmpty()) {
return;
}
rewriter.remove(suite, group);
ListRewrite modifiers= rewriter.getListRewrite(type, TypeDeclaration.MODIFIERS2_PROPERTY);
MarkerAnnotation suiteAnnotation= ast.newMarkerAnnotation();
suiteAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_SUITE));
modifiers.insertFirst(suiteAnnotation, group);
SingleMemberAnnotation selectClasses= ast.newSingleMemberAnnotation();
selectClasses.setTypeName(ast.newSimpleName(ANNOTATION_SELECT_CLASSES));
if (selectedTypes.size() == 1) {
selectClasses.setValue(newTypeLiteral(ast, String.valueOf(selectedTypes.get(0))));
} else {
ArrayInitializer initializer= ast.newArrayInitializer();
for (Object selected : selectedTypes) {
addExpression(initializer, newTypeLiteral(ast, String.valueOf(selected)));
}
selectClasses.setValue(initializer);
}
modifiers.insertAfter(selectClasses, suiteAnnotation, group);
importRewriter.addImport(ORG_JUNIT_JUPITER_SUITE);
importRewriter.addImport(ORG_JUNIT_PLATFORM_SUITE_API_SELECT_CLASSES);
importRewriter.removeImport(JUnit3SuiteModel.JUNIT3_TEST);
importRewriter.removeImport(JUnit3SuiteModel.JUNIT3_TEST_SUITE);
}
@SuppressWarnings("unchecked")
private static void addExpression(ArrayInitializer initializer, TypeLiteral literal) {
initializer.expressions().add(literal);
}
private static TypeLiteral newTypeLiteral(AST ast, String typeName) {
TypeLiteral literal= ast.newTypeLiteral();
literal.setType(ast.newSimpleType(ast.newName(typeName)));
return literal;
}
@Override
public String getPreview(boolean afterRefactoring) {
if (afterRefactoring) {
return """
@Suite
@SelectClasses({ FooTest.class, BarTest.class })
public class AllTests {
}
"""; //$NON-NLS-1$
}
return """
public class AllTests {
public static Test suite() {
TestSuite suite= new TestSuite();
suite.addTestSuite(FooTest.class);
suite.addTestSuite(BarTest.class);
return suite;
}
}
"""; //$NON-NLS-1$
}
}