RunWithMigrationEligibility.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.*;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
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.VariableDeclarationFragment;
/**
* Fail-closed assessment shared by strict and best-effort runner analysis.
*
* <p>The assessment mirrors the source shapes the existing runner rewriters can
* actually transform. A runner name alone is never sufficient for complex
* Enclosed, Theories, Categories, or Parameterized migrations.</p>
*/
public final class RunWithMigrationEligibility {
private RunWithMigrationEligibility() {
throw new UnsupportedOperationException("Utility class"); //$NON-NLS-1$
}
/** Returns whether this exact runner and source shape can be migrated. */
public static boolean canMigrate(TypeDeclaration type, String qualifiedRunnerName) {
if (type == null || qualifiedRunnerName == null || qualifiedRunnerName.isBlank()) {
return false;
}
if (ORG_JUNIT_RUNNERS_PARAMETERIZED.equals(qualifiedRunnerName)) {
return ParameterizedMigrationEligibility.assess(type).eligible();
}
Annotation runWith= runWithAnnotation(type);
if (!(runWith instanceof SingleMemberAnnotation single)
|| !(single.getValue() instanceof TypeLiteral literal)) {
return false;
}
ITypeBinding runnerBinding= literal.getType().resolveBinding();
if (runnerBinding == null
|| !qualifiedRunnerName.equals(runnerBinding.getErasure().getQualifiedName())) {
return false;
}
if (ORG_JUNIT_SUITE.equals(qualifiedRunnerName)
|| ORG_MOCKITO_JUNIT_MOCKITO_JUNIT_RUNNER.equals(qualifiedRunnerName)
|| ORG_MOCKITO_RUNNERS_MOCKITO_JUNIT_RUNNER.equals(qualifiedRunnerName)
|| ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_RUNNER.equals(qualifiedRunnerName)
|| ORG_SPRINGFRAMEWORK_TEST_CONTEXT_JUNIT4_SPRING_JUNIT4_CLASS_RUNNER.equals(qualifiedRunnerName)) {
return true;
}
if (JdtUiInheritedTestsRunnerMigration.assess(type, qualifiedRunnerName).eligible()) {
return true;
}
if (ORG_JUNIT_EXPERIMENTAL_RUNNERS_ENCLOSED.equals(qualifiedRunnerName)) {
return hasMigratableNestedType(type);
}
if (ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORIES.equals(qualifiedRunnerName)) {
return hasClosedTheoriesShape(type);
}
if (ORG_JUNIT_EXPERIMENTAL_CATEGORIES_CATEGORIES.equals(qualifiedRunnerName)) {
return hasSuiteClasses(type);
}
return false;
}
private static Annotation runWithAnnotation(TypeDeclaration type) {
for (Object modifier : type.modifiers()) {
if (modifier instanceof Annotation annotation
&& isAnnotation(annotation, ORG_JUNIT_RUNWITH, "RunWith")) { //$NON-NLS-1$
return annotation;
}
}
return null;
}
private static boolean hasMigratableNestedType(TypeDeclaration type) {
for (TypeDeclaration nested : type.getTypes()) {
if (Modifier.isStatic(nested.getModifiers()) && hasTestMethod(nested)) {
return true;
}
}
return false;
}
private static boolean hasTestMethod(TypeDeclaration type) {
for (MethodDeclaration method : type.getMethods()) {
for (Object modifier : method.modifiers()) {
if (modifier instanceof Annotation annotation
&& (isAnnotation(annotation, ORG_JUNIT_TEST, "Test") //$NON-NLS-1$
|| isAnnotation(annotation, ORG_JUNIT_JUPITER_TEST, "Test"))) { //$NON-NLS-1$
return true;
}
}
}
return false;
}
private static boolean hasClosedTheoriesShape(TypeDeclaration type) {
int dataPointFields= 0;
int theoryMethods= 0;
for (FieldDeclaration field : type.getFields()) {
for (Object modifier : field.modifiers()) {
if (modifier instanceof Annotation annotation
&& isAnnotation(annotation, ORG_JUNIT_EXPERIMENTAL_THEORIES_DATAPOINTS,
"DataPoints")) { //$NON-NLS-1$
dataPointFields++;
if (field.fragments().size() != 1
|| !(field.fragments().get(0) instanceof VariableDeclarationFragment fragment)
|| !(fragment.getInitializer() instanceof ArrayInitializer)) {
return false;
}
}
}
}
for (MethodDeclaration method : type.getMethods()) {
for (Object modifier : method.modifiers()) {
if (modifier instanceof Annotation annotation
&& isAnnotation(annotation, ORG_JUNIT_EXPERIMENTAL_THEORIES_THEORY,
"Theory")) { //$NON-NLS-1$
theoryMethods++;
}
}
}
return dataPointFields == 1 && theoryMethods == 1;
}
private static boolean hasSuiteClasses(TypeDeclaration type) {
for (Object modifier : type.modifiers()) {
if (modifier instanceof SingleMemberAnnotation annotation
&& isAnnotation(annotation, ORG_JUNIT_SUITE_SUITECLASSES, "SuiteClasses")) { //$NON-NLS-1$
return true;
}
}
return false;
}
private static boolean isAnnotation(Annotation annotation, String qualifiedName, String simpleName) {
ITypeBinding binding= annotation.resolveTypeBinding();
if (binding != null) {
return qualifiedName.equals(binding.getQualifiedName());
}
String sourceName= annotation.getTypeName().getFullyQualifiedName();
return simpleName.equals(sourceName) || qualifiedName.equals(sourceName)
|| sourceName.endsWith('.' + simpleName);
}
}