RunWithJUnitPlugin.java
/*******************************************************************************
* Copyright (c) 2021 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;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.*;
import java.util.ArrayList;
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.Annotation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
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.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.corext.util.AnnotationUtils;
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.JunitHolder;
import org.sandbox.jdt.internal.corext.fix.helper.lib.TriggerPatternCleanupPlugin;
import org.sandbox.jdt.triggerpattern.api.CleanupPattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
/**
* Consolidated plugin to migrate ALL JUnit 4 {@code @RunWith(...)} variants
* and {@code @Suite.SuiteClasses} to JUnit 5 equivalents.
*
* <p>This single plugin handles all runner types via internal dispatch:
* <ul>
* <li>{@code @RunWith(Suite.class)} → {@code @Suite}</li>
* <li>{@code @RunWith(Enclosed.class)} → {@code @Nested} inner classes</li>
* <li>{@code @RunWith(Theories.class)} → {@code @ParameterizedTest} + {@code @ValueSource}</li>
* <li>{@code @RunWith(Categories.class)} → {@code @Suite} + {@code @IncludeTags/@ExcludeTags}</li>
* <li>{@code @RunWith(MockitoJUnitRunner.class)} → {@code @ExtendWith(MockitoExtension.class)}</li>
* <li>{@code @RunWith(SpringRunner.class)} → {@code @ExtendWith(SpringExtension.class)}</li>
* </ul>
*
* <p>Previously these were four separate plugins (RunWithJUnitPlugin,
* RunWithEnclosedJUnitPlugin, RunWithTheoriesJUnitPlugin,
* RunWithCategoriesJUnitPlugin) which all matched on {@code @RunWith} and shared
* the {@code nodesprocessed} set. This caused interference: when one plugin
* marked a node as processed but could not handle that runner type, the correct
* plugin never got a chance to process it. Consolidating into a single plugin
* eliminates this conflict.</p>
*
* <p>This plugin overrides {@code find()} because it needs to search for BOTH
* {@code @RunWith} and {@code @Suite.SuiteClasses} annotations — the base
* TriggerPatternCleanupPlugin only searches for the pattern in @CleanupPattern.</p>
*
* @since 1.3.0
*/
@CleanupPattern(value = "@RunWith($runner)", kind = PatternKind.ANNOTATION, qualifiedType = ORG_JUNIT_RUNWITH, cleanupId = "cleanup.junit.runwith", description = "Migrate @RunWith to JUnit 5 equivalents", displayName = "JUnit 4 @RunWith → JUnit 5 @ExtendWith/@Suite")
public class RunWithJUnitPlugin extends TriggerPatternCleanupPlugin {
// ---- Data classes for complex runner transformations ----
private static class TheoriesData {
Annotation runWithAnnotation;
FieldDeclaration dataPointsField;
MethodDeclaration theoryMethod;
ArrayInitializer dataPointsArray;
}
private static class CategoriesData {
Annotation runWithAnnotation;
TypeDeclaration typeDeclaration;
List<Annotation> includeCategories = new ArrayList<>();
List<Annotation> excludeCategories = new ArrayList<>();
Annotation suiteClasses;
}
// ---- find() — scan for @RunWith and @Suite.SuiteClasses ----
/**
* Override find() because we need to search for TWO annotation types:
* {@code @RunWith} and {@code @Suite.SuiteClasses}.
*/
@Override
public void find(JUnitCleanUpFixCore fixcore, CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed) {
ReferenceHolder<Integer, JunitHolder> dataHolder = ReferenceHolder.createIndexed();
// Find @RunWith annotations
HelperVisitorFactory.forAnnotation(ORG_JUNIT_RUNWITH).in(compilationUnit).excluding(nodesprocessed)
.processEach(dataHolder, (visited, aholder) -> {
if (visited instanceof SingleMemberAnnotation) {
return processFoundNodeRunWith(fixcore, operations, (Annotation) visited, aholder);
}
return true;
});
// Find @Suite.SuiteClasses annotations
HelperVisitorFactory.forAnnotation(ORG_JUNIT_SUITE_SUITECLASSES).in(compilationUnit).excluding(nodesprocessed)
.processEach(dataHolder, (visited, aholder) -> {
if (visited instanceof SingleMemberAnnotation) {
return processFoundNodeSuite(fixcore, operations, (Annotation) visited, aholder);
}
return true;
});
}
// ---- Runner type resolution ----
/**
* Resolves the fully qualified runner class name from a TypeLiteral
* (e.g., {@code Suite.class} → {@code "org.junit.runners.Suite"}).
*/
private String resolveRunnerQualifiedName(TypeLiteral myvalue) {
String runnerQualifiedName = null;
// Try to get qualified name from binding
ITypeBinding classBinding = myvalue.resolveTypeBinding();
if (classBinding != null) {
Type type = myvalue.getType();
if (type != null) {
ITypeBinding typeBinding = type.resolveBinding();
if (typeBinding != null) {
runnerQualifiedName = typeBinding.getQualifiedName();
}
}
}
// If binding resolution failed, try to get fully qualified name from the AST
if (runnerQualifiedName == null || runnerQualifiedName.isEmpty()) {
Type runnerType = myvalue.getType();
if (runnerType != null) {
String typeName = runnerType.toString();
// Only use it if it's a fully qualified name (contains a dot)
if (typeName.contains(".")) { //$NON-NLS-1$
runnerQualifiedName = typeName;
}
// Fallback: resolve well-known JUnit simple names
else {
runnerQualifiedName = resolveSimpleName(typeName);
}
}
}
return runnerQualifiedName;
}
/**
* Maps well-known JUnit simple class names to their qualified names
* when binding resolution fails.
*/
private String resolveSimpleName(String typeName) {
return switch (typeName) {
case "Suite" -> ORG_JUNIT_SUITE; //$NON-NLS-1$
case "Enclosed" -> ORG_JUNIT_EXPERIMENTAL_RUNNERS_ENCLOSED; //$NON-NLS-1$
case "Theories" -> ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORIES; //$NON-NLS-1$
case "Categories" -> ORG_JUNIT_EXPERIMENTAL_CATEGORIES_CATEGORIES; //$NON-NLS-1$
default -> null;
};
}
// ---- find() node processing ----
private boolean processFoundNodeRunWith(JUnitCleanUpFixCore fixcore,
Set<CompilationUnitRewriteOperationWithSourceRange> operations, Annotation node,
ReferenceHolder<Integer, JunitHolder> dataHolder) {
if (!(node instanceof SingleMemberAnnotation mynode)) {
return true;
}
Expression value = mynode.getValue();
if (!(value instanceof TypeLiteral myvalue)) {
return true;
}
String runnerQualifiedName = resolveRunnerQualifiedName(myvalue);
if (runnerQualifiedName == null) {
return true;
}
JunitHolder mh = new JunitHolder();
mh.setMinv(node);
mh.setMinvname(node.getTypeName().getFullyQualifiedName());
// --- Suite ---
if (ORG_JUNIT_SUITE.equals(runnerQualifiedName)) {
mh.setValue(ORG_JUNIT_RUNWITH);
dataHolder.put(dataHolder.size(), mh);
operations.add(fixcore.rewrite(dataHolder));
return true;
}
// --- Enclosed ---
if (ORG_JUNIT_EXPERIMENTAL_RUNNERS_ENCLOSED.equals(runnerQualifiedName)) {
mh.setValue(ORG_JUNIT_EXPERIMENTAL_RUNNERS_ENCLOSED);
dataHolder.put(dataHolder.size(), mh);
operations.add(fixcore.rewrite(dataHolder));
return true;
}
// --- Theories ---
if (ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORIES.equals(runnerQualifiedName)) {
TheoriesData data = buildTheoriesData(node);
if (data != null) {
mh.setValue(ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORIES);
mh.setAdditionalInfo(data);
dataHolder.put(dataHolder.size(), mh);
operations.add(fixcore.rewrite(dataHolder));
}
return true;
}
// --- Categories ---
if (ORG_JUNIT_EXPERIMENTAL_CATEGORIES_CATEGORIES.equals(runnerQualifiedName)) {
CategoriesData data = buildCategoriesData(node);
if (data != null) {
mh.setValue(ORG_JUNIT_EXPERIMENTAL_CATEGORIES_CATEGORIES);
mh.setAdditionalInfo(data);
dataHolder.put(dataHolder.size(), mh);
operations.add(fixcore.rewrite(dataHolder));
}
return true;
}
// --- Mockito ---
if (ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER.equals(runnerQualifiedName)
|| ORG_MOCKITO_RUNNERS_MOCKITO_JUNIT_RUNNER.equals(runnerQualifiedName)) {
mh.setValue(ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER);
dataHolder.put(dataHolder.size(), mh);
operations.add(fixcore.rewrite(dataHolder));
return true;
}
// --- Spring ---
if (ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER.equals(runnerQualifiedName)
|| ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_JUNIT4_CLASS_RUNNER.equals(runnerQualifiedName)) {
mh.setValue(ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER);
dataHolder.put(dataHolder.size(), mh);
operations.add(fixcore.rewrite(dataHolder));
return true;
}
return true;
}
private boolean processFoundNodeSuite(JUnitCleanUpFixCore fixcore,
Set<CompilationUnitRewriteOperationWithSourceRange> operations, Annotation node,
ReferenceHolder<Integer, JunitHolder> dataHolder) {
JunitHolder mh = new JunitHolder();
mh.setMinv(node);
mh.setMinvname(node.getTypeName().getFullyQualifiedName());
mh.setValue(ORG_JUNIT_SUITE_SUITECLASSES);
dataHolder.put(dataHolder.size(), mh);
operations.add(fixcore.rewrite(dataHolder));
return true;
}
// ---- Rewrite dispatch ----
@Override
protected void process2Rewrite(TextEditGroup group, ASTRewrite rewriter, AST ast, ImportRewrite importRewriter,
JunitHolder junitHolder) {
String runnerValue = junitHolder.getValue();
if (ORG_JUNIT_SUITE_SUITECLASSES.equals(runnerValue)) {
rewriteSuiteClasses(group, rewriter, ast, importRewriter, junitHolder);
} else if (ORG_JUNIT_RUNWITH.equals(runnerValue)) {
rewriteSuiteRunner(group, rewriter, ast, importRewriter, junitHolder);
} else if (ORG_JUNIT_EXPERIMENTAL_RUNNERS_ENCLOSED.equals(runnerValue)) {
rewriteEnclosed(group, rewriter, ast, importRewriter, junitHolder);
} else if (ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORIES.equals(runnerValue)) {
rewriteTheories(group, rewriter, ast, importRewriter, junitHolder);
} else if (ORG_JUNIT_EXPERIMENTAL_CATEGORIES_CATEGORIES.equals(runnerValue)) {
rewriteCategories(group, rewriter, ast, importRewriter, junitHolder);
} else if (ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER.equals(runnerValue)) {
rewriteMockito(group, rewriter, ast, importRewriter, junitHolder);
} else if (ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER.equals(runnerValue)) {
rewriteSpring(group, rewriter, ast, importRewriter, junitHolder);
}
}
// ---- @Suite.SuiteClasses → @SelectClasses ----
private void rewriteSuiteClasses(TextEditGroup group, ASTRewrite rewriter, AST ast,
ImportRewrite importRewriter, JunitHolder junitHolder) {
Annotation minv = junitHolder.getAnnotation();
SingleMemberAnnotation mynode = (SingleMemberAnnotation) minv;
SingleMemberAnnotation newAnnotation = ast.newSingleMemberAnnotation();
newAnnotation.setValue(ASTNodes.createMoveTarget(rewriter, mynode.getValue()));
newAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_SELECT_CLASSES));
importRewriter.addImport(ORG_JUNIT_PLATFORM_SUITE_API_SELECT_CLASSES);
importRewriter.removeImport(ORG_JUNIT_SUITE_SUITECLASSES);
importRewriter.removeImport(ORG_JUNIT_SUITE);
ASTNodes.replaceButKeepComment(rewriter, minv, newAnnotation, group);
importRewriter.removeImport(ORG_JUNIT_RUNWITH);
}
// ---- @RunWith(Suite.class) → @Suite ----
private void rewriteSuiteRunner(TextEditGroup group, ASTRewrite rewriter, AST ast,
ImportRewrite importRewriter, JunitHolder junitHolder) {
Annotation minv = junitHolder.getAnnotation();
Annotation newAnnotation = AnnotationUtils.createMarkerAnnotation(ast, ANNOTATION_SUITE);
importRewriter.addImport(ORG_JUNIT_JUPITER_SUITE);
importRewriter.removeImport(ORG_JUNIT_SUITE);
importRewriter.removeImport(ORG_JUNIT_RUNWITH);
ASTNodes.replaceButKeepComment(rewriter, minv, newAnnotation, group);
importRewriter.removeImport(ORG_JUNIT_RUNWITH);
}
// ---- @RunWith(Enclosed.class) → @Nested inner classes ----
private void rewriteEnclosed(TextEditGroup group, ASTRewrite rewriter, AST ast,
ImportRewrite importRewriter, JunitHolder junitHolder) {
Annotation runWithAnnotation = junitHolder.getAnnotation();
// Remove @RunWith(Enclosed.class) annotation
rewriter.remove(runWithAnnotation, group);
// Find the enclosing TypeDeclaration
TypeDeclaration outerClass = findEnclosingType(runWithAnnotation);
if (outerClass == null) {
return;
}
// Transform inner static classes to @Nested classes
for (TypeDeclaration innerType : outerClass.getTypes()) {
if (Modifier.isStatic(innerType.getModifiers()) && hasTestMethods(innerType)) {
ListRewrite modifiersRewrite = rewriter.getListRewrite(innerType,
TypeDeclaration.MODIFIERS2_PROPERTY);
for (Object modifier : innerType.modifiers()) {
if (modifier instanceof Modifier mod && mod.isStatic()) {
modifiersRewrite.remove(mod, group);
}
if (modifier instanceof Modifier mod && mod.isPublic()) {
modifiersRewrite.remove(mod, group);
}
}
MarkerAnnotation nestedAnnotation = AnnotationUtils.createMarkerAnnotation(ast, ANNOTATION_NESTED);
modifiersRewrite.insertFirst(nestedAnnotation, group);
}
}
importRewriter.addImport(ORG_JUNIT_JUPITER_API_NESTED);
importRewriter.removeImport(ORG_JUNIT_EXPERIMENTAL_RUNNERS_ENCLOSED);
importRewriter.removeImport(ORG_JUNIT_RUNWITH);
}
// ---- @RunWith(Theories.class) → @ParameterizedTest + @ValueSource ----
private TheoriesData buildTheoriesData(Annotation node) {
TypeDeclaration typeDecl = findEnclosingType(node);
if (typeDecl == null) {
return null;
}
TheoriesData data = new TheoriesData();
data.runWithAnnotation = node;
findTheoriesComponents(typeDecl, data);
if (data.dataPointsField == null || data.theoryMethod == null) {
return null;
}
return data;
}
private void rewriteTheories(TextEditGroup group, ASTRewrite rewriter, AST ast,
ImportRewrite importRewriter, JunitHolder junitHolder) {
TheoriesData theoriesData = (TheoriesData) junitHolder.getAdditionalInfo();
// Remove @RunWith(Theories.class)
rewriter.remove(theoriesData.runWithAnnotation, group);
// Remove @DataPoints field
rewriter.remove(theoriesData.dataPointsField, group);
// Transform @Theory method
transformTheoryMethod(theoriesData, rewriter, ast, group);
importRewriter.addImport(ORG_JUNIT_JUPITER_PARAMS_PARAMETERIZED_TEST);
importRewriter.addImport(ORG_JUNIT_JUPITER_PARAMS_PROVIDER_VALUE_SOURCE);
importRewriter.removeImport(ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORIES);
importRewriter.removeImport(ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORY);
importRewriter.removeImport(ORG_JUNIT_EXPERIMENTAL_THEORIES_DATAPOINTS);
importRewriter.removeImport(ORG_JUNIT_RUNWITH);
}
// ---- @RunWith(Categories.class) → @Suite + @IncludeTags/@ExcludeTags ----
private CategoriesData buildCategoriesData(Annotation node) {
TypeDeclaration typeDecl = findEnclosingType(node);
if (typeDecl == null) {
return null;
}
CategoriesData data = new CategoriesData();
data.runWithAnnotation = node;
data.typeDeclaration = typeDecl;
findCategoryAnnotations(typeDecl, data);
return data;
}
private void rewriteCategories(TextEditGroup group, ASTRewrite rewriter, AST ast,
ImportRewrite importRewriter, JunitHolder junitHolder) {
CategoriesData categoriesData = (CategoriesData) junitHolder.getAdditionalInfo();
TypeDeclaration typeDecl = categoriesData.typeDeclaration;
ListRewrite modifiersRewrite = rewriter.getListRewrite(typeDecl, TypeDeclaration.MODIFIERS2_PROPERTY);
// Replace @RunWith(Categories.class) with @Suite
MarkerAnnotation suiteAnnotation = AnnotationUtils.createMarkerAnnotation(ast, ANNOTATION_SUITE);
modifiersRewrite.replace(categoriesData.runWithAnnotation, suiteAnnotation, group);
for (Annotation includeCategory : categoriesData.includeCategories) {
modifiersRewrite.replace(includeCategory, createIncludeTagsAnnotation(ast, includeCategory), group);
}
for (Annotation excludeCategory : categoriesData.excludeCategories) {
modifiersRewrite.replace(excludeCategory, createExcludeTagsAnnotation(ast, excludeCategory), group);
}
if (categoriesData.suiteClasses instanceof SingleMemberAnnotation suiteClassesAnnotation) {
SingleMemberAnnotation selectClassesAnnotation = ast.newSingleMemberAnnotation();
selectClassesAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_SELECT_CLASSES));
selectClassesAnnotation.setValue(ASTNodes.createMoveTarget(rewriter, suiteClassesAnnotation.getValue()));
modifiersRewrite.replace(categoriesData.suiteClasses, selectClassesAnnotation, group);
}
importRewriter.addImport(ORG_JUNIT_JUPITER_SUITE);
importRewriter.addImport(ORG_JUNIT_PLATFORM_SUITE_API_INCLUDE_TAGS);
importRewriter.addImport(ORG_JUNIT_PLATFORM_SUITE_API_EXCLUDE_TAGS);
importRewriter.addImport(ORG_JUNIT_PLATFORM_SUITE_API_SELECT_CLASSES);
importRewriter.removeImport(ORG_JUNIT_EXPERIMENTAL_CATEGORIES_CATEGORIES);
importRewriter.removeImport(ORG_JUNIT_EXPERIMENTAL_CATEGORIES_INCLUDE_CATEGORY);
importRewriter.removeImport(ORG_JUNIT_EXPERIMENTAL_CATEGORIES_EXCLUDE_CATEGORY);
importRewriter.removeImport(ORG_JUNIT_SUITE_SUITECLASSES);
importRewriter.removeImport(ORG_JUNIT_RUNWITH);
}
// ---- @RunWith(MockitoJUnitRunner.class) → @ExtendWith(MockitoExtension.class) ----
private void rewriteMockito(TextEditGroup group, ASTRewrite rewriter, AST ast,
ImportRewrite importRewriter, JunitHolder junitHolder) {
Annotation minv = junitHolder.getAnnotation();
SingleMemberAnnotation extendWithAnnotation = ast.newSingleMemberAnnotation();
extendWithAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_EXTEND_WITH));
TypeLiteral typeLiteral = ast.newTypeLiteral();
typeLiteral.setType(ast.newSimpleType(ast.newName(MOCKITO_EXTENSION)));
extendWithAnnotation.setValue(typeLiteral);
importRewriter.addImport(ORG_JUNIT_JUPITER_API_EXTENSION_EXTEND_WITH);
importRewriter.addImport(ORG_MOCKITO_JUNIT_JUPITER_MOCKITO_EXTENSION);
importRewriter.removeImport(ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER);
importRewriter.removeImport(ORG_MOCKITO_RUNNERS_MOCKITO_JUNIT_RUNNER);
ASTNodes.replaceButKeepComment(rewriter, minv, extendWithAnnotation, group);
importRewriter.removeImport(ORG_JUNIT_RUNWITH);
}
// ---- @RunWith(SpringRunner.class) → @ExtendWith(SpringExtension.class) ----
private void rewriteSpring(TextEditGroup group, ASTRewrite rewriter, AST ast,
ImportRewrite importRewriter, JunitHolder junitHolder) {
Annotation minv = junitHolder.getAnnotation();
SingleMemberAnnotation extendWithAnnotation = ast.newSingleMemberAnnotation();
extendWithAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_EXTEND_WITH));
TypeLiteral typeLiteral = ast.newTypeLiteral();
typeLiteral.setType(ast.newSimpleType(ast.newName(SPRING_EXTENSION)));
extendWithAnnotation.setValue(typeLiteral);
importRewriter.addImport(ORG_JUNIT_JUPITER_API_EXTENSION_EXTEND_WITH);
importRewriter.addImport(ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT_JUPITER_SPRING_EXTENSION);
importRewriter.removeImport(ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER);
importRewriter.removeImport(ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_JUNIT4_CLASS_RUNNER);
ASTNodes.replaceButKeepComment(rewriter, minv, extendWithAnnotation, group);
importRewriter.removeImport(ORG_JUNIT_RUNWITH);
}
// ---- Helper methods (from former Enclosed, Theories, Categories plugins) ----
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null) {
if (parent instanceof TypeDeclaration td) {
return td;
}
parent = parent.getParent();
}
return null;
}
private boolean hasTestMethods(TypeDeclaration typeDecl) {
for (MethodDeclaration method : typeDecl.getMethods()) {
for (Object modifier : method.modifiers()) {
if (modifier instanceof Annotation annotation) {
String annotationName = annotation.getTypeName().getFullyQualifiedName();
if ("Test".equals(annotationName) || ORG_JUNIT_TEST.equals(annotationName) //$NON-NLS-1$
|| ORG_JUNIT_JUPITER_TEST.equals(annotationName)) {
return true;
}
}
}
}
return false;
}
private void findTheoriesComponents(TypeDeclaration typeDecl, TheoriesData data) {
for (FieldDeclaration field : typeDecl.getFields()) {
for (Object modifier : field.modifiers()) {
if (modifier instanceof Annotation annotation) {
String annotationName = annotation.getTypeName().getFullyQualifiedName();
if ("DataPoints".equals(annotationName) //$NON-NLS-1$
|| ORG_JUNIT_EXPERIMENTAL_THEORIES_DATAPOINTS.equals(annotationName)) {
data.dataPointsField = field;
List<?> fragments = field.fragments();
if (!fragments.isEmpty()
&& fragments.get(0) instanceof VariableDeclarationFragment fragment) {
Expression initializer = fragment.getInitializer();
if (initializer instanceof ArrayInitializer arrayInit) {
data.dataPointsArray = arrayInit;
}
}
break;
}
}
}
}
for (MethodDeclaration method : typeDecl.getMethods()) {
for (Object modifier : method.modifiers()) {
if (modifier instanceof Annotation annotation) {
String annotationName = annotation.getTypeName().getFullyQualifiedName();
if ("Theory".equals(annotationName) //$NON-NLS-1$
|| ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORY.equals(annotationName)) {
data.theoryMethod = method;
break;
}
}
}
}
}
private void transformTheoryMethod(TheoriesData data, ASTRewrite rewriter, AST ast, TextEditGroup group) {
MethodDeclaration theoryMethod = data.theoryMethod;
ListRewrite modifiersRewrite = rewriter.getListRewrite(theoryMethod, MethodDeclaration.MODIFIERS2_PROPERTY);
for (Object modifier : theoryMethod.modifiers()) {
if (modifier instanceof Annotation annotation) {
String annotationName = annotation.getTypeName().getFullyQualifiedName();
if ("Theory".equals(annotationName) //$NON-NLS-1$
|| ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORY.equals(annotationName)) {
Annotation parameterizedTest = AnnotationUtils.createMarkerAnnotation(ast,
ANNOTATION_PARAMETERIZED_TEST);
NormalAnnotation valueSource = createValueSourceAnnotation(ast, data);
modifiersRewrite.replace(annotation, parameterizedTest, group);
modifiersRewrite.insertAfter(valueSource, parameterizedTest, group);
break;
}
}
}
}
private NormalAnnotation createValueSourceAnnotation(AST ast, TheoriesData data) {
NormalAnnotation valueSource = ast.newNormalAnnotation();
valueSource.setTypeName(ast.newSimpleName(ANNOTATION_VALUE_SOURCE));
String memberName = determineValueSourceMember(data.dataPointsField);
MemberValuePair valuePair = ast.newMemberValuePair();
valuePair.setName(ast.newSimpleName(memberName));
if (data.dataPointsArray != null) {
ArrayInitializer newArray = (ArrayInitializer) ASTNode.copySubtree(ast, data.dataPointsArray);
valuePair.setValue(newArray);
}
valueSource.values().add(valuePair);
return valueSource;
}
private String determineValueSourceMember(FieldDeclaration field) {
if (field == null) {
return "ints"; //$NON-NLS-1$
}
Type fieldType = field.getType();
if (fieldType == null) {
return "ints"; //$NON-NLS-1$
}
String typeName = fieldType.toString();
if (typeName.endsWith("[]")) { //$NON-NLS-1$
String baseType = typeName.substring(0, typeName.length() - 2);
return switch (baseType) {
case "int" -> "ints"; //$NON-NLS-1$ //$NON-NLS-2$
case "String" -> "strings"; //$NON-NLS-1$ //$NON-NLS-2$
case "double" -> "doubles"; //$NON-NLS-1$ //$NON-NLS-2$
case "long" -> "longs"; //$NON-NLS-1$ //$NON-NLS-2$
case "short" -> "shorts"; //$NON-NLS-1$ //$NON-NLS-2$
case "byte" -> "bytes"; //$NON-NLS-1$ //$NON-NLS-2$
case "float" -> "floats"; //$NON-NLS-1$ //$NON-NLS-2$
case "char" -> "chars"; //$NON-NLS-1$ //$NON-NLS-2$
case "boolean" -> "booleans"; //$NON-NLS-1$ //$NON-NLS-2$
case "Class" -> "classes"; //$NON-NLS-1$ //$NON-NLS-2$
default -> "ints"; //$NON-NLS-1$
};
}
return "ints"; //$NON-NLS-1$
}
private void findCategoryAnnotations(TypeDeclaration typeDecl, CategoriesData data) {
for (Object modifier : typeDecl.modifiers()) {
if (modifier instanceof Annotation annotation) {
String annotationName = annotation.getTypeName().getFullyQualifiedName();
if ("IncludeCategory".equals(annotationName) //$NON-NLS-1$
|| ORG_JUNIT_EXPERIMENTAL_CATEGORIES_INCLUDE_CATEGORY.equals(annotationName)
|| "Categories.IncludeCategory".equals(annotationName)) { //$NON-NLS-1$
data.includeCategories.add(annotation);
} else if ("ExcludeCategory".equals(annotationName) //$NON-NLS-1$
|| ORG_JUNIT_EXPERIMENTAL_CATEGORIES_EXCLUDE_CATEGORY.equals(annotationName)
|| "Categories.ExcludeCategory".equals(annotationName)) { //$NON-NLS-1$
data.excludeCategories.add(annotation);
} else if ("SuiteClasses".equals(annotationName) //$NON-NLS-1$
|| ORG_JUNIT_SUITE_SUITECLASSES.equals(annotationName)
|| "Suite.SuiteClasses".equals(annotationName)) { //$NON-NLS-1$
data.suiteClasses = annotation;
}
}
}
}
private SingleMemberAnnotation createIncludeTagsAnnotation(AST ast, Annotation includeCategory) {
SingleMemberAnnotation includeTagsAnnotation = ast.newSingleMemberAnnotation();
includeTagsAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_INCLUDE_TAGS));
if (includeCategory instanceof SingleMemberAnnotation categoryAnnotation) {
String tagName = extractTagNameFromValue(categoryAnnotation.getValue());
StringLiteral tagLiteral = ast.newStringLiteral();
tagLiteral.setLiteralValue(tagName);
includeTagsAnnotation.setValue(tagLiteral);
}
return includeTagsAnnotation;
}
private SingleMemberAnnotation createExcludeTagsAnnotation(AST ast, Annotation excludeCategory) {
SingleMemberAnnotation excludeTagsAnnotation = ast.newSingleMemberAnnotation();
excludeTagsAnnotation.setTypeName(ast.newSimpleName(ANNOTATION_EXCLUDE_TAGS));
if (excludeCategory instanceof SingleMemberAnnotation categoryAnnotation) {
String tagName = extractTagNameFromValue(categoryAnnotation.getValue());
StringLiteral tagLiteral = ast.newStringLiteral();
tagLiteral.setLiteralValue(tagName);
excludeTagsAnnotation.setValue(tagLiteral);
}
return excludeTagsAnnotation;
}
private String extractTagNameFromValue(Expression value) {
if (value instanceof TypeLiteral typeLiteral) {
Type type = typeLiteral.getType();
if (type != null) {
return type.toString();
}
}
return "Unknown"; //$NON-NLS-1$
}
// ---- Preview and identity ----
@Override
public String getPreview(boolean afterRefactoring) {
if (afterRefactoring) {
return """
@Suite
@SelectClasses({
MyTest.class
})
"""; //$NON-NLS-1$
}
return """
@RunWith(Suite.class)
@Suite.SuiteClasses({
MyTest.class
})
"""; //$NON-NLS-1$
}
@Override
public String toString() {
return "RunWith"; //$NON-NLS-1$
}
}