JUnitMultiFilePlanner.java
/*******************************************************************************
* Copyright (c) 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
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.multifile;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ORG_JUNIT_CLASS_RULE;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ORG_JUNIT_RULE;
import static org.sandbox.jdt.internal.corext.fix.helper.lib.JUnitConstants.ORG_JUNIT_RULES_EXTERNAL_RESOURCE;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTRequestor;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
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.IVariableBinding;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.sandbox.jdt.cleanup.multifile.JavaProjectCompilationUnits;
import org.sandbox.jdt.cleanup.multifile.MultiFileCandidateDiagnostic;
import org.sandbox.jdt.cleanup.multifile.MultiFileCleanUpDiagnostics;
import org.sandbox.jdt.cleanup.multifile.MultiFileCleanUpPlanResult;
import org.sandbox.jdt.cleanup.multifile.MultiFilePlanningBudget;
import org.sandbox.jdt.cleanup.multifile.MultiFilePlanningLimits;
import org.sandbox.jdt.cleanup.multifile.MultiFilePlanningMetrics;
import org.sandbox.jdt.cleanup.multifile.MultiFileScopeDiagnostic;
import org.sandbox.jdt.cleanup.multifile.SelectedCompilationUnitPlan;
import org.sandbox.jdt.internal.corext.fix.helper.ParameterizedMigrationEligibility;
import org.sandbox.jdt.internal.corext.fix.helper.ParameterizedMigrationEligibility.Assessment;
/** Builds the source-wide JUnit migration plan before per-file rewrites start. */
public final class JUnitMultiFilePlanner {
private static final String CLEANUP_ID= "junit-coordinated-migration"; //$NON-NLS-1$
/** Enabled project-planning contracts for one cleanup run. */
public record PlanningOptions(boolean migrateExternalResourceRules,
boolean migrateJUnit3Hierarchies,
boolean diagnoseParameterizedCandidates) {
boolean hasPlanningWork() {
return migrateExternalResourceRules || migrateJUnit3Hierarchies
|| diagnoseParameterizedCandidates;
}
}
private record ResourceType(String compilationUnitHandle, String typeBindingKey, String typeName) {
}
/**
* One {@code ExternalResource} fixture type together with the source types it
* inherits its callback contract from, ordered from the fixture towards the
* direct {@code ExternalResource} subclass.
*/
private record ResourceChain(ResourceType fixture, List<ResourceType> types) {
ResourceChain {
types= List.copyOf(types);
}
}
private record RuleField(String compilationUnitHandle, String fieldBindingKey, String resourceTypeKey,
boolean classRule) {
}
private record ResourceScope(Map<String, ResourceChain> chainsByTypeKey,
List<MultiFileCandidateDiagnostic> diagnostics) {
}
private record MigrationResult(List<ExternalResourceRuleMigration> migrations,
List<MultiFileCandidateDiagnostic> diagnostics) {
MigrationResult {
migrations= List.copyOf(migrations);
diagnostics= List.copyOf(diagnostics);
}
}
private enum RuleKind {
NONE,
INSTANCE,
CLASS
}
private JUnitMultiFilePlanner() {
}
/** Compatibility entry point for ExternalResource-only planning. */
public static MultiFileCleanUpPlanResult<JUnitMigrationPlan> create(IJavaProject project,
ICompilationUnit[] selectedUnits, boolean migrateExternalResourceRules, IProgressMonitor monitor)
throws CoreException {
SelectedCompilationUnitPlan selectedScope= SelectedCompilationUnitPlan.of(project, selectedUnits);
if (!migrateExternalResourceRules || selectedUnits.length == 0) {
return MultiFileCleanUpPlanResult.success(emptyPlan(selectedScope));
}
if (monitor != null && monitor.isCanceled()) {
throw new OperationCanceledException();
}
List<ICompilationUnit> allProjectUnits= JavaProjectCompilationUnits.collect(project);
Set<String> projectHandles= new LinkedHashSet<>();
for (ICompilationUnit unit : allProjectUnits) {
projectHandles.add(unit.getPrimary().getHandleIdentifier());
}
return create(project, selectedUnits, true,
selectedScope.compilationUnitHandles().equals(projectHandles), monitor);
}
/** Compatibility entry point for ExternalResource-only closed-scope planning. */
public static MultiFileCleanUpPlanResult<JUnitMigrationPlan> create(IJavaProject project,
ICompilationUnit[] selectedUnits, boolean migrateExternalResourceRules, boolean closedScope,
IProgressMonitor monitor) throws CoreException {
return createCoordinated(project, selectedUnits, migrateExternalResourceRules, false, closedScope, monitor);
}
/** Compatibility entry point for established coordinated planning callers. */
public static MultiFileCleanUpPlanResult<JUnitMigrationPlan> createCoordinated(IJavaProject project,
ICompilationUnit[] selectedUnits, boolean migrateExternalResourceRules,
boolean migrateJUnit3Hierarchies, IProgressMonitor monitor) throws CoreException {
return createCoordinated(project, selectedUnits,
new PlanningOptions(migrateExternalResourceRules, migrateJUnit3Hierarchies, false), monitor);
}
/** Compatibility entry point for established closed-scope planning callers. */
public static MultiFileCleanUpPlanResult<JUnitMigrationPlan> createCoordinated(IJavaProject project,
ICompilationUnit[] selectedUnits, boolean migrateExternalResourceRules,
boolean migrateJUnit3Hierarchies, boolean closedScope, IProgressMonitor monitor) throws CoreException {
return createCoordinated(project, selectedUnits,
new PlanningOptions(migrateExternalResourceRules, migrateJUnit3Hierarchies, false), closedScope, monitor);
}
/** Plans all enabled coordinated JUnit migrations for a project selection. */
public static MultiFileCleanUpPlanResult<JUnitMigrationPlan> createCoordinated(IJavaProject project,
ICompilationUnit[] selectedUnits, PlanningOptions options,
IProgressMonitor monitor) throws CoreException {
Objects.requireNonNull(options);
SelectedCompilationUnitPlan selectedScope= SelectedCompilationUnitPlan.of(project, selectedUnits);
if (!options.hasPlanningWork() || selectedUnits.length == 0) {
return MultiFileCleanUpPlanResult.success(emptyPlan(selectedScope));
}
if (monitor != null && monitor.isCanceled()) {
throw new OperationCanceledException();
}
List<ICompilationUnit> allProjectUnits= JavaProjectCompilationUnits.collect(project);
Set<String> projectHandles= new LinkedHashSet<>();
for (ICompilationUnit unit : allProjectUnits) {
projectHandles.add(unit.getPrimary().getHandleIdentifier());
}
return createCoordinated(project, selectedUnits, options,
selectedScope.compilationUnitHandles().equals(projectHandles), monitor);
}
/** Plans enabled migrations after the caller has classified the selected source scope. */
public static MultiFileCleanUpPlanResult<JUnitMigrationPlan> createCoordinated(IJavaProject project,
ICompilationUnit[] selectedUnits, PlanningOptions options,
boolean closedScope, IProgressMonitor monitor) throws CoreException {
Objects.requireNonNull(options);
SelectedCompilationUnitPlan selectedScope= SelectedCompilationUnitPlan.of(project, selectedUnits);
if (!options.hasPlanningWork() || selectedUnits.length == 0) {
return MultiFileCleanUpPlanResult.success(emptyPlan(selectedScope));
}
if (!closedScope && !options.diagnoseParameterizedCandidates()) {
JUnitTestTypeInventory inventory= options.migrateJUnit3Hierarchies()
? JUnitTestTypeInventory.capture(project, monitor)
: new JUnitTestTypeInventory(List.of());
MultiFileCleanUpDiagnostics diagnostics= diagnostics(selectedUnits, false, List.of());
JUnitMigrationPlan plan= new JUnitMigrationPlan(selectedScope, List.of(), List.of(), inventory);
return MultiFileCleanUpPlanResult.success(plan, new RefactoringStatus(),
MultiFilePlanningMetrics.empty(), diagnostics);
}
MultiFilePlanningBudget.checkCanceled(monitor);
long planningStarted= System.nanoTime();
MultiFilePlanningBudget.Assessment budget= MultiFilePlanningBudget.assess(selectedUnits,
MultiFilePlanningLimits.fromSystemProperties(), monitor);
if (!budget.mayProceed()) {
return new MultiFileCleanUpPlanResult<>(null, budget.status(), budget.metrics(),
diagnostics(selectedUnits, closedScope, List.of()));
}
long parseStarted= System.nanoTime();
Map<String, CompilationUnit> rootsByHandle= parse(project, selectedUnits, monitor);
long parseNanos= System.nanoTime() - parseStarted;
RefactoringStatus status= budget.status();
List<MultiFileCandidateDiagnostic> parameterizedDiagnostics=
options.diagnoseParameterizedCandidates()
? diagnoseParameterizedCandidates(rootsByHandle, monitor)
: List.of();
if (!closedScope) {
JUnitTestTypeInventory inventory= options.migrateJUnit3Hierarchies()
? JUnitTestTypeInventory.capture(project, monitor)
: new JUnitTestTypeInventory(List.of());
MultiFilePlanningMetrics metrics= budget.metrics()
.withDurations(parseNanos, System.nanoTime() - planningStarted)
.withRetainedPlanEntries(0);
MultiFileCleanUpDiagnostics diagnostics= diagnostics(selectedUnits, false,
parameterizedDiagnostics);
JUnitMigrationPlan plan= new JUnitMigrationPlan(selectedScope, List.of(), List.of(), inventory);
return MultiFileCleanUpPlanResult.success(plan, status, metrics, diagnostics);
}
MigrationResult externalResult= options.migrateExternalResourceRules()
? planExternalResources(rootsByHandle, status, monitor)
: new MigrationResult(List.of(), List.of());
MultiFilePlanningBudget.checkCanceled(monitor);
JUnit3HierarchyPlanner.Result junit3Result= options.migrateJUnit3Hierarchies()
? JUnit3HierarchyPlanner.create(project, selectedUnits, rootsByHandle, true, monitor)
: new JUnit3HierarchyPlanner.Result(List.of(), List.of(), new JUnitTestTypeInventory(List.of()));
List<MultiFileCandidateDiagnostic> candidateDiagnostics= new ArrayList<>(parameterizedDiagnostics);
candidateDiagnostics.addAll(externalResult.diagnostics());
candidateDiagnostics.addAll(junit3Result.diagnostics());
int retainedEntries= externalResult.migrations().size() + junit3Result.migrations().size();
MultiFilePlanningMetrics metrics= budget.metrics()
.withDurations(parseNanos, System.nanoTime() - planningStarted)
.withRetainedPlanEntries(retainedEntries);
MultiFileCleanUpDiagnostics diagnostics= diagnostics(selectedUnits, true, candidateDiagnostics);
if (status.hasFatalError()) {
return new MultiFileCleanUpPlanResult<>(null, status, metrics, diagnostics);
}
JUnitMigrationPlan plan= new JUnitMigrationPlan(selectedScope, externalResult.migrations(),
junit3Result.migrations(), junit3Result.inventory());
return MultiFileCleanUpPlanResult.success(plan, status, metrics, diagnostics);
}
private static JUnitMigrationPlan emptyPlan(SelectedCompilationUnitPlan selectedScope) {
return new JUnitMigrationPlan(selectedScope, List.of(), List.of(), new JUnitTestTypeInventory(List.of()));
}
private static MigrationResult planExternalResources(Map<String, CompilationUnit> rootsByHandle,
RefactoringStatus status, IProgressMonitor monitor) {
MultiFilePlanningBudget.checkCanceled(monitor);
ResourceScope resourceScope= collectResourceChains(rootsByHandle, monitor);
MultiFilePlanningBudget.checkCanceled(monitor);
List<RuleField> ruleFields= collectRuleFields(rootsByHandle, resourceScope.chainsByTypeKey(), monitor);
MultiFilePlanningBudget.checkCanceled(monitor);
MigrationResult result= createMigrations(ruleFields, resourceScope.chainsByTypeKey(), status, monitor);
if (resourceScope.diagnostics().isEmpty()) {
return result;
}
List<MultiFileCandidateDiagnostic> diagnostics= new ArrayList<>(resourceScope.diagnostics());
diagnostics.addAll(result.diagnostics());
return new MigrationResult(result.migrations(), diagnostics);
}
private static List<MultiFileCandidateDiagnostic> diagnoseParameterizedCandidates(
Map<String, CompilationUnit> rootsByHandle, IProgressMonitor monitor) {
List<MultiFileCandidateDiagnostic> diagnostics= new ArrayList<>();
for (Map.Entry<String, CompilationUnit> entry : rootsByHandle.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
String ownerHandle= entry.getKey();
entry.getValue().accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
MultiFilePlanningBudget.checkCanceled(monitor);
if (!ParameterizedMigrationEligibility.hasParameterizedRunner(node)) {
return true;
}
Assessment assessment= ParameterizedMigrationEligibility.assess(node);
if (!assessment.eligible()) {
String typeName= sourceTypeName(node);
String message= "Parameterized test " + typeName //$NON-NLS-1$
+ " was left unchanged: " + assessment.explanation(); //$NON-NLS-1$
diagnostics.add(MultiFileCandidateDiagnostic.rejected(
"parameterized:" + typeName, ownerHandle, //$NON-NLS-1$
assessment.reasonCode(), message, List.of(ownerHandle)));
}
return true;
}
});
}
return diagnostics;
}
private static String sourceTypeName(TypeDeclaration type) {
ITypeBinding binding= type.resolveBinding();
if (binding != null && binding.getQualifiedName() != null
&& !binding.getQualifiedName().isBlank()) {
return binding.getQualifiedName();
}
return type.getName().getIdentifier();
}
private static MultiFileCleanUpDiagnostics diagnostics(ICompilationUnit[] selectedUnits, boolean complete,
List<MultiFileCandidateDiagnostic> candidates) {
List<String> selectedHandles= java.util.Arrays.stream(selectedUnits)
.map(ICompilationUnit::getPrimary)
.map(ICompilationUnit::getHandleIdentifier)
.toList();
MultiFileScopeDiagnostic scope= complete
? new MultiFileScopeDiagnostic(selectedHandles, List.of(), "CLOSED_SOURCE_SCOPE", //$NON-NLS-1$
"The selected compilation units form a closed coordinated JUnit migration scope.", true) //$NON-NLS-1$
: new MultiFileScopeDiagnostic(selectedHandles, List.of(), "INCOMPLETE_SOURCE_SCOPE", //$NON-NLS-1$
"The selected compilation units do not contain every source unit required by the enabled JUnit migrations.", //$NON-NLS-1$
false);
return new MultiFileCleanUpDiagnostics(CLEANUP_ID, scope, candidates);
}
private static Map<String, CompilationUnit> parse(IJavaProject project, ICompilationUnit[] units,
IProgressMonitor monitor) {
Map<String, CompilationUnit> roots= new LinkedHashMap<>();
ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
parser.setProject(project);
parser.setResolveBindings(true);
parser.setBindingsRecovery(IASTSharedValues.SHARED_BINDING_RECOVERY);
parser.setStatementsRecovery(IASTSharedValues.SHARED_AST_STATEMENT_RECOVERY);
parser.setCompilerOptions(RefactoringASTParser.getCompilerOptions(project));
parser.createASTs(units, new String[0], new ASTRequestor() {
@Override
public void acceptAST(ICompilationUnit source, CompilationUnit ast) {
roots.put(source.getPrimary().getHandleIdentifier(), ast);
}
}, monitor);
return roots;
}
/**
* Collects every source fixture type whose superclass chain reaches
* {@code org.junit.rules.ExternalResource}. A fixture is only planned when every
* type between the fixture and {@code ExternalResource} is available as source in
* the current scope, because the inherited {@code before()}/{@code after()}
* callbacks have to be renamed together with the fixture.
*/
private static ResourceScope collectResourceChains(Map<String, CompilationUnit> rootsByHandle,
IProgressMonitor monitor) {
Map<String, ResourceType> sourceTypes= new LinkedHashMap<>();
Map<String, ITypeBinding> bindings= new LinkedHashMap<>();
for (Map.Entry<String, CompilationUnit> entry : rootsByHandle.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
String unitHandle= entry.getKey();
entry.getValue().accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
MultiFilePlanningBudget.checkCanceled(monitor);
ITypeBinding binding= node.resolveBinding();
String typeKey= JUnitMigrationPlan.typeKey(binding);
if (typeKey != null) {
sourceTypes.put(typeKey, new ResourceType(unitHandle, typeKey, binding.getQualifiedName()));
bindings.put(typeKey, binding);
}
return true;
}
});
}
Map<String, ResourceChain> chains= new LinkedHashMap<>();
List<MultiFileCandidateDiagnostic> diagnostics= new ArrayList<>();
for (Map.Entry<String, ITypeBinding> entry : bindings.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
ResourceType fixture= sourceTypes.get(entry.getKey());
List<ResourceType> chain= resourceChain(entry.getValue(), sourceTypes);
if (chain != null) {
chains.put(entry.getKey(), new ResourceChain(fixture, chain));
} else if (extendsExternalResource(entry.getValue())) {
String message= "ExternalResource fixture " + fixture.typeName() //$NON-NLS-1$
+ " inherits its callbacks from a type that is not part of the selected source scope."; //$NON-NLS-1$
diagnostics.add(MultiFileCandidateDiagnostic.rejected(
"external-resource:" + fixture.typeName(), fixture.compilationUnitHandle(), //$NON-NLS-1$
"EXTERNAL_RESOURCE_BASE_OUT_OF_SCOPE", message, //$NON-NLS-1$
List.of(fixture.compilationUnitHandle())));
}
}
return new ResourceScope(chains, diagnostics);
}
/**
* Returns the fixture type and all its source superclasses up to the direct
* {@code ExternalResource} subclass, or {@code null} when the chain leaves the
* source scope or never reaches {@code ExternalResource}.
*/
private static List<ResourceType> resourceChain(ITypeBinding binding, Map<String, ResourceType> sourceTypes) {
List<ResourceType> chain= new ArrayList<>();
ITypeBinding current= binding;
while (current != null) {
ResourceType sourceType= sourceTypes.get(JUnitMigrationPlan.typeKey(current));
if (sourceType == null) {
return null;
}
chain.add(sourceType);
ITypeBinding superclass= current.getSuperclass();
if (superclass == null) {
return null;
}
if (ORG_JUNIT_RULES_EXTERNAL_RESOURCE.equals(superclass.getErasure().getQualifiedName())) {
return chain;
}
current= superclass;
}
return null;
}
private static List<RuleField> collectRuleFields(Map<String, CompilationUnit> rootsByHandle,
Map<String, ResourceChain> chainsByTypeKey, IProgressMonitor monitor) {
List<RuleField> result= new ArrayList<>();
for (Map.Entry<String, CompilationUnit> entry : rootsByHandle.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
String unitHandle= entry.getKey();
entry.getValue().accept(new ASTVisitor() {
@Override
public boolean visit(FieldDeclaration node) {
MultiFilePlanningBudget.checkCanceled(monitor);
RuleKind kind= ruleKind(node);
boolean classRule= kind == RuleKind.CLASS;
if (kind == RuleKind.NONE || node.fragments().size() != 1
|| classRule != Modifier.isStatic(node.getModifiers())) {
return true;
}
VariableDeclarationFragment fragment= (VariableDeclarationFragment) node.fragments().get(0);
IVariableBinding fieldBinding= fragment.resolveBinding();
ITypeBinding resourceBinding= resourceTypeBinding(fragment, fieldBinding);
String resourceTypeKey= JUnitMigrationPlan.typeKey(resourceBinding);
ResourceChain chain= chainsByTypeKey.get(resourceTypeKey);
if (fieldBinding == null || chain == null) {
return true;
}
String fieldKey= fieldBinding.getVariableDeclaration().getKey();
if (fieldKey != null) {
result.add(new RuleField(unitHandle, fieldKey, resourceTypeKey, classRule));
}
return true;
}
});
}
return result;
}
private static MigrationResult createMigrations(List<RuleField> fields,
Map<String, ResourceChain> chainsByTypeKey, RefactoringStatus status, IProgressMonitor monitor) {
// Every type between the rule field type and ExternalResource takes part in the
// same migration, because its inherited callbacks are renamed as well.
Map<String, List<RuleField>> fieldsByResourceType= new LinkedHashMap<>();
Map<String, ResourceType> resourcesByTypeKey= new LinkedHashMap<>();
for (RuleField field : fields) {
for (ResourceType resource : chainsByTypeKey.get(field.resourceTypeKey()).types()) {
resourcesByTypeKey.putIfAbsent(resource.typeBindingKey(), resource);
fieldsByResourceType.computeIfAbsent(resource.typeBindingKey(), ignored -> new ArrayList<>())
.add(field);
}
}
List<ExternalResourceRuleMigration> migrations= new ArrayList<>();
List<MultiFileCandidateDiagnostic> diagnostics= new ArrayList<>();
for (Map.Entry<String, List<RuleField>> entry : fieldsByResourceType.entrySet()) {
MultiFilePlanningBudget.checkCanceled(monitor);
ResourceType resource= resourcesByTypeKey.get(entry.getKey());
List<RuleField> resourceFields= entry.getValue();
boolean hasRule= resourceFields.stream().anyMatch(field -> !field.classRule());
boolean hasClassRule= resourceFields.stream().anyMatch(RuleField::classRule);
List<String> relatedHandles= new ArrayList<>();
relatedHandles.add(resource.compilationUnitHandle());
resourceFields.stream().map(RuleField::compilationUnitHandle).forEach(relatedHandles::add);
String candidateId= "external-resource:" + resource.typeName(); //$NON-NLS-1$
if (hasRule && hasClassRule) {
String message= "ExternalResource type " + resource.typeName() //$NON-NLS-1$
+ " is used by both @Rule and @ClassRule fields; one callback lifecycle cannot safely represent both usages."; //$NON-NLS-1$
status.addFatalError(message);
diagnostics.add(MultiFileCandidateDiagnostic.rejected(candidateId,
resource.compilationUnitHandle(), "MIXED_RULE_LIFECYCLE", message, relatedHandles)); //$NON-NLS-1$
continue;
}
for (RuleField field : resourceFields) {
migrations.add(new ExternalResourceRuleMigration(field.compilationUnitHandle(), field.fieldBindingKey(),
resource.compilationUnitHandle(), resource.typeBindingKey(), field.classRule()));
}
String lifecycle= hasClassRule ? "class" : "instance"; //$NON-NLS-1$ //$NON-NLS-2$
diagnostics.add(MultiFileCandidateDiagnostic.transformed(candidateId,
resource.compilationUnitHandle(), "Migrates " + resourceFields.size() + " " + lifecycle //$NON-NLS-1$ //$NON-NLS-2$
+ " rule field(s) together with " + resource.typeName() + ".", relatedHandles)); //$NON-NLS-1$ //$NON-NLS-2$
}
return new MigrationResult(migrations, diagnostics);
}
private static RuleKind ruleKind(FieldDeclaration field) {
for (Object modifier : field.modifiers()) {
if (modifier instanceof Annotation annotation) {
ITypeBinding binding= annotation.resolveTypeBinding();
if (binding == null) {
continue;
}
if (ORG_JUNIT_RULE.equals(binding.getQualifiedName())) {
return RuleKind.INSTANCE;
}
if (ORG_JUNIT_CLASS_RULE.equals(binding.getQualifiedName())) {
return RuleKind.CLASS;
}
}
}
return RuleKind.NONE;
}
private static ITypeBinding resourceTypeBinding(VariableDeclarationFragment fragment,
IVariableBinding fieldBinding) {
Expression initializer= fragment.getInitializer();
if (initializer instanceof ClassInstanceCreation creation) {
ITypeBinding binding= creation.resolveTypeBinding();
if (binding != null) {
return binding;
}
}
return fieldBinding == null ? null : fieldBinding.getType();
}
private static boolean extendsExternalResource(ITypeBinding binding) {
for (ITypeBinding current= binding == null ? null : binding.getSuperclass(); current != null;
current= current.getSuperclass()) {
if (ORG_JUNIT_RULES_EXTERNAL_RESOURCE.equals(current.getErasure().getQualifiedName())) {
return true;
}
}
return false;
}
}